feat: experimental js instrumentation release! and tons of other bug
fixes and improvements releases widget@0.1.37; standalone@2.1.5
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
demo/** linguist-vendored
|
||||
standalone/public/tester.html linguist-vendored
|
||||
standalone/public/login.html linguist-vendored
|
||||
standalone/public/index.html linguist-vendored
|
||||
widget/test.html linguist-vendored
|
||||
wasm/test/browser.html linguist-vendored
|
||||
@@ -1,34 +0,0 @@
|
||||
# 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
|
||||
@@ -1 +0,0 @@
|
||||
# Please see [github.com/tiagozip/cap](https://github.com/tiagozip/cap)
|
||||
@@ -1,33 +0,0 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "@cap.js/cli",
|
||||
"dependencies": {
|
||||
"@cap.js/solver": "0.0.3",
|
||||
"kleur": "^4.1.5",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@cap.js/solver": ["@cap.js/solver@0.0.3", "", {}, "sha512-J/Vso8LllJGBmVuj68mXalSsOg8a+Ry0/lnOYu63srk37UFlbQVWdcm63hARnEdOduR33DWS0Ot/yGHPliJhqw=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.2.10", "", { "dependencies": { "bun-types": "1.2.10" } }, "sha512-eilv6WFM3M0c9ztJt7/g80BDusK98z/FrFwseZgT4bXCq2vPhXD4z8R3oddmAn+R/Nmz9vBn4kweJKmGTZj+lg=="],
|
||||
|
||||
"@types/node": ["@types/node@22.15.2", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-uKXqKN9beGoMdBfcaTY1ecwz6ctxuJAcUlwE55938g0ZJ8lRxwAZqRz2AJ4pzpt5dHdTPMB863UZ0ESiFUcP7A=="],
|
||||
|
||||
"bun-types": ["bun-types@1.2.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-b5ITZMnVdf3m1gMvJHG+gIfeJHiQPJak0f7925Hxu6ZN5VKA8AGy4GZ4lM+Xkn6jtWxg5S3ldWvfmXdvnkp3GQ=="],
|
||||
|
||||
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
|
||||
|
||||
"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=="],
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import solver from "@cap.js/solver";
|
||||
import kleur from "kleur";
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
(async () => {
|
||||
if (args.length === 0) {
|
||||
console.log(`${kleur.bold("@cap.js/cli")} ${kleur.gray(
|
||||
"cli solver for cap challenges"
|
||||
)}${
|
||||
!process.versions?.bun
|
||||
? `\n\n ${kleur.yellow().bold("warn")}${kleur.dim(
|
||||
":"
|
||||
)} bun is not installed and recommended`
|
||||
: ""
|
||||
}
|
||||
|
||||
Usage:
|
||||
${kleur.dim("$")} ${kleur.bold().green("bunx @cap.js/cli")} ${kleur.cyan(
|
||||
"<challenges>"
|
||||
)}
|
||||
|
||||
Options:
|
||||
${kleur.cyan("<challenges>")} The challenges to solve in
|
||||
format \`salt:target\``);
|
||||
return;
|
||||
}
|
||||
|
||||
const challenges = args.map((arg) => {
|
||||
const [salt, target] = arg.split(":");
|
||||
|
||||
if (!salt || !target) {
|
||||
console.error(
|
||||
`${kleur.red("Error:")} ${kleur.yellow(arg)} is not a valid challenge`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return [salt, target];
|
||||
});
|
||||
|
||||
console.log(
|
||||
`${kleur.cyan("@cap.js/cli")} ${kleur.dim(
|
||||
`${challenges.length} challenges`
|
||||
)}\n\n\n`
|
||||
);
|
||||
|
||||
const start = performance.now();
|
||||
|
||||
await solver(challenges, {
|
||||
onProgress: (status) => {
|
||||
process.stdout.moveCursor(0, -2);
|
||||
process.stdout.clearScreenDown();
|
||||
console.log(
|
||||
`${status.result.salt}:${status.result.target}:${status.result.nonce}`
|
||||
);
|
||||
console.log(kleur.dim("\nSolving challenges… " + status.progress + "%"));
|
||||
},
|
||||
});
|
||||
|
||||
const end = performance.now();
|
||||
|
||||
process.stdout.moveCursor(0, -2);
|
||||
process.stdout.clearScreenDown();
|
||||
console.log(
|
||||
kleur.dim(`\nSolved challenges in ${kleur.bold(Math.round(end - start))}ms`)
|
||||
);
|
||||
})();
|
||||
@@ -1,72 +0,0 @@
|
||||
{
|
||||
"version": "0.0.5",
|
||||
"name": "@cap.js/cli",
|
||||
"module": "index.js",
|
||||
"description": "CLI server-side solver for Cap, a lightweight, modern open-source CAPTCHA alternative designed using SHA-256 PoW.",
|
||||
"type": "module",
|
||||
"private": false,
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tiagozip/cap.git"
|
||||
},
|
||||
"scripts": {
|
||||
"npm:publish": "npm publish --access public"
|
||||
},
|
||||
"author": "Tiago",
|
||||
"keywords": [
|
||||
"security",
|
||||
"detection",
|
||||
"prevention",
|
||||
"defense",
|
||||
"protection",
|
||||
"anti-abuse",
|
||||
"anti-automation",
|
||||
"anti-bot",
|
||||
"anti-ddos",
|
||||
"anti-dos",
|
||||
"anti-exploitation",
|
||||
"anti-spam",
|
||||
"anti-scraping",
|
||||
"attack-mitigation",
|
||||
"protocol",
|
||||
"client-server",
|
||||
"computational-puzzle",
|
||||
"complexity",
|
||||
"puzzle",
|
||||
"crypto",
|
||||
"cryptographic",
|
||||
"algorithm",
|
||||
"cybersecurity",
|
||||
"ddos",
|
||||
"hashcash",
|
||||
"hcaptcha",
|
||||
"captcha-alternative",
|
||||
"verification",
|
||||
"invisible",
|
||||
"pow",
|
||||
"proof-of-work",
|
||||
"recaptcha",
|
||||
"spam",
|
||||
"bots",
|
||||
"filtering",
|
||||
"turing-test"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@cap.js/solver": "0.0.3",
|
||||
"kleur": "^4.1.5"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"cli": "./index.js"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/tiagozip/cap/issues"
|
||||
},
|
||||
"homepage": "https://github.com/tiagozip/cap#readme"
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
FROM oven/bun:1 AS base
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
FROM base AS install
|
||||
RUN mkdir -p /temp/dev
|
||||
COPY package.json bun.lock /temp/dev/
|
||||
RUN cd /temp/dev && bun install
|
||||
|
||||
FROM base AS prod
|
||||
RUN mkdir -p /temp/prod
|
||||
COPY package.json bun.lock /temp/prod/
|
||||
RUN cd /temp/prod && bun install
|
||||
|
||||
FROM base AS prerelease
|
||||
COPY --from=install /temp/dev/node_modules node_modules
|
||||
COPY . .
|
||||
|
||||
FROM base AS release
|
||||
COPY --from=prod /temp/prod/node_modules node_modules
|
||||
COPY --from=prerelease /usr/src/app .
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
RUN mkdir -p /usr/src/app/.data && chown -R bun:bun /usr/src/app/.data
|
||||
|
||||
EXPOSE 3000/tcp
|
||||
|
||||
USER bun
|
||||
|
||||
ENTRYPOINT [ "bun", "run", "./src/index.js" ]
|
||||
@@ -1,79 +0,0 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "app",
|
||||
"dependencies": {
|
||||
"@cap.js/server": "^1.0.13",
|
||||
"@elysiajs/cors": "^1.3.3",
|
||||
"@elysiajs/static": "^1.3.0",
|
||||
"elysia": "^1.3.1",
|
||||
"elysia-rate-limit": "^4.3.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"bun-types": "latest",
|
||||
},
|
||||
},
|
||||
},
|
||||
"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=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
"@sinclair/typebox": ["@sinclair/typebox@0.34.33", "", {}, "sha512-5HAV9exOMcXRUxo+9iYB5n09XxzCXnfy4VTNW4xnDv+FgjzAGY989C28BIdljKqmF+ZltUwujE3aossvcVtq6g=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
"bun-types": ["bun-types@1.2.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-Kuh4Ub28ucMRWeiUUWMHsT9Wcbr4H3kLIO72RZZElSDxSu7vpetRvxIUDUaW6QtaIeixIpm7OXtNnZPf82EzwA=="],
|
||||
|
||||
"clone": ["clone@2.1.2", "", {}, "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w=="],
|
||||
|
||||
"cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="],
|
||||
|
||||
"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-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=="],
|
||||
|
||||
"exact-mirror": ["exact-mirror@0.1.2", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-wFCPCDLmHbKGUb8TOi/IS7jLsgR8WVDGtDK3CzcB4Guf/weq7G+I+DkXiRSZfbemBFOxOINKpraM6ml78vo8Zw=="],
|
||||
|
||||
"fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||
|
||||
"ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="],
|
||||
|
||||
"node-cache": ["node-cache@5.1.2", "", { "dependencies": { "clone": "2.x" } }, "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg=="],
|
||||
|
||||
"openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="],
|
||||
|
||||
"peek-readable": ["peek-readable@7.0.0", "", {}, "sha512-nri2TO5JE3/mRryik9LlHFT53cgHfRK0Lt0BAZQXku/AW3E6XLt2GaY8siWi7dvW/m1z0ecn+J+bpDa9ZN3IsQ=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
|
||||
|
||||
"uint8array-extras": ["uint8array-extras@1.4.0", "", {}, "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ=="],
|
||||
|
||||
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
|
||||
"@tokenizer/inflate/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
|
||||
|
||||
"@tokenizer/inflate/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
{
|
||||
"name": "cap-standalone",
|
||||
"version": "1.0.17",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"dev": "bun run --watch src/index.js",
|
||||
"docker:publish": "docker buildx build --platform linux/amd64,linux/arm64 -t tiago2/cap:latest . --push"
|
||||
},
|
||||
"keywords": [
|
||||
"security",
|
||||
"detection",
|
||||
"prevention",
|
||||
"defense",
|
||||
"protection",
|
||||
"anti-abuse",
|
||||
"anti-automation",
|
||||
"anti-bot",
|
||||
"anti-ddos",
|
||||
"anti-dos",
|
||||
"anti-exploitation",
|
||||
"anti-spam",
|
||||
"anti-scraping",
|
||||
"attack-mitigation",
|
||||
"protocol",
|
||||
"client-server",
|
||||
"computational-puzzle",
|
||||
"complexity",
|
||||
"puzzle",
|
||||
"crypto",
|
||||
"cryptographic",
|
||||
"algorithm",
|
||||
"cybersecurity",
|
||||
"ddos",
|
||||
"hashcash",
|
||||
"hcaptcha",
|
||||
"captcha-alternative",
|
||||
"verification",
|
||||
"invisible",
|
||||
"pow",
|
||||
"proof-of-work",
|
||||
"recaptcha",
|
||||
"spam",
|
||||
"bots",
|
||||
"filtering",
|
||||
"turing-test"
|
||||
],
|
||||
"dependencies": {
|
||||
"@cap.js/server": "^1.0.13",
|
||||
"@elysiajs/cors": "^1.3.3",
|
||||
"@elysiajs/static": "^1.3.0",
|
||||
"elysia": "^1.3.1",
|
||||
"elysia-rate-limit": "^4.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"bun-types": "latest"
|
||||
},
|
||||
"module": "src/index.js"
|
||||
}
|
||||
@@ -1,326 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Cap</title>
|
||||
<link rel="icon" href="./public/logo.png" />
|
||||
<style>
|
||||
* {
|
||||
font-family: system, -apple-system, "BlinkMacSystemFont", ".SFNSText-Regular",
|
||||
"San Francisco", "Roboto", "Segoe UI", "Helvetica Neue", "Lucida Grande", "Ubuntu",
|
||||
"arial", sans-serif;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
max-width: 600px;
|
||||
padding: 15px;
|
||||
margin: 3rem auto;
|
||||
background-color: #121214;
|
||||
color: #e8e8ea;
|
||||
}
|
||||
|
||||
nav .logo {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: block;
|
||||
margin-bottom: -4px;
|
||||
}
|
||||
|
||||
nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
nav a {
|
||||
margin-left: auto;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
nav a:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
nav h1 {
|
||||
font-weight: 500;
|
||||
margin: 0px;
|
||||
font-size: 20px;
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
p.text {
|
||||
line-height: 1.5;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-weight: 600;
|
||||
font-size: 23px;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.title button {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
button.btn {
|
||||
padding: 6px 10px;
|
||||
border-radius: 6px;
|
||||
border: none;
|
||||
color: white;
|
||||
background: #0076d8;
|
||||
background-origin: border-box;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
touch-action: manipulation;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
button.btn:focus {
|
||||
box-shadow: inset 0px 0.8px 0px -0.25px rgba(0, 118, 216, 0.2),
|
||||
0px 0.5px 1.5px rgba(54, 122, 246, 0.25), 0px 0px 0px 3.5px rgba(0, 118, 216, 0.5);
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
button.btn:disabled {
|
||||
opacity: 0.4;
|
||||
filter: grayscale(1);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.dialog-wrapper {
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
animation: fade-in 0.2s;
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-wrapper .dialog {
|
||||
max-width: 450px;
|
||||
width: 100%;
|
||||
background-color: #18181a;
|
||||
border-radius: 18px;
|
||||
padding: 20px;
|
||||
position: relative;
|
||||
animation: dialog-in 0.2s;
|
||||
box-shadow: rgba(0, 0, 0, 0.25) 0px 54px 55px, rgba(0, 0, 0, 0.12) 0px -12px 30px,
|
||||
rgba(0, 0, 0, 0.12) 0px 4px 6px, rgba(0, 0, 0, 0.17) 0px 12px 13px,
|
||||
rgba(0, 0, 0, 0.09) 0px -3px 5px;
|
||||
}
|
||||
|
||||
@keyframes dialog-in {
|
||||
0% {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.dialog .close-button {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
color: #e8e8ea;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dialog h2 {
|
||||
margin: 0px;
|
||||
font-size: 20px;
|
||||
margin-bottom: 16px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.dialog .actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.key-creation-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 18px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.key-creation-form label {
|
||||
font-size: 14px;
|
||||
margin-bottom: 6px;
|
||||
opacity: 0.9;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.key-creation-form input {
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
padding: 10px 14px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.17);
|
||||
background-color: transparent;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.key-creation-form input:focus {
|
||||
outline: none;
|
||||
box-shadow: 0px 0px 0px 2px rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.key-creation-form input:disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.keys p {
|
||||
text-align: center;
|
||||
margin: 0px;
|
||||
margin-top: 8px;
|
||||
opacity: 0.8;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.keys .copy-key {
|
||||
padding: 6px 10px;
|
||||
border-radius: 6px;
|
||||
border: none;
|
||||
color: #fff;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
background-origin: border-box;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
touch-action: manipulation;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.keys .copy-key:focus {
|
||||
box-shadow: inset 0px 0.8px 0px -0.25px rgba(255, 255, 255, 0.2),
|
||||
0px 0.5px 1.5px rgba(54, 122, 246, 0.25), 0px 0px 0px 3.5px rgba(255, 255, 255, 0.1);
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.keys .edit-key {
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
touch-action: manipulation;
|
||||
cursor: pointer;
|
||||
opacity: 0.8;
|
||||
color: inherit;
|
||||
padding: 0px;
|
||||
margin-right: 8px;
|
||||
margin-bottom: -4px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.keys .edit-key:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.keys .key {
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
padding-bottom: 10px;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.key .key-name {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.action-secundary {
|
||||
border: none;
|
||||
padding: 0px;
|
||||
background-color: transparent;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
opacity: 0.8;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.action-secundary:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.dialog .actions-col {
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.key-id {
|
||||
opacity: 0.8;
|
||||
font-family: monospace;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav>
|
||||
<img src="./public/logo.png" alt="Cap logo" class="logo" />
|
||||
<h1>Cap</h1>
|
||||
<a href="/internal/auth/logout" class="logout">Logout</a>
|
||||
</nav>
|
||||
|
||||
<div class="title">
|
||||
<h2>Your keys</h2>
|
||||
<button class="new-keys btn">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="lucide lucide-plus-icon lucide-plus"
|
||||
>
|
||||
<path d="M5 12h14" />
|
||||
<path d="M12 5v14" />
|
||||
</svg>
|
||||
Create key
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="keys">
|
||||
<p>Loading keys...</p>
|
||||
</div>
|
||||
</body>
|
||||
<script src="/public/index.js"></script>
|
||||
</html>
|
||||
@@ -1,407 +0,0 @@
|
||||
const createKeyBtn = document.querySelector(".new-keys");
|
||||
|
||||
createKeyBtn.addEventListener("click", async () => {
|
||||
const modal = document.createElement("div");
|
||||
modal.classList.add("dialog-wrapper");
|
||||
modal.innerHTML = `
|
||||
<div class="dialog key-creation-dialog">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="close-button"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
||||
|
||||
<h2>Create a new key</h2>
|
||||
<div class="key-creation-form">
|
||||
<label for="keyName">Key name</label>
|
||||
<input type="text" id="keyName" placeholder="E.g. Blog comments" />
|
||||
</div>
|
||||
|
||||
<div class="key-creation-form">
|
||||
<label for="challengesCount">Challenges count</label>
|
||||
<input type="number" id="challengesCount" placeholder="18" value="18" min="1"/>
|
||||
</div>
|
||||
|
||||
<div class="key-creation-form">
|
||||
<label for="challengeSize">Challenge size</label>
|
||||
<input type="number" id="challengeSize" placeholder="32" value="32" min="1" />
|
||||
</div>
|
||||
|
||||
<div class="key-creation-form">
|
||||
<label for="challengeDifficulty">Target size</label>
|
||||
<input type="number" id="challengeDifficulty" placeholder="4" value="4" min="1" />
|
||||
</div>
|
||||
|
||||
<div class="key-creation-form">
|
||||
<label for="expiresMs">Challenge expiration (ms)</label>
|
||||
<input type="number" id="expiresMs" placeholder="600000" value="600000" min="1" />
|
||||
</div>
|
||||
|
||||
|
||||
<div class="actions">
|
||||
<button class="create-key btn" disabled>Create key</button>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
document.body.appendChild(modal);
|
||||
|
||||
const inputs = [
|
||||
"keyName",
|
||||
"challengesCount",
|
||||
"challengeSize",
|
||||
"challengeDifficulty",
|
||||
"expiresMs",
|
||||
];
|
||||
|
||||
const createKeyBtn = modal.querySelector(".create-key");
|
||||
|
||||
modal.addEventListener("click", (e) => {
|
||||
if (e.target === modal) {
|
||||
modal.remove();
|
||||
}
|
||||
});
|
||||
|
||||
const closeButton = modal.querySelector(".close-button");
|
||||
closeButton.addEventListener("click", () => {
|
||||
modal.remove();
|
||||
});
|
||||
|
||||
modal.querySelector("#keyName").focus();
|
||||
|
||||
inputs.forEach((input) => {
|
||||
input = modal.querySelector(`#${input}`);
|
||||
input.addEventListener("input", () => {
|
||||
createKeyBtn.disabled = !inputs.every((i) => {
|
||||
const inputEl = modal.querySelector(`#${i}`);
|
||||
return inputEl.value.trim() !== "";
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
createKeyBtn.addEventListener("click", async () => {
|
||||
createKeyBtn.disabled = true;
|
||||
let props = {};
|
||||
|
||||
inputs.forEach((i) => {
|
||||
i.disabled = true;
|
||||
props[i] = modal.querySelector(`#${i}`).value;
|
||||
});
|
||||
createKeyBtn.innerText = "Creating key...";
|
||||
const { message, publicKey, privateKey } = await (
|
||||
await fetch("/internal/createKey", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(props),
|
||||
})
|
||||
).json();
|
||||
|
||||
createKeyBtn.disabled = false;
|
||||
inputs.forEach((i) => {
|
||||
i.disabled = false;
|
||||
});
|
||||
createKeyBtn.innerText = "Create key";
|
||||
|
||||
if (message) return alert(message);
|
||||
|
||||
modal.remove();
|
||||
reloadKeysList();
|
||||
|
||||
const newModal = document.createElement("div");
|
||||
newModal.classList.add("dialog-wrapper");
|
||||
newModal.innerHTML = `
|
||||
<div class="dialog key-creation-dialog">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="close-button"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
||||
<h2>Key details</h2>
|
||||
<p>Make sure to copy your secret key — it's required to validate tokens and you won't be able to see it again.</p>
|
||||
|
||||
<div class="key-creation-form">
|
||||
<label for="public-key">Key ID</label>
|
||||
<input type="text" id="public-key" value="${publicKey}" readonly style="opacity: 1;user-select: all;"/>
|
||||
</div>
|
||||
|
||||
<div class="key-creation-form">
|
||||
<label for="private-key">Secret key</label>
|
||||
<input type="text" id="private-key" value="${privateKey}" readonly style="opacity: 1;user-select: all;"/>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
document.body.appendChild(newModal);
|
||||
|
||||
const newCloseButton = newModal.querySelector(".close-button");
|
||||
newCloseButton.addEventListener("click", () => {
|
||||
newModal.remove();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const reloadKeysList = async () => {
|
||||
const { keys } = await (await fetch("/internal/listKeys")).json();
|
||||
|
||||
const keysEl = document.querySelector(".keys");
|
||||
if (!keys.length) {
|
||||
keysEl.innerHTML = "<p>You don't have any keys.</p>";
|
||||
return;
|
||||
}
|
||||
|
||||
keysEl.innerHTML = "";
|
||||
|
||||
keys.forEach((key) => {
|
||||
const keyEl = document.createElement("div");
|
||||
keyEl.innerHTML = `<span class="key-name"></span><button class="edit-key" title="Edit key"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-pencil-icon lucide-pencil"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"/><path d="m15 5 4 4"/></svg></button> <button class="copy-key" title="Copy key ID"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-clipboard-icon lucide-clipboard"><rect width="8" height="4" x="8" y="2" rx="1" ry="1"/><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/></svg> <span>Copy key</span></button>`;
|
||||
keyEl.querySelector("span.key-name").innerText = key.name;
|
||||
keyEl.title = key.publicKey;
|
||||
keyEl.classList.add("key");
|
||||
|
||||
keyEl.querySelector(".copy-key").addEventListener("click", () => {
|
||||
navigator.clipboard.writeText(key.publicKey);
|
||||
keyEl.querySelector(
|
||||
".copy-key"
|
||||
).innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-check-icon lucide-check"><path d="M20 6 9 17l-5-5"/></svg> <span>Copied</span>`;
|
||||
setTimeout(() => {
|
||||
keyEl.querySelector(
|
||||
".copy-key"
|
||||
).innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-clipboard-icon lucide-clipboard"><rect width="8" height="4" x="8" y="2" rx="1" ry="1"/><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/></svg> <span>Copy key</span>`;
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
keyEl.querySelector(".edit-key").addEventListener("click", () => {
|
||||
const modal = document.createElement("div");
|
||||
modal.classList.add("dialog-wrapper");
|
||||
modal.innerHTML = `
|
||||
<div class="dialog key-creation-dialog">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="close-button"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
||||
|
||||
<h2>Edit key</h2>
|
||||
<div class="key-creation-form">
|
||||
<label for="keyName">Key name</label>
|
||||
<input type="text" id="keyName" placeholder="E.g. Blog comments" />
|
||||
</div>
|
||||
|
||||
<div class="key-creation-form">
|
||||
<label for="challengesCount">Challenges count</label>
|
||||
<input type="number" id="challengesCount" placeholder="18" value="18" min="1"/>
|
||||
</div>
|
||||
|
||||
<div class="key-creation-form">
|
||||
<label for="challengeSize">Challenge size</label>
|
||||
<input type="number" id="challengeSize" placeholder="32" value="32" min="1" />
|
||||
</div>
|
||||
|
||||
<div class="key-creation-form">
|
||||
<label for="challengeDifficulty">Target size</label>
|
||||
<input type="number" id="challengeDifficulty" placeholder="4" value="4" min="1" />
|
||||
</div>
|
||||
|
||||
<div class="key-creation-form">
|
||||
<label for="expiresMs">Challenge expiration (ms)</label>
|
||||
<input type="number" id="expiresMs" placeholder="600000" value="600000" min="1" />
|
||||
</div>
|
||||
|
||||
<div class="actions actions-col">
|
||||
<button class="rotate-key action-secundary"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-rotate-cw-icon lucide-rotate-cw"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/></svg> Rotate secret key</button>
|
||||
<button class="delete-key action-secundary"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-trash2-icon lucide-trash-2"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/></svg> Delete key</button>
|
||||
<button class="create-key btn">Save changes</button>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
document.body.appendChild(modal);
|
||||
|
||||
modal.querySelector(".delete-key").addEventListener("click", async () => {
|
||||
modal.remove();
|
||||
|
||||
const newModal = document.createElement("div");
|
||||
newModal.classList.add("dialog-wrapper");
|
||||
newModal.innerHTML = `
|
||||
<div class="dialog key-creation-dialog">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="close-button"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
||||
|
||||
<h2>Delete key?</h2>
|
||||
<p style="line-height:1.4;font-size:15px;">This is permanent. Once your key is deleted, you'll no longer be able to interact with it through the API</p>
|
||||
<div class="actions">
|
||||
<button class="confirm-delete btn">Delete</button>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
document.body.appendChild(newModal);
|
||||
|
||||
const confirmDeleteBtn = newModal.querySelector(".confirm-delete");
|
||||
|
||||
newModal.addEventListener("click", (e) => {
|
||||
if (e.target === newModal) {
|
||||
newModal.remove();
|
||||
}
|
||||
});
|
||||
|
||||
const closeButton = newModal.querySelector(".close-button");
|
||||
closeButton.addEventListener("click", () => {
|
||||
newModal.remove();
|
||||
});
|
||||
|
||||
confirmDeleteBtn.addEventListener("click", async () => {
|
||||
confirmDeleteBtn.disabled = true;
|
||||
await fetch("/internal/deleteKey", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
publicKey: key.publicKey,
|
||||
}),
|
||||
});
|
||||
await reloadKeysList();
|
||||
newModal.remove();
|
||||
});
|
||||
|
||||
confirmDeleteBtn.focus();
|
||||
});
|
||||
|
||||
modal.querySelector(".rotate-key").addEventListener("click", async () => {
|
||||
modal.remove();
|
||||
|
||||
const newModal = document.createElement("div");
|
||||
newModal.classList.add("dialog-wrapper");
|
||||
newModal.innerHTML = `
|
||||
<div class="dialog key-creation-dialog">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="close-button"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
||||
|
||||
<h2>Rotate secret key</h2>
|
||||
<p style="line-height:1.4;font-size:15px;">This will rotate your secret key while keeping your key ID, invalidating the old one.</p>
|
||||
<div class="actions">
|
||||
<button class="confirm-rotate btn">Rotate</button>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
document.body.appendChild(newModal);
|
||||
|
||||
const confirmRotateBtn = newModal.querySelector(".confirm-rotate");
|
||||
|
||||
newModal.addEventListener("click", (e) => {
|
||||
if (e.target === newModal) {
|
||||
newModal.remove();
|
||||
}
|
||||
});
|
||||
|
||||
const closeButton = newModal.querySelector(".close-button");
|
||||
closeButton.addEventListener("click", () => {
|
||||
newModal.remove();
|
||||
});
|
||||
|
||||
confirmRotateBtn.addEventListener("click", async () => {
|
||||
confirmRotateBtn.disabled = true;
|
||||
|
||||
const { privateKey } = await (await fetch("/internal/rotateKey", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
publicKey: key.publicKey,
|
||||
}),
|
||||
})).json();
|
||||
await reloadKeysList();
|
||||
newModal.remove();
|
||||
|
||||
const successModal = document.createElement("div");
|
||||
successModal.classList.add("dialog-wrapper");
|
||||
successModal.innerHTML = `
|
||||
<div class="dialog key-creation-dialog">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="close-button"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
||||
<h2>Secret key rotated</h2>
|
||||
|
||||
<p>Make sure to copy your new secret key — it's required to validate tokens and you won't be able to see it again.</p>
|
||||
|
||||
<div class="key-creation-form">
|
||||
<label for="public-key">Key ID</label>
|
||||
<input type="text" id="public-key" value="${key.publicKey}" readonly style="opacity: 1;user-select: all;"/>
|
||||
</div>
|
||||
|
||||
<div class="key-creation-form">
|
||||
<label for="private-key">Secret key</label>
|
||||
<input type="text" id="private-key" value="${privateKey}" readonly style="opacity: 1;user-select: all;"/>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
document.body.appendChild(successModal);
|
||||
|
||||
const newCloseButton = successModal.querySelector(".close-button");
|
||||
newCloseButton.addEventListener("click", () => {
|
||||
successModal.remove();
|
||||
});
|
||||
});
|
||||
|
||||
confirmRotateBtn.focus();
|
||||
});
|
||||
|
||||
const inputs = [
|
||||
"keyName",
|
||||
"challengesCount",
|
||||
"challengeSize",
|
||||
"challengeDifficulty",
|
||||
"expiresMs",
|
||||
];
|
||||
|
||||
const createKeyBtn = modal.querySelector(".create-key");
|
||||
|
||||
modal.addEventListener("click", (e) => {
|
||||
if (e.target === modal) {
|
||||
modal.remove();
|
||||
}
|
||||
});
|
||||
|
||||
const closeButton = modal.querySelector(".close-button");
|
||||
closeButton.addEventListener("click", () => {
|
||||
modal.remove();
|
||||
});
|
||||
|
||||
modal.querySelector("#keyName").focus();
|
||||
|
||||
inputs.forEach((input) => {
|
||||
modal.querySelector(`#${input}`).value =
|
||||
key[input === "keyName" ? "name" : input];
|
||||
input = modal.querySelector(`#${input}`);
|
||||
input.addEventListener("input", () => {
|
||||
createKeyBtn.disabled = !inputs.every((i) => {
|
||||
const inputEl = modal.querySelector(`#${i}`);
|
||||
return inputEl.value.trim() !== "";
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
createKeyBtn.addEventListener("click", async () => {
|
||||
createKeyBtn.disabled = true;
|
||||
let props = {};
|
||||
|
||||
inputs.forEach((i) => {
|
||||
i.disabled = true;
|
||||
props[i] = modal.querySelector(`#${i}`).value;
|
||||
});
|
||||
createKeyBtn.innerText = "Saving changes...";
|
||||
const { message } = await (
|
||||
await fetch("/internal/editKey", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
publicKey: key.publicKey,
|
||||
...props,
|
||||
}),
|
||||
})
|
||||
).json();
|
||||
|
||||
createKeyBtn.disabled = false;
|
||||
inputs.forEach((i) => {
|
||||
i.disabled = false;
|
||||
});
|
||||
createKeyBtn.innerText = "Save changes";
|
||||
|
||||
if (message) return alert(message);
|
||||
|
||||
modal.remove();
|
||||
reloadKeysList();
|
||||
});
|
||||
});
|
||||
|
||||
keysEl.appendChild(keyEl);
|
||||
});
|
||||
};
|
||||
|
||||
reloadKeysList();
|
||||
@@ -1,237 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Login to Cap</title>
|
||||
<link rel="icon" href="./public/logo.png" />
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen-Sans, Ubuntu,
|
||||
Cantarell, Helvetica Neue, sans-serif;
|
||||
}
|
||||
body {
|
||||
text-align: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
min-height: 100vh;
|
||||
background-color: #121214;
|
||||
color: #e8e8ea;
|
||||
}
|
||||
form {
|
||||
max-width: 300px;
|
||||
margin: 0px auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
img {
|
||||
width: 60px;
|
||||
}
|
||||
label {
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
display: block;
|
||||
margin-bottom: 3px;
|
||||
color: #d9dfe5eb;
|
||||
}
|
||||
|
||||
.password-wrapper {
|
||||
border: 1px solid #e3e4e810;
|
||||
background-color: #e3e4e807;
|
||||
display: flex;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.showhide-password {
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: white;
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.password {
|
||||
font-size: 24px;
|
||||
line-height: 1.33333333;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
height: 40px;
|
||||
color: inherit;
|
||||
font-weight: 400;
|
||||
letter-spacing: -2px;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.password.show-password {
|
||||
letter-spacing: 0px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.password:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.password-wrapper:has(.password:focus) {
|
||||
box-shadow: 0px 0px 0px 2px rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.submit {
|
||||
padding: 6px 10px;
|
||||
border-radius: 6px;
|
||||
height: 40px;
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
color: black;
|
||||
background: white;
|
||||
background-origin: border-box;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
touch-action: manipulation;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.submit:focus {
|
||||
box-shadow: inset 0px 0.8px 0px -0.25px rgba(255, 255, 255, 0.2),
|
||||
0px 0.5px 1.5px rgba(54, 122, 246, 0.25), 0px 0px 0px 3.5px rgba(255, 255, 255, 0.5);
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.submit:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
p.credits {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
p.err {
|
||||
color: #ffffff;
|
||||
display: none;
|
||||
margin: 0px;
|
||||
margin-top: 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: rgb(255, 255, 255);
|
||||
opacity: 0.4;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.logo img {
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<form action="/internal/auth" method="POST">
|
||||
<div class="logo">
|
||||
<img src="./public/logo.png" alt="Cap logo" />
|
||||
</div>
|
||||
<label for="key">Admin key</label>
|
||||
<div class="password-wrapper">
|
||||
<input type="password" name="password" placeholder="••••••••••••••" class="password" />
|
||||
<button title="Show/hide password" class="showhide-password" type="button">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="lucide lucide-eye-icon lucide-eye"
|
||||
>
|
||||
<path
|
||||
d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"
|
||||
/>
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="submit">Continue</button>
|
||||
<p class="err"></p>
|
||||
<p class="credits">
|
||||
<a href="https://capjs.js.org/" target="_blank">Powered by Cap</a>
|
||||
</p>
|
||||
</form>
|
||||
</body>
|
||||
<script>
|
||||
document.querySelector("input").focus();
|
||||
|
||||
document.querySelector("form").addEventListener("submit", async function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
const password = document.querySelector("input[name=password]").value;
|
||||
|
||||
document.querySelector("button[type=submit]").disabled = true;
|
||||
|
||||
const { success } = await (
|
||||
await fetch("/internal/auth", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
password,
|
||||
}),
|
||||
})
|
||||
).json();
|
||||
|
||||
if (success) {
|
||||
document.querySelector("p.err").style.display = "none";
|
||||
window.location.href = "/";
|
||||
return;
|
||||
}
|
||||
|
||||
document.querySelector("button[type=submit]").disabled = false;
|
||||
document.querySelector("p.err").style.display = "block";
|
||||
document.querySelector("p.err").innerText = "Incorrect admin key. Please try again.";
|
||||
document.querySelector("input[name=password]").value = "";
|
||||
});
|
||||
|
||||
document.querySelector(".showhide-password").addEventListener("click", () => {
|
||||
const passwordInput = document.querySelector("input[name=password]");
|
||||
const showhideButton = document.querySelector(".showhide-password");
|
||||
passwordInput.classList.toggle("show-password");
|
||||
|
||||
if (passwordInput.type === "password") {
|
||||
passwordInput.type = "text";
|
||||
passwordInput.placeholder = "";
|
||||
showhideButton.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-eye-off-icon lucide-eye-off"><path d="M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49"/><path d="M14.084 14.158a3 3 0 0 1-4.242-4.242"/><path d="M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143"/><path d="m2 2 20 20"/></svg>`;
|
||||
} else {
|
||||
passwordInput.type = "password";
|
||||
passwordInput.placeholder = "••••••••••••••";
|
||||
showhideButton.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-eye-icon lucide-eye"><path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"/><circle cx="12" cy="12" r="3"/></svg>`;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</html>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 32 KiB |
@@ -1,258 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Challenge tester</title>
|
||||
<style>
|
||||
* {
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
|
||||
Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
|
||||
}
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
}
|
||||
#inputArea {
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
width: 90%;
|
||||
max-width: 400px;
|
||||
}
|
||||
#inputArea label {
|
||||
font-weight: 500;
|
||||
}
|
||||
#inputArea input {
|
||||
padding: 8px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #eee;
|
||||
}
|
||||
#inputArea button {
|
||||
padding: 10px 15px;
|
||||
background-color: black;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 1em;
|
||||
}
|
||||
#inputArea button:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
pre {
|
||||
font-family: monospace;
|
||||
font-size: 14px;
|
||||
max-width: 80vw;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
width: 600px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid #eee;
|
||||
background-color: #fdfdfd;
|
||||
padding: 10px;
|
||||
}
|
||||
#chartContainer {
|
||||
width: 90%;
|
||||
max-width: 700px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
h2 {
|
||||
margin-top: 30px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
cap-widget {
|
||||
position: fixed;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
display: none;
|
||||
z-index: 1000;
|
||||
}
|
||||
</style>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="inputArea">
|
||||
<label for="keyIdInput">Key ID:</label>
|
||||
<input type="text" id="keyIdInput" placeholder="e.g. 363908a53261" required />
|
||||
|
||||
<label for="secretKeyInput">Secret key:</label>
|
||||
<input
|
||||
type="password"
|
||||
id="secretKeyInput"
|
||||
placeholder="e.g. 5dcddeaba58972b52c7afe750..."
|
||||
required
|
||||
/>
|
||||
|
||||
<button id="startButton">Start</button>
|
||||
</div>
|
||||
|
||||
<pre class="logs">No logs</pre>
|
||||
|
||||
<div id="chartContainer">
|
||||
<canvas id="progressChart"></canvas>
|
||||
</div>
|
||||
|
||||
<script src="/assets/widget.js"></script>
|
||||
|
||||
<script>
|
||||
const logsElement = document.querySelector(".logs");
|
||||
const keyIdInput = document.getElementById("keyIdInput");
|
||||
const secretKeyInput = document.getElementById("secretKeyInput");
|
||||
const startButton = document.getElementById("startButton");
|
||||
const ctx = document.querySelector("#progressChart").getContext("2d");
|
||||
|
||||
let progressTimings = {};
|
||||
let chartInstance = null;
|
||||
let cap = null;
|
||||
let startTime = 0;
|
||||
|
||||
const log = (msg) => {
|
||||
logsElement.innerText += `${msg}\n`;
|
||||
logsElement.scrollTop = logsElement.scrollHeight;
|
||||
};
|
||||
|
||||
const clearLogs = () => {
|
||||
logsElement.innerText = "";
|
||||
};
|
||||
|
||||
function renderProgressChart() {
|
||||
const sortedProgressSteps = Object.keys(progressTimings)
|
||||
.map(Number)
|
||||
.sort((a, b) => a - b);
|
||||
|
||||
const labels = sortedProgressSteps.map((p) => `${p}%`);
|
||||
const data = sortedProgressSteps.map((p) => progressTimings[p]);
|
||||
|
||||
if (chartInstance) {
|
||||
chartInstance.destroy();
|
||||
}
|
||||
|
||||
chartInstance = new Chart(ctx, {
|
||||
type: "line",
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [
|
||||
{
|
||||
label: "Time to reach step (ms)",
|
||||
data: data,
|
||||
borderColor: "rgb(75, 192, 192)",
|
||||
backgroundColor: "rgba(75, 192, 192, 0.2)",
|
||||
fill: true,
|
||||
tension: 0.1,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: true,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
title: { display: true, text: "Time elapsed (ms)" },
|
||||
},
|
||||
x: {
|
||||
title: { display: true, text: "Challenge progress (%)" },
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
legend: { display: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function validateToken(keyId, secretKey, token) {
|
||||
log(`Solved successfully, validating token...`);
|
||||
|
||||
const { success } = await (
|
||||
await fetch(`/${keyId}/siteverify`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
secret: secretKey,
|
||||
response: token,
|
||||
}),
|
||||
})
|
||||
).json();
|
||||
log(`Token: ${token}`);
|
||||
log(`Token validation response: ${success}`);
|
||||
}
|
||||
|
||||
startButton.addEventListener("click", () => {
|
||||
const KEY_ID = keyIdInput.value.trim();
|
||||
const SECRET_KEY = secretKeyInput.value.trim();
|
||||
|
||||
if (!SECRET_KEY || !KEY_ID) {
|
||||
clearLogs();
|
||||
log("Missing secret key or key ID.");
|
||||
return;
|
||||
}
|
||||
|
||||
clearLogs();
|
||||
progressTimings = {};
|
||||
startTime = Date.now();
|
||||
progressTimings[0] = 0;
|
||||
startButton.disabled = true;
|
||||
log(
|
||||
`Requesting challenge for key ${KEY_ID}...\nSecret key: ${SECRET_KEY.substring(
|
||||
0,
|
||||
5
|
||||
)}...\n\n${"*".repeat(60)}\n\n`
|
||||
);
|
||||
|
||||
if (cap && cap.widget && cap.widget.parentNode) {
|
||||
cap.widget.parentNode.removeChild(cap.widget);
|
||||
}
|
||||
cap = new Cap({
|
||||
apiEndpoint: `/${KEY_ID}/api/`,
|
||||
});
|
||||
|
||||
cap.addEventListener("progress", (e) => {
|
||||
const currentTime = Date.now();
|
||||
const elapsed = currentTime - startTime;
|
||||
const progress = e.detail.progress;
|
||||
log(`Solving... ${progress}% [${elapsed}ms]`);
|
||||
if (progressTimings[progress] === undefined) {
|
||||
progressTimings[progress] = elapsed;
|
||||
}
|
||||
});
|
||||
|
||||
cap.addEventListener("error", (e) => {
|
||||
log(`Error: ${e.detail.error}`);
|
||||
startButton.disabled = false;
|
||||
if (cap.widget) {
|
||||
cap.widget.style.display = "none";
|
||||
}
|
||||
});
|
||||
|
||||
cap.widget.style.display = "block";
|
||||
|
||||
cap
|
||||
.solve()
|
||||
.then(async (token) => {
|
||||
const solveEndTime = Date.now();
|
||||
const totalSolveTime = solveEndTime - startTime;
|
||||
log(`Challenge solved in ${totalSolveTime}ms`);
|
||||
progressTimings[100] = totalSolveTime;
|
||||
|
||||
renderProgressChart();
|
||||
|
||||
await validateToken(KEY_ID, SECRET_KEY, token.token);
|
||||
})
|
||||
.catch((err) => {
|
||||
log(`Error during solve: ${err}`);
|
||||
})
|
||||
.finally(() => {
|
||||
startButton.disabled = false;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,451 +0,0 @@
|
||||
import { timingSafeEqual } from "node:crypto";
|
||||
import fs from "node:fs/promises";
|
||||
import Cap from "@cap.js/server";
|
||||
import { cors } from "@elysiajs/cors";
|
||||
import { staticPlugin } from "@elysiajs/static";
|
||||
import crypto from "crypto";
|
||||
import { Elysia, file, NotFoundError } from "elysia";
|
||||
import { rateLimit } from "elysia-rate-limit";
|
||||
import path from "path";
|
||||
|
||||
const ADMIN_KEY = process.env.ADMIN_KEY?.trim();
|
||||
const dataDir = "./.data";
|
||||
const keysStorePath = path.join(dataDir, "keys.json");
|
||||
|
||||
function generateAdminKeyHint() {
|
||||
return `\nWe've generated this one for you to use: \n\n${crypto
|
||||
.randomBytes(30)
|
||||
.toString("hex")}\n\nThen, restart this process.\n\n`;
|
||||
}
|
||||
|
||||
if (!ADMIN_KEY) {
|
||||
console.error(
|
||||
`\nNo admin key has been set. Make sure to set it using the \n\`ADMIN_KEY\` environment variable.${generateAdminKeyHint()}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (ADMIN_KEY === "your_secret_key") {
|
||||
console.error(
|
||||
`\nDon't leave the admin key as default! Make sure to set it \nusing the \`ADMIN_KEY\` environment variable.${generateAdminKeyHint()}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (ADMIN_KEY.length < 20) {
|
||||
console.warn(
|
||||
`\n${"*".repeat(
|
||||
60,
|
||||
)}\n\nThe admin key you're using is quite short. We recommend \nusing a longer one.${generateAdminKeyHint()}${"*".repeat(
|
||||
60,
|
||||
)}\n`,
|
||||
);
|
||||
}
|
||||
|
||||
let keys = [];
|
||||
const capInstances = new Map();
|
||||
|
||||
const updateCache = async () => {
|
||||
let cacheConfig = {};
|
||||
|
||||
try {
|
||||
cacheConfig = JSON.parse(
|
||||
await fs.readFile(path.join(dataDir, "assets-cache.json"), "utf-8"),
|
||||
);
|
||||
} catch {}
|
||||
|
||||
const lastUpdate = cacheConfig["lastUpdate"] || 0;
|
||||
const currentTime = Date.now();
|
||||
const updateInterval = 1000 * 60 * 60 * 24; // 1 day
|
||||
|
||||
if (!(currentTime - lastUpdate > updateInterval)) return;
|
||||
|
||||
const CACHE_HOST = process.env.CACHE_HOST || "https://cdn.jsdelivr.net";
|
||||
if (CACHE_HOST === "disable") return;
|
||||
|
||||
const WIDGET_VERSION = process.env.WIDGET_VERSION || "latest";
|
||||
const WASM_VERSION = process.env.WASM_VERSION || "latest";
|
||||
|
||||
try {
|
||||
const [widgetSource, floatingSource, wasmSource, wasmLoaderSource] =
|
||||
await Promise.all([
|
||||
fetch(`${CACHE_HOST}/npm/@cap.js/widget@${WIDGET_VERSION}`).then((r) =>
|
||||
r.text(),
|
||||
),
|
||||
fetch(
|
||||
`${CACHE_HOST}/npm/@cap.js/widget@${WIDGET_VERSION}/cap-floating.min.js`,
|
||||
).then((r) => r.text()),
|
||||
fetch(
|
||||
`${CACHE_HOST}/npm/@cap.js/wasm@${WASM_VERSION}/browser/cap_wasm_bg.wasm`,
|
||||
).then((r) => r.arrayBuffer()),
|
||||
fetch(
|
||||
`${CACHE_HOST}/npm/@cap.js/wasm@${WASM_VERSION}/browser/cap_wasm.min.js`,
|
||||
).then((r) => r.text()),
|
||||
]);
|
||||
|
||||
cacheConfig["lastUpdate"] = currentTime;
|
||||
await fs.writeFile(
|
||||
path.join(dataDir, "assets-cache.json"),
|
||||
JSON.stringify(cacheConfig),
|
||||
);
|
||||
|
||||
await fs.writeFile(path.join(dataDir, "assets-widget.js"), widgetSource);
|
||||
await fs.writeFile(
|
||||
path.join(dataDir, "assets-floating.js"),
|
||||
floatingSource,
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(dataDir, "assets-cap_wasm_bg.wasm"),
|
||||
Buffer.from(wasmSource),
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(dataDir, "assets-cap_wasm.js"),
|
||||
wasmLoaderSource,
|
||||
);
|
||||
|
||||
console.log("[asset server] updated assets cache");
|
||||
} catch (e) {
|
||||
console.error(
|
||||
"[asset server] error updating assets cache, trying to load them might fail:",
|
||||
e,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const init = async () => {
|
||||
try {
|
||||
await fs.mkdir(dataDir, { recursive: true });
|
||||
|
||||
const data = await fs.readFile(keysStorePath, "utf-8");
|
||||
keys = JSON.parse(data);
|
||||
capInstances.clear();
|
||||
|
||||
keys.forEach((key) => {
|
||||
capInstances.set(
|
||||
key.publicKey,
|
||||
new Cap({
|
||||
tokens_store_path: path.join(dataDir, `tokens-${key.publicKey}.json`),
|
||||
}),
|
||||
);
|
||||
});
|
||||
} catch {
|
||||
await fs.writeFile(keysStorePath, "[]");
|
||||
keys = [];
|
||||
capInstances.clear();
|
||||
}
|
||||
|
||||
await updateCache();
|
||||
};
|
||||
|
||||
const saveKeys = async () => {
|
||||
await fs.writeFile(keysStorePath, JSON.stringify(keys));
|
||||
};
|
||||
|
||||
const getCapInstance = (publicKey) => {
|
||||
if (!capInstances.has(publicKey) || !capInstances.get(publicKey)) {
|
||||
const keyData = keys.find((k) => k.publicKey === publicKey);
|
||||
if (keyData) {
|
||||
capInstances.set(
|
||||
publicKey,
|
||||
new Cap({
|
||||
tokens_store_path: path.join(dataDir, `tokens-${publicKey}.json`),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
return capInstances.get(publicKey);
|
||||
};
|
||||
|
||||
const auth = new Elysia({
|
||||
prefix: "/internal/auth",
|
||||
})
|
||||
.use(
|
||||
rateLimit({
|
||||
scoping: "scoped",
|
||||
count: 10,
|
||||
duration: 15000,
|
||||
}),
|
||||
)
|
||||
.post("/", async ({ body, cookie, set }) => {
|
||||
try {
|
||||
if (
|
||||
!body?.password ||
|
||||
!timingSafeEqual(Buffer.from(body.password), Buffer.from(ADMIN_KEY))
|
||||
) {
|
||||
set.status = 401;
|
||||
return { success: false };
|
||||
}
|
||||
} catch {
|
||||
set.status = 500;
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
cookie["cap-admin-key"].set({
|
||||
value: await Bun.password.hash(body.password),
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: "lax",
|
||||
path: "/",
|
||||
maxAge: 86400 * 7,
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
})
|
||||
.get("/logout", ({ cookie, redirect }) => {
|
||||
cookie["cap-admin-key"].remove();
|
||||
return redirect("/");
|
||||
});
|
||||
|
||||
const internal = new Elysia({ prefix: "/internal" })
|
||||
.onBeforeHandle(async ({ cookie, set }) => {
|
||||
const authCookie = cookie["cap-admin-key"]?.value;
|
||||
if (!authCookie || !(await Bun.password.verify(ADMIN_KEY, authCookie))) {
|
||||
set.status = 401;
|
||||
return {
|
||||
success: false,
|
||||
message: "Unauthorized",
|
||||
};
|
||||
}
|
||||
})
|
||||
.use(
|
||||
rateLimit({
|
||||
scoping: "scoped",
|
||||
number: 60,
|
||||
duration: 10000,
|
||||
}),
|
||||
)
|
||||
.post(
|
||||
"/createKey",
|
||||
async ({
|
||||
body: {
|
||||
keyName,
|
||||
challengesCount = 18,
|
||||
challengeSize = 32,
|
||||
challengeDifficulty = 4,
|
||||
expiresMs = 600000,
|
||||
},
|
||||
set,
|
||||
}) => {
|
||||
if (!keyName?.trim()) {
|
||||
set.status = 400;
|
||||
return { success: false, message: "Key name is required" };
|
||||
}
|
||||
|
||||
const publicKey = crypto.randomBytes(6).toString("hex");
|
||||
const privateKey = crypto.randomBytes(25).toString("hex");
|
||||
const privateKeyHash = await Bun.password.hash(privateKey);
|
||||
|
||||
const newKey = {
|
||||
name: keyName,
|
||||
publicKey,
|
||||
privateKey: privateKeyHash,
|
||||
challengesCount: Number(challengesCount),
|
||||
challengeSize: Number(challengeSize),
|
||||
challengeDifficulty: Number(challengeDifficulty),
|
||||
expiresMs: Number(expiresMs),
|
||||
};
|
||||
|
||||
keys.push(newKey);
|
||||
await saveKeys();
|
||||
|
||||
getCapInstance(publicKey);
|
||||
|
||||
set.status = 201;
|
||||
return {
|
||||
success: true,
|
||||
publicKey,
|
||||
privateKey,
|
||||
};
|
||||
},
|
||||
)
|
||||
.post(
|
||||
"/editKey",
|
||||
async ({
|
||||
body: {
|
||||
publicKey,
|
||||
keyName,
|
||||
challengesCount,
|
||||
challengeSize,
|
||||
challengeDifficulty,
|
||||
expiresMs,
|
||||
},
|
||||
set,
|
||||
}) => {
|
||||
const keyIndex = keys.findIndex((key) => key.publicKey === publicKey);
|
||||
|
||||
if (keyIndex === -1) {
|
||||
set.status = 404;
|
||||
return { success: false, message: "Key not found" };
|
||||
}
|
||||
|
||||
if (!keyName.trim()) {
|
||||
set.status = 400;
|
||||
return { success: false, message: "Key name is required" };
|
||||
}
|
||||
|
||||
const existingKey = keys[keyIndex];
|
||||
keys[keyIndex] = {
|
||||
...existingKey,
|
||||
name: keyName,
|
||||
challengesCount: Number(challengesCount),
|
||||
challengeSize: Number(challengeSize),
|
||||
challengeDifficulty: Number(challengeDifficulty),
|
||||
expiresMs: Number(expiresMs),
|
||||
};
|
||||
|
||||
await saveKeys();
|
||||
|
||||
return { success: true };
|
||||
},
|
||||
)
|
||||
.get("/listKeys", async () => {
|
||||
return {
|
||||
keys: keys.map(({ privateKey, ...rest }) => rest),
|
||||
};
|
||||
})
|
||||
.post("/deleteKey", async ({ body: { publicKey }, set }) => {
|
||||
const initialLength = keys.length;
|
||||
keys = keys.filter((key) => key.publicKey !== publicKey);
|
||||
|
||||
if (keys.length === initialLength) {
|
||||
set.status = 404;
|
||||
return { success: false, message: "Key not found" };
|
||||
}
|
||||
|
||||
await saveKeys();
|
||||
|
||||
const tokenFilePath = path.join(dataDir, `tokens-${publicKey}.json`);
|
||||
try {
|
||||
await fs.unlink(tokenFilePath);
|
||||
} catch (error) {
|
||||
if (error.code !== "ENOENT") {
|
||||
console.warn(
|
||||
`Could not delete token file ${tokenFilePath}:`,
|
||||
error.message,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
capInstances.delete(publicKey);
|
||||
|
||||
set.status = 200;
|
||||
return { success: true };
|
||||
})
|
||||
.post("/rotateKey", async ({ body: { publicKey }, set }) => {
|
||||
const keyIndex = keys.findIndex((key) => key.publicKey === publicKey);
|
||||
|
||||
if (keyIndex === -1) {
|
||||
set.status = 404;
|
||||
return { success: false, message: "Key not found" };
|
||||
}
|
||||
|
||||
const newPrivateKey = crypto.randomBytes(25).toString("hex");
|
||||
const newPrivateKeyHash = await Bun.password.hash(newPrivateKey);
|
||||
|
||||
keys[keyIndex].privateKey = newPrivateKeyHash;
|
||||
|
||||
await saveKeys();
|
||||
|
||||
return { success: true, privateKey: newPrivateKey };
|
||||
});
|
||||
|
||||
const api = new Elysia({ prefix: "/:key" })
|
||||
.use(
|
||||
cors({
|
||||
origin: process.env.CORS_ORIGIN?.split(",") || true,
|
||||
}),
|
||||
)
|
||||
.use(
|
||||
rateLimit({
|
||||
scoping: "scoped",
|
||||
number: 80,
|
||||
duration: 1000,
|
||||
}),
|
||||
)
|
||||
.derive(({ params }) => {
|
||||
const keyData = keys.find((k) => k.publicKey === params.key);
|
||||
if (!keyData) {
|
||||
throw new NotFoundError("Key not found");
|
||||
}
|
||||
return { keyData, capInstance: getCapInstance(params.key) };
|
||||
})
|
||||
.post("/api/challenge", async ({ keyData, capInstance }) => {
|
||||
return await capInstance.createChallenge({
|
||||
challengeSize: keyData.challengeSize,
|
||||
challengeDifficulty: keyData.challengeDifficulty,
|
||||
challengeCount: keyData.challengesCount,
|
||||
expiresMs: keyData.expiresMs,
|
||||
});
|
||||
})
|
||||
.post("/api/redeem", async ({ body, set, capInstance }) => {
|
||||
const { token, solutions } = body;
|
||||
|
||||
if (!token || !solutions) {
|
||||
set.status = 400;
|
||||
return { success: false, message: "Missing solutions and/or token" };
|
||||
}
|
||||
|
||||
return await capInstance.redeemChallenge({ token, solutions });
|
||||
})
|
||||
.post("/siteverify", async ({ body, set, keyData, capInstance }) => {
|
||||
const { secret, response } = body;
|
||||
|
||||
if (!secret || !response) {
|
||||
set.status = 400;
|
||||
return { success: false, message: "Missing secret or/and response" };
|
||||
}
|
||||
|
||||
if (!(await Bun.password.verify(secret, keyData.privateKey))) {
|
||||
set.status = 400;
|
||||
return { success: false, message: "Invalid secret" };
|
||||
}
|
||||
|
||||
return await capInstance.validateToken(response, {
|
||||
keepToken: false,
|
||||
});
|
||||
});
|
||||
|
||||
const assetsServer = new Elysia({ prefix: "/assets" })
|
||||
.use(
|
||||
cors({
|
||||
origin: process.env.CORS_ORIGIN?.split(",") || true,
|
||||
methods: ["GET"],
|
||||
}),
|
||||
)
|
||||
.onBeforeHandle(({ set }) => {
|
||||
set.headers["Cache-Control"] = "max-age=31536000, immutable";
|
||||
})
|
||||
.get("/widget.js", ({ set }) => {
|
||||
set.headers["Content-Type"] = "text/javascript";
|
||||
return file(path.join(dataDir, "assets-widget.js"));
|
||||
})
|
||||
.get("/floating.js", ({ set }) => {
|
||||
set.headers["Content-Type"] = "text/javascript";
|
||||
return file(path.join(dataDir, "assets-floating.js"));
|
||||
})
|
||||
.get("/cap_wasm_bg.wasm", ({ set }) => {
|
||||
set.headers["Content-Type"] = "application/wasm";
|
||||
return file(path.join(dataDir, "assets-cap_wasm_bg.wasm"));
|
||||
})
|
||||
.get("/cap_wasm.js", ({ set }) => {
|
||||
set.headers["Content-Type"] = "text/javascript";
|
||||
return file(path.join(dataDir, "assets-cap_wasm.js"));
|
||||
});
|
||||
|
||||
new Elysia()
|
||||
.use(staticPlugin())
|
||||
.use(auth)
|
||||
.use(internal)
|
||||
.use(api)
|
||||
.use(assetsServer)
|
||||
.get("/", async ({ cookie }) => {
|
||||
const authCookie = cookie["cap-admin-key"]?.value;
|
||||
const isAuthed =
|
||||
authCookie && (await Bun.password.verify(ADMIN_KEY, authCookie));
|
||||
|
||||
return file(isAuthed ? "./public/index.html" : "./public/lock.html");
|
||||
})
|
||||
.listen(3000);
|
||||
|
||||
console.log(`Cap standalone running on http://localhost:3000`);
|
||||
init();
|
||||
@@ -1,55 +0,0 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "@cap.js/middleware-elysia",
|
||||
"dependencies": {
|
||||
"@cap.js/server": "^1.0.15",
|
||||
"elysia": "^1.3.3",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@cap.js/server": ["@cap.js/server@1.0.15", "", { "dependencies": { "@types/node": "^22.14.1", "typescript": "^5.8.3" } }, "sha512-mcDbHAL4ar5O4gnDE+62ZPeXhLXxd1KKHzJCsZfn81HY8Fgo5ousxYCnzhsLfki5sH6sO6jpNtS4MZXlKDq+fA=="],
|
||||
|
||||
"@sinclair/typebox": ["@sinclair/typebox@0.34.33", "", {}, "sha512-5HAV9exOMcXRUxo+9iYB5n09XxzCXnfy4VTNW4xnDv+FgjzAGY989C28BIdljKqmF+ZltUwujE3aossvcVtq6g=="],
|
||||
|
||||
"@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.24", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-w9CZGm9RDjzTh/D+hFwlBJ3ziUaVw7oufKA3vOFSOZlzmW9AkZnfjPb+DLnrV6qtgL/LNmP0/2zBNCFHL3F0ng=="],
|
||||
|
||||
"cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="],
|
||||
|
||||
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
|
||||
|
||||
"elysia": ["elysia@1.3.3", "", { "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-x6a89d4h0xX9TB0CSGkUuKNqIK776HBJw0WHtK1B3ViQqZijsLnOor6be/7TwVl8MG6m59NHGHbmbBS6lnCXSw=="],
|
||||
|
||||
"exact-mirror": ["exact-mirror@0.1.2", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-wFCPCDLmHbKGUb8TOi/IS7jLsgR8WVDGtDK3CzcB4Guf/weq7G+I+DkXiRSZfbemBFOxOINKpraM6ml78vo8Zw=="],
|
||||
|
||||
"fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="],
|
||||
|
||||
"fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="],
|
||||
|
||||
"peek-readable": ["peek-readable@7.0.0", "", {}, "sha512-nri2TO5JE3/mRryik9LlHFT53cgHfRK0Lt0BAZQXku/AW3E6XLt2GaY8siWi7dvW/m1z0ecn+J+bpDa9ZN3IsQ=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
|
||||
|
||||
"uint8array-extras": ["uint8array-extras@1.4.0", "", {}, "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ=="],
|
||||
|
||||
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
}
|
||||
}
|
||||
Vendored
-59
@@ -1,59 +0,0 @@
|
||||
import Cap from "@cap.js/server";
|
||||
import type { Elysia } from "elysia";
|
||||
|
||||
export interface CapMiddlewareOptions {
|
||||
/**
|
||||
* Duration in hours for which the authentication token remains valid
|
||||
* @default 32
|
||||
*/
|
||||
token_validity_hours?: number;
|
||||
|
||||
/**
|
||||
* File path where authentication tokens are stored
|
||||
* @default ".data/middlewareTokens.json"
|
||||
*/
|
||||
tokens_store_path?: string;
|
||||
|
||||
/**
|
||||
* Size of the generated token in bytes
|
||||
* @default 16
|
||||
*/
|
||||
token_size?: number;
|
||||
|
||||
/**
|
||||
* Path to the HTML template used for verification
|
||||
* @default "./index.html" (relative to middleware file)
|
||||
*/
|
||||
verification_template_path?: string;
|
||||
|
||||
/**
|
||||
* Determines how the middleware is applied
|
||||
* @default "global"
|
||||
*/
|
||||
scoping?: "global" | "scoped";
|
||||
}
|
||||
|
||||
export interface TokenValidationResult {
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export interface ChallengeRedeemBody {
|
||||
token: string;
|
||||
solutions: unknown[];
|
||||
}
|
||||
|
||||
export interface ChallengeRedeemResponse {
|
||||
success: boolean;
|
||||
token?: string;
|
||||
expires?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cap middleware for Elysia that provides challenge-based bot protection
|
||||
*
|
||||
* @param userOptions Configuration options for the middleware
|
||||
* @returns An Elysia plugin instance configured with Cap protection
|
||||
*/
|
||||
export declare function capMiddleware(
|
||||
userOptions?: CapMiddlewareOptions
|
||||
): Elysia;
|
||||
@@ -1,200 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Verifying you are a human…</title>
|
||||
<meta name="title" content="Verifying you are a human…" />
|
||||
<meta
|
||||
name="description"
|
||||
content="A preview of this link is unavailable while a browser check is in progress."
|
||||
/>
|
||||
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:title" content="Verifying you are a human…" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="A preview of this link is unavailable while a browser check is in progress."
|
||||
/>
|
||||
|
||||
<meta property="twitter:title" content="Verifying you are a human…" />
|
||||
<meta
|
||||
property="twitter:description"
|
||||
content="A preview of this link is unavailable while a browser check is in progress."
|
||||
/>
|
||||
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
max-width: 690px;
|
||||
margin: 3em auto;
|
||||
padding: 18px;
|
||||
font-family: system-ui;
|
||||
}
|
||||
h1 {
|
||||
font-weight: 600;
|
||||
font-size: 26px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
h2 {
|
||||
font-weight: 400;
|
||||
font-size: 20px;
|
||||
margin-top: 7px;
|
||||
margin-bottom: 1.5em;
|
||||
color: #171717;
|
||||
}
|
||||
cap-widget {
|
||||
margin-bottom: 4em;
|
||||
}
|
||||
hr {
|
||||
border: 0;
|
||||
border-top: 1px solid #dddddd8f;
|
||||
margin: 2em 0;
|
||||
}
|
||||
h3 {
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
margin-top: 1.5em;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
p {
|
||||
font-weight: 400;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
margin-top: 10px;
|
||||
color: #171717;
|
||||
}
|
||||
footer {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
footer .credit {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
color: #171717;
|
||||
}
|
||||
footer .credit:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
footer .credit img {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
}
|
||||
footer .date {
|
||||
font-size: 15px;
|
||||
color: #888;
|
||||
margin: 0px;
|
||||
margin-left: auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Checking your browser...</h1>
|
||||
<script>
|
||||
document.querySelector("h1").innerText = location.host;
|
||||
</script>
|
||||
<h2>Verifying you are a human before proceeding...</h2>
|
||||
|
||||
<cap-widget id="cap" data-cap-api-endpoint="/__cap_clearance/"></cap-widget>
|
||||
|
||||
<noscript>
|
||||
<style>
|
||||
.info {
|
||||
display: none;
|
||||
}
|
||||
a {
|
||||
color: #0a91e7;
|
||||
}
|
||||
h3 {
|
||||
line-height: 1.3;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
</style>
|
||||
<h3>
|
||||
JavaScript is disabled and we were unable to verify you. To access this
|
||||
page, please
|
||||
<a
|
||||
href="https://www.whatismybrowser.com/guides/how-to-enable-javascript/auto"
|
||||
target="_blank"
|
||||
rel="nofollow noopener"
|
||||
>
|
||||
enable JavaScript
|
||||
</a>
|
||||
</h3>
|
||||
<hr />
|
||||
</noscript>
|
||||
|
||||
<div class="info">
|
||||
<hr />
|
||||
|
||||
<h3>Why am I seeing this page?</h3>
|
||||
<p>
|
||||
To keep our site secure, we need to confirm you're a human and not a
|
||||
robot. This quick check helps stop spam and abuse.
|
||||
</p>
|
||||
|
||||
<h3>What should I do?</h3>
|
||||
<p>
|
||||
No action is required on your end. Once verified, you'll continue to
|
||||
your destination. If you're stuck, try refreshing the page or checking
|
||||
your connection.
|
||||
</p>
|
||||
|
||||
<hr />
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<a
|
||||
href="https://capjs.js.org/"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="credit"
|
||||
>
|
||||
<img
|
||||
src="data:image/webp;base64,UklGRnwEAABXRUJQVlA4WAoAAAAQAAAATwAATwAAQUxQSDcCAAABkAXZtmk7+73Ytm3btm3btm3btvNn206ebfsdzvCcvfe6f6lURcQEsH/Ui3Sb2quAA0k7zU2D9qFb9rwF8ufOms6Z3hgVv0d+dHF3+/b6ztn1oxsXSkUoa+svsB/vfnFS5dQ0Ss99FgfOIRd7ZpWv7DpviFQe9s8oV87ZnhCtXK8jU9P7JiQMGJ9GlrTTQiCnsj2rHNm2q5D2cDYZchyHzAcyist0BFIbK1OISrnGlAvx/UUNToTsLuXElHOF/MfSiEh1CAQTuotoGUMBD7PzS30OJPUR/OpG0sCzHNzWgqg+hFeON1RwJxOnxnFkkjpymga6p1NycT5BKLgql2zPCGEpl1JelN7m4tEggpLSjUfnBEo47sxhkEIqem/XAk52RmukAOX7gR75rY3TiQFQv+1qn9PCFIMegKQ3q+ql/9M00yEAiLoxprBjAcxvs7I6FsC8VJhNNki5HP9sWgLOZR6rE3JfVNyp4Jh7CZaShw5XyYRsLM1+z9z2SIAFnOmbbC86VoqEU7Wd2F9TlJ/zJP4vR9rH2wi7Obtus4MBwoxH3dMw61la7ficBMCvdd1wC7rftSnV0jPGnCsufqcKcZmSg3HM22bBtlUNnIq7/ynqzZ7+pdOwv+fucyGEm//akkxkxntAxOvDY2plZ3ZTV5n7MJaH75bKTPDMgLXVszkxvpkbLb4TqFmKvD+rnBMTnaGMMxOZvly3hcfuvnPxcHl7Y8+YmpmYQ3RKlz1foXzZ0rD/AwMAVlA4IB4CAABwDwCdASpQAFAAPok6lUelI6IhL1auwKARCWgAyJAhz7eq/ihjJ3OI5P5V3p3mJ/ar10vRB/298A5832YAmiLa0mutah4B6eOgBjpWeAPuDnTJNdrbbJGDv05E/kTVtFErBAMEp9Sdw2xY7Adta5Ru6IDt1ffejnz6tUPNbgJLZIFBZHAA/vz4VF/Qshf/4bK2z23dL/pV2OnhttId5Jomv/wz7rSmfFojRqvMm0mkOsKUbevn1KAeivy2swXV7qG8yi6ZCW/0Be54wB4Y5ZvU3j/AJTo2YSp/T5twW6LjpC9FfJJ99yn0GjUbICt2Rs1Fh+k7CQ5ze4+AHDqQ+1vxZ+itKOvFDB6CfDAqpTpqtdPaJlqFvq1bwvulRC44jnvPvhuyS04lhAJYP2iOQEh5lydE+sXpBfX+N3RCMY4KNY/8ubEOok/6+so2trq8Tc67BTprcrZ5sy0gYjn7XL5Jceobj+a8X+qaXc31i61vBeUpVO1eHth1iA9BXz3QBrRY0ahjBc3Fl9J8ouV6/svohQze+Xv4r47H18ryTUSTVDWnw6TuHgfYgtZQM98u1OqzLlB3Jz9AjlflcsnuakRtmeMGkkxiVuoPb3S36krL+oTNPGlyL6TcUTgeLl0ZZPRGdlmCbnR9JcEnKOqK7KwVYegLD5vlXrZwACMgve4dHJ8OEkf9ppwm6OGGGwG/1uHEJeN1zIOxgTCxqAAAAA=="
|
||||
aria-hidden="true"
|
||||
title="Cap logo"
|
||||
alt="Cap logo"
|
||||
/>
|
||||
<span>Cap</span>
|
||||
</a>
|
||||
<p class="date">{{TIME}}</p>
|
||||
</footer>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@cap.js/widget@0.1.17"></script>
|
||||
<script>
|
||||
window.CAP_CUSTOM_FETCH = function (url, options) {
|
||||
console.log(url);
|
||||
if (url.endsWith("/challenge")) {
|
||||
return {
|
||||
json: () => {
|
||||
return window.CAP_CHALLENGE;
|
||||
},
|
||||
};
|
||||
}
|
||||
return fetch(url, options);
|
||||
};
|
||||
|
||||
const widget = document.querySelector("cap-widget");
|
||||
|
||||
widget.addEventListener("solve", (event) => {
|
||||
document.querySelector("h2").innerText =
|
||||
"Continuing to your destination...";
|
||||
|
||||
document.cookie = `__cap_clearance=${
|
||||
event.detail.token
|
||||
}; path=/; max-age=${
|
||||
window.TOKEN_VALIDITY_HOURS * 3_600
|
||||
}; SameSite=Strict`;
|
||||
|
||||
setTimeout(() => {
|
||||
location.reload();
|
||||
}, 300);
|
||||
});
|
||||
|
||||
widget.solve();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,165 +0,0 @@
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import Cap from "@cap.js/server";
|
||||
import Elysia from "elysia";
|
||||
|
||||
export const capMiddleware = (userOptions) => {
|
||||
const options = {
|
||||
token_validity_hours: 32,
|
||||
tokens_store_path: ".data/middlewareTokens.json",
|
||||
token_size: 16, // token size in bytes
|
||||
verification_template_path: join(
|
||||
dirname(fileURLToPath(import.meta.url)),
|
||||
"./index.html",
|
||||
),
|
||||
scoping: "global", // 'global' | 'scoped'
|
||||
|
||||
...userOptions,
|
||||
};
|
||||
|
||||
const cap = new Cap({
|
||||
noFSState: true,
|
||||
});
|
||||
|
||||
let tokensCache = null;
|
||||
let cacheLastModified = 0;
|
||||
|
||||
fs.mkdir(dirname(options.tokens_store_path), { recursive: true });
|
||||
|
||||
async function loadCustomTokens() {
|
||||
try {
|
||||
const stats = await fs.stat(options.tokens_store_path);
|
||||
const fileModified = stats.mtime.getTime();
|
||||
|
||||
if (tokensCache && fileModified <= cacheLastModified) {
|
||||
return tokensCache;
|
||||
}
|
||||
|
||||
const fileContent = await fs.readFile(options.tokens_store_path, "utf-8");
|
||||
tokensCache = JSON.parse(fileContent);
|
||||
cacheLastModified = Date.now();
|
||||
return tokensCache;
|
||||
} catch {
|
||||
tokensCache = {};
|
||||
cacheLastModified = Date.now();
|
||||
return tokensCache;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveCustomTokens(tokens) {
|
||||
await fs.writeFile(options.tokens_store_path, JSON.stringify(tokens));
|
||||
tokensCache = tokens;
|
||||
cacheLastModified = Date.now();
|
||||
}
|
||||
|
||||
async function storeCustomToken(token) {
|
||||
const tokens = await loadCustomTokens();
|
||||
|
||||
tokens[token] = Date.now() + options.token_validity_hours * 60 * 60 * 1000;
|
||||
await saveCustomTokens(tokens);
|
||||
return token;
|
||||
}
|
||||
|
||||
async function validateCustomToken(token) {
|
||||
const tokens = await loadCustomTokens();
|
||||
const tokenData = tokens[token];
|
||||
|
||||
if (!tokenData) return { success: false };
|
||||
|
||||
if (Date.now() > tokenData) {
|
||||
delete tokens[token];
|
||||
await saveCustomTokens(tokens);
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async function cleanupExpiredTokens() {
|
||||
const tokens = await loadCustomTokens();
|
||||
const now = Date.now();
|
||||
let hasChanges = false;
|
||||
|
||||
for (const [token, data] of Object.entries(tokens)) {
|
||||
if (now > data) {
|
||||
delete tokens[token];
|
||||
hasChanges = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasChanges) {
|
||||
await saveCustomTokens(tokens);
|
||||
}
|
||||
}
|
||||
|
||||
const plugin = new Elysia({
|
||||
name: "@cap.js/middleware-elysia",
|
||||
seed: options,
|
||||
});
|
||||
|
||||
plugin.onBeforeHandle({ as: options.scoping }, async ({ path, cookie }) => {
|
||||
if (path === "/__cap_clearance/redeem") {
|
||||
return;
|
||||
}
|
||||
|
||||
const customToken = cookie["__cap_clearance"]?.value;
|
||||
if (customToken) {
|
||||
const validation = await validateCustomToken(customToken);
|
||||
if (validation.success) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const challenge = cap.createChallenge();
|
||||
|
||||
return new Response(
|
||||
(await fs.readFile(options.verification_template_path, "utf-8"))
|
||||
.replace("window.CAP_CHALLENGE", JSON.stringify(challenge))
|
||||
.replace("window.TOKEN_VALIDITY_HOURS", options.token_validity_hours)
|
||||
.replace(
|
||||
"{{TIME}}",
|
||||
new Date()
|
||||
.toISOString()
|
||||
.replace("T", " ")
|
||||
.replace(/\.\d+Z$/, "Z"),
|
||||
),
|
||||
{
|
||||
headers: { "Content-Type": "text/html" },
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
plugin.post("/__cap_clearance/redeem", async ({ body, set }) => {
|
||||
const { token, solutions } = body;
|
||||
|
||||
if (!token || !solutions) {
|
||||
set.status = 400;
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
const challengeResult = await cap.redeemChallenge({ token, solutions });
|
||||
|
||||
cap.validateToken(challengeResult.token);
|
||||
|
||||
if (challengeResult.success) {
|
||||
const customToken = crypto
|
||||
.randomBytes(options.token_size)
|
||||
.toString("hex");
|
||||
await storeCustomToken(customToken);
|
||||
|
||||
await cleanupExpiredTokens();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
token: customToken,
|
||||
expires: challengeResult.expires,
|
||||
};
|
||||
}
|
||||
|
||||
return challengeResult;
|
||||
});
|
||||
|
||||
return plugin;
|
||||
};
|
||||
@@ -1,61 +0,0 @@
|
||||
{
|
||||
"name": "@cap.js/middleware-elysia",
|
||||
"version": "0.0.3",
|
||||
"description": "Elysia Cloudflare-like middleware for Cap, a lightweight, modern open-source CAPTCHA alternative designed using SHA-256 PoW.",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tiagozip/cap.git"
|
||||
},
|
||||
"keywords": [
|
||||
"security",
|
||||
"detection",
|
||||
"prevention",
|
||||
"defense",
|
||||
"protection",
|
||||
"anti-abuse",
|
||||
"anti-automation",
|
||||
"anti-bot",
|
||||
"anti-ddos",
|
||||
"anti-dos",
|
||||
"anti-exploitation",
|
||||
"anti-spam",
|
||||
"anti-scraping",
|
||||
"attack-mitigation",
|
||||
"protocol",
|
||||
"client-server",
|
||||
"computational-puzzle",
|
||||
"complexity",
|
||||
"puzzle",
|
||||
"crypto",
|
||||
"cryptographic",
|
||||
"algorithm",
|
||||
"cybersecurity",
|
||||
"ddos",
|
||||
"hashcash",
|
||||
"hcaptcha",
|
||||
"captcha-alternative",
|
||||
"verification",
|
||||
"invisible",
|
||||
"pow",
|
||||
"proof-of-work",
|
||||
"recaptcha",
|
||||
"spam",
|
||||
"bots",
|
||||
"filtering",
|
||||
"turing-test"
|
||||
],
|
||||
"author": "Tiago",
|
||||
"license": "Apache-2.0",
|
||||
"bugs": {
|
||||
"url": "https://github.com/tiagozip/cap/issues"
|
||||
},
|
||||
"homepage": "https://github.com/tiagozip/cap#readme",
|
||||
"dependencies": {
|
||||
"@cap.js/server": "^1.0.15",
|
||||
"elysia": "^1.3.3"
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "@cap.js/checkpoint-hono",
|
||||
"dependencies": {
|
||||
"@cap.js/server": "^1.0.15",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@cap.js/server": ["@cap.js/server@1.0.15", "", { "dependencies": { "@types/node": "^22.14.1", "typescript": "^5.8.3" } }, "sha512-mcDbHAL4ar5O4gnDE+62ZPeXhLXxd1KKHzJCsZfn81HY8Fgo5ousxYCnzhsLfki5sH6sO6jpNtS4MZXlKDq+fA=="],
|
||||
|
||||
"@types/node": ["@types/node@22.15.28", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-I0okKVDmyKR281I0UIFV7EWAWRnR0gkuSKob5wVcByyyhr7Px/slhkQapcYX4u00ekzNWaS1gznKZnuzxwo4pw=="],
|
||||
|
||||
"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=="],
|
||||
}
|
||||
}
|
||||
@@ -1,200 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Verifying you are a human…</title>
|
||||
<meta name="title" content="Verifying you are a human…" />
|
||||
<meta
|
||||
name="description"
|
||||
content="A preview of this link is unavailable while a browser check is in progress."
|
||||
/>
|
||||
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:title" content="Verifying you are a human…" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="A preview of this link is unavailable while a browser check is in progress."
|
||||
/>
|
||||
|
||||
<meta property="twitter:title" content="Verifying you are a human…" />
|
||||
<meta
|
||||
property="twitter:description"
|
||||
content="A preview of this link is unavailable while a browser check is in progress."
|
||||
/>
|
||||
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
max-width: 690px;
|
||||
margin: 3em auto;
|
||||
padding: 18px;
|
||||
font-family: system-ui;
|
||||
}
|
||||
h1 {
|
||||
font-weight: 600;
|
||||
font-size: 26px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
h2 {
|
||||
font-weight: 400;
|
||||
font-size: 20px;
|
||||
margin-top: 7px;
|
||||
margin-bottom: 1.5em;
|
||||
color: #171717;
|
||||
}
|
||||
cap-widget {
|
||||
margin-bottom: 4em;
|
||||
}
|
||||
hr {
|
||||
border: 0;
|
||||
border-top: 1px solid #dddddd8f;
|
||||
margin: 2em 0;
|
||||
}
|
||||
h3 {
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
margin-top: 1.5em;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
p {
|
||||
font-weight: 400;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
margin-top: 10px;
|
||||
color: #171717;
|
||||
}
|
||||
footer {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
footer .credit {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
color: #171717;
|
||||
}
|
||||
footer .credit:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
footer .credit img {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
}
|
||||
footer .date {
|
||||
font-size: 15px;
|
||||
color: #888;
|
||||
margin: 0px;
|
||||
margin-left: auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Checking your browser...</h1>
|
||||
<script>
|
||||
document.querySelector("h1").innerText = location.host;
|
||||
</script>
|
||||
<h2>Verifying you are a human before proceeding...</h2>
|
||||
|
||||
<cap-widget id="cap" data-cap-api-endpoint="/__cap_clearance/"></cap-widget>
|
||||
|
||||
<noscript>
|
||||
<style>
|
||||
.info {
|
||||
display: none;
|
||||
}
|
||||
a {
|
||||
color: #0a91e7;
|
||||
}
|
||||
h3 {
|
||||
line-height: 1.3;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
</style>
|
||||
<h3>
|
||||
JavaScript is disabled and we were unable to verify you. To access this
|
||||
page, please
|
||||
<a
|
||||
href="https://www.whatismybrowser.com/guides/how-to-enable-javascript/auto"
|
||||
target="_blank"
|
||||
rel="nofollow noopener"
|
||||
>
|
||||
enable JavaScript
|
||||
</a>
|
||||
</h3>
|
||||
<hr />
|
||||
</noscript>
|
||||
|
||||
<div class="info">
|
||||
<hr />
|
||||
|
||||
<h3>Why am I seeing this page?</h3>
|
||||
<p>
|
||||
To keep our site secure, we need to confirm you're a human and not a
|
||||
robot. This quick check helps stop spam and abuse.
|
||||
</p>
|
||||
|
||||
<h3>What should I do?</h3>
|
||||
<p>
|
||||
No action is required on your end. Once verified, you'll continue to
|
||||
your destination. If you're stuck, try refreshing the page or checking
|
||||
your connection.
|
||||
</p>
|
||||
|
||||
<hr />
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<a
|
||||
href="https://capjs.js.org/"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="credit"
|
||||
>
|
||||
<img
|
||||
src="data:image/webp;base64,UklGRnwEAABXRUJQVlA4WAoAAAAQAAAATwAATwAAQUxQSDcCAAABkAXZtmk7+73Ytm3btm3btm3btvNn206ebfsdzvCcvfe6f6lURcQEsH/Ui3Sb2quAA0k7zU2D9qFb9rwF8ufOms6Z3hgVv0d+dHF3+/b6ztn1oxsXSkUoa+svsB/vfnFS5dQ0Ss99FgfOIRd7ZpWv7DpviFQe9s8oV87ZnhCtXK8jU9P7JiQMGJ9GlrTTQiCnsj2rHNm2q5D2cDYZchyHzAcyist0BFIbK1OISrnGlAvx/UUNToTsLuXElHOF/MfSiEh1CAQTuotoGUMBD7PzS30OJPUR/OpG0sCzHNzWgqg+hFeON1RwJxOnxnFkkjpymga6p1NycT5BKLgql2zPCGEpl1JelN7m4tEggpLSjUfnBEo47sxhkEIqem/XAk52RmukAOX7gR75rY3TiQFQv+1qn9PCFIMegKQ3q+ql/9M00yEAiLoxprBjAcxvs7I6FsC8VJhNNki5HP9sWgLOZR6rE3JfVNyp4Jh7CZaShw5XyYRsLM1+z9z2SIAFnOmbbC86VoqEU7Wd2F9TlJ/zJP4vR9rH2wi7Obtus4MBwoxH3dMw61la7ficBMCvdd1wC7rftSnV0jPGnCsufqcKcZmSg3HM22bBtlUNnIq7/ynqzZ7+pdOwv+fucyGEm//akkxkxntAxOvDY2plZ3ZTV5n7MJaH75bKTPDMgLXVszkxvpkbLb4TqFmKvD+rnBMTnaGMMxOZvly3hcfuvnPxcHl7Y8+YmpmYQ3RKlz1foXzZ0rD/AwMAVlA4IB4CAABwDwCdASpQAFAAPok6lUelI6IhL1auwKARCWgAyJAhz7eq/ihjJ3OI5P5V3p3mJ/ar10vRB/298A5832YAmiLa0mutah4B6eOgBjpWeAPuDnTJNdrbbJGDv05E/kTVtFErBAMEp9Sdw2xY7Adta5Ru6IDt1ffejnz6tUPNbgJLZIFBZHAA/vz4VF/Qshf/4bK2z23dL/pV2OnhttId5Jomv/wz7rSmfFojRqvMm0mkOsKUbevn1KAeivy2swXV7qG8yi6ZCW/0Be54wB4Y5ZvU3j/AJTo2YSp/T5twW6LjpC9FfJJ99yn0GjUbICt2Rs1Fh+k7CQ5ze4+AHDqQ+1vxZ+itKOvFDB6CfDAqpTpqtdPaJlqFvq1bwvulRC44jnvPvhuyS04lhAJYP2iOQEh5lydE+sXpBfX+N3RCMY4KNY/8ubEOok/6+so2trq8Tc67BTprcrZ5sy0gYjn7XL5Jceobj+a8X+qaXc31i61vBeUpVO1eHth1iA9BXz3QBrRY0ahjBc3Fl9J8ouV6/svohQze+Xv4r47H18ryTUSTVDWnw6TuHgfYgtZQM98u1OqzLlB3Jz9AjlflcsnuakRtmeMGkkxiVuoPb3S36krL+oTNPGlyL6TcUTgeLl0ZZPRGdlmCbnR9JcEnKOqK7KwVYegLD5vlXrZwACMgve4dHJ8OEkf9ppwm6OGGGwG/1uHEJeN1zIOxgTCxqAAAAA=="
|
||||
aria-hidden="true"
|
||||
title="Cap logo"
|
||||
alt="Cap logo"
|
||||
/>
|
||||
<span>Cap</span>
|
||||
</a>
|
||||
<p class="date">{{TIME}}</p>
|
||||
</footer>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@cap.js/widget@0.1.17"></script>
|
||||
<script>
|
||||
window.CAP_CUSTOM_FETCH = function (url, options) {
|
||||
console.log(url);
|
||||
if (url.endsWith("/challenge")) {
|
||||
return {
|
||||
json: () => {
|
||||
return window.CAP_CHALLENGE;
|
||||
},
|
||||
};
|
||||
}
|
||||
return fetch(url, options);
|
||||
};
|
||||
|
||||
const widget = document.querySelector("cap-widget");
|
||||
|
||||
widget.addEventListener("solve", (event) => {
|
||||
document.querySelector("h2").innerText =
|
||||
"Continuing to your destination...";
|
||||
|
||||
document.cookie = `__cap_clearance=${
|
||||
event.detail.token
|
||||
}; path=/; max-age=${
|
||||
window.TOKEN_VALIDITY_HOURS * 3_600
|
||||
}; SameSite=Strict`;
|
||||
|
||||
setTimeout(() => {
|
||||
location.reload();
|
||||
}, 300);
|
||||
});
|
||||
|
||||
widget.solve();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,159 +0,0 @@
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import Cap from "@cap.js/server";
|
||||
|
||||
export const capCheckpoint = (userOptions) => {
|
||||
const options = {
|
||||
token_validity_hours: 32,
|
||||
tokens_store_path: ".data/middlewareTokens.json",
|
||||
token_size: 16,
|
||||
verification_template_path: join(
|
||||
dirname(fileURLToPath(import.meta.url)),
|
||||
"./index.html",
|
||||
),
|
||||
...userOptions,
|
||||
};
|
||||
|
||||
const cap = new Cap({
|
||||
noFSState: true,
|
||||
});
|
||||
|
||||
let tokensCache = null;
|
||||
let cacheLastModified = 0;
|
||||
|
||||
fs.mkdir(dirname(options.tokens_store_path), { recursive: true });
|
||||
|
||||
async function loadCustomTokens() {
|
||||
try {
|
||||
const stats = await fs.stat(options.tokens_store_path);
|
||||
|
||||
if (tokensCache && stats.mtime.getTime() <= cacheLastModified) {
|
||||
return tokensCache;
|
||||
}
|
||||
|
||||
tokensCache = JSON.parse(
|
||||
await fs.readFile(options.tokens_store_path, "utf-8"),
|
||||
);
|
||||
cacheLastModified = Date.now();
|
||||
|
||||
return tokensCache;
|
||||
} catch {
|
||||
tokensCache = {};
|
||||
cacheLastModified = Date.now();
|
||||
|
||||
return tokensCache;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveCustomTokens(tokens) {
|
||||
await fs.writeFile(options.tokens_store_path, JSON.stringify(tokens));
|
||||
tokensCache = tokens;
|
||||
cacheLastModified = Date.now();
|
||||
}
|
||||
|
||||
async function storeCustomToken(token) {
|
||||
const tokens = await loadCustomTokens();
|
||||
|
||||
tokens[token] = Date.now() + options.token_validity_hours * 60 * 60 * 1000;
|
||||
await saveCustomTokens(tokens);
|
||||
return token;
|
||||
}
|
||||
|
||||
async function validateCustomToken(token) {
|
||||
const tokens = await loadCustomTokens();
|
||||
const tokenData = tokens[token];
|
||||
|
||||
if (!tokenData) return { success: false };
|
||||
|
||||
if (Date.now() > tokenData) {
|
||||
delete tokens[token];
|
||||
await saveCustomTokens(tokens);
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async function cleanupExpiredTokens() {
|
||||
const tokens = await loadCustomTokens();
|
||||
const now = Date.now();
|
||||
let hasChanges = false;
|
||||
|
||||
for (const [token, data] of Object.entries(tokens)) {
|
||||
if (now > data) {
|
||||
delete tokens[token];
|
||||
hasChanges = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasChanges) {
|
||||
await saveCustomTokens(tokens);
|
||||
}
|
||||
}
|
||||
|
||||
return async (req, res, next) => {
|
||||
const { path, method } = req;
|
||||
|
||||
if (method === "POST" && path === "/__cap_clearance/redeem") {
|
||||
const { token, solutions } = req.body;
|
||||
|
||||
if (!token || !solutions) {
|
||||
return res.status(400).json({ success: false });
|
||||
}
|
||||
|
||||
const challengeResult = await cap.redeemChallenge({ token, solutions });
|
||||
|
||||
cap.validateToken(challengeResult.token);
|
||||
|
||||
if (challengeResult.success) {
|
||||
const customToken = crypto
|
||||
.randomBytes(options.token_size)
|
||||
.toString("hex");
|
||||
await storeCustomToken(customToken);
|
||||
|
||||
await cleanupExpiredTokens();
|
||||
|
||||
res.cookie("__cap_clearance", customToken, {
|
||||
maxAge: options.token_validity_hours * 60 * 60 * 1000,
|
||||
sameSite: "Strict",
|
||||
});
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
token: customToken,
|
||||
expires: challengeResult.expires,
|
||||
});
|
||||
}
|
||||
|
||||
return res.json(challengeResult);
|
||||
}
|
||||
|
||||
const customToken = req.cookies?.__cap_clearance;
|
||||
if (customToken) {
|
||||
const validation = await validateCustomToken(customToken);
|
||||
if (validation.success) {
|
||||
return next();
|
||||
}
|
||||
}
|
||||
|
||||
const challenge = cap.createChallenge();
|
||||
|
||||
const html = (
|
||||
await fs.readFile(options.verification_template_path, "utf-8")
|
||||
)
|
||||
.replace("window.CAP_CHALLENGE", JSON.stringify(challenge))
|
||||
.replace("window.TOKEN_VALIDITY_HOURS", options.token_validity_hours)
|
||||
.replace(
|
||||
"{{TIME}}",
|
||||
new Date()
|
||||
.toISOString()
|
||||
.replace("T", " ")
|
||||
.replace(/\.\d+Z$/, "Z"),
|
||||
);
|
||||
|
||||
res.setHeader("Content-Type", "text/html");
|
||||
res.send(html);
|
||||
};
|
||||
};
|
||||
@@ -1,60 +0,0 @@
|
||||
{
|
||||
"name": "@cap.js/checkpoint-express",
|
||||
"version": "0.0.2",
|
||||
"description": "Express Cloudflare-like checkpoint for Cap, a lightweight, modern open-source CAPTCHA alternative designed using SHA-256 PoW.",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tiagozip/cap.git"
|
||||
},
|
||||
"keywords": [
|
||||
"security",
|
||||
"detection",
|
||||
"prevention",
|
||||
"defense",
|
||||
"protection",
|
||||
"anti-abuse",
|
||||
"anti-automation",
|
||||
"anti-bot",
|
||||
"anti-ddos",
|
||||
"anti-dos",
|
||||
"anti-exploitation",
|
||||
"anti-spam",
|
||||
"anti-scraping",
|
||||
"attack-mitigation",
|
||||
"protocol",
|
||||
"client-server",
|
||||
"computational-puzzle",
|
||||
"complexity",
|
||||
"puzzle",
|
||||
"crypto",
|
||||
"cryptographic",
|
||||
"algorithm",
|
||||
"cybersecurity",
|
||||
"ddos",
|
||||
"hashcash",
|
||||
"hcaptcha",
|
||||
"captcha-alternative",
|
||||
"verification",
|
||||
"invisible",
|
||||
"pow",
|
||||
"proof-of-work",
|
||||
"recaptcha",
|
||||
"spam",
|
||||
"bots",
|
||||
"filtering",
|
||||
"turing-test"
|
||||
],
|
||||
"author": "Tiago",
|
||||
"license": "Apache-2.0",
|
||||
"bugs": {
|
||||
"url": "https://github.com/tiagozip/cap/issues"
|
||||
},
|
||||
"homepage": "https://github.com/tiagozip/cap#readme",
|
||||
"dependencies": {
|
||||
"@cap.js/server": "^1.0.15"
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "@cap.js/checkpoint-hono",
|
||||
"dependencies": {
|
||||
"@cap.js/server": "^1.0.15",
|
||||
"hono": "^4.7.10",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@cap.js/server": ["@cap.js/server@1.0.15", "", { "dependencies": { "@types/node": "^22.14.1", "typescript": "^5.8.3" } }, "sha512-mcDbHAL4ar5O4gnDE+62ZPeXhLXxd1KKHzJCsZfn81HY8Fgo5ousxYCnzhsLfki5sH6sO6jpNtS4MZXlKDq+fA=="],
|
||||
|
||||
"@types/node": ["@types/node@22.15.28", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-I0okKVDmyKR281I0UIFV7EWAWRnR0gkuSKob5wVcByyyhr7Px/slhkQapcYX4u00ekzNWaS1gznKZnuzxwo4pw=="],
|
||||
|
||||
"hono": ["hono@4.7.10", "", {}, "sha512-QkACju9MiN59CKSY5JsGZCYmPZkA6sIW6OFCUp7qDjZu6S6KHtJHhAc9Uy9mV9F8PJ1/HQ3ybZF2yjCa/73fvQ=="],
|
||||
|
||||
"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=="],
|
||||
}
|
||||
}
|
||||
@@ -1,200 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Verifying you are a human…</title>
|
||||
<meta name="title" content="Verifying you are a human…" />
|
||||
<meta
|
||||
name="description"
|
||||
content="A preview of this link is unavailable while a browser check is in progress."
|
||||
/>
|
||||
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:title" content="Verifying you are a human…" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="A preview of this link is unavailable while a browser check is in progress."
|
||||
/>
|
||||
|
||||
<meta property="twitter:title" content="Verifying you are a human…" />
|
||||
<meta
|
||||
property="twitter:description"
|
||||
content="A preview of this link is unavailable while a browser check is in progress."
|
||||
/>
|
||||
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
max-width: 690px;
|
||||
margin: 3em auto;
|
||||
padding: 18px;
|
||||
font-family: system-ui;
|
||||
}
|
||||
h1 {
|
||||
font-weight: 600;
|
||||
font-size: 26px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
h2 {
|
||||
font-weight: 400;
|
||||
font-size: 20px;
|
||||
margin-top: 7px;
|
||||
margin-bottom: 1.5em;
|
||||
color: #171717;
|
||||
}
|
||||
cap-widget {
|
||||
margin-bottom: 4em;
|
||||
}
|
||||
hr {
|
||||
border: 0;
|
||||
border-top: 1px solid #dddddd8f;
|
||||
margin: 2em 0;
|
||||
}
|
||||
h3 {
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
margin-top: 1.5em;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
p {
|
||||
font-weight: 400;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
margin-top: 10px;
|
||||
color: #171717;
|
||||
}
|
||||
footer {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
footer .credit {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
color: #171717;
|
||||
}
|
||||
footer .credit:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
footer .credit img {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
}
|
||||
footer .date {
|
||||
font-size: 15px;
|
||||
color: #888;
|
||||
margin: 0px;
|
||||
margin-left: auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Checking your browser...</h1>
|
||||
<script>
|
||||
document.querySelector("h1").innerText = location.host;
|
||||
</script>
|
||||
<h2>Verifying you are a human before proceeding...</h2>
|
||||
|
||||
<cap-widget id="cap" data-cap-api-endpoint="/__cap_clearance/"></cap-widget>
|
||||
|
||||
<noscript>
|
||||
<style>
|
||||
.info {
|
||||
display: none;
|
||||
}
|
||||
a {
|
||||
color: #0a91e7;
|
||||
}
|
||||
h3 {
|
||||
line-height: 1.3;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
</style>
|
||||
<h3>
|
||||
JavaScript is disabled and we were unable to verify you. To access this
|
||||
page, please
|
||||
<a
|
||||
href="https://www.whatismybrowser.com/guides/how-to-enable-javascript/auto"
|
||||
target="_blank"
|
||||
rel="nofollow noopener"
|
||||
>
|
||||
enable JavaScript
|
||||
</a>
|
||||
</h3>
|
||||
<hr />
|
||||
</noscript>
|
||||
|
||||
<div class="info">
|
||||
<hr />
|
||||
|
||||
<h3>Why am I seeing this page?</h3>
|
||||
<p>
|
||||
To keep our site secure, we need to confirm you're a human and not a
|
||||
robot. This quick check helps stop spam and abuse.
|
||||
</p>
|
||||
|
||||
<h3>What should I do?</h3>
|
||||
<p>
|
||||
No action is required on your end. Once verified, you'll continue to
|
||||
your destination. If you're stuck, try refreshing the page or checking
|
||||
your connection.
|
||||
</p>
|
||||
|
||||
<hr />
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<a
|
||||
href="https://capjs.js.org/"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="credit"
|
||||
>
|
||||
<img
|
||||
src="data:image/webp;base64,UklGRnwEAABXRUJQVlA4WAoAAAAQAAAATwAATwAAQUxQSDcCAAABkAXZtmk7+73Ytm3btm3btm3btvNn206ebfsdzvCcvfe6f6lURcQEsH/Ui3Sb2quAA0k7zU2D9qFb9rwF8ufOms6Z3hgVv0d+dHF3+/b6ztn1oxsXSkUoa+svsB/vfnFS5dQ0Ss99FgfOIRd7ZpWv7DpviFQe9s8oV87ZnhCtXK8jU9P7JiQMGJ9GlrTTQiCnsj2rHNm2q5D2cDYZchyHzAcyist0BFIbK1OISrnGlAvx/UUNToTsLuXElHOF/MfSiEh1CAQTuotoGUMBD7PzS30OJPUR/OpG0sCzHNzWgqg+hFeON1RwJxOnxnFkkjpymga6p1NycT5BKLgql2zPCGEpl1JelN7m4tEggpLSjUfnBEo47sxhkEIqem/XAk52RmukAOX7gR75rY3TiQFQv+1qn9PCFIMegKQ3q+ql/9M00yEAiLoxprBjAcxvs7I6FsC8VJhNNki5HP9sWgLOZR6rE3JfVNyp4Jh7CZaShw5XyYRsLM1+z9z2SIAFnOmbbC86VoqEU7Wd2F9TlJ/zJP4vR9rH2wi7Obtus4MBwoxH3dMw61la7ficBMCvdd1wC7rftSnV0jPGnCsufqcKcZmSg3HM22bBtlUNnIq7/ynqzZ7+pdOwv+fucyGEm//akkxkxntAxOvDY2plZ3ZTV5n7MJaH75bKTPDMgLXVszkxvpkbLb4TqFmKvD+rnBMTnaGMMxOZvly3hcfuvnPxcHl7Y8+YmpmYQ3RKlz1foXzZ0rD/AwMAVlA4IB4CAABwDwCdASpQAFAAPok6lUelI6IhL1auwKARCWgAyJAhz7eq/ihjJ3OI5P5V3p3mJ/ar10vRB/298A5832YAmiLa0mutah4B6eOgBjpWeAPuDnTJNdrbbJGDv05E/kTVtFErBAMEp9Sdw2xY7Adta5Ru6IDt1ffejnz6tUPNbgJLZIFBZHAA/vz4VF/Qshf/4bK2z23dL/pV2OnhttId5Jomv/wz7rSmfFojRqvMm0mkOsKUbevn1KAeivy2swXV7qG8yi6ZCW/0Be54wB4Y5ZvU3j/AJTo2YSp/T5twW6LjpC9FfJJ99yn0GjUbICt2Rs1Fh+k7CQ5ze4+AHDqQ+1vxZ+itKOvFDB6CfDAqpTpqtdPaJlqFvq1bwvulRC44jnvPvhuyS04lhAJYP2iOQEh5lydE+sXpBfX+N3RCMY4KNY/8ubEOok/6+so2trq8Tc67BTprcrZ5sy0gYjn7XL5Jceobj+a8X+qaXc31i61vBeUpVO1eHth1iA9BXz3QBrRY0ahjBc3Fl9J8ouV6/svohQze+Xv4r47H18ryTUSTVDWnw6TuHgfYgtZQM98u1OqzLlB3Jz9AjlflcsnuakRtmeMGkkxiVuoPb3S36krL+oTNPGlyL6TcUTgeLl0ZZPRGdlmCbnR9JcEnKOqK7KwVYegLD5vlXrZwACMgve4dHJ8OEkf9ppwm6OGGGwG/1uHEJeN1zIOxgTCxqAAAAA=="
|
||||
aria-hidden="true"
|
||||
title="Cap logo"
|
||||
alt="Cap logo"
|
||||
/>
|
||||
<span>Cap</span>
|
||||
</a>
|
||||
<p class="date">{{TIME}}</p>
|
||||
</footer>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@cap.js/widget@0.1.17"></script>
|
||||
<script>
|
||||
window.CAP_CUSTOM_FETCH = function (url, options) {
|
||||
console.log(url);
|
||||
if (url.endsWith("/challenge")) {
|
||||
return {
|
||||
json: () => {
|
||||
return window.CAP_CHALLENGE;
|
||||
},
|
||||
};
|
||||
}
|
||||
return fetch(url, options);
|
||||
};
|
||||
|
||||
const widget = document.querySelector("cap-widget");
|
||||
|
||||
widget.addEventListener("solve", (event) => {
|
||||
document.querySelector("h2").innerText =
|
||||
"Continuing to your destination...";
|
||||
|
||||
document.cookie = `__cap_clearance=${
|
||||
event.detail.token
|
||||
}; path=/; max-age=${
|
||||
window.TOKEN_VALIDITY_HOURS * 3_600
|
||||
}; SameSite=Strict`;
|
||||
|
||||
setTimeout(() => {
|
||||
location.reload();
|
||||
}, 300);
|
||||
});
|
||||
|
||||
widget.solve();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,164 +0,0 @@
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import Cap from "@cap.js/server";
|
||||
import { getCookie, setCookie } from "hono/cookie";
|
||||
import { createMiddleware } from "hono/factory";
|
||||
|
||||
export const capCheckpoint = (userOptions) => {
|
||||
const options = {
|
||||
token_validity_hours: 32,
|
||||
tokens_store_path: ".data/middlewareTokens.json",
|
||||
token_size: 16, // token size in bytes
|
||||
verification_template_path: join(
|
||||
dirname(fileURLToPath(import.meta.url)),
|
||||
"./index.html",
|
||||
),
|
||||
|
||||
...userOptions,
|
||||
};
|
||||
|
||||
const cap = new Cap({
|
||||
noFSState: true,
|
||||
});
|
||||
|
||||
let tokensCache = null;
|
||||
let cacheLastModified = 0;
|
||||
|
||||
fs.mkdir(dirname(options.tokens_store_path), { recursive: true });
|
||||
|
||||
async function loadCustomTokens() {
|
||||
try {
|
||||
const stats = await fs.stat(options.tokens_store_path);
|
||||
|
||||
if (tokensCache && stats.mtime.getTime() <= cacheLastModified) {
|
||||
return tokensCache;
|
||||
}
|
||||
|
||||
tokensCache = JSON.parse(
|
||||
await fs.readFile(options.tokens_store_path, "utf-8"),
|
||||
);
|
||||
cacheLastModified = Date.now();
|
||||
|
||||
return tokensCache;
|
||||
} catch {
|
||||
tokensCache = {};
|
||||
cacheLastModified = Date.now();
|
||||
|
||||
return tokensCache;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveCustomTokens(tokens) {
|
||||
await fs.writeFile(options.tokens_store_path, JSON.stringify(tokens));
|
||||
tokensCache = tokens;
|
||||
cacheLastModified = Date.now();
|
||||
}
|
||||
|
||||
async function storeCustomToken(token) {
|
||||
const tokens = await loadCustomTokens();
|
||||
|
||||
tokens[token] = Date.now() + options.token_validity_hours * 60 * 60 * 1000;
|
||||
await saveCustomTokens(tokens);
|
||||
return token;
|
||||
}
|
||||
|
||||
async function validateCustomToken(token) {
|
||||
const tokens = await loadCustomTokens();
|
||||
const tokenData = tokens[token];
|
||||
|
||||
if (!tokenData) return { success: false };
|
||||
|
||||
if (Date.now() > tokenData) {
|
||||
delete tokens[token];
|
||||
await saveCustomTokens(tokens);
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async function cleanupExpiredTokens() {
|
||||
const tokens = await loadCustomTokens();
|
||||
const now = Date.now();
|
||||
let hasChanges = false;
|
||||
|
||||
for (const [token, data] of Object.entries(tokens)) {
|
||||
if (now > data) {
|
||||
delete tokens[token];
|
||||
hasChanges = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasChanges) {
|
||||
await saveCustomTokens(tokens);
|
||||
}
|
||||
}
|
||||
|
||||
const middleware = createMiddleware(async (c, next) => {
|
||||
const { path, method } = c.req;
|
||||
|
||||
if (method === "POST" && path === "/__cap_clearance/redeem") {
|
||||
const body = await c.req.json();
|
||||
const { token, solutions } = body;
|
||||
|
||||
if (!token || !solutions) {
|
||||
return c.json({ success: false }, 400);
|
||||
}
|
||||
|
||||
const challengeResult = await cap.redeemChallenge({ token, solutions });
|
||||
|
||||
cap.validateToken(challengeResult.token);
|
||||
|
||||
if (challengeResult.success) {
|
||||
const customToken = crypto
|
||||
.randomBytes(options.token_size)
|
||||
.toString("hex");
|
||||
await storeCustomToken(customToken);
|
||||
|
||||
await cleanupExpiredTokens();
|
||||
|
||||
setCookie(c, "__cap_clearance", customToken, {
|
||||
maxAge: options.token_validity_hours * 60 * 60,
|
||||
sameSite: "Strict",
|
||||
});
|
||||
|
||||
return c.json({
|
||||
success: true,
|
||||
token: customToken,
|
||||
expires: challengeResult.expires,
|
||||
});
|
||||
}
|
||||
|
||||
return c.json(challengeResult);
|
||||
}
|
||||
|
||||
const customToken = getCookie(c, "__cap_clearance");
|
||||
if (customToken) {
|
||||
const validation = await validateCustomToken(customToken);
|
||||
if (validation.success) {
|
||||
return await next();
|
||||
}
|
||||
}
|
||||
|
||||
const challenge = cap.createChallenge();
|
||||
|
||||
const html = (
|
||||
await fs.readFile(options.verification_template_path, "utf-8")
|
||||
)
|
||||
.replace("window.CAP_CHALLENGE", JSON.stringify(challenge))
|
||||
.replace("window.TOKEN_VALIDITY_HOURS", options.token_validity_hours)
|
||||
.replace(
|
||||
"{{TIME}}",
|
||||
new Date()
|
||||
.toISOString()
|
||||
.replace("T", " ")
|
||||
.replace(/\.\d+Z$/, "Z"),
|
||||
);
|
||||
|
||||
return c.html(html);
|
||||
});
|
||||
|
||||
return middleware;
|
||||
};
|
||||
@@ -1,61 +0,0 @@
|
||||
{
|
||||
"name": "@cap.js/checkpoint-hono",
|
||||
"version": "0.0.2",
|
||||
"description": "Hono Cloudflare-like checkpoint for Cap, a lightweight, modern open-source CAPTCHA alternative designed using SHA-256 PoW.",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tiagozip/cap.git"
|
||||
},
|
||||
"keywords": [
|
||||
"security",
|
||||
"detection",
|
||||
"prevention",
|
||||
"defense",
|
||||
"protection",
|
||||
"anti-abuse",
|
||||
"anti-automation",
|
||||
"anti-bot",
|
||||
"anti-ddos",
|
||||
"anti-dos",
|
||||
"anti-exploitation",
|
||||
"anti-spam",
|
||||
"anti-scraping",
|
||||
"attack-mitigation",
|
||||
"protocol",
|
||||
"client-server",
|
||||
"computational-puzzle",
|
||||
"complexity",
|
||||
"puzzle",
|
||||
"crypto",
|
||||
"cryptographic",
|
||||
"algorithm",
|
||||
"cybersecurity",
|
||||
"ddos",
|
||||
"hashcash",
|
||||
"hcaptcha",
|
||||
"captcha-alternative",
|
||||
"verification",
|
||||
"invisible",
|
||||
"pow",
|
||||
"proof-of-work",
|
||||
"recaptcha",
|
||||
"spam",
|
||||
"bots",
|
||||
"filtering",
|
||||
"turing-test"
|
||||
],
|
||||
"author": "Tiago",
|
||||
"license": "Apache-2.0",
|
||||
"bugs": {
|
||||
"url": "https://github.com/tiagozip/cap/issues"
|
||||
},
|
||||
"homepage": "https://github.com/tiagozip/cap#readme",
|
||||
"dependencies": {
|
||||
"@cap.js/server": "^1.0.15",
|
||||
"hono": "^4.7.10"
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import fs from "node:fs/promises";
|
||||
|
||||
const template = await fs.readFile("./template.html", "utf-8");
|
||||
|
||||
const folders = (await fs.readdir("./", { withFileTypes: true }))
|
||||
.filter((dirent) => dirent.isDirectory())
|
||||
.map((dirent) => dirent.name);
|
||||
|
||||
console.log(
|
||||
`build command:\n${folders
|
||||
.map((folder) => `cd ${folder} && bun publish --access public && cd ..`)
|
||||
.join("\n")}\n`,
|
||||
);
|
||||
|
||||
folders.forEach(async (folder) => {
|
||||
const path = `./${folder}/index.html`;
|
||||
await fs.writeFile(path, template);
|
||||
|
||||
console.log(`wrote ${path}`);
|
||||
});
|
||||
@@ -1,200 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Verifying you are a human…</title>
|
||||
<meta name="title" content="Verifying you are a human…" />
|
||||
<meta
|
||||
name="description"
|
||||
content="A preview of this link is unavailable while a browser check is in progress."
|
||||
/>
|
||||
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:title" content="Verifying you are a human…" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="A preview of this link is unavailable while a browser check is in progress."
|
||||
/>
|
||||
|
||||
<meta property="twitter:title" content="Verifying you are a human…" />
|
||||
<meta
|
||||
property="twitter:description"
|
||||
content="A preview of this link is unavailable while a browser check is in progress."
|
||||
/>
|
||||
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
max-width: 690px;
|
||||
margin: 3em auto;
|
||||
padding: 18px;
|
||||
font-family: system-ui;
|
||||
}
|
||||
h1 {
|
||||
font-weight: 600;
|
||||
font-size: 26px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
h2 {
|
||||
font-weight: 400;
|
||||
font-size: 20px;
|
||||
margin-top: 7px;
|
||||
margin-bottom: 1.5em;
|
||||
color: #171717;
|
||||
}
|
||||
cap-widget {
|
||||
margin-bottom: 4em;
|
||||
}
|
||||
hr {
|
||||
border: 0;
|
||||
border-top: 1px solid #dddddd8f;
|
||||
margin: 2em 0;
|
||||
}
|
||||
h3 {
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
margin-top: 1.5em;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
p {
|
||||
font-weight: 400;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
margin-top: 10px;
|
||||
color: #171717;
|
||||
}
|
||||
footer {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
footer .credit {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
color: #171717;
|
||||
}
|
||||
footer .credit:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
footer .credit img {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
}
|
||||
footer .date {
|
||||
font-size: 15px;
|
||||
color: #888;
|
||||
margin: 0px;
|
||||
margin-left: auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Checking your browser...</h1>
|
||||
<script>
|
||||
document.querySelector("h1").innerText = location.host;
|
||||
</script>
|
||||
<h2>Verifying you are a human before proceeding...</h2>
|
||||
|
||||
<cap-widget id="cap" data-cap-api-endpoint="/__cap_clearance/"></cap-widget>
|
||||
|
||||
<noscript>
|
||||
<style>
|
||||
.info {
|
||||
display: none;
|
||||
}
|
||||
a {
|
||||
color: #0a91e7;
|
||||
}
|
||||
h3 {
|
||||
line-height: 1.3;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
</style>
|
||||
<h3>
|
||||
JavaScript is disabled and we were unable to verify you. To access this
|
||||
page, please
|
||||
<a
|
||||
href="https://www.whatismybrowser.com/guides/how-to-enable-javascript/auto"
|
||||
target="_blank"
|
||||
rel="nofollow noopener"
|
||||
>
|
||||
enable JavaScript
|
||||
</a>
|
||||
</h3>
|
||||
<hr />
|
||||
</noscript>
|
||||
|
||||
<div class="info">
|
||||
<hr />
|
||||
|
||||
<h3>Why am I seeing this page?</h3>
|
||||
<p>
|
||||
To keep our site secure, we need to confirm you're a human and not a
|
||||
robot. This quick check helps stop spam and abuse.
|
||||
</p>
|
||||
|
||||
<h3>What should I do?</h3>
|
||||
<p>
|
||||
No action is required on your end. Once verified, you'll continue to
|
||||
your destination. If you're stuck, try refreshing the page or checking
|
||||
your connection.
|
||||
</p>
|
||||
|
||||
<hr />
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<a
|
||||
href="https://capjs.js.org/"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="credit"
|
||||
>
|
||||
<img
|
||||
src="data:image/webp;base64,UklGRnwEAABXRUJQVlA4WAoAAAAQAAAATwAATwAAQUxQSDcCAAABkAXZtmk7+73Ytm3btm3btm3btvNn206ebfsdzvCcvfe6f6lURcQEsH/Ui3Sb2quAA0k7zU2D9qFb9rwF8ufOms6Z3hgVv0d+dHF3+/b6ztn1oxsXSkUoa+svsB/vfnFS5dQ0Ss99FgfOIRd7ZpWv7DpviFQe9s8oV87ZnhCtXK8jU9P7JiQMGJ9GlrTTQiCnsj2rHNm2q5D2cDYZchyHzAcyist0BFIbK1OISrnGlAvx/UUNToTsLuXElHOF/MfSiEh1CAQTuotoGUMBD7PzS30OJPUR/OpG0sCzHNzWgqg+hFeON1RwJxOnxnFkkjpymga6p1NycT5BKLgql2zPCGEpl1JelN7m4tEggpLSjUfnBEo47sxhkEIqem/XAk52RmukAOX7gR75rY3TiQFQv+1qn9PCFIMegKQ3q+ql/9M00yEAiLoxprBjAcxvs7I6FsC8VJhNNki5HP9sWgLOZR6rE3JfVNyp4Jh7CZaShw5XyYRsLM1+z9z2SIAFnOmbbC86VoqEU7Wd2F9TlJ/zJP4vR9rH2wi7Obtus4MBwoxH3dMw61la7ficBMCvdd1wC7rftSnV0jPGnCsufqcKcZmSg3HM22bBtlUNnIq7/ynqzZ7+pdOwv+fucyGEm//akkxkxntAxOvDY2plZ3ZTV5n7MJaH75bKTPDMgLXVszkxvpkbLb4TqFmKvD+rnBMTnaGMMxOZvly3hcfuvnPxcHl7Y8+YmpmYQ3RKlz1foXzZ0rD/AwMAVlA4IB4CAABwDwCdASpQAFAAPok6lUelI6IhL1auwKARCWgAyJAhz7eq/ihjJ3OI5P5V3p3mJ/ar10vRB/298A5832YAmiLa0mutah4B6eOgBjpWeAPuDnTJNdrbbJGDv05E/kTVtFErBAMEp9Sdw2xY7Adta5Ru6IDt1ffejnz6tUPNbgJLZIFBZHAA/vz4VF/Qshf/4bK2z23dL/pV2OnhttId5Jomv/wz7rSmfFojRqvMm0mkOsKUbevn1KAeivy2swXV7qG8yi6ZCW/0Be54wB4Y5ZvU3j/AJTo2YSp/T5twW6LjpC9FfJJ99yn0GjUbICt2Rs1Fh+k7CQ5ze4+AHDqQ+1vxZ+itKOvFDB6CfDAqpTpqtdPaJlqFvq1bwvulRC44jnvPvhuyS04lhAJYP2iOQEh5lydE+sXpBfX+N3RCMY4KNY/8ubEOok/6+so2trq8Tc67BTprcrZ5sy0gYjn7XL5Jceobj+a8X+qaXc31i61vBeUpVO1eHth1iA9BXz3QBrRY0ahjBc3Fl9J8ouV6/svohQze+Xv4r47H18ryTUSTVDWnw6TuHgfYgtZQM98u1OqzLlB3Jz9AjlflcsnuakRtmeMGkkxiVuoPb3S36krL+oTNPGlyL6TcUTgeLl0ZZPRGdlmCbnR9JcEnKOqK7KwVYegLD5vlXrZwACMgve4dHJ8OEkf9ppwm6OGGGwG/1uHEJeN1zIOxgTCxqAAAAA=="
|
||||
aria-hidden="true"
|
||||
title="Cap logo"
|
||||
alt="Cap logo"
|
||||
/>
|
||||
<span>Cap</span>
|
||||
</a>
|
||||
<p class="date">{{TIME}}</p>
|
||||
</footer>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@cap.js/widget@0.1.17"></script>
|
||||
<script>
|
||||
window.CAP_CUSTOM_FETCH = function (url, options) {
|
||||
console.log(url);
|
||||
if (url.endsWith("/challenge")) {
|
||||
return {
|
||||
json: () => {
|
||||
return window.CAP_CHALLENGE;
|
||||
},
|
||||
};
|
||||
}
|
||||
return fetch(url, options);
|
||||
};
|
||||
|
||||
const widget = document.querySelector("cap-widget");
|
||||
|
||||
widget.addEventListener("solve", (event) => {
|
||||
document.querySelector("h2").innerText =
|
||||
"Continuing to your destination...";
|
||||
|
||||
document.cookie = `__cap_clearance=${
|
||||
event.detail.token
|
||||
}; path=/; max-age=${
|
||||
window.TOKEN_VALIDITY_HOURS * 3_600
|
||||
}; SameSite=Strict`;
|
||||
|
||||
setTimeout(() => {
|
||||
location.reload();
|
||||
}, 300);
|
||||
});
|
||||
|
||||
widget.solve();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -51,10 +51,7 @@ export default withMermaid({
|
||||
},
|
||||
],
|
||||
["meta", { property: "og:url", content: "https://capjs.js.org" }],
|
||||
[
|
||||
"meta",
|
||||
{ property: "og:image", content: "https://capjs.js.org/logo.png" },
|
||||
],
|
||||
["meta", { property: "og:image", content: "https://capjs.js.org/logo.png" }],
|
||||
["meta", { name: "twitter:card", content: "summary_large_image" }],
|
||||
[
|
||||
"meta",
|
||||
@@ -71,10 +68,7 @@ export default withMermaid({
|
||||
"Lightweight, privacy-first, and designed to put you first. Switch from reCAPTCHA in minutes.",
|
||||
},
|
||||
],
|
||||
[
|
||||
"meta",
|
||||
{ name: "twitter:image", content: "https://capjs.js.org/logo.png" },
|
||||
],
|
||||
["meta", { name: "twitter:image", content: "https://capjs.js.org/logo.png" }],
|
||||
[
|
||||
"meta",
|
||||
{
|
||||
@@ -206,8 +200,7 @@ export default withMermaid({
|
||||
|
||||
footer: {
|
||||
message: "Built in Europe 🇪🇺<br>Released under the Apache 2.0 License.",
|
||||
copyright:
|
||||
"Copyright © 2025-present <a href='https://tiago.zip' target='_blank'>Tiago</a>",
|
||||
copyright: "Copyright © 2025-present <a href='https://tiago.zip' target='_blank'>Tiago</a>",
|
||||
},
|
||||
},
|
||||
markdown: {
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
Want to use Cap without the standalone server and with a different language? Here are some community-maintained libraries that might help. If you want to add a library, feel free to open a pull request!
|
||||
|
||||
**Note:** These libraries frequently do **not** support new features such as seeded challenges or instrumentation challenges.
|
||||
|
||||
## Widgets
|
||||
|
||||
These are wrappers around Cap's widget. They're usually not required as the default widget should work everywhere but can result in better development experience.
|
||||
|
||||
+5
-6
@@ -4,7 +4,7 @@ outline: deep
|
||||
|
||||
# Quickstart
|
||||
|
||||
Cap is a modern, lightweight, and self-hosted CAPTCHA alternative using SHA-256 proof-of-work.
|
||||
Cap is a modern, lightweight, and self-hosted CAPTCHA alternative using SHA-256 proof-of-work and instrumentation challenges.
|
||||
|
||||
Unlike traditional CAPTCHAs, Cap's fast, unobtrusive, has no telemetry or tracking, and uses accessible proof-of-work instead of annoying visual puzzles.
|
||||
|
||||
@@ -32,7 +32,6 @@ Using `cdn.jsdelivr.net` is optional. If preferred, you can self-host the script
|
||||
|
||||
:::
|
||||
|
||||
|
||||
Next, you can either add the widget component directly to your code:
|
||||
|
||||
```html
|
||||
@@ -59,12 +58,12 @@ You can also use get a token programmatically without displaying the widget by u
|
||||
|
||||
```js
|
||||
const cap = new Cap({
|
||||
apiEndpoint: "/api/"
|
||||
apiEndpoint: "/api/",
|
||||
});
|
||||
const solution = await cap.solve();
|
||||
|
||||
// you can attach event listeners to track progress
|
||||
cap.addEventListener("progress", (event) => {
|
||||
cap.addEventListener("progress", (event) => {
|
||||
console.log(`Solving... ${event.detail.progress}% done`);
|
||||
});
|
||||
|
||||
@@ -75,6 +74,6 @@ console.log(solution.token);
|
||||
|
||||
Cap is fully self-hosted, so you'll need to start a server exposing an API for Cap's protocol.
|
||||
|
||||
For most use cases, we recommend using the Docker **[Standalone server](./standalone/index.md)**, as it exposes a simple HTTP API and provides a simple web dashboard with analytics.
|
||||
For most use cases, **we recommend using the Docker [Standalone server](./standalone/index.md)**, as it exposes a simple HTTP API and provides a simple web dashboard with analytics. It also supports our JavaScript instrumentation challenges, which force bots to use real browsers, along with optional headless browser detection.
|
||||
|
||||
However, if you can't use Docker or need a more lightweight solution, you can use the [server library](./server.md) on Node and Bun or a [community library](./community.md) in other languages.
|
||||
However, if you can't use Docker or need a more lightweight solution, you can use the [server library](./server.md) on Node and Bun or a [community library](./community.md) in other languages.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# M2M
|
||||
|
||||
`@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.
|
||||
`@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**.
|
||||
|
||||
This package does not bypass any actual proof-of-work.
|
||||
This package does not bypass any actual proof-of-work. **It does not support instrumentation challenges.**
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -22,7 +22,7 @@ console.log(
|
||||
c: 50, // challenge count
|
||||
s: 32, // salt size
|
||||
d: 4, // difficulty
|
||||
})
|
||||
}),
|
||||
);
|
||||
```
|
||||
|
||||
@@ -55,4 +55,4 @@ The 2nd argument is optional but can always be provided. It's always an object.
|
||||
|
||||
- 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
|
||||
- For **seeded challenges only**, it is used to specify the number of solutions to generate, the size of the challenges, and the difficulty
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Cap Standalone
|
||||
|
||||
Cap Standalone is the default way of self-hosting Cap's backend. It provides a simple HTTP API for the widget to use and a siteverify endpoint compatible with hCaptcha or reCAPTCHA's, along with the ability to use multiple site keys.
|
||||
Cap Standalone is the default way of self-hosting Cap's backend along with instrumentation challenges. It provides an API for the widget and a siteverify endpoint compatible with hCaptcha or reCAPTCHA's, along with the ability to use multiple site keys.
|
||||
|
||||
## Installation
|
||||
|
||||
|
||||
@@ -62,4 +62,10 @@ We recommend keeping this to the default value, which is `sqlite://./.data/db.sq
|
||||
|
||||
## Error messages
|
||||
|
||||
Error messages are redacted by default and instead logged to the console. To disable error logging, set `DISABLE_ERROR_LOGGING=true`. To disable error message redaction, set `SHOW_ERRORS=true`.
|
||||
Error messages are redacted by default and instead logged to the console. To disable error logging, set `DISABLE_ERROR_LOGGING=true`. To disable error message redaction, set `SHOW_ERRORS=true`.
|
||||
|
||||
## Instrumentation challenges
|
||||
|
||||
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"
|
||||
@@ -0,0 +1,67 @@
|
||||
---
|
||||
sidebar: false
|
||||
editLink: false
|
||||
prev: false
|
||||
next: false
|
||||
footer: false
|
||||
---
|
||||
|
||||
# Troubleshooting
|
||||
|
||||
This verification can fail for various reasons, not solely due to bot activity. If you're having trouble completing verification, follow these steps to resolve the issue.
|
||||
|
||||
## 1. Try incognito or private mode
|
||||
|
||||
Open your browser's incognito or private mode to rule out issues caused by extensions or cached data.
|
||||
|
||||
- **Chrome / Edge:** Press `Ctrl+Shift+N` (Windows) or `Cmd+Shift+N` (Mac)
|
||||
- **Firefox:** Press `Ctrl+Shift+P` (Windows) or `Cmd+Shift+P` (Mac)
|
||||
- **Safari:** Go to **File → New Private Window**
|
||||
|
||||
## 2. Disable your browser extensions
|
||||
|
||||
Some extensions may interfere with the verification process. Try disabling them temporarily:
|
||||
|
||||
1. Open your browser's extensions or add-ons settings
|
||||
2. Temporarily disable **all** extensions
|
||||
3. Reload the page and try again
|
||||
|
||||
If this fixes the issue, re-enable extensions one by one to find the culprit.
|
||||
|
||||
## 3. Try a different browser or device
|
||||
|
||||
The issue may be specific to your current browser. Switch to another browser or device to test.
|
||||
|
||||
- Try **Chrome**, **Firefox**, **Edge**, or **Safari**
|
||||
- Note: **Internet Explorer is not supported** — use a modern browser instead
|
||||
- If possible, try on a completely different device (e.g., your phone)
|
||||
|
||||
## 4. Update your browser
|
||||
|
||||
An outdated browser can cause verification to fail.
|
||||
|
||||
1. Open your browser's menu
|
||||
2. Go to **Help → About** (or similar)
|
||||
3. Install any available updates and restart your browser
|
||||
|
||||
## 5. Switch to a different network
|
||||
|
||||
Your current network may have restrictions that interfere with verification.
|
||||
|
||||
- Connect to a **different Wi-Fi network**
|
||||
- Try using a **mobile hotspot** from your phone
|
||||
- If you're on a corporate or school network, those often have strict filtering that can block verification
|
||||
|
||||
## 6. Close any automated browser sessions
|
||||
|
||||
If you're using a browser controlled by automation software (such as Selenium, Puppeteer, or Playwright), verification will be blocked.
|
||||
|
||||
1. **Fully close** the automated browser session
|
||||
2. Open the page in a **regular, manually-operated browser**
|
||||
3. Complete the verification there
|
||||
|
||||
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).
|
||||
+11
-6
@@ -34,6 +34,7 @@ import Cap from '@cap.js/widget';
|
||||
</script>
|
||||
<script src="https://<server url>/assets/widget.js"></script>
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
You can now use the `<cap-widget>` component in your HTML.
|
||||
@@ -48,6 +49,7 @@ The following attributes are supported:
|
||||
|
||||
- `data-cap-api-endpoint`: API endpoint (required if not using custom fetch)
|
||||
- `data-cap-worker-count`: Number of workers to use (defaults to `navigator.hardwareConcurrency || 8`)
|
||||
- `data-cap-troubleshooting-url`: Custom URL for the "Troubleshooting" link shown when verification is blocked (defaults to `https://capjs.js.org/guide/troubleshooting.html`). See [Troubleshooting](/guide/troubleshooting.md)
|
||||
- `onsolve=""`: Event listener for the `solve` event
|
||||
- [i18n attributes](#i18n)
|
||||
|
||||
@@ -66,13 +68,14 @@ widget.addEventListener("solve", function (e) {
|
||||
Alternatively, you can use `onsolve=""` directly within the widget or wrap the widget in a `<form></form>` (where Cap will automatically submit the token alongside other form data. for this, it'll create a hidden field with name set to its `data-cap-hidden-field-name` attribute or `cap-token`).
|
||||
|
||||
## Invisible mode
|
||||
|
||||
You can use `new Cap({ ... })` in your client-side JavaScript to create a new Cap instance and use the `solve()` method to solve the challenge. This is helpful for situations where you don't want the Cap widget to be visible but still want security, e.g. on a social media app when posting something.
|
||||
|
||||
```js
|
||||
import Cap from '@cap.js/widget';
|
||||
import Cap from "@cap.js/widget";
|
||||
|
||||
const cap = new Cap({
|
||||
apiEndpoint: '/api/cap/'
|
||||
apiEndpoint: "/api/cap/",
|
||||
});
|
||||
|
||||
const token = await cap.solve();
|
||||
@@ -99,11 +102,12 @@ You can change the text on each label of the widget by setting the `data-cap-i18
|
||||
data-cap-i18n-initial-state="I'm a human"
|
||||
data-cap-i18n-solved-label="I'm a human"
|
||||
data-cap-i18n-error-label="Error"
|
||||
data-cap-i18n-troubleshooting-label="Troubleshooting"
|
||||
data-cap-i18n-wasm-disabled="Enable WASM for significantly faster solving"
|
||||
></cap-widget>
|
||||
```
|
||||
|
||||
`verify-aria-label`, `verifying-aria-label`, `verified-aria-label`, `error-aria-label` are also supported for screen readers.
|
||||
`verify-aria-label`, `verifying-aria-label`, `verified-aria-label`, `error-aria-label` are also supported for screen readers. `troubleshooting-label` controls the text of the "Troubleshooting" link shown when a user is blocked by instrumentation (defaults to "Troubleshooting").
|
||||
|
||||
## Customizing the widget
|
||||
|
||||
@@ -142,8 +146,9 @@ cap-widget {
|
||||
--cap-checkbox-border-radius: 6px;
|
||||
--cap-checkbox-background: #fafafa91;
|
||||
--cap-checkbox-margin: 2px;
|
||||
--cap-font: system, -apple-system, "BlinkMacSystemFont", ".SFNSText-Regular", "San Francisco",
|
||||
"Roboto", "Segoe UI", "Helvetica Neue", "Lucida Grande", "Ubuntu", "arial", sans-serif;
|
||||
--cap-font:
|
||||
system, -apple-system, "BlinkMacSystemFont", ".SFNSText-Regular", "San Francisco", "Roboto",
|
||||
"Segoe UI", "Helvetica Neue", "Lucida Grande", "Ubuntu", "arial", sans-serif;
|
||||
--cap-spinner-color: #000;
|
||||
--cap-spinner-background-color: #eee;
|
||||
--cap-spinner-thickness: 5px;
|
||||
@@ -156,4 +161,4 @@ cap-widget {
|
||||
|
||||
## Types
|
||||
|
||||
Cap's widget is fully typed. You can find the type definitions in the `cap.d.ts` file.
|
||||
Cap's widget is fully typed. You can find the type definitions in the `cap.d.ts` file.
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"@elysiajs/swagger": "^1.3.1",
|
||||
"elysia": "^1.4.12",
|
||||
"elysia-rate-limit": "4.4.0",
|
||||
"javascript-obfuscator": "^4.1.1",
|
||||
},
|
||||
"devDependencies": {
|
||||
"bun-types": "latest",
|
||||
@@ -28,6 +29,16 @@
|
||||
|
||||
"@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=="],
|
||||
|
||||
"@inversifyjs/common": ["@inversifyjs/common@1.3.3", "", {}, "sha512-ZH0wrgaJwIo3s9gMCDM2wZoxqrJ6gB97jWXncROfYdqZJv8f3EkqT57faZqN5OTeHWgtziQ6F6g3L8rCvGceCw=="],
|
||||
|
||||
"@inversifyjs/core": ["@inversifyjs/core@1.3.4", "", { "dependencies": { "@inversifyjs/common": "1.3.3", "@inversifyjs/reflect-metadata-utils": "0.2.3" } }, "sha512-gCCmA4BdbHEFwvVZ2elWgHuXZWk6AOu/1frxsS+2fWhjEk2c/IhtypLo5ytSUie1BCiT6i9qnEo4bruBomQsAA=="],
|
||||
|
||||
"@inversifyjs/reflect-metadata-utils": ["@inversifyjs/reflect-metadata-utils@0.2.3", "", { "peerDependencies": { "reflect-metadata": "0.2.2" } }, "sha512-d3D0o9TeSlvaGM2I24wcNw/Aj3rc4OYvHXOKDC09YEph5fMMiKd6fq1VTQd9tOkDNWvVbw+cnt45Wy9P/t5Lvw=="],
|
||||
|
||||
"@javascript-obfuscator/escodegen": ["@javascript-obfuscator/escodegen@2.3.1", "", { "dependencies": { "@javascript-obfuscator/estraverse": "^5.3.0", "esprima": "^4.0.1", "esutils": "^2.0.2", "optionator": "^0.8.1" }, "optionalDependencies": { "source-map": "~0.6.1" } }, "sha512-Z0HEAVwwafOume+6LFXirAVZeuEMKWuPzpFbQhCEU9++BMz0IwEa9bmedJ+rMn/IlXRBID9j3gQ0XYAa6jM10g=="],
|
||||
|
||||
"@javascript-obfuscator/estraverse": ["@javascript-obfuscator/estraverse@5.4.0", "", {}, "sha512-CZFX7UZVN9VopGbjTx4UXaXsi9ewoM1buL0kY7j1ftYdSs7p2spv9opxFjHlQ/QGTgh4UqufYqJJ0WKLml7b6w=="],
|
||||
|
||||
"@scalar/openapi-types": ["@scalar/openapi-types@0.1.1", "", {}, "sha512-NMy3QNk6ytcCoPUGJH0t4NNr36OWXgZhA3ormr3TvhX1NDgoF95wFyodGVH8xiHeUyn2/FxtETm8UBLbB5xEmg=="],
|
||||
|
||||
"@scalar/themes": ["@scalar/themes@0.9.86", "", { "dependencies": { "@scalar/types": "0.1.7" } }, "sha512-QUHo9g5oSWi+0Lm1vJY9TaMZRau8LHg+vte7q5BVTBnu6NuQfigCaN+ouQ73FqIVd96TwMO6Db+dilK1B+9row=="],
|
||||
@@ -40,48 +51,252 @@
|
||||
|
||||
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
|
||||
|
||||
"@types/minimatch": ["@types/minimatch@3.0.5", "", {}, "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ=="],
|
||||
|
||||
"@types/node": ["@types/node@24.0.10", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA=="],
|
||||
|
||||
"@types/react": ["@types/react@19.1.11", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-lr3jdBw/BGj49Eps7EvqlUaoeA0xpj3pc0RoJkHpYaCHkVK7i28dKyImLQb3JVlqs3aYSXf7qYuWOW/fgZnTXQ=="],
|
||||
|
||||
"@types/validator": ["@types/validator@13.15.10", "", {}, "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA=="],
|
||||
|
||||
"@unhead/schema": ["@unhead/schema@1.11.20", "", { "dependencies": { "hookable": "^5.5.3", "zhead": "^2.2.4" } }, "sha512-0zWykKAaJdm+/Y7yi/Yds20PrUK7XabLe9c3IRcjnwYmSWY6z0Cr19VIs3ozCj8P+GhR+/TI2mwtGlueCEYouA=="],
|
||||
|
||||
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
||||
|
||||
"ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
|
||||
|
||||
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
|
||||
|
||||
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"array-differ": ["array-differ@3.0.0", "", {}, "sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg=="],
|
||||
|
||||
"array-union": ["array-union@2.1.0", "", {}, "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw=="],
|
||||
|
||||
"arrify": ["arrify@2.0.1", "", {}, "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug=="],
|
||||
|
||||
"assert": ["assert@2.1.0", "", { "dependencies": { "call-bind": "^1.0.2", "is-nan": "^1.3.2", "object-is": "^1.1.5", "object.assign": "^4.1.4", "util": "^0.12.5" } }, "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw=="],
|
||||
|
||||
"atomically": ["atomically@2.1.1", "", { "dependencies": { "stubborn-fs": "^2.0.0", "when-exit": "^2.1.4" } }, "sha512-P4w9o2dqARji6P7MHprklbfiArZAWvo07yW7qs3pdljb3BWr12FIB7W+p0zJiuiVsUpRO0iZn1kFFcpPegg0tQ=="],
|
||||
|
||||
"available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
|
||||
|
||||
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||
|
||||
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.0", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-u8X0thhx+yJ0KmkxuEo9HAtdfgCBaM/aI9K90VQcQioAmkVp3SG3FkwWGibUFz3WdXAdcsqOcbU40lK7tbHdkQ=="],
|
||||
|
||||
"call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="],
|
||||
|
||||
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||
|
||||
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
|
||||
|
||||
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||
|
||||
"chance": ["chance@1.1.13", "", {}, "sha512-V6lQCljcLznE7tUYUM9EOAnnKXbctE6j/rdQkYOHIWbfGQbrzTsAXNW9CdU5XCo4ArXQCj/rb6HgxPlmGJcaUg=="],
|
||||
|
||||
"char-regex": ["char-regex@1.0.2", "", {}, "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw=="],
|
||||
|
||||
"charenc": ["charenc@0.0.2", "", {}, "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA=="],
|
||||
|
||||
"class-validator": ["class-validator@0.14.3", "", { "dependencies": { "@types/validator": "^13.15.3", "libphonenumber-js": "^1.11.1", "validator": "^13.15.20" } }, "sha512-rXXekcjofVN1LTOSw+u4u9WXVEUvNBVjORW154q/IdmYWy1nMbOU9aNtZB0t8m+FJQ9q91jlr2f9CwwUFdFMRA=="],
|
||||
|
||||
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||
|
||||
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||
|
||||
"commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="],
|
||||
|
||||
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
|
||||
|
||||
"conf": ["conf@15.0.2", "", { "dependencies": { "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "atomically": "^2.0.3", "debounce-fn": "^6.0.0", "dot-prop": "^10.0.0", "env-paths": "^3.0.0", "json-schema-typed": "^8.0.1", "semver": "^7.7.2", "uint8array-extras": "^1.5.0" } }, "sha512-JBSrutapCafTrddF9dH3lc7+T2tBycGF4uPkI4Js+g4vLLEhG6RZcFi3aJd5zntdf5tQxAejJt8dihkoQ/eSJw=="],
|
||||
|
||||
"cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="],
|
||||
|
||||
"crypt": ["crypt@0.0.2", "", {}, "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow=="],
|
||||
|
||||
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||
|
||||
"debounce-fn": ["debounce-fn@6.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-rBMW+F2TXryBwB54Q0d8drNEI+TfoS9JpNTAoVpukbWEhjXQq4rySFYLaqXMFXwdv61Zb2OHtj5bviSoimqxRQ=="],
|
||||
|
||||
"debug": ["debug@4.3.4", "", { "dependencies": { "ms": "2.1.2" } }, "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ=="],
|
||||
|
||||
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
|
||||
|
||||
"define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="],
|
||||
|
||||
"define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
|
||||
|
||||
"dot-prop": ["dot-prop@10.1.0", "", { "dependencies": { "type-fest": "^5.0.0" } }, "sha512-MVUtAugQMOff5RnBy2d9N31iG0lNwg1qAoAOn7pOK5wf94WIaE3My2p3uwTQuvS2AcqchkcR3bHByjaM0mmi7Q=="],
|
||||
|
||||
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||
|
||||
"elysia": ["elysia@1.4.12", "", { "dependencies": { "cookie": "^1.0.2", "exact-mirror": "0.2.2", "fast-decode-uri-component": "^1.0.1" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-wbd0BkrobsjWSloIfYeF3f7G7rR0UWMa6tuLUhf6ZvwjiCEX3FVfhDsM+KaqqRRxkZpPDw42q4yIZlBLyE32ww=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="],
|
||||
|
||||
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||
|
||||
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||
|
||||
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
||||
|
||||
"eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
|
||||
|
||||
"eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
|
||||
|
||||
"esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
|
||||
|
||||
"esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
|
||||
|
||||
"estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
|
||||
|
||||
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
|
||||
|
||||
"exact-mirror": ["exact-mirror@0.2.2", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-CrGe+4QzHZlnrXZVlo/WbUZ4qQZq8C0uATQVGVgXIrNXgHDBBNFD1VRfssRA2C9t3RYvh3MadZSdg2Wy7HBoQA=="],
|
||||
|
||||
"fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="],
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||
|
||||
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
|
||||
|
||||
"fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
|
||||
|
||||
"fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="],
|
||||
|
||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||
|
||||
"generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="],
|
||||
|
||||
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||
|
||||
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
||||
|
||||
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||
|
||||
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
||||
|
||||
"has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="],
|
||||
|
||||
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
||||
|
||||
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
|
||||
|
||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||
|
||||
"hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="],
|
||||
|
||||
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||
|
||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||
|
||||
"inversify": ["inversify@6.1.4", "", { "dependencies": { "@inversifyjs/common": "1.3.3", "@inversifyjs/core": "1.3.4" } }, "sha512-PbxrZH/gTa1fpPEEGAjJQzK8tKMIp5gRg6EFNJlCtzUcycuNdmhv3uk5P8Itm/RIjgHJO16oQRLo9IHzQN51bA=="],
|
||||
|
||||
"is-arguments": ["is-arguments@1.2.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA=="],
|
||||
|
||||
"is-buffer": ["is-buffer@1.1.6", "", {}, "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="],
|
||||
|
||||
"is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="],
|
||||
|
||||
"is-generator-function": ["is-generator-function@1.1.2", "", { "dependencies": { "call-bound": "^1.0.4", "generator-function": "^2.0.0", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA=="],
|
||||
|
||||
"is-nan": ["is-nan@1.3.2", "", { "dependencies": { "call-bind": "^1.0.0", "define-properties": "^1.1.3" } }, "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w=="],
|
||||
|
||||
"is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="],
|
||||
|
||||
"is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="],
|
||||
|
||||
"javascript-obfuscator": ["javascript-obfuscator@4.2.2", "", { "dependencies": { "@javascript-obfuscator/escodegen": "2.3.1", "@javascript-obfuscator/estraverse": "5.4.0", "acorn": "8.15.0", "assert": "2.1.0", "chalk": "4.1.2", "chance": "1.1.13", "class-validator": "0.14.3", "commander": "12.1.0", "conf": "15.0.2", "eslint-scope": "8.4.0", "eslint-visitor-keys": "4.2.1", "fast-deep-equal": "3.1.3", "inversify": "6.1.4", "js-string-escape": "1.0.1", "md5": "2.3.0", "mkdirp": "3.0.1", "multimatch": "5.0.0", "process": "0.11.10", "reflect-metadata": "0.2.2", "source-map-support": "0.5.21", "string-template": "1.0.0", "stringz": "2.1.0", "tslib": "2.8.1" }, "bin": { "javascript-obfuscator": "bin/javascript-obfuscator" } }, "sha512-+7oXAUnFCA6vS0omIGHcWpSr67dUBIF7FKGYSXyzxShSLqM6LBgdugWKFl0XrYtGWyJMGfQR5F4LL85iCefkRA=="],
|
||||
|
||||
"js-string-escape": ["js-string-escape@1.0.1", "", {}, "sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg=="],
|
||||
|
||||
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||
|
||||
"json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="],
|
||||
|
||||
"levn": ["levn@0.3.0", "", { "dependencies": { "prelude-ls": "~1.1.2", "type-check": "~0.3.2" } }, "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA=="],
|
||||
|
||||
"libphonenumber-js": ["libphonenumber-js@1.12.38", "", {}, "sha512-vwzxmasAy9hZigxtqTbFEwp8ZdZ975TiqVDwj5bKx5sR+zi5ucUQy9mbVTkKM9GzqdLdxux/hTw2nmN5J7POMA=="],
|
||||
|
||||
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||
|
||||
"md5": ["md5@2.3.0", "", { "dependencies": { "charenc": "0.0.2", "crypt": "0.0.2", "is-buffer": "~1.1.6" } }, "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g=="],
|
||||
|
||||
"mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="],
|
||||
|
||||
"minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
|
||||
|
||||
"mkdirp": ["mkdirp@3.0.1", "", { "bin": { "mkdirp": "dist/cjs/src/bin.js" } }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="],
|
||||
|
||||
"ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="],
|
||||
|
||||
"multimatch": ["multimatch@5.0.0", "", { "dependencies": { "@types/minimatch": "^3.0.3", "array-differ": "^3.0.0", "array-union": "^2.1.0", "arrify": "^2.0.1", "minimatch": "^3.0.4" } }, "sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA=="],
|
||||
|
||||
"nanoid": ["nanoid@5.1.5", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw=="],
|
||||
|
||||
"object-is": ["object-is@1.1.6", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1" } }, "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q=="],
|
||||
|
||||
"object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="],
|
||||
|
||||
"object.assign": ["object.assign@4.1.7", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0", "has-symbols": "^1.1.0", "object-keys": "^1.1.1" } }, "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw=="],
|
||||
|
||||
"openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="],
|
||||
|
||||
"optionator": ["optionator@0.8.3", "", { "dependencies": { "deep-is": "~0.1.3", "fast-levenshtein": "~2.0.6", "levn": "~0.3.0", "prelude-ls": "~1.1.2", "type-check": "~0.3.2", "word-wrap": "~1.2.3" } }, "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA=="],
|
||||
|
||||
"pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="],
|
||||
|
||||
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
|
||||
|
||||
"prelude-ls": ["prelude-ls@1.1.2", "", {}, "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w=="],
|
||||
|
||||
"process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="],
|
||||
|
||||
"reflect-metadata": ["reflect-metadata@0.2.2", "", {}, "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q=="],
|
||||
|
||||
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
||||
|
||||
"safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="],
|
||||
|
||||
"semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
||||
|
||||
"set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="],
|
||||
|
||||
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||
|
||||
"source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],
|
||||
|
||||
"string-template": ["string-template@1.0.0", "", {}, "sha512-SLqR3GBUXuoPP5MmYtD7ompvXiG87QjT6lzOszyXjTM86Uu7At7vNnt2xgyTLq5o9T4IxTYFyGxcULqpsmsfdg=="],
|
||||
|
||||
"stringz": ["stringz@2.1.0", "", { "dependencies": { "char-regex": "^1.0.2" } }, "sha512-KlywLT+MZ+v0IRepfMxRtnSvDCMc3nR1qqCs3m/qIbSOWkNZYT8XHQA31rS3TnKp0c5xjZu3M4GY/2aRKSi/6A=="],
|
||||
|
||||
"strtok3": ["strtok3@10.3.1", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-3JWEZM6mfix/GCJBBUrkA8p2Id2pBkyTkVCJKto55w080QBKZ+8R171fGrbiSp+yMO/u6F8/yUh7K4V9K+YCnw=="],
|
||||
|
||||
"stubborn-fs": ["stubborn-fs@2.0.0", "", { "dependencies": { "stubborn-utils": "^1.0.1" } }, "sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA=="],
|
||||
|
||||
"stubborn-utils": ["stubborn-utils@1.0.2", "", {}, "sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg=="],
|
||||
|
||||
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||
|
||||
"tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="],
|
||||
|
||||
"token-types": ["token-types@6.0.3", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-IKJ6EzuPPWtKtEIEPpIdXv9j5j2LGJEYk0CKY2efgKoYKLBiZdh6iQkLVBow/CB3phyWAWCyk+bZeaimJn6uRQ=="],
|
||||
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"type-check": ["type-check@0.3.2", "", { "dependencies": { "prelude-ls": "~1.1.2" } }, "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg=="],
|
||||
|
||||
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
|
||||
|
||||
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
|
||||
@@ -90,6 +305,16 @@
|
||||
|
||||
"undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
|
||||
|
||||
"util": ["util@0.12.5", "", { "dependencies": { "inherits": "^2.0.3", "is-arguments": "^1.0.4", "is-generator-function": "^1.0.7", "is-typed-array": "^1.1.3", "which-typed-array": "^1.1.2" } }, "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA=="],
|
||||
|
||||
"validator": ["validator@13.15.26", "", {}, "sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA=="],
|
||||
|
||||
"when-exit": ["when-exit@2.1.5", "", {}, "sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg=="],
|
||||
|
||||
"which-typed-array": ["which-typed-array@1.1.20", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg=="],
|
||||
|
||||
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
|
||||
|
||||
"zhead": ["zhead@2.2.4", "", {}, "sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag=="],
|
||||
|
||||
"zod": ["zod@3.25.67", "", {}, "sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw=="],
|
||||
@@ -100,6 +325,10 @@
|
||||
|
||||
"@tokenizer/inflate/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
|
||||
|
||||
"conf/uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="],
|
||||
|
||||
"dot-prop/type-fest": ["type-fest@5.4.4", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
+24
-24
@@ -1,59 +1,59 @@
|
||||
{
|
||||
"name": "cap-standalone",
|
||||
"version": "2.1.4",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"dev": "bun run --watch src/index.js",
|
||||
"docker:publish": "docker buildx build --platform linux/amd64,linux/arm64 -t tiago2/cap:latest . --push"
|
||||
},
|
||||
"version": "2.1.5",
|
||||
"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",
|
||||
"recaptcha",
|
||||
"security",
|
||||
"spam",
|
||||
"bots",
|
||||
"filtering",
|
||||
"turing-test"
|
||||
"turing-test",
|
||||
"verification"
|
||||
],
|
||||
"module": "src/index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"dev": "bun run --watch src/index.js",
|
||||
"docker:publish": "docker buildx build --platform linux/amd64,linux/arm64 -t tiago2/cap:latest . --push"
|
||||
},
|
||||
"dependencies": {
|
||||
"@cap.js/server": "^4.0.3",
|
||||
"@elysiajs/cors": "^1.4.0",
|
||||
"@elysiajs/static": "^1.4.4",
|
||||
"@elysiajs/swagger": "^1.3.1",
|
||||
"elysia": "^1.4.12",
|
||||
"elysia-rate-limit": "4.4.0"
|
||||
"elysia-rate-limit": "4.4.0",
|
||||
"javascript-obfuscator": "^4.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"bun-types": "latest"
|
||||
},
|
||||
"module": "src/index.js"
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+75
-14
@@ -520,7 +520,7 @@
|
||||
}
|
||||
|
||||
.config-actions {
|
||||
margin-top: 16px;
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
@@ -545,6 +545,7 @@
|
||||
.save-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
filter: grayscale(.3);
|
||||
}
|
||||
|
||||
.danger-section {
|
||||
@@ -916,7 +917,7 @@
|
||||
color: var(--ctp-text);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
|
||||
.about-logo {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
@@ -997,6 +998,75 @@
|
||||
color: black;
|
||||
}
|
||||
|
||||
.switch-field {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
grid-column: 1 / -1;
|
||||
padding-top: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.switch {
|
||||
position: relative;
|
||||
width: 40px;
|
||||
height: 22px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.switch-track {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: var(--ctp-surface1);
|
||||
border-radius: 11px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.switch-track::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 3px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: var(--ctp-overlay0);
|
||||
border-radius: 50%;
|
||||
transition:
|
||||
transform 0.2s,
|
||||
background 0.2s;
|
||||
}
|
||||
|
||||
.switch input:checked + .switch-track {
|
||||
background: var(--ctp-blue);
|
||||
}
|
||||
|
||||
.switch input:checked + .switch-track::after {
|
||||
transform: translateX(18px);
|
||||
background: var(--ctp-crust);
|
||||
}
|
||||
|
||||
.switch input:focus-visible + .switch-track {
|
||||
box-shadow: 0 0 0 2px rgba(137, 180, 250, 0.3);
|
||||
}
|
||||
|
||||
.switch-label {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--ctp-subtext1);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
@@ -1045,12 +1115,7 @@
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<div class="search-create">
|
||||
<input
|
||||
type="text"
|
||||
class="search-input"
|
||||
id="searchInput"
|
||||
placeholder="Find keys"
|
||||
/>
|
||||
<input type="text" class="search-input" id="searchInput" placeholder="Find keys" />
|
||||
<button class="create-btn" id="createKeyBtn">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
@@ -1083,12 +1148,8 @@
|
||||
class="icon icon-tabler icons-tabler-outline icon-tabler-hourglass-empty"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M6 20v-2a6 6 0 1 1 12 0v2a1 1 0 0 1 -1 1h-10a1 1 0 0 1 -1 -1"
|
||||
/>
|
||||
<path
|
||||
d="M6 4v2a6 6 0 1 0 12 0v-2a1 1 0 0 0 -1 -1h-10a1 1 0 0 0 -1 1"
|
||||
/>
|
||||
<path d="M6 20v-2a6 6 0 1 1 12 0v2a1 1 0 0 1 -1 1h-10a1 1 0 0 1 -1 -1" />
|
||||
<path d="M6 4v2a6 6 0 1 0 12 0v-2a1 1 0 0 0 -1 -1h-10a1 1 0 0 0 -1 1" />
|
||||
</svg>
|
||||
<h2>Select or create a key to see details</h2>
|
||||
</div>
|
||||
|
||||
@@ -30,7 +30,7 @@ const api = async (method, path, body) => {
|
||||
|
||||
return await (await fetch(`/server${path}`, opts)).json();
|
||||
} catch (e) {
|
||||
console.error("standalone:", e)
|
||||
console.error("standalone:", e);
|
||||
return { error: e.message };
|
||||
}
|
||||
};
|
||||
@@ -52,9 +52,7 @@ const formatRelative = (date) => {
|
||||
for (const [u, v] of Object.entries(ms)) {
|
||||
if (d >= v) {
|
||||
const val = Math.floor(d / v);
|
||||
return past
|
||||
? `${val} ${u}${val > 1 ? "s" : ""} ago`
|
||||
: `in ${val} ${u}${val > 1 ? "s" : ""}`;
|
||||
return past ? `${val} ${u}${val > 1 ? "s" : ""} ago` : `in ${val} ${u}${val > 1 ? "s" : ""}`;
|
||||
}
|
||||
}
|
||||
return past ? "just now" : "in a moment";
|
||||
@@ -75,8 +73,7 @@ const randKey = () => {
|
||||
|
||||
async function init() {
|
||||
if (!localStorage.getItem("cap_auth")) {
|
||||
document.cookie =
|
||||
"cap_authed=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
|
||||
document.cookie = "cap_authed=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -87,8 +84,7 @@ async function loadKeys() {
|
||||
keys = await api("GET", "/keys");
|
||||
if (keys.error?.includes?.("Unauthorized")) {
|
||||
localStorage.removeItem("cap_auth");
|
||||
document.cookie =
|
||||
"cap_authed=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
|
||||
document.cookie = "cap_authed=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
|
||||
location.reload();
|
||||
return;
|
||||
}
|
||||
@@ -100,9 +96,7 @@ async function loadKeys() {
|
||||
}
|
||||
|
||||
function renderKeysList(filter = "") {
|
||||
const filtered = keys.filter((k) =>
|
||||
k.name.toLowerCase().includes(filter.toLowerCase()),
|
||||
);
|
||||
const filtered = keys.filter((k) => k.name.toLowerCase().includes(filter.toLowerCase()));
|
||||
|
||||
if (filtered.length === 0) {
|
||||
keysList.innerHTML = `
|
||||
@@ -166,10 +160,7 @@ async function selectKey(siteKey) {
|
||||
const data = await api("GET", `/keys/${siteKey}`);
|
||||
document.querySelector("#detailPanel").style.opacity = "1";
|
||||
if (data.error) {
|
||||
showModal(
|
||||
"Error",
|
||||
`<div class="modal-body"><p>Failed to load key: ${data.error}</p></div>`,
|
||||
);
|
||||
showModal("Error", `<div class="modal-body"><p>Failed to load key: ${data.error}</p></div>`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -277,9 +268,23 @@ function renderKeyDetail() {
|
||||
<label for="cfgSaltSize">Salt size</label>
|
||||
<input type="number" id="cfgSaltSize" value="${key.config.saltSize}" min="7">
|
||||
</div>
|
||||
<div class="switch-field">
|
||||
<label class="switch">
|
||||
<input type="checkbox" id="cfgInstrumentation" ${key.config.instrumentation ? "checked" : ""}>
|
||||
<span class="switch-track"></span>
|
||||
</label>
|
||||
<label for="cfgInstrumentation" class="switch-label">Instrumentation challenges (experimental)</label>
|
||||
</div>
|
||||
<div class="switch-field" id="blockAutomatedBrowsersField" style="display: ${key.config.instrumentation ? "flex" : "none"}; align-items: normal;">
|
||||
<label class="switch">
|
||||
<input type="checkbox" id="cfgBlockAutomatedBrowsers" ${key.config.blockAutomatedBrowsers ? "checked" : ""}>
|
||||
<span class="switch-track"></span>
|
||||
</label>
|
||||
<label for="cfgBlockAutomatedBrowsers" class="switch-label">Attempt to block headless browsers<br><span style="font-size: 12px;color: var(--ctp-overlay1);font-weight: 400;line-height: 1.3;">This may cause issues with testing or agent browsers and is not entirely foolproof.</span></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-actions">
|
||||
<button class="save-btn" id="saveConfigBtn">Save changes</button>
|
||||
<button class="save-btn" id="saveConfigBtn" disabled>Save changes</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -331,12 +336,44 @@ function renderKeyDetail() {
|
||||
loadChartData(e.target.value);
|
||||
});
|
||||
|
||||
document
|
||||
.getElementById("saveConfigBtn")
|
||||
.addEventListener("click", saveConfig);
|
||||
document
|
||||
.getElementById("rotateSecretBtn")
|
||||
.addEventListener("click", rotateSecret);
|
||||
function checkDirty() {
|
||||
const name = document.getElementById("cfgName").value.trim();
|
||||
const difficulty = parseInt(document.getElementById("cfgDifficulty").value, 10);
|
||||
const challengeCount = parseInt(document.getElementById("cfgChallengeCount").value, 10);
|
||||
const saltSize = parseInt(document.getElementById("cfgSaltSize").value, 10);
|
||||
const instrumentation = document.getElementById("cfgInstrumentation").checked;
|
||||
const blockAutomatedBrowsers = document.getElementById("cfgBlockAutomatedBrowsers").checked;
|
||||
|
||||
const dirty =
|
||||
name !== key.name ||
|
||||
difficulty !== key.config.difficulty ||
|
||||
challengeCount !== key.config.challengeCount ||
|
||||
saltSize !== key.config.saltSize ||
|
||||
instrumentation !== key.config.instrumentation ||
|
||||
blockAutomatedBrowsers !== key.config.blockAutomatedBrowsers;
|
||||
|
||||
document.getElementById("saveConfigBtn").disabled = !dirty;
|
||||
}
|
||||
|
||||
for (const id of ["cfgName", "cfgDifficulty", "cfgChallengeCount", "cfgSaltSize"]) {
|
||||
document.getElementById(id).addEventListener("input", checkDirty);
|
||||
}
|
||||
|
||||
document.getElementById("cfgInstrumentation").addEventListener("change", (e) => {
|
||||
const blockField = document.getElementById("blockAutomatedBrowsersField");
|
||||
if (e.target.checked) {
|
||||
blockField.style.display = "flex";
|
||||
} else {
|
||||
blockField.style.display = "none";
|
||||
document.getElementById("cfgBlockAutomatedBrowsers").checked = false;
|
||||
}
|
||||
checkDirty();
|
||||
});
|
||||
|
||||
document.getElementById("cfgBlockAutomatedBrowsers").addEventListener("change", checkDirty);
|
||||
|
||||
document.getElementById("saveConfigBtn").addEventListener("click", saveConfig);
|
||||
document.getElementById("rotateSecretBtn").addEventListener("click", rotateSecret);
|
||||
document.getElementById("deleteKeyBtn").addEventListener("click", deleteKey);
|
||||
|
||||
renderChart(key.chartData);
|
||||
@@ -345,10 +382,7 @@ function renderKeyDetail() {
|
||||
async function loadChartData(duration) {
|
||||
document.getElementById("chartLoading").classList.add("visible");
|
||||
|
||||
const data = await api(
|
||||
"GET",
|
||||
`/keys/${selectedKey.siteKey}?chartDuration=${duration}`,
|
||||
);
|
||||
const data = await api("GET", `/keys/${selectedKey.siteKey}?chartDuration=${duration}`);
|
||||
|
||||
document.getElementById("chartLoading").classList.remove("visible");
|
||||
|
||||
@@ -466,15 +500,11 @@ async function saveConfig() {
|
||||
btn.disabled = true;
|
||||
|
||||
const name = document.getElementById("cfgName").value.trim();
|
||||
const difficulty = parseInt(
|
||||
document.getElementById("cfgDifficulty").value,
|
||||
10,
|
||||
);
|
||||
const challengeCount = parseInt(
|
||||
document.getElementById("cfgChallengeCount").value,
|
||||
10,
|
||||
);
|
||||
const difficulty = parseInt(document.getElementById("cfgDifficulty").value, 10);
|
||||
const challengeCount = parseInt(document.getElementById("cfgChallengeCount").value, 10);
|
||||
const saltSize = parseInt(document.getElementById("cfgSaltSize").value, 10);
|
||||
const instrumentation = document.getElementById("cfgInstrumentation").checked;
|
||||
const blockAutomatedBrowsers = document.getElementById("cfgBlockAutomatedBrowsers").checked;
|
||||
|
||||
if (!name || difficulty < 2 || challengeCount < 1 || saltSize < 7) {
|
||||
showModal(
|
||||
@@ -490,21 +520,25 @@ async function saveConfig() {
|
||||
difficulty,
|
||||
challengeCount,
|
||||
saltSize,
|
||||
instrumentation,
|
||||
blockAutomatedBrowsers,
|
||||
});
|
||||
|
||||
btn.disabled = false;
|
||||
|
||||
if (res.success) {
|
||||
await loadKeys();
|
||||
selectedKey.name = name;
|
||||
selectedKey.config = { difficulty, challengeCount, saltSize };
|
||||
selectedKey.config = {
|
||||
difficulty,
|
||||
challengeCount,
|
||||
saltSize,
|
||||
instrumentation,
|
||||
blockAutomatedBrowsers,
|
||||
};
|
||||
document.querySelector(".key-header h1").textContent = name;
|
||||
renderKeysList(searchInput.value);
|
||||
} else {
|
||||
showModal(
|
||||
"Error",
|
||||
`<div class="modal-body"><p>Failed to save configuration.</p></div>`,
|
||||
);
|
||||
showModal("Error", `<div class="modal-body"><p>Failed to save configuration.</p></div>`);
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -514,10 +548,7 @@ function rotateSecret() {
|
||||
"This will generate a new secret key. Your existing integrations will stop working until updated.",
|
||||
"Rotate",
|
||||
async () => {
|
||||
const res = await api(
|
||||
"POST",
|
||||
`/keys/${selectedKey.siteKey}/rotate-secret`,
|
||||
);
|
||||
const res = await api("POST", `/keys/${selectedKey.siteKey}/rotate-secret`);
|
||||
if (res.secretKey) {
|
||||
showModal(
|
||||
"Rotated secret key",
|
||||
@@ -532,10 +563,7 @@ function rotateSecret() {
|
||||
`,
|
||||
);
|
||||
} else {
|
||||
showModal(
|
||||
"Error",
|
||||
`<div class="modal-body"><p>Failed to rotate secret key.</p></div>`,
|
||||
);
|
||||
showModal("Error", `<div class="modal-body"><p>Failed to rotate secret key.</p></div>`);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -554,10 +582,7 @@ function deleteKey() {
|
||||
keyDetail.style.display = "none";
|
||||
await loadKeys();
|
||||
} else {
|
||||
showModal(
|
||||
"Error",
|
||||
`<div class="modal-body"><p>Failed to delete key.</p></div>`,
|
||||
);
|
||||
showModal("Error", `<div class="modal-body"><p>Failed to delete key.</p></div>`);
|
||||
}
|
||||
},
|
||||
true,
|
||||
@@ -601,8 +626,7 @@ function closeModal() {
|
||||
const overlay = document.querySelector(".modal-overlay");
|
||||
if (overlay) {
|
||||
overlay.style.opacity = "0";
|
||||
overlay.querySelector(".modal").style.transform =
|
||||
"scale(0.95) translateY(10px)";
|
||||
overlay.querySelector(".modal").style.transform = "scale(0.95) translateY(10px)";
|
||||
overlay.querySelector(".modal").style.filter = "blur(2px)";
|
||||
document.removeEventListener("keydown", escapeHandler);
|
||||
|
||||
@@ -616,13 +640,7 @@ function showModal(title, content) {
|
||||
createModal(title, content);
|
||||
}
|
||||
|
||||
function showConfirmModal(
|
||||
title,
|
||||
message,
|
||||
confirmText,
|
||||
onConfirm,
|
||||
isDanger = false,
|
||||
) {
|
||||
function showConfirmModal(title, message, confirmText, onConfirm, isDanger = false) {
|
||||
const modal = createModal(
|
||||
title,
|
||||
`
|
||||
@@ -710,10 +728,7 @@ function openCreateKeyModal(prefill = "") {
|
||||
await loadKeys();
|
||||
selectKey(res.siteKey);
|
||||
} else {
|
||||
showModal(
|
||||
"Error",
|
||||
`<div class="modal-body"><p>Failed to create key.</p></div>`,
|
||||
);
|
||||
showModal("Error", `<div class="modal-body"><p>Failed to create key.</p></div>`);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -802,8 +817,7 @@ async function openSettings() {
|
||||
await api("POST", "/logout", { session: token });
|
||||
if (currentHash.endsWith(token)) {
|
||||
localStorage.removeItem("cap_auth");
|
||||
document.cookie =
|
||||
"cap_authed=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
|
||||
document.cookie = "cap_authed=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
|
||||
location.reload();
|
||||
}
|
||||
});
|
||||
@@ -918,10 +932,7 @@ function openCreateApiKeyModal() {
|
||||
`,
|
||||
);
|
||||
} else {
|
||||
showModal(
|
||||
"Error",
|
||||
`<div class="modal-body"><p>Failed to create API key.</p></div>`,
|
||||
);
|
||||
showModal("Error", `<div class="modal-body"><p>Failed to create API key.</p></div>`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Vendored
+43
-40
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
@@ -8,8 +8,17 @@
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
font-family: InterVariable, -apple-system, BlinkMacSystemFont, Segoe UI,
|
||||
Roboto, Oxygen-Sans, Ubuntu, Cantarell, Helvetica Neue, sans-serif;
|
||||
font-family:
|
||||
InterVariable,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
Segoe UI,
|
||||
Roboto,
|
||||
Oxygen-Sans,
|
||||
Ubuntu,
|
||||
Cantarell,
|
||||
Helvetica Neue,
|
||||
sans-serif;
|
||||
}
|
||||
body {
|
||||
text-align: center;
|
||||
@@ -34,8 +43,7 @@
|
||||
margin-bottom: 28px;
|
||||
margin-top: 0;
|
||||
user-select: none;
|
||||
|
||||
|
||||
|
||||
animation: title-in 0.6s both;
|
||||
animation-delay: 0.1s;
|
||||
}
|
||||
@@ -117,7 +125,8 @@
|
||||
}
|
||||
|
||||
.password-wrapper:has(.password:focus) {
|
||||
box-shadow: inset 0 0 0 1px rgba(137, 180, 250, 1),
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(137, 180, 250, 1),
|
||||
0px 0px 0px 2px rgba(137, 180, 250, 1);
|
||||
transition: box-shadow 0.12s;
|
||||
}
|
||||
@@ -142,16 +151,18 @@
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
z-index: 12;
|
||||
transition: box-shadow 0.13s, opacity 0.2s;
|
||||
|
||||
|
||||
transition:
|
||||
box-shadow 0.13s,
|
||||
opacity 0.2s;
|
||||
|
||||
will-change: transform, filter, opacity;
|
||||
animation: title-in 0.5s both;
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
|
||||
.submit:focus {
|
||||
box-shadow: inset 0px 0.8px 0px -0.25px rgba(255, 255, 255, 0.2),
|
||||
box-shadow:
|
||||
inset 0px 0.8px 0px -0.25px rgba(255, 255, 255, 0.2),
|
||||
0px 0.5px 1.5px rgba(137, 180, 250, 0.25),
|
||||
0px 0px 0px 3.5px rgba(137, 180, 250, 0.5);
|
||||
outline: 0;
|
||||
@@ -167,8 +178,7 @@
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-top: 24px;
|
||||
|
||||
|
||||
|
||||
will-change: transform, filter, opacity;
|
||||
animation: title-in 0.5s both;
|
||||
animation-delay: 0.5s;
|
||||
@@ -192,7 +202,10 @@
|
||||
border-bottom-right-radius: calc(0.8rem - 2px);
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
transition: opacity .2s, margin-top .2s, color .2s;
|
||||
transition:
|
||||
opacity 0.2s,
|
||||
margin-top 0.2s,
|
||||
color 0.2s;
|
||||
}
|
||||
|
||||
@starting-style {
|
||||
@@ -234,11 +247,7 @@
|
||||
placeholder="•••••••••••••••••••••"
|
||||
class="password"
|
||||
/>
|
||||
<button
|
||||
title="Show/hide password"
|
||||
class="showhide-password"
|
||||
type="button"
|
||||
>
|
||||
<button title="Show/hide password" class="showhide-password" type="button">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
@@ -289,13 +298,9 @@
|
||||
>
|
||||
Report issues
|
||||
</a>
|
||||
<a href="https://capjs.js.org/" target="_blank" class="poweredby">
|
||||
Powered by Cap
|
||||
</a>
|
||||
<a href="https://capjs.js.org/" target="_blank" class="poweredby"> Powered by Cap </a>
|
||||
</p>
|
||||
</form>
|
||||
|
||||
|
||||
</body>
|
||||
<script>
|
||||
document.querySelector("input").focus();
|
||||
@@ -331,7 +336,7 @@
|
||||
token: session_token,
|
||||
expires: expires,
|
||||
hash: hashed_token,
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
document.querySelector("p.err").style.display = "none";
|
||||
@@ -345,22 +350,20 @@
|
||||
document.querySelector("input[name=password]").value = "";
|
||||
});
|
||||
|
||||
document
|
||||
.querySelector(".showhide-password")
|
||||
.addEventListener("click", () => {
|
||||
const passwordInput = document.querySelector("input[name=password]");
|
||||
const showhideButton = document.querySelector(".showhide-password");
|
||||
passwordInput.classList.toggle("show-password");
|
||||
document.querySelector(".showhide-password").addEventListener("click", () => {
|
||||
const passwordInput = document.querySelector("input[name=password]");
|
||||
const showhideButton = document.querySelector(".showhide-password");
|
||||
passwordInput.classList.toggle("show-password");
|
||||
|
||||
if (passwordInput.type === "password") {
|
||||
passwordInput.type = "text";
|
||||
passwordInput.placeholder = "correct horse battery staple";
|
||||
showhideButton.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-eye-off-icon lucide-eye-off"><path d="M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49"/><path d="M14.084 14.158a3 3 0 0 1-4.242-4.242"/><path d="M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143"/><path d="m2 2 20 20"/></svg>`;
|
||||
} else {
|
||||
passwordInput.type = "password";
|
||||
passwordInput.placeholder = "•••••••••••••••••••••";
|
||||
showhideButton.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-eye-icon lucide-eye"><path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"/><circle cx="12" cy="12" r="3"/></svg>`;
|
||||
}
|
||||
});
|
||||
if (passwordInput.type === "password") {
|
||||
passwordInput.type = "text";
|
||||
passwordInput.placeholder = "correct horse battery staple";
|
||||
showhideButton.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-eye-off-icon lucide-eye-off"><path d="M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49"/><path d="M14.084 14.158a3 3 0 0 1-4.242-4.242"/><path d="M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143"/><path d="m2 2 20 20"/></svg>`;
|
||||
} else {
|
||||
passwordInput.type = "password";
|
||||
passwordInput.placeholder = "•••••••••••••••••••••";
|
||||
showhideButton.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-eye-icon lucide-eye"><path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"/><circle cx="12" cy="12" r="3"/></svg>`;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</html>
|
||||
|
||||
Vendored
+16
-12
@@ -1,9 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Challenge Tester</title>
|
||||
<title>Challenge tester</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: system-ui, sans-serif;
|
||||
@@ -16,6 +16,17 @@
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.nav-link {
|
||||
display: inline-block;
|
||||
margin-bottom: 1rem;
|
||||
color: #555;
|
||||
font-size: 0.85rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
.nav-link:hover {
|
||||
color: #333;
|
||||
text-decoration: underline;
|
||||
}
|
||||
label,
|
||||
input,
|
||||
button {
|
||||
@@ -56,11 +67,7 @@
|
||||
<input type="text" id="keyIdInput" placeholder="e.g. 19d7e51172" />
|
||||
|
||||
<label for="secretKeyInput">Secret key:</label>
|
||||
<input
|
||||
type="password"
|
||||
id="secretKeyInput"
|
||||
placeholder="e.g. eOAdXZvmQecVrvh..."
|
||||
/>
|
||||
<input type="password" id="secretKeyInput" placeholder="e.g. eOAdXZvmQecVrvh..." />
|
||||
|
||||
<button id="startButton">Start</button>
|
||||
|
||||
@@ -97,8 +104,7 @@
|
||||
const secretKey = secretKeyInput.value.trim();
|
||||
|
||||
if (!keyId || !secretKey) {
|
||||
logsElement.textContent =
|
||||
"Error: Key ID and Secret Key are required.";
|
||||
logsElement.textContent = "Error: Key ID and Secret Key are required.";
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -110,9 +116,7 @@
|
||||
cap = new Cap({ apiEndpoint: `/${keyId}/` });
|
||||
cap.widget.style.display = "block";
|
||||
|
||||
cap.addEventListener("progress", (e) =>
|
||||
log(`Solving... ${e.detail.progress}%`)
|
||||
);
|
||||
cap.addEventListener("progress", (e) => log(`Solving... ${e.detail.progress}%`));
|
||||
cap.addEventListener("error", (e) => log(`ERROR: ${e.detail.error}`));
|
||||
|
||||
try {
|
||||
|
||||
+84
-87
@@ -4,112 +4,109 @@ import { cors } from "@elysiajs/cors";
|
||||
import { Elysia, file } from "elysia";
|
||||
|
||||
if (
|
||||
process.env.ENABLE_ASSETS_SERVER === "true" &&
|
||||
(process.env.WIDGET_VERSION === "latest" ||
|
||||
process.env.WASM_VERSION === "latest")
|
||||
process.env.ENABLE_ASSETS_SERVER === "true" &&
|
||||
(process.env.WIDGET_VERSION === "latest" || process.env.WASM_VERSION === "latest")
|
||||
) {
|
||||
console.warn(
|
||||
"📦 [asset server] using 'latest' version for assets is not recommended for production!\n make sure to pin it to a set version using the WIDGET_VERSION and WASM_VERSION env variables.",
|
||||
);
|
||||
console.warn(
|
||||
"📦 [asset server] using 'latest' version for assets is not recommended for production!\n make sure to pin it to a set version using the WIDGET_VERSION and WASM_VERSION env variables.",
|
||||
);
|
||||
}
|
||||
|
||||
const dataDir = process.env.DATA_PATH || "./.data";
|
||||
|
||||
const writeAtomic = async (filename, content) => {
|
||||
const finalPath = path.join(dataDir, filename);
|
||||
const tempPath = `${finalPath}.tmp`;
|
||||
|
||||
await fs.writeFile(tempPath, content);
|
||||
|
||||
await fs.rename(tempPath, finalPath);
|
||||
};
|
||||
|
||||
const updateCache = async () => {
|
||||
if (process.env.ENABLE_ASSETS_SERVER !== "true") return;
|
||||
if (process.env.ENABLE_ASSETS_SERVER !== "true") return;
|
||||
|
||||
const cacheConfigPath = path.join(dataDir, "assets-cache.json");
|
||||
let cacheConfig = {};
|
||||
const cacheConfigPath = path.join(dataDir, "assets-cache.json");
|
||||
let cacheConfig = {};
|
||||
|
||||
try {
|
||||
cacheConfig = JSON.parse(await fs.readFile(cacheConfigPath, "utf-8"));
|
||||
} catch {}
|
||||
try {
|
||||
cacheConfig = JSON.parse(await fs.readFile(cacheConfigPath, "utf-8"));
|
||||
} catch {}
|
||||
|
||||
const lastUpdate = cacheConfig.lastUpdate || 0;
|
||||
const currentTime = Date.now();
|
||||
const updateInterval = 1000 * 60 * 60 * 24; // 1 day
|
||||
const intervalExceeded = currentTime - lastUpdate > updateInterval;
|
||||
const lastUpdate = cacheConfig.lastUpdate || 0;
|
||||
const currentTime = Date.now();
|
||||
const updateInterval = 1000 * 60 * 60 * 24; // 1 day
|
||||
const intervalExceeded = currentTime - lastUpdate > updateInterval;
|
||||
|
||||
const WIDGET_VERSION = process.env.WIDGET_VERSION || "latest";
|
||||
const WASM_VERSION = process.env.WASM_VERSION || "latest";
|
||||
const WIDGET_VERSION = process.env.WIDGET_VERSION || "latest";
|
||||
const WASM_VERSION = process.env.WASM_VERSION || "latest";
|
||||
|
||||
if (!cacheConfig.versions) cacheConfig.versions = {};
|
||||
const versionsChanged = cacheConfig.versions.widget !== WIDGET_VERSION
|
||||
|| cacheConfig.versions.wasm !== WASM_VERSION;
|
||||
if (!cacheConfig.versions) cacheConfig.versions = {};
|
||||
const versionsChanged =
|
||||
cacheConfig.versions.widget !== WIDGET_VERSION || cacheConfig.versions.wasm !== WASM_VERSION;
|
||||
|
||||
if (!intervalExceeded && !versionsChanged) return;
|
||||
if (!intervalExceeded && !versionsChanged) return;
|
||||
|
||||
const CACHE_HOST = process.env.CACHE_HOST || "https://cdn.jsdelivr.net";
|
||||
const CACHE_HOST = process.env.CACHE_HOST || "https://cdn.jsdelivr.net";
|
||||
|
||||
try {
|
||||
const [widgetSource, floatingSource, wasmSource, wasmLoaderSource] =
|
||||
await Promise.all([
|
||||
fetch(`${CACHE_HOST}/npm/@cap.js/widget@${WIDGET_VERSION}`).then((r) =>
|
||||
r.text(),
|
||||
),
|
||||
fetch(
|
||||
`${CACHE_HOST}/npm/@cap.js/widget@${WIDGET_VERSION}/cap-floating.min.js`,
|
||||
).then((r) => r.text()),
|
||||
fetch(
|
||||
`${CACHE_HOST}/npm/@cap.js/wasm@${WASM_VERSION}/browser/cap_wasm_bg.wasm`,
|
||||
).then((r) => r.arrayBuffer()),
|
||||
fetch(
|
||||
`${CACHE_HOST}/npm/@cap.js/wasm@${WASM_VERSION}/browser/cap_wasm.min.js`,
|
||||
).then((r) => r.text()),
|
||||
]);
|
||||
try {
|
||||
const [widgetSource, floatingSource, wasmSource, wasmLoaderSource] = await Promise.all([
|
||||
fetch(`${CACHE_HOST}/npm/@cap.js/widget@${WIDGET_VERSION}`).then((r) => r.text()),
|
||||
fetch(`${CACHE_HOST}/npm/@cap.js/widget@${WIDGET_VERSION}/cap-floating.min.js`).then((r) =>
|
||||
r.text(),
|
||||
),
|
||||
fetch(`${CACHE_HOST}/npm/@cap.js/wasm@${WASM_VERSION}/browser/cap_wasm_bg.wasm`).then((r) =>
|
||||
r.arrayBuffer(),
|
||||
),
|
||||
fetch(`${CACHE_HOST}/npm/@cap.js/wasm@${WASM_VERSION}/browser/cap_wasm.min.js`).then((r) =>
|
||||
r.text(),
|
||||
),
|
||||
]);
|
||||
|
||||
cacheConfig.lastUpdate = currentTime;
|
||||
cacheConfig.versions.widget = WIDGET_VERSION;
|
||||
cacheConfig.versions.wasm = WASM_VERSION;
|
||||
await fs.writeFile(cacheConfigPath, JSON.stringify(cacheConfig));
|
||||
cacheConfig.lastUpdate = currentTime;
|
||||
cacheConfig.versions.widget = WIDGET_VERSION;
|
||||
cacheConfig.versions.wasm = WASM_VERSION;
|
||||
|
||||
await fs.writeFile(path.join(dataDir, "assets-widget.js"), widgetSource);
|
||||
await fs.writeFile(
|
||||
path.join(dataDir, "assets-floating.js"),
|
||||
floatingSource,
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(dataDir, "assets-cap_wasm_bg.wasm"),
|
||||
Buffer.from(wasmSource),
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(dataDir, "assets-cap_wasm.js"),
|
||||
wasmLoaderSource,
|
||||
);
|
||||
} catch (e) {
|
||||
console.error("📦 [asset server] failed to update assets cache:", e);
|
||||
}
|
||||
await writeAtomic("assets-cache.json", JSON.stringify(cacheConfig));
|
||||
|
||||
await writeAtomic("assets-widget.js", widgetSource);
|
||||
await writeAtomic("assets-floating.js", floatingSource);
|
||||
await writeAtomic("assets-cap_wasm_bg.wasm", Buffer.from(wasmSource));
|
||||
await writeAtomic("assets-cap_wasm.js", wasmLoaderSource);
|
||||
} catch (e) {
|
||||
console.error("📦 [asset server] failed to update assets cache:", e);
|
||||
}
|
||||
};
|
||||
|
||||
updateCache();
|
||||
setInterval(updateCache, 1000 * 60 * 60); // 1 hour
|
||||
setInterval(updateCache, 1000 * 60 * 60);
|
||||
|
||||
export const assetsServer = new Elysia({
|
||||
prefix: "/assets",
|
||||
detail: { tags: ["Assets"] },
|
||||
prefix: "/assets",
|
||||
detail: { tags: ["Assets"] },
|
||||
})
|
||||
.use(
|
||||
cors({
|
||||
origin: process.env.CORS_ORIGIN?.split(",") || true,
|
||||
methods: ["GET"],
|
||||
}),
|
||||
)
|
||||
.onBeforeHandle(({ set }) => {
|
||||
set.headers["Cache-Control"] = "max-age=31536000, immutable";
|
||||
})
|
||||
.get("/widget.js", ({ set }) => {
|
||||
set.headers["Content-Type"] = "text/javascript";
|
||||
return file(path.join(dataDir, "assets-widget.js"));
|
||||
})
|
||||
.get("/floating.js", ({ set }) => {
|
||||
set.headers["Content-Type"] = "text/javascript";
|
||||
return file(path.join(dataDir, "assets-floating.js"));
|
||||
})
|
||||
.get("/cap_wasm_bg.wasm", ({ set }) => {
|
||||
set.headers["Content-Type"] = "application/wasm";
|
||||
return file(path.join(dataDir, "assets-cap_wasm_bg.wasm"));
|
||||
})
|
||||
.get("/cap_wasm.js", ({ set }) => {
|
||||
set.headers["Content-Type"] = "text/javascript";
|
||||
return file(path.join(dataDir, "assets-cap_wasm.js"));
|
||||
});
|
||||
.use(
|
||||
cors({
|
||||
origin: process.env.CORS_ORIGIN?.split(",") || true,
|
||||
methods: ["GET"],
|
||||
}),
|
||||
)
|
||||
.onBeforeHandle(({ set }) => {
|
||||
set.headers["Cache-Control"] = "max-age=31536000, immutable";
|
||||
})
|
||||
.get("/widget.js", ({ set }) => {
|
||||
set.headers["Content-Type"] = "text/javascript";
|
||||
return file(path.join(dataDir, "assets-widget.js"));
|
||||
})
|
||||
.get("/floating.js", ({ set }) => {
|
||||
set.headers["Content-Type"] = "text/javascript";
|
||||
return file(path.join(dataDir, "assets-floating.js"));
|
||||
})
|
||||
.get("/cap_wasm_bg.wasm", ({ set }) => {
|
||||
set.headers["Content-Type"] = "application/wasm";
|
||||
return file(path.join(dataDir, "assets-cap_wasm_bg.wasm"));
|
||||
})
|
||||
.get("/cap_wasm.js", ({ set }) => {
|
||||
set.headers["Content-Type"] = "text/javascript";
|
||||
return file(path.join(dataDir, "assets-cap_wasm.js"));
|
||||
});
|
||||
|
||||
+8
-34
@@ -1,4 +1,4 @@
|
||||
import { randomBytes, timingSafeEqual } from "node:crypto";
|
||||
import { randomBytes } from "node:crypto";
|
||||
import { Elysia } from "elysia";
|
||||
import { rateLimit } from "elysia-rate-limit";
|
||||
import { db } from "./db.js";
|
||||
@@ -8,9 +8,7 @@ const { ADMIN_KEY } = process.env;
|
||||
|
||||
if (!ADMIN_KEY) throw new Error("auth: Admin key missing. Please add one");
|
||||
if (ADMIN_KEY.length < 12)
|
||||
throw new Error(
|
||||
"auth: Admin key too short. Please use one that's at least 12 characters"
|
||||
);
|
||||
throw new Error("auth: Admin key too short. Please use one that's at least 12 characters");
|
||||
|
||||
export const auth = new Elysia({
|
||||
prefix: "/auth",
|
||||
@@ -21,33 +19,14 @@ export const auth = new Elysia({
|
||||
max: 20_000,
|
||||
scoping: "scoped",
|
||||
generator: ratelimitGenerator,
|
||||
})
|
||||
}),
|
||||
)
|
||||
.post("/login", async ({ body, set, cookie }) => {
|
||||
const { admin_key } = body;
|
||||
|
||||
const a = Buffer.from(admin_key, "utf8");
|
||||
const b = Buffer.from(ADMIN_KEY, "utf8");
|
||||
|
||||
if (!a || !b || a.length !== b.length) {
|
||||
set.status = 401;
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
if (!timingSafeEqual(a, b)) {
|
||||
set.status = 401;
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
if (admin_key !== ADMIN_KEY) {
|
||||
// as a last check, in case an attacker somehow bypasses
|
||||
// timingSafeEqual, we're checking AGAIN to see if the tokens
|
||||
// are right.
|
||||
|
||||
// yes, this is vulnerable to timing attacks, but those are
|
||||
// hard to execute and literally just accepting an invalid token
|
||||
// is worse.
|
||||
const hash = (v) => new Bun.CryptoHasher("sha256").update(v).digest();
|
||||
|
||||
if (!crypto.timingSafeEqual(hash(admin_key), hash(ADMIN_KEY))) {
|
||||
set.status = 401;
|
||||
return { success: false };
|
||||
}
|
||||
@@ -87,9 +66,7 @@ export const authBeforeHandle = async ({ set, headers }) => {
|
||||
return { success: false, error: "Unauthorized. Invalid bot token." };
|
||||
}
|
||||
|
||||
const apiKey = await db`SELECT * FROM api_keys WHERE id = ${id}`.then(
|
||||
(rows) => rows[0]
|
||||
);
|
||||
const apiKey = await db`SELECT * FROM api_keys WHERE id = ${id}`.then((rows) => rows[0]);
|
||||
|
||||
if (!apiKey || !apiKey.tokenHash) {
|
||||
set.status = 401;
|
||||
@@ -111,14 +88,11 @@ export const authBeforeHandle = async ({ set, headers }) => {
|
||||
set.status = 401;
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
"Unauthorized. An API key or session token is required to use this endpoint.",
|
||||
error: "Unauthorized. An API key or session token is required to use this endpoint.",
|
||||
};
|
||||
}
|
||||
|
||||
const { token, hash } = JSON.parse(
|
||||
atob(authorization.replace("Bearer ", "").trim())
|
||||
);
|
||||
const { token, hash } = JSON.parse(atob(authorization.replace("Bearer ", "").trim()));
|
||||
|
||||
const [validToken] = await db`
|
||||
SELECT * FROM sessions WHERE token = ${hash} AND expires > ${Date.now()} LIMIT 1
|
||||
|
||||
+162
-79
@@ -4,110 +4,193 @@ import { Elysia } from "elysia";
|
||||
import { rateLimit } from "elysia-rate-limit";
|
||||
|
||||
import { db } from "./db.js";
|
||||
import {
|
||||
generateInstrumentationChallenge,
|
||||
verifyInstrumentationResult,
|
||||
} from "./instrumentation.js";
|
||||
import { ratelimitGenerator } from "./ratelimit.js";
|
||||
|
||||
export const capServer = new Elysia({
|
||||
detail: {
|
||||
tags: ["Challenges"],
|
||||
},
|
||||
detail: {
|
||||
tags: ["Challenges"],
|
||||
},
|
||||
})
|
||||
.use(
|
||||
rateLimit({
|
||||
scoping: "scoped",
|
||||
max: 45,
|
||||
duration: 5_000,
|
||||
generator: ratelimitGenerator,
|
||||
}),
|
||||
)
|
||||
.use(
|
||||
cors({
|
||||
origin: process.env.CORS_ORIGIN?.split(",") || true,
|
||||
methods: ["POST"],
|
||||
}),
|
||||
)
|
||||
.post("/:siteKey/challenge", async ({ set, params }) => {
|
||||
const cap = new Cap({
|
||||
noFSState: true,
|
||||
});
|
||||
const [_keyConfig] = await db`SELECT (config) FROM keys WHERE siteKey = ${params.siteKey}`;
|
||||
.use(
|
||||
rateLimit({
|
||||
scoping: "scoped",
|
||||
max: 45,
|
||||
duration: 5_000,
|
||||
generator: ratelimitGenerator,
|
||||
}),
|
||||
)
|
||||
.use(
|
||||
cors({
|
||||
origin: process.env.CORS_ORIGIN?.split(",") || true,
|
||||
methods: ["POST"],
|
||||
}),
|
||||
)
|
||||
.post("/:siteKey/challenge", async ({ set, params }) => {
|
||||
const cap = new Cap({
|
||||
noFSState: true,
|
||||
});
|
||||
const [_keyConfig] = await db`SELECT (config) FROM keys WHERE siteKey = ${params.siteKey}`;
|
||||
|
||||
if (!_keyConfig) {
|
||||
set.status = 404;
|
||||
return { error: "Invalid site key or secret" };
|
||||
}
|
||||
if (!_keyConfig) {
|
||||
set.status = 404;
|
||||
return { error: "Invalid site key or secret" };
|
||||
}
|
||||
|
||||
const keyConfig = JSON.parse(_keyConfig.config);
|
||||
const keyConfig = JSON.parse(_keyConfig.config);
|
||||
|
||||
const challenge = await cap.createChallenge({
|
||||
challengeCount: keyConfig.challengeCount,
|
||||
challengeSize: keyConfig.saltSize,
|
||||
challengeDifficulty: keyConfig.difficulty,
|
||||
});
|
||||
const challenge = await cap.createChallenge({
|
||||
challengeCount: keyConfig.challengeCount,
|
||||
challengeSize: keyConfig.saltSize,
|
||||
challengeDifficulty: keyConfig.difficulty,
|
||||
});
|
||||
|
||||
await db`
|
||||
await db`
|
||||
INSERT INTO challenges (siteKey, token, data, expires)
|
||||
VALUES (${params.siteKey}, ${challenge.token}, ${Object.values(challenge.challenge).join(",")}, ${challenge.expires})
|
||||
VALUES (${params.siteKey}, ${challenge.token}, ${`${challenge.challenge.c},${challenge.challenge.s},${challenge.challenge.d}`}, ${challenge.expires})
|
||||
`;
|
||||
|
||||
return challenge;
|
||||
})
|
||||
.post("/:siteKey/redeem", async ({ body, set, params }) => {
|
||||
const [challenge] = await db`
|
||||
SELECT * FROM challenges WHERE siteKey = ${params.siteKey} AND token = ${body.token}
|
||||
if (keyConfig.instrumentation) {
|
||||
const instr = await generateInstrumentationChallenge(keyConfig);
|
||||
|
||||
const instrMeta = JSON.stringify({
|
||||
id: instr.id,
|
||||
validStates: instr.validStates,
|
||||
vars: instr.vars,
|
||||
blockAutomatedBrowsers: instr.blockAutomatedBrowsers,
|
||||
expires: instr.expires,
|
||||
});
|
||||
|
||||
await db`
|
||||
INSERT INTO challenges (siteKey, token, data, expires)
|
||||
VALUES (
|
||||
${params.siteKey},
|
||||
${"instr_" + challenge.token},
|
||||
${instrMeta},
|
||||
${challenge.expires}
|
||||
)
|
||||
`;
|
||||
|
||||
return {
|
||||
...challenge,
|
||||
instrumentation: instr.instrumentation,
|
||||
};
|
||||
}
|
||||
|
||||
return challenge;
|
||||
})
|
||||
.post("/:siteKey/redeem", async ({ body, set, params }) => {
|
||||
if (!body || !body.token || !body.solutions) {
|
||||
set.status = 400;
|
||||
return { error: "Missing required fields" };
|
||||
}
|
||||
|
||||
const instrTokenKey = "instr_" + body.token;
|
||||
|
||||
const deleted = await db`
|
||||
DELETE FROM challenges
|
||||
WHERE siteKey = ${params.siteKey}
|
||||
AND token IN (${body.token}, ${instrTokenKey})
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
try {
|
||||
await db`DELETE FROM challenges WHERE siteKey = ${params.siteKey} AND token = ${body.token}`;
|
||||
} catch {
|
||||
set.status = 404;
|
||||
return { error: "Challenge not found" };
|
||||
}
|
||||
const challenge = deleted.find((r) => r.token === body.token);
|
||||
const instrRow = deleted.find((r) => r.token === instrTokenKey);
|
||||
|
||||
if (!challenge) {
|
||||
set.status = 404;
|
||||
return { error: "Challenge not found" };
|
||||
}
|
||||
if (!challenge) {
|
||||
set.status = 404;
|
||||
return { error: "Challenge not found" };
|
||||
}
|
||||
|
||||
const cap = new Cap({
|
||||
noFSState: true,
|
||||
state: {
|
||||
challengesList: {
|
||||
[challenge.token]: {
|
||||
challenge: {
|
||||
c: challenge.data.split(",")[0],
|
||||
s: challenge.data.split(",")[1],
|
||||
d: challenge.data.split(",")[2],
|
||||
},
|
||||
expires: challenge.expires,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
if (challenge.expires < Date.now()) {
|
||||
set.status = 403;
|
||||
return { error: "Challenge expired" };
|
||||
}
|
||||
|
||||
const { success, token, expires } = await cap.redeemChallenge(body);
|
||||
const cap = new Cap({
|
||||
noFSState: true,
|
||||
state: {
|
||||
challengesList: {
|
||||
[challenge.token]: {
|
||||
challenge: {
|
||||
c: Number(challenge.data.split(",")[0]),
|
||||
s: Number(challenge.data.split(",")[1]),
|
||||
d: Number(challenge.data.split(",")[2]),
|
||||
},
|
||||
expires: challenge.expires,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!success) {
|
||||
set.status = 403;
|
||||
return { error: "Invalid solution" };
|
||||
}
|
||||
const { success, token, expires } = await cap.redeemChallenge(body);
|
||||
|
||||
await db`
|
||||
if (!success) {
|
||||
set.status = 403;
|
||||
return { error: "Invalid solution" };
|
||||
}
|
||||
|
||||
let instrResult = null;
|
||||
|
||||
if (instrRow) {
|
||||
let challengeMeta;
|
||||
try {
|
||||
challengeMeta = JSON.parse(instrRow.data);
|
||||
} catch {
|
||||
challengeMeta = null;
|
||||
}
|
||||
|
||||
if (challengeMeta && body.instr_blocked === true) {
|
||||
if (challengeMeta.blockAutomatedBrowsers) {
|
||||
set.status = 403;
|
||||
return { instr_error: true, error: "Blocked by instrumentation" };
|
||||
}
|
||||
|
||||
set.status = 403;
|
||||
return { instr_error: true, error: "Blocked by instrumentation" };
|
||||
} else if (challengeMeta && body.instr) {
|
||||
if (challengeMeta.expires && Date.now() > challengeMeta.expires) {
|
||||
instrResult = { valid: false, env: null, reason: "expired" };
|
||||
} else {
|
||||
instrResult = verifyInstrumentationResult(challengeMeta, body.instr);
|
||||
}
|
||||
|
||||
if (!instrResult.valid) {
|
||||
set.status = 403;
|
||||
return { instr_error: true, error: "Blocked by instrumentation" };
|
||||
}
|
||||
} else if (challengeMeta && body.instr_timeout === true) {
|
||||
set.status = 429;
|
||||
return { instr_error: true, error: "Instrumentation timeout" };
|
||||
} else if (challengeMeta && !body.instr && !body.instr_blocked) {
|
||||
set.status = 403;
|
||||
return { instr_error: true, error: "Blocked by instrumentation" };
|
||||
} else if (!challengeMeta) {
|
||||
set.status = 403;
|
||||
return { instr_error: true, error: "Blocked by instrumentation" };
|
||||
}
|
||||
}
|
||||
|
||||
await db`
|
||||
INSERT INTO tokens (siteKey, token, expires)
|
||||
VALUES (${params.siteKey}, ${token}, ${expires})
|
||||
`;
|
||||
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const hourlyBucket = Math.floor(now / 3600) * 3600;
|
||||
await db`
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const hourlyBucket = Math.floor(now / 3600) * 3600;
|
||||
await db`
|
||||
INSERT INTO solutions (siteKey, bucket, count)
|
||||
VALUES (${params.siteKey}, ${hourlyBucket}, 1)
|
||||
ON CONFLICT (siteKey, bucket)
|
||||
DO UPDATE SET count = count + 1
|
||||
`;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
token,
|
||||
expires,
|
||||
};
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
token,
|
||||
expires,
|
||||
};
|
||||
});
|
||||
|
||||
+21
-7
@@ -9,10 +9,13 @@ fs.mkdirSync(process.env.DATA_PATH || "./.data", {
|
||||
let db;
|
||||
|
||||
async function initDb() {
|
||||
const dbUrl = process.env.DB_URL || `sqlite://${join(process.env.DATA_PATH || "./.data", "db.sqlite")}`;
|
||||
const dbUrl =
|
||||
process.env.DB_URL || `sqlite://${join(process.env.DATA_PATH || "./.data", "db.sqlite")}`;
|
||||
|
||||
db = new SQL(dbUrl);
|
||||
|
||||
|
||||
await db`PRAGMA journal_mode = WAL;`.simple();
|
||||
await db`PRAGMA synchronous = NORMAL;`.simple();
|
||||
await db`create table if not exists sessions (
|
||||
token text primary key not null,
|
||||
expires integer not null,
|
||||
@@ -49,6 +52,12 @@ async function initDb() {
|
||||
primary key (siteKey, token)
|
||||
)`.simple();
|
||||
|
||||
await db`create table if not exists ip_bans (
|
||||
ip text primary key not null,
|
||||
reason text not null,
|
||||
expires integer not null
|
||||
)`.simple();
|
||||
|
||||
await db`create table if not exists api_keys (
|
||||
id text not null,
|
||||
name text not null,
|
||||
@@ -58,19 +67,24 @@ async function initDb() {
|
||||
)`.simple();
|
||||
|
||||
setInterval(async () => {
|
||||
const now = Date.now();
|
||||
try {
|
||||
const now = Date.now();
|
||||
|
||||
await db`delete from sessions where expires < ${now}`;
|
||||
await db`delete from tokens where expires < ${now}`;
|
||||
await db`delete from challenges where expires < ${now}`;
|
||||
await db`delete from sessions where expires < ${now}`;
|
||||
await db`delete from tokens where expires < ${now}`;
|
||||
await db`delete from challenges where expires < ${now}`;
|
||||
await db`delete from ip_bans where expires < ${now}`;
|
||||
} catch (e) {
|
||||
console.error("failed to cleanup:", e)
|
||||
}
|
||||
}, 60 * 1000);
|
||||
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
await db`delete from sessions where expires < ${now}`;
|
||||
await db`delete from tokens where expires < ${now}`;
|
||||
await db`delete from challenges where expires < ${now}`;
|
||||
await db`delete from ip_bans where expires < ${now}`;
|
||||
|
||||
return db;
|
||||
}
|
||||
|
||||
+16
-14
@@ -26,8 +26,7 @@ new Elysia({
|
||||
tags: [
|
||||
{
|
||||
name: "Keys",
|
||||
description:
|
||||
"Managing, creating and viewing keys. Requires API or session token",
|
||||
description: "Managing, creating and viewing keys. Requires API or session token",
|
||||
},
|
||||
{
|
||||
name: "Settings",
|
||||
@@ -55,7 +54,7 @@ new Elysia({
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}),
|
||||
)
|
||||
.onBeforeHandle(({ set }) => {
|
||||
set.headers["X-Powered-By"] = "Cap Standalone";
|
||||
@@ -65,22 +64,25 @@ new Elysia({
|
||||
return {
|
||||
success: false,
|
||||
error: error.code || "Internal server error",
|
||||
detail: error
|
||||
}
|
||||
detail: error,
|
||||
};
|
||||
}
|
||||
|
||||
const errorId = Bun.randomUUIDv7().split("-").pop();
|
||||
|
||||
if (process.env.DISABLE_ERROR_LOGGING !== "true") {
|
||||
console.error(`[${error.code || "ERR"} ${errorId}]`, JSON.stringify({
|
||||
timestamp: new Date().toISOString(),
|
||||
error,
|
||||
env: {
|
||||
bun: process.versions.bun,
|
||||
platform: process.platform,
|
||||
mem: process.memoryUsage()
|
||||
}
|
||||
}));
|
||||
console.error(
|
||||
`[${error.code || "ERR"} ${errorId}]`,
|
||||
JSON.stringify({
|
||||
timestamp: new Date().toISOString(),
|
||||
error,
|
||||
env: {
|
||||
bun: process.versions.bun,
|
||||
platform: process.platform,
|
||||
mem: process.memoryUsage(),
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
import { parentPort } from "node:worker_threads";
|
||||
import { randomBytes, randomInt } from "node:crypto";
|
||||
import { deflateRawSync } from "node:zlib";
|
||||
import JavaScriptObfuscator from "javascript-obfuscator";
|
||||
|
||||
const rHex = (len = 16) => randomBytes(len).toString("hex").slice(0, len);
|
||||
const rnd = (a, b) => randomInt(a, b + 1);
|
||||
const rVar = (len = 12) =>
|
||||
"_" +
|
||||
randomBytes(len)
|
||||
.toString("hex")
|
||||
.slice(0, len - 1);
|
||||
const toInt32 = (n) => n | 0;
|
||||
|
||||
function domSumMock(x, y, z) {
|
||||
const root = { isRoot: true };
|
||||
|
||||
function buildChain(parent, val) {
|
||||
let cur = parent;
|
||||
let v = val;
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const child = { parentNode: cur, innerText: String(v) };
|
||||
if ((v & 1) === 0) cur = child;
|
||||
v = v >> 1;
|
||||
}
|
||||
return cur;
|
||||
}
|
||||
|
||||
function walk(node, rootRef, sum) {
|
||||
if (!node || node === rootRef) return sum % 256;
|
||||
return walk(node.parentNode, rootRef, sum + parseInt(node.innerText, 10));
|
||||
}
|
||||
|
||||
return toInt32(walk(buildChain(buildChain(buildChain(root, x), y), z), root, 0));
|
||||
}
|
||||
|
||||
function buildClientScript({ id, vars, initVals, clientEqs, blockAutomatedBrowsers }) {
|
||||
const blockChecks = blockAutomatedBrowsers
|
||||
? `
|
||||
let _blocked = false;
|
||||
try {
|
||||
if (navigator.webdriver || (typeof window.cdc_adoQpoasnfa76pfcZLmcfl_ !== 'undefined') || (typeof window.__nightmare !== 'undefined') || (typeof window.__selenium_unwrapped !== 'undefined') || (typeof window.__driver_evaluate !== 'undefined') || (typeof window.__webdriver_script_fn !== 'undefined') || (typeof window.__$webdriverAsyncExecutor !== 'undefined') || (typeof window.__webdriver_evaluate !== 'undefined') || (typeof window.__selenium_evaluate !== 'undefined') || (typeof window.__webdriver_script_func !== 'undefined') || (typeof window.__webdriver_async_script !== 'undefined') || (navigator.plugins && navigator.plugins.length === 0) || (typeof window.exposedFn !== 'undefined') || window.exposedFn.toString()?.includes('exposeBindingHandle supports a single argument') || (window.__pwInitScripts !== undefined) || (Object.getOwnPropertyNames(navigator).length !== 0) || (Object.getOwnPropertyDescriptor(navigator, 'webdriver') !== undefined) || (new Error('Cap')).stack.toString().includes('pptr:') || (new Error('Cap')).stack.toString().includes('UtilityScript.')) {
|
||||
_blocked = true;
|
||||
}
|
||||
|
||||
for (const key in window) {
|
||||
if (key.startsWith('puppeteer_')) {
|
||||
_blocked = true;
|
||||
break
|
||||
}
|
||||
if (key === '__playwright__binding__') {
|
||||
_blocked = true;
|
||||
break
|
||||
}
|
||||
if (typeof window[key] === 'function' && window[key].__installed === true) {
|
||||
_blocked = true;
|
||||
break
|
||||
}
|
||||
}
|
||||
} catch(_e) {}
|
||||
|
||||
if (_blocked) {
|
||||
parent.postMessage({ type: 'cap:instr', nonce: ${JSON.stringify(id)}, result: '', blocked: true }, '*');
|
||||
return;
|
||||
}
|
||||
`
|
||||
: "";
|
||||
|
||||
return `(function(){
|
||||
'use strict';
|
||||
|
||||
var doVerification = function() {
|
||||
if (navigator.toString() !== '[object Navigator]' || window.toString() !== '[object Window]' || document.toString() !== '[object HTMLDocument]' || typeof process !== 'undefined' || navigator.mimeTypes["application/pdf"].type !== "application/pdf") {
|
||||
return -1
|
||||
}
|
||||
|
||||
if (window.chrome) {
|
||||
if (JSON.stringify(window.chrome.csi()).length < 70) {
|
||||
return -1
|
||||
}
|
||||
if (typeof window.chrome.app.isInstalled !== "boolean") {
|
||||
return -1
|
||||
}
|
||||
if (window.chrome.app.getDetails.toString() !== 'function getDetails() { [native code] }') {
|
||||
return -1
|
||||
}
|
||||
} else if (window.Mozilla) {
|
||||
if (JSON.stringify(window.Mozilla).length < 300) {
|
||||
return -1
|
||||
}
|
||||
if (typeof window.Mozilla.dntEnabled() !== "boolean") {
|
||||
return -1
|
||||
}
|
||||
} else {
|
||||
throw new Error("Unsupported browser")
|
||||
}
|
||||
|
||||
if (
|
||||
!navigator.userAgent ||
|
||||
!navigator.platform
|
||||
) return -1
|
||||
|
||||
if (
|
||||
navigator.plugins.length === 0 ||
|
||||
!navigator.mimeTypes.length
|
||||
) return -1
|
||||
|
||||
const nativeFns = ['alert','confirm','prompt','fetch']
|
||||
for (const fn of nativeFns) {
|
||||
if (!window[fn] || window[fn].toString().indexOf('[native code]') === -1) return -1
|
||||
}
|
||||
|
||||
try {
|
||||
const canvas = document.createElement('canvas')
|
||||
const ctx = canvas.getContext('2d')
|
||||
ctx.fillText('1', 10, 10)
|
||||
const data = canvas.toDataURL()
|
||||
if (data.length < 50) return -1
|
||||
} catch {
|
||||
return -1
|
||||
}
|
||||
|
||||
try {
|
||||
const gl = document.createElement('canvas').getContext('webgl') || document.createElement('canvas').getContext('experimental-webgl')
|
||||
const renderer = gl?.getParameter(gl.RENDERER) || ''
|
||||
if (!renderer || renderer.toLowerCase().includes('swiftshader') || renderer.toLowerCase().includes('llvmpipe')) return -1
|
||||
} catch {
|
||||
return -1
|
||||
}
|
||||
|
||||
try {
|
||||
const AudioContext = window.OfflineAudioContext || window.webkitOfflineAudioContext
|
||||
let ctx
|
||||
if (AudioContext.length === 0) {
|
||||
ctx = new AudioContext()
|
||||
} else {
|
||||
ctx = new AudioContext(1, 44100, 44100)
|
||||
}
|
||||
} catch {
|
||||
return -1
|
||||
}
|
||||
|
||||
${blockChecks}
|
||||
var ${vars[0]} = ${initVals[0]};
|
||||
var ${vars[1]} = ${initVals[1]};
|
||||
var ${vars[2]} = ${initVals[2]};
|
||||
var ${vars[3]} = ${initVals[3]};
|
||||
|
||||
${clientEqs}
|
||||
|
||||
var res = {};
|
||||
res["${vars[0]}"] = ${vars[0]};
|
||||
res["${vars[1]}"] = ${vars[1]};
|
||||
res["${vars[2]}"] = ${vars[2]};
|
||||
res["${vars[3]}"] = ${vars[3]};
|
||||
return res;
|
||||
};
|
||||
|
||||
window.onload = function() {
|
||||
try {
|
||||
var result = doVerification();
|
||||
if (!result) return;
|
||||
parent.postMessage({ type: 'cap:instr', nonce: ${JSON.stringify(id)}, result: { i: ${JSON.stringify(id)}, state: result, ts: Date.now() } }, '*');
|
||||
} catch(e) {
|
||||
parent.postMessage({ type: 'cap:error', reason: String(e) }, '*');
|
||||
}
|
||||
};
|
||||
|
||||
})();`;
|
||||
}
|
||||
|
||||
const TTL = 90_000;
|
||||
|
||||
function generateInstrumentationChallenge(keyConfig = {}) {
|
||||
const id = rHex(32);
|
||||
const vars = Array.from({ length: 4 }, () => rVar(12));
|
||||
const blockAutomatedBrowsers = keyConfig.blockAutomatedBrowsers === true;
|
||||
|
||||
let states, clientEqs, initVals;
|
||||
|
||||
for (let attempt = 0; attempt < 20; attempt++) {
|
||||
initVals = Array.from({ length: 4 }, () => rnd(10, 250));
|
||||
|
||||
let wdTrueKey = rnd(1000, 9000);
|
||||
let wdFalseKey = rnd(1000, 9000);
|
||||
while (wdFalseKey === wdTrueKey) {
|
||||
wdFalseKey = rnd(1000, 9000);
|
||||
}
|
||||
|
||||
states = [
|
||||
{ env: { webdriver: false }, vals: [...initVals] },
|
||||
{ env: { webdriver: true }, vals: [...initVals] },
|
||||
];
|
||||
for (const s of states) {
|
||||
s.vals[0] = toInt32(s.vals[0] ^ (s.env.webdriver ? wdTrueKey : wdFalseKey));
|
||||
}
|
||||
|
||||
clientEqs = `${vars[0]} = ${vars[0]} ^ (navigator.webdriver ? ${wdTrueKey} : ${wdFalseKey});\n`;
|
||||
|
||||
for (let i = 0; i < 35; i++) {
|
||||
const op = rnd(0, 5);
|
||||
const dest = rnd(0, 3);
|
||||
const src1 = rnd(0, 3);
|
||||
const src2 = rnd(0, 3);
|
||||
const src3 = rnd(0, 3);
|
||||
const vD = vars[dest];
|
||||
const vS1 = vars[src1];
|
||||
const vS2 = vars[src2];
|
||||
const vS3 = vars[src3];
|
||||
|
||||
if (op === 0) {
|
||||
clientEqs += `${vD} = ~(${vD} & ${vS1});\n`;
|
||||
for (const s of states) s.vals[dest] = toInt32(~(s.vals[dest] & s.vals[src1]));
|
||||
} else if (op === 1) {
|
||||
clientEqs += `${vD} = ${vD} ^ ${vS1};\n`;
|
||||
for (const s of states) s.vals[dest] = toInt32(s.vals[dest] ^ s.vals[src1]);
|
||||
} else if (op === 2) {
|
||||
clientEqs += `${vD} = ${vD} | ${vS1};\n`;
|
||||
for (const s of states) s.vals[dest] = toInt32(s.vals[dest] | s.vals[src1]);
|
||||
} else if (op === 3) {
|
||||
clientEqs += `${vD} = ${vD} & ${vS1};\n`;
|
||||
for (const s of states) s.vals[dest] = toInt32(s.vals[dest] & s.vals[src1]);
|
||||
} else if (op === 4) {
|
||||
clientEqs += `${vD} = function(a,b,c){function F(d){this.v=function(){return this.k^d;}};var p={k:c};var i=new F(a);i.k=b;F.prototype=p;return i.v()|(new F(b)).v();}(${vS1}, ${vS2}, ${vD});\n`;
|
||||
for (const s of states) {
|
||||
s.vals[dest] = toInt32((s.vals[src2] ^ s.vals[src1]) | (s.vals[dest] ^ s.vals[src2]));
|
||||
}
|
||||
} else {
|
||||
clientEqs += `${vD} = function(x,y,z){var d=document.createElement('div');d.style.display='none';document.body.appendChild(d);function A(p,v){for(var i=0;i<8;i++){var c=document.createElement('div');p.appendChild(c);c.innerText=v;if((v&1)==0)p=c;v=v>>1;}return p;}function B(n,r,s){if(!n||n==r)return s%256;while(n.children.length>0)n.removeChild(n.lastElementChild);return B(n.parentNode,r,s+parseInt(n.innerText));}var s=B(A(A(A(d,x),y),z),d,0);d.parentNode.removeChild(d);return s;}(${vS1}, ${vS2}, ${vS3});\n`;
|
||||
for (const s of states) {
|
||||
s.vals[dest] = toInt32(domSumMock(s.vals[src1], s.vals[src2], s.vals[src3]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!states[0].vals.every((v, i) => v === states[1].vals[i])) break;
|
||||
}
|
||||
|
||||
const script = buildClientScript({ id, vars, initVals, clientEqs, blockAutomatedBrowsers });
|
||||
|
||||
const obfuscated = JavaScriptObfuscator.obfuscate(script, {
|
||||
compact: true,
|
||||
controlFlowFlattening: true,
|
||||
controlFlowFlatteningThreshold: 0.6,
|
||||
deadCodeInjection: true,
|
||||
deadCodeInjectionThreshold: 0.4,
|
||||
stringArray: true,
|
||||
stringArrayEncoding: ["rc4"],
|
||||
stringArrayThreshold: 0.8,
|
||||
identifierNamesGenerator: "mangled-shuffled",
|
||||
splitStrings: true,
|
||||
splitStringsChunkLength: randomInt(5, 26),
|
||||
debugProtection: false,
|
||||
disableConsoleOutput: false,
|
||||
selfDefending: false,
|
||||
ignoreRequireImports: true,
|
||||
}).getObfuscatedCode();
|
||||
|
||||
const compressed = deflateRawSync(Buffer.from(obfuscated, "utf8"), { level: 1 });
|
||||
|
||||
return {
|
||||
id,
|
||||
expires: Date.now() + TTL,
|
||||
validStates: states,
|
||||
vars,
|
||||
blockAutomatedBrowsers,
|
||||
instrumentation: Buffer.from(compressed).toString("base64"),
|
||||
};
|
||||
}
|
||||
|
||||
parentPort.on("message", (msg) => {
|
||||
try {
|
||||
const result = generateInstrumentationChallenge(msg.keyConfig || {});
|
||||
parentPort.postMessage({ ok: true, result });
|
||||
} catch (err) {
|
||||
parentPort.postMessage({ ok: false, error: String(err) });
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,188 @@
|
||||
import { Worker } from "node:worker_threads";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, join } from "node:path";
|
||||
import { cpus } from "node:os";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const WORKER_PATH = join(__dirname, "instrumentation-worker.js");
|
||||
|
||||
const POOL_SIZE = process.env.INSTRUMENTATION_WORKERS || Math.min(4, cpus().length || 4);
|
||||
const WORKER_TIMEOUT = 15_000;
|
||||
const MAX_QUEUE_SIZE = 50;
|
||||
const QUEUE_ITEM_TTL = 20_000;
|
||||
|
||||
class InstrumentationWorkerPool {
|
||||
constructor(size) {
|
||||
this._size = size;
|
||||
this._workers = [];
|
||||
this._queue = [];
|
||||
this._available = [];
|
||||
|
||||
for (let i = 0; i < size; i++) {
|
||||
this._spawnWorker(i);
|
||||
}
|
||||
}
|
||||
|
||||
_spawnWorker(index) {
|
||||
const worker = new Worker(WORKER_PATH);
|
||||
this._workers[index] = worker;
|
||||
this._available.push(index);
|
||||
|
||||
worker.on("error", (err) => {
|
||||
console.error(`[cap] instrumentation worker ${index} error:`, err);
|
||||
try {
|
||||
worker.terminate();
|
||||
} catch {}
|
||||
this._spawnWorker(index);
|
||||
});
|
||||
|
||||
worker.on("exit", (code) => {
|
||||
if (code !== 0) {
|
||||
console.warn(`[cap] instrumentation worker ${index} exited with code ${code}, respawning`);
|
||||
this._spawnWorker(index);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
run(keyConfig) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this._queue.length >= MAX_QUEUE_SIZE) {
|
||||
return reject(new Error("Instrumentation queue is full, try again later"));
|
||||
}
|
||||
|
||||
const task = { keyConfig, resolve, reject, queuedAt: Date.now() };
|
||||
|
||||
if (this._available.length > 0) {
|
||||
this._dispatch(task);
|
||||
} else {
|
||||
const ttlTimer = setTimeout(() => {
|
||||
const idx = this._queue.indexOf(task);
|
||||
if (idx !== -1) {
|
||||
this._queue.splice(idx, 1);
|
||||
}
|
||||
reject(new Error("Instrumentation task expired while waiting in queue"));
|
||||
}, QUEUE_ITEM_TTL);
|
||||
|
||||
if (ttlTimer.unref) ttlTimer.unref();
|
||||
|
||||
task.ttlTimer = ttlTimer;
|
||||
this._queue.push(task);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_dispatch(task) {
|
||||
const index = this._available.shift();
|
||||
const worker = this._workers[index];
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
cleanup();
|
||||
try {
|
||||
worker.terminate();
|
||||
} catch {}
|
||||
this._spawnWorker(index);
|
||||
task.reject(new Error("Instrumentation worker timed out"));
|
||||
}, WORKER_TIMEOUT);
|
||||
|
||||
const onMessage = (msg) => {
|
||||
cleanup();
|
||||
this._release(index);
|
||||
|
||||
if (msg.ok) {
|
||||
task.resolve(msg.result);
|
||||
} else {
|
||||
task.reject(new Error(msg.error || "Worker generation failed"));
|
||||
}
|
||||
};
|
||||
|
||||
const onError = (err) => {
|
||||
cleanup();
|
||||
this._release(index);
|
||||
task.reject(err);
|
||||
};
|
||||
|
||||
const cleanup = () => {
|
||||
clearTimeout(timeout);
|
||||
worker.removeListener("message", onMessage);
|
||||
worker.removeListener("error", onError);
|
||||
};
|
||||
|
||||
worker.on("message", onMessage);
|
||||
worker.on("error", onError);
|
||||
worker.postMessage({ keyConfig: task.keyConfig });
|
||||
}
|
||||
|
||||
_release(index) {
|
||||
this._available.push(index);
|
||||
|
||||
if (this._queue.length > 0) {
|
||||
const next = this._queue.shift();
|
||||
|
||||
if (next.ttlTimer) {
|
||||
clearTimeout(next.ttlTimer);
|
||||
next.ttlTimer = null;
|
||||
}
|
||||
|
||||
this._dispatch(next);
|
||||
}
|
||||
}
|
||||
|
||||
async terminate() {
|
||||
for (const w of this._workers) {
|
||||
if (w) {
|
||||
try {
|
||||
await w.terminate();
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
this._workers = [];
|
||||
this._available = [];
|
||||
for (const task of this._queue) {
|
||||
if (task.ttlTimer) {
|
||||
clearTimeout(task.ttlTimer);
|
||||
task.ttlTimer = null;
|
||||
}
|
||||
task.reject(new Error("Pool terminated"));
|
||||
}
|
||||
this._queue = [];
|
||||
}
|
||||
}
|
||||
|
||||
const pool = new InstrumentationWorkerPool(POOL_SIZE);
|
||||
|
||||
export async function generateInstrumentationChallenge(keyConfig = {}) {
|
||||
return pool.run(keyConfig);
|
||||
}
|
||||
|
||||
export function verifyInstrumentationResult(challengeMeta, payload) {
|
||||
if (!payload || typeof payload !== "object") {
|
||||
return { valid: false, env: null, reason: "missing_output" };
|
||||
}
|
||||
|
||||
if (payload.i !== challengeMeta.id) {
|
||||
return { valid: false, env: null, reason: "id_mismatch" };
|
||||
}
|
||||
|
||||
const actual = payload.state;
|
||||
if (!actual || typeof actual !== "object") {
|
||||
return { valid: false, env: null, reason: "invalid_state" };
|
||||
}
|
||||
|
||||
let matchedEnv = null;
|
||||
for (const s of challengeMeta.validStates) {
|
||||
if (challengeMeta.vars.every((v, i) => actual[v] === s.vals[i])) {
|
||||
matchedEnv = s.env;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!matchedEnv) {
|
||||
return { valid: false, env: null, reason: "failed_challenge" };
|
||||
}
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
env: matchedEnv,
|
||||
};
|
||||
}
|
||||
+46
-37
@@ -9,6 +9,8 @@ const keyDefaults = {
|
||||
difficulty: 4,
|
||||
challengeCount: 80,
|
||||
saltSize: 32,
|
||||
instrumentation: false,
|
||||
blockAutomatedBrowsers: false,
|
||||
};
|
||||
|
||||
export const server = new Elysia({
|
||||
@@ -27,7 +29,7 @@ export const server = new Elysia({
|
||||
max: 150,
|
||||
duration: 10_000,
|
||||
generator: ratelimitGenerator,
|
||||
})
|
||||
}),
|
||||
)
|
||||
.onBeforeHandle(authBeforeHandle)
|
||||
.get(
|
||||
@@ -59,8 +61,7 @@ export const server = new Elysia({
|
||||
|
||||
if (previous > 0) {
|
||||
change = ((current - previous) / previous) * 100;
|
||||
direction =
|
||||
current > previous ? "up" : current < previous ? "down" : "";
|
||||
direction = current > previous ? "up" : current < previous ? "down" : "";
|
||||
} else if (current > 0) {
|
||||
change = 100;
|
||||
direction = "up";
|
||||
@@ -76,14 +77,14 @@ export const server = new Elysia({
|
||||
direction,
|
||||
},
|
||||
};
|
||||
})
|
||||
}),
|
||||
);
|
||||
},
|
||||
{
|
||||
detail: {
|
||||
tags: ["Keys"],
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
.post(
|
||||
"/keys",
|
||||
@@ -112,7 +113,7 @@ export const server = new Elysia({
|
||||
detail: {
|
||||
tags: ["Keys"],
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
.get(
|
||||
"/keys/:siteKey",
|
||||
@@ -210,16 +211,9 @@ export const server = new Elysia({
|
||||
chartDuration === "last28days" ||
|
||||
chartDuration === "last91days"
|
||||
) {
|
||||
const days =
|
||||
chartDuration === "last7days"
|
||||
? 7
|
||||
: chartDuration === "last28days"
|
||||
? 28
|
||||
: 91;
|
||||
const days = chartDuration === "last7days" ? 7 : chartDuration === "last28days" ? 28 : 91;
|
||||
const completeData = [];
|
||||
const dataMap = new Map(
|
||||
historyData.map((item) => [item.bucket, item.count])
|
||||
);
|
||||
const dataMap = new Map(historyData.map((item) => [item.bucket, item.count]));
|
||||
|
||||
const currentDayStart = Math.floor(now / 86400) * 86400;
|
||||
|
||||
@@ -234,9 +228,7 @@ export const server = new Elysia({
|
||||
historyData = completeData;
|
||||
} else if (chartDuration === "today") {
|
||||
const completeData = [];
|
||||
const dataMap = new Map(
|
||||
historyData.map((item) => [item.bucket, item.count])
|
||||
);
|
||||
const dataMap = new Map(historyData.map((item) => [item.bucket, item.count]));
|
||||
|
||||
const currentHour = Math.floor(now / 3600);
|
||||
const startHour = Math.floor(startTime / 3600);
|
||||
@@ -287,13 +279,13 @@ export const server = new Elysia({
|
||||
t.Literal("last28days"),
|
||||
t.Literal("last91days"),
|
||||
t.Literal("alltime"),
|
||||
])
|
||||
]),
|
||||
),
|
||||
}),
|
||||
detail: {
|
||||
tags: ["Keys"],
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
.put(
|
||||
"/keys/:siteKey/config",
|
||||
@@ -303,13 +295,25 @@ export const server = new Elysia({
|
||||
return { success: false, error: "Key not found" };
|
||||
}
|
||||
|
||||
const { name, difficulty, challengeCount, saltSize } = body;
|
||||
const config = {
|
||||
...keyDefaults,
|
||||
const existingConfig = JSON.parse(key.config);
|
||||
const {
|
||||
name,
|
||||
difficulty,
|
||||
challengeCount,
|
||||
saltSize,
|
||||
instrumentation,
|
||||
blockAutomatedBrowsers,
|
||||
} = body;
|
||||
const config = {
|
||||
...keyDefaults,
|
||||
...existingConfig,
|
||||
name,
|
||||
difficulty,
|
||||
challengeCount,
|
||||
saltSize,
|
||||
instrumentation: instrumentation ?? existingConfig.instrumentation ?? false,
|
||||
blockAutomatedBrowsers:
|
||||
blockAutomatedBrowsers ?? existingConfig.blockAutomatedBrowsers ?? false,
|
||||
};
|
||||
|
||||
await db`UPDATE keys SET name = ${config.name || key.name}, config = ${JSON.stringify(config)} WHERE siteKey = ${params.siteKey}`;
|
||||
@@ -322,11 +326,13 @@ export const server = new Elysia({
|
||||
difficulty: t.Optional(t.Number()),
|
||||
challengeCount: t.Optional(t.Number()),
|
||||
saltSize: t.Optional(t.Number()),
|
||||
instrumentation: t.Optional(t.Boolean()),
|
||||
blockAutomatedBrowsers: t.Optional(t.Boolean()),
|
||||
}),
|
||||
detail: {
|
||||
tags: ["Keys"],
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
.delete(
|
||||
"/keys/:siteKey",
|
||||
@@ -349,7 +355,7 @@ export const server = new Elysia({
|
||||
detail: {
|
||||
tags: ["Keys"],
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
.post(
|
||||
"/keys/:siteKey/rotate-secret",
|
||||
@@ -376,7 +382,7 @@ export const server = new Elysia({
|
||||
detail: {
|
||||
tags: ["Keys"],
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
.get(
|
||||
"/settings/sessions",
|
||||
@@ -392,7 +398,7 @@ export const server = new Elysia({
|
||||
detail: {
|
||||
tags: ["Settings"],
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
.get(
|
||||
"/settings/apikeys",
|
||||
@@ -409,7 +415,7 @@ export const server = new Elysia({
|
||||
detail: {
|
||||
tags: ["Settings"],
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
.post(
|
||||
"/settings/apikeys",
|
||||
@@ -438,7 +444,7 @@ export const server = new Elysia({
|
||||
detail: {
|
||||
tags: ["Settings"],
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
.delete(
|
||||
"/settings/apikeys/:id",
|
||||
@@ -458,7 +464,7 @@ export const server = new Elysia({
|
||||
detail: {
|
||||
tags: ["Settings"],
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
.get(
|
||||
"/about",
|
||||
@@ -470,7 +476,7 @@ export const server = new Elysia({
|
||||
ver: pkg.default.version,
|
||||
};
|
||||
},
|
||||
{}
|
||||
{},
|
||||
)
|
||||
.post(
|
||||
"/logout",
|
||||
@@ -481,9 +487,7 @@ export const server = new Elysia({
|
||||
return { success: false, error: "Unauthorized" };
|
||||
}
|
||||
|
||||
const { hash } = JSON.parse(
|
||||
atob(authorization.replace("Bearer ", "").trim())
|
||||
);
|
||||
const { hash } = JSON.parse(atob(authorization.replace("Bearer ", "").trim()));
|
||||
|
||||
let session = hash;
|
||||
|
||||
@@ -491,7 +495,12 @@ export const server = new Elysia({
|
||||
// body.session are the last characters of the session token
|
||||
// e.g. body.session = (...)8KdbcHjqxWPR6Q
|
||||
|
||||
const sessionRows = await db`SELECT token FROM sessions WHERE token LIKE ${'%' + body.session}`;
|
||||
if (body.session < 10) {
|
||||
return { success: false, error: "Session code too short" };
|
||||
}
|
||||
|
||||
const sessionRows =
|
||||
await db`SELECT token FROM sessions WHERE token LIKE ${`%${body.session}`}`;
|
||||
const sessionRow = sessionRows[0];
|
||||
|
||||
if (!sessionRow) {
|
||||
@@ -509,10 +518,10 @@ export const server = new Elysia({
|
||||
body: t.Optional(
|
||||
t.Object({
|
||||
session: t.Optional(t.String()),
|
||||
})
|
||||
}),
|
||||
),
|
||||
detail: {
|
||||
tags: ["Settings"],
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -4,79 +4,81 @@ import { Elysia } from "elysia";
|
||||
import { db } from "./db.js";
|
||||
import { ratelimitGenerator } from "./ratelimit.js";
|
||||
|
||||
const blockedIPs = new Map();
|
||||
|
||||
setInterval(() => {
|
||||
const now = Date.now();
|
||||
for (const [ip, unblockTime] of blockedIPs.entries()) {
|
||||
if (now >= unblockTime) {
|
||||
blockedIPs.delete(ip);
|
||||
}
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
export const siteverifyServer = new Elysia({
|
||||
detail: {
|
||||
tags: ["Challenges"],
|
||||
},
|
||||
detail: {
|
||||
tags: ["Challenges"],
|
||||
},
|
||||
})
|
||||
.use(
|
||||
cors({
|
||||
origin: process.env.CORS_ORIGIN?.split(",") || true,
|
||||
methods: ["POST"],
|
||||
}),
|
||||
)
|
||||
.post("/:siteKey/siteverify", async ({ body, set, params, request, server }) => {
|
||||
const ip = ratelimitGenerator(request, server);
|
||||
const now = Date.now();
|
||||
|
||||
const unblockTime = blockedIPs.get(ip);
|
||||
if (unblockTime && now < unblockTime) {
|
||||
const retryAfter = Math.ceil((unblockTime - now) / 1000);
|
||||
set.status = 429;
|
||||
set.headers["Retry-After"] = retryAfter.toString();
|
||||
set.headers["X-RateLimit-Limit"] = "1";
|
||||
set.headers["X-RateLimit-Remaining"] = "0";
|
||||
set.headers["X-RateLimit-Reset"] = Math.ceil(unblockTime / 1000).toString();
|
||||
return { "success":false, error: "You were temporarily blocked for using an invalid secret key. Please try again later." };
|
||||
}
|
||||
.use(
|
||||
cors({
|
||||
origin: process.env.CORS_ORIGIN?.split(",") || true,
|
||||
methods: ["POST"],
|
||||
}),
|
||||
)
|
||||
.post("/:siteKey/siteverify", async ({ body, set, params, request, server }) => {
|
||||
const ip = ratelimitGenerator(request, server);
|
||||
const now = Date.now();
|
||||
|
||||
const sitekey = params.siteKey;
|
||||
const { secret, response } = body;
|
||||
if (ip) {
|
||||
const [ban] = await db`SELECT * FROM ip_bans WHERE ip = ${ip} AND expires > ${now}`;
|
||||
if (ban) {
|
||||
const retryAfter = Math.ceil((ban.expires - now) / 1000);
|
||||
set.status = 429;
|
||||
set.headers["Retry-After"] = retryAfter.toString();
|
||||
set.headers["X-RateLimit-Limit"] = "1";
|
||||
set.headers["X-RateLimit-Remaining"] = "0";
|
||||
set.headers["X-RateLimit-Reset"] = Math.ceil(ban.expires / 1000).toString();
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
"You were temporarily blocked for using an invalid secret key. Please try again later.",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (!sitekey || !secret || !response) {
|
||||
set.status = 400;
|
||||
return { "success":false, error: "Missing required parameters" };
|
||||
}
|
||||
const sitekey = params.siteKey;
|
||||
const { secret, response } = body;
|
||||
|
||||
const [keyData] = await db`SELECT * FROM keys WHERE siteKey = ${sitekey}`;
|
||||
const keyHash = keyData?.secretHash;
|
||||
if (!keyHash || !secret) {
|
||||
set.status = 404;
|
||||
return { "success":false, error: "Invalid site key or secret" };
|
||||
}
|
||||
if (!sitekey || !secret || !response) {
|
||||
set.status = 400;
|
||||
return { success: false, error: "Missing required parameters" };
|
||||
}
|
||||
|
||||
const isValidSecret = await Bun.password.verify(secret, keyHash);
|
||||
|
||||
if (!isValidSecret) {
|
||||
blockedIPs.set(ip, now + 250);
|
||||
set.status = 403;
|
||||
return { "success":false, error: "Invalid site key or secret" };
|
||||
}
|
||||
const [keyData] = await db`SELECT * FROM keys WHERE siteKey = ${sitekey}`;
|
||||
const keyHash = keyData?.secretHash;
|
||||
if (!keyHash || !secret) {
|
||||
set.status = 404;
|
||||
return { success: false, error: "Invalid site key or secret" };
|
||||
}
|
||||
|
||||
const [token] = await db`SELECT * FROM tokens WHERE siteKey = ${params.siteKey} AND token = ${response}`;
|
||||
const isValidSecret = await Bun.password.verify(secret, keyHash);
|
||||
|
||||
if (!token) {
|
||||
set.status = 404;
|
||||
return { "success":false, error: "Token not found" };
|
||||
}
|
||||
if (!isValidSecret) {
|
||||
if (ip) {
|
||||
const banExpires = now + 1_000;
|
||||
await db`
|
||||
INSERT INTO ip_bans (ip, reason, expires)
|
||||
VALUES (${ip}, ${"invalid_secret"}, ${banExpires})
|
||||
ON CONFLICT (ip)
|
||||
DO UPDATE SET reason = ${"invalid_secret"}, expires = ${banExpires}
|
||||
`;
|
||||
}
|
||||
set.status = 403;
|
||||
return { success: false, error: "Invalid site key or secret" };
|
||||
}
|
||||
|
||||
if (token.expires < Date.now()) {
|
||||
await db`DELETE FROM tokens WHERE siteKey = ${params.siteKey} AND token = ${response}`;
|
||||
set.status = 403;
|
||||
return { "success":false,error: "Token expired" };
|
||||
}
|
||||
const [token] =
|
||||
await db`DELETE FROM tokens WHERE siteKey = ${params.siteKey} AND token = ${response} RETURNING *`;
|
||||
|
||||
await db`DELETE FROM tokens WHERE siteKey = ${params.siteKey} AND token = ${response}`;
|
||||
return { success: true };
|
||||
});
|
||||
if (!token) {
|
||||
set.status = 404;
|
||||
return { success: false, error: "Token not found" };
|
||||
}
|
||||
|
||||
if (token.expires < Date.now()) {
|
||||
set.status = 403;
|
||||
return { success: false, error: "Token expired" };
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@cap.js/widget",
|
||||
"version": "0.1.36",
|
||||
"version": "0.1.37",
|
||||
"description": "Client-side widget for Cap, a lightweight, modern open-source CAPTCHA alternative designed using SHA-256 PoW.",
|
||||
"keywords": [
|
||||
"security",
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
setTimeout(() => {
|
||||
element.onclick = null;
|
||||
handlers.forEach((h) => element.removeEventListener("click", h));
|
||||
handlers.forEach((h) => { return element.removeEventListener("click", h) });
|
||||
element.onclick = (e) => handleClick(e, element, capWidget, handlers);
|
||||
}, 50);
|
||||
};
|
||||
@@ -98,7 +98,7 @@
|
||||
|
||||
if (handlers.length) {
|
||||
element.onclick = null;
|
||||
handlers.forEach((h) => element.removeEventListener("click", h));
|
||||
handlers.forEach((h) => { return element.removeEventListener("click", h) });
|
||||
}
|
||||
|
||||
element.addEventListener("click", (e) => {
|
||||
|
||||
@@ -229,3 +229,16 @@
|
||||
text-decoration-thickness: 0.08em;
|
||||
z-index: 6;
|
||||
}
|
||||
|
||||
.cap-troubleshoot-link {
|
||||
color: #0066cc;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 0.15em;
|
||||
text-decoration-thickness: 0.08em;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
font-size: inherit;
|
||||
}
|
||||
.cap-troubleshoot-link:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
+231
-18
@@ -54,6 +54,101 @@
|
||||
return result.substring(0, length);
|
||||
}
|
||||
|
||||
async function runInstrumentationChallenge(instrBytes) {
|
||||
const b64ToUint8 = (b64) => {
|
||||
const bin = atob(b64);
|
||||
const arr = new Uint8Array(bin.length);
|
||||
for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);
|
||||
return arr;
|
||||
};
|
||||
|
||||
var compressed = b64ToUint8(instrBytes);
|
||||
|
||||
const scriptText = await new Promise(function (resolve, reject) {
|
||||
try {
|
||||
var ds = new DecompressionStream("deflate-raw");
|
||||
var writer = ds.writable.getWriter();
|
||||
var reader = ds.readable.getReader();
|
||||
var chunks = [];
|
||||
function pump(res) {
|
||||
if (res.done) {
|
||||
var len = 0,
|
||||
off = 0;
|
||||
for (var i = 0; i < chunks.length; i++) len += chunks[i].length;
|
||||
var out = new Uint8Array(len);
|
||||
for (var i = 0; i < chunks.length; i++) {
|
||||
out.set(chunks[i], off);
|
||||
off += chunks[i].length;
|
||||
}
|
||||
resolve(new TextDecoder().decode(out));
|
||||
} else {
|
||||
chunks.push(res.value);
|
||||
reader.read().then(pump).catch(reject);
|
||||
}
|
||||
}
|
||||
reader.read().then(pump).catch(reject);
|
||||
writer
|
||||
.write(compressed)
|
||||
.then(function () {
|
||||
writer.close();
|
||||
})
|
||||
.catch(reject);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
|
||||
return new Promise(function (resolve) {
|
||||
var timeout = setTimeout(function () {
|
||||
cleanup();
|
||||
resolve({ __timeout: true });
|
||||
}, 20000);
|
||||
|
||||
var iframe = document.createElement("iframe");
|
||||
iframe.setAttribute("sandbox", "allow-scripts");
|
||||
iframe.setAttribute("aria-hidden", "true");
|
||||
iframe.style.cssText =
|
||||
"position:absolute;width:1px;height:1px;top:-9999px;left:-9999px;border:none;opacity:0;pointer-events:none;";
|
||||
|
||||
var resolved = false;
|
||||
function cleanup() {
|
||||
if (resolved) return;
|
||||
resolved = true;
|
||||
clearTimeout(timeout);
|
||||
window.removeEventListener("message", handler);
|
||||
if (iframe.parentNode) iframe.parentNode.removeChild(iframe);
|
||||
}
|
||||
|
||||
function handler(ev) {
|
||||
var d = ev.data;
|
||||
if (!d || typeof d !== "object") return;
|
||||
if (d.type === "cap:instr") {
|
||||
cleanup();
|
||||
if (d.blocked) {
|
||||
resolve({ __blocked: true, blockReason: d.blockReason || "automated_browser" });
|
||||
} else if (d.result) {
|
||||
resolve(d.result);
|
||||
} else {
|
||||
resolve({ __timeout: true });
|
||||
}
|
||||
} else if (d.type === "cap:error") {
|
||||
cleanup();
|
||||
resolve({ __timeout: true });
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("message", handler);
|
||||
|
||||
iframe.srcdoc =
|
||||
'<!DOCTYPE html><html><head><meta charset="UTF-8"></head><body><script>' +
|
||||
scriptText +
|
||||
"\n</scr" +
|
||||
"ipt></body></html>";
|
||||
|
||||
document.body.appendChild(iframe);
|
||||
});
|
||||
}
|
||||
|
||||
let wasmModulePromise = null;
|
||||
|
||||
const getWasmModule = () => {
|
||||
@@ -214,11 +309,22 @@
|
||||
apiEndpoint += "/";
|
||||
}
|
||||
|
||||
const { challenge, token } = await (
|
||||
await capFetch(`${apiEndpoint}challenge`, {
|
||||
method: "POST",
|
||||
})
|
||||
).json();
|
||||
const challengeRaw = await capFetch(`${apiEndpoint}challenge`, {
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
let challengeResp;
|
||||
try {
|
||||
challengeResp = await challengeRaw.json();
|
||||
} catch (parseErr) {
|
||||
throw new Error("Failed to parse challenge response from server");
|
||||
}
|
||||
|
||||
if (challengeResp.error) {
|
||||
throw new Error(challengeResp.error);
|
||||
}
|
||||
|
||||
const { challenge, token } = challengeResp;
|
||||
|
||||
let challenges = challenge;
|
||||
|
||||
@@ -232,19 +338,89 @@
|
||||
});
|
||||
}
|
||||
|
||||
const solutions = await this.solveChallenges(challenges);
|
||||
const instrPromise = challengeResp.instrumentation
|
||||
? runInstrumentationChallenge(challengeResp.instrumentation)
|
||||
: Promise.resolve(null);
|
||||
|
||||
const resp = await (
|
||||
await capFetch(`${apiEndpoint}redeem`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ token, solutions }),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
})
|
||||
).json();
|
||||
const powPromise = this.solveChallenges(challenges);
|
||||
|
||||
const instrErrorPromise = instrPromise.then((result) => {
|
||||
if (result && result.__timeout) return result;
|
||||
return null;
|
||||
});
|
||||
|
||||
const instrEarlyError = await Promise.race([
|
||||
instrErrorPromise,
|
||||
powPromise.then(() => null),
|
||||
]);
|
||||
|
||||
if (instrEarlyError && instrEarlyError.__timeout) {
|
||||
const errMsg = "Instrumentation timeout — please try again later";
|
||||
this.updateUIBlocked(this.getI18nText("error-label", "Error"), true);
|
||||
this.#div.setAttribute(
|
||||
"aria-label",
|
||||
this.getI18nText("error-aria-label", "An error occurred, please try again"),
|
||||
);
|
||||
this.removeEventListener("error", this.boundHandleError);
|
||||
const errEvent = new CustomEvent("error", {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
detail: { isCap: true, message: errMsg },
|
||||
});
|
||||
super.dispatchEvent(errEvent);
|
||||
this.addEventListener("error", this.boundHandleError);
|
||||
this.executeAttributeCode("onerror", errEvent);
|
||||
console.error("[cap]", errMsg);
|
||||
this.#solving = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const [solutions, instrOut] = await Promise.all([powPromise, instrPromise]);
|
||||
|
||||
if (instrOut?.__timeout || instrOut?.__blocked) {
|
||||
this.updateUIBlocked(
|
||||
this.getI18nText("error-label", "Error"),
|
||||
instrOut && instrOut.__blocked,
|
||||
);
|
||||
this.#div.setAttribute(
|
||||
"aria-label",
|
||||
this.getI18nText("error-aria-label", "An error occurred, please try again"),
|
||||
);
|
||||
|
||||
this.removeEventListener("error", this.boundHandleError);
|
||||
const errEvent = new CustomEvent("error", {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
detail: { isCap: true, message: "Instrumentation failed" },
|
||||
});
|
||||
super.dispatchEvent(errEvent);
|
||||
this.addEventListener("error", this.boundHandleError);
|
||||
|
||||
this.executeAttributeCode("onerror", errEvent);
|
||||
console.error("[cap]", "Instrumentation failed");
|
||||
this.#solving = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const redeemResponse = await capFetch(`${apiEndpoint}redeem`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
token,
|
||||
solutions,
|
||||
...(instrOut && { instr: instrOut }),
|
||||
}),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
|
||||
let resp;
|
||||
try {
|
||||
resp = await redeemResponse.json();
|
||||
} catch {
|
||||
throw new Error("Failed to parse server response");
|
||||
}
|
||||
|
||||
this.dispatchEvent("progress", { progress: 100 });
|
||||
|
||||
if (!resp.success) throw new Error("Invalid solution");
|
||||
if (!resp.success) throw new Error(resp.error || "Invalid solution");
|
||||
const fieldName = this.getAttribute("data-cap-hidden-field-name") || "cap-token";
|
||||
if (this.querySelector(`input[name='${fieldName}']`)) {
|
||||
this.querySelector(`input[name='${fieldName}']`).value = resp.token;
|
||||
@@ -406,9 +582,7 @@
|
||||
"Verify you're human",
|
||||
)}</span></p><a part="attribution" aria-label="Secured by Cap" href="https://capjs.js.org/" class="credits" target="_blank" rel="follow noopener" title="Secured by Cap: Self-hosted CAPTCHA for the modern web.">Cap</a>`;
|
||||
|
||||
const css = `%%capCSS%%`;
|
||||
|
||||
this.#shadow.innerHTML = `<style${window.CAP_CSS_NONCE ? ` nonce=${window.CAP_CSS_NONCE}` : ""}>${css}</style>`;
|
||||
this.#shadow.innerHTML = `<style${window.CAP_CSS_NONCE ? ` nonce=${window.CAP_CSS_NONCE}` : ""}>%%capCSS%%</style>`;
|
||||
|
||||
this.#shadow.appendChild(this.#div);
|
||||
}
|
||||
@@ -489,6 +663,43 @@
|
||||
}
|
||||
}
|
||||
|
||||
updateUIBlocked(label, showTroubleshooting = false) {
|
||||
if (!this.#div) return;
|
||||
|
||||
this.#div.setAttribute("data-state", "error");
|
||||
this.#div.removeAttribute("disabled");
|
||||
|
||||
const wrapper = this.#div.querySelector(".label-wrapper");
|
||||
if (!wrapper) return;
|
||||
|
||||
const troubleshootingUrl =
|
||||
this.getAttribute("data-cap-troubleshooting-url") ||
|
||||
"https://capjs.js.org/guide/troubleshooting/instrumentation.html";
|
||||
|
||||
const current = wrapper.querySelector(".label.active");
|
||||
const next = document.createElement("span");
|
||||
next.className = "label";
|
||||
next.innerHTML = showTroubleshooting
|
||||
? `${label} · <a class="cap-troubleshoot-link" href="${troubleshootingUrl}" target="_blank" rel="noopener">${this.getI18nText("troubleshooting-label", "Troubleshoot")}</a>`
|
||||
: label;
|
||||
wrapper.appendChild(next);
|
||||
|
||||
void next.offsetWidth;
|
||||
next.classList.add("active");
|
||||
if (current) {
|
||||
current.classList.remove("active");
|
||||
current.classList.add("exit");
|
||||
current.addEventListener("transitionend", () => current.remove(), { once: true });
|
||||
}
|
||||
|
||||
const link = next.querySelector(".cap-troubleshoot-link");
|
||||
if (link) {
|
||||
link.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleProgress(event) {
|
||||
if (!this.#div) return;
|
||||
|
||||
@@ -531,6 +742,8 @@
|
||||
if (!code) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.error("[cap] using `onxxx='…'` is strongly discouraged and will be deprecated soon. please use `addEventListener` callbacks instead.");
|
||||
|
||||
new Function("event", code).call(this, event);
|
||||
}
|
||||
|
||||
@@ -4,11 +4,18 @@
|
||||
const batchSize = 50000;
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
const targetBytes = new Uint8Array(target.length / 2);
|
||||
for (let k = 0; k < targetBytes.length; k++) {
|
||||
targetBytes[k] = parseInt(target.substring(k * 2, k * 2 + 2), 16);
|
||||
const targetBits = target.length * 4;
|
||||
const fullBytes = Math.floor(targetBits / 8);
|
||||
const remainingBits = targetBits % 8;
|
||||
|
||||
const paddedTarget = target.length % 2 === 0 ? target : target + "0";
|
||||
const targetBytesLength = paddedTarget.length / 2;
|
||||
const targetBytes = new Uint8Array(targetBytesLength);
|
||||
for (let k = 0; k < targetBytesLength; k++) {
|
||||
targetBytes[k] = parseInt(paddedTarget.substring(k * 2, k * 2 + 2), 16);
|
||||
}
|
||||
const targetBytesLength = targetBytes.length;
|
||||
|
||||
const partialMask = remainingBits > 0 ? (0xff << (8 - remainingBits)) & 0xff : 0;
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
@@ -18,16 +25,23 @@
|
||||
|
||||
const hashBuffer = await crypto.subtle.digest("SHA-256", inputBytes);
|
||||
|
||||
const hashBytes = new Uint8Array(hashBuffer, 0, targetBytesLength);
|
||||
const hashBytes = new Uint8Array(hashBuffer);
|
||||
|
||||
let matches = true;
|
||||
for (let k = 0; k < targetBytesLength; k++) {
|
||||
|
||||
for (let k = 0; k < fullBytes; k++) {
|
||||
if (hashBytes[k] !== targetBytes[k]) {
|
||||
matches = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (matches && remainingBits > 0) {
|
||||
if ((hashBytes[fullBytes] & partialMask) !== (targetBytes[fullBytes] & partialMask)) {
|
||||
matches = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (matches) {
|
||||
self.postMessage({ nonce, found: true });
|
||||
return;
|
||||
@@ -123,7 +137,7 @@
|
||||
|
||||
const instance = new WebAssembly.Instance(wasmModule, imports);
|
||||
wasm = instance.exports;
|
||||
|
||||
|
||||
if (wasm.__wbindgen_start) wasm.__wbindgen_start();
|
||||
|
||||
solve_pow_function = (salt, target) => {
|
||||
|
||||
Reference in New Issue
Block a user