diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b1c91b5..fa4ed7a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,4 +14,8 @@ run `bun install` in each Bun package you want to work on to install dependencie - we recommend [Biome](https://biomejs.dev/) for formatting and linting. please fix all errors and warnings before submitting a PR -- [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) are preferred \ No newline at end of file +- [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) are preferred + +## current roadmap + +javascript challenges and reputation-based difficulties are the main new feature focus. diff --git a/demo/basic-store/bun.lock b/demo/basic-store/bun.lock index d66696c..ea4a1bc 100644 --- a/demo/basic-store/bun.lock +++ b/demo/basic-store/bun.lock @@ -1,11 +1,13 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "cap-demo", "dependencies": { "@cap.js/server": "^1.0.13", "elysia": "^1.3.4", + "lightningcss": "^1.31.1", }, }, }, @@ -24,6 +26,8 @@ "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + "elysia": ["elysia@1.3.4", "", { "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-kAfM3Zwovy3z255IZgTKVxBw91HbgKhYl3TqrGRdZqqr+Fd+4eKOfvxgaKij22+MZLczPzIHtscAmvfpI3+q/A=="], "exact-mirror": ["exact-mirror@0.1.2", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-wFCPCDLmHbKGUb8TOi/IS7jLsgR8WVDGtDK3CzcB4Guf/weq7G+I+DkXiRSZfbemBFOxOINKpraM6ml78vo8Zw=="], @@ -36,6 +40,30 @@ "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + "lightningcss": ["lightningcss@1.31.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.31.1", "lightningcss-darwin-arm64": "1.31.1", "lightningcss-darwin-x64": "1.31.1", "lightningcss-freebsd-x64": "1.31.1", "lightningcss-linux-arm-gnueabihf": "1.31.1", "lightningcss-linux-arm64-gnu": "1.31.1", "lightningcss-linux-arm64-musl": "1.31.1", "lightningcss-linux-x64-gnu": "1.31.1", "lightningcss-linux-x64-musl": "1.31.1", "lightningcss-win32-arm64-msvc": "1.31.1", "lightningcss-win32-x64-msvc": "1.31.1" } }, "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ=="], + + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.31.1", "", { "os": "android", "cpu": "arm64" }, "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.31.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.31.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.31.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.31.1", "", { "os": "linux", "cpu": "arm" }, "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.31.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.31.1", "", { "os": "win32", "cpu": "x64" }, "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], diff --git a/demo/basic-store/index.js b/demo/basic-store/index.js index ab8f735..0ba7473 100644 --- a/demo/basic-store/index.js +++ b/demo/basic-store/index.js @@ -1,7 +1,23 @@ import fs from "node:fs/promises"; import { Elysia, file } from "elysia"; +import { transform } from "lightningcss"; import Cap from "../../server/index.js"; +const processCSS = async () => { + const raw = await fs.readFile("../../widget/src/src/cap.css", "utf-8"); + const { code } = transform({ + filename: "cap.css", + code: Buffer.from(raw), + minify: true, + targets: { + chrome: 90 << 16, + firefox: 90 << 16, + safari: 14 << 16, + }, + }); + return code.toString(); +}; + const cap = new Cap({ tokens_store_path: ".data/tokensList.json", }); @@ -11,14 +27,15 @@ const app = new Elysia(); app.get("/", () => file("./index.html")); app.get("/cap.js", async ({ set }) => { - // in the newest version, the worker is injected into the main file + // in the newest version, the worker and CSS are injected into the main file // by a build script. since we don't have a build script here, // we'll need to run a minimal build ourselves. const main = await fs.readFile("../../widget/src/src/cap.js", "utf-8"); const worker = await fs.readFile("../../widget/src/src/worker.js", "utf-8"); + const css = await processCSS(); - const bundle = main.replace("%%workerScript%%", worker); + const bundle = main.replace("%%workerScript%%", worker).replace("%%capCSS%%", css); set.headers = { "Content-Type": "application/javascript", @@ -29,6 +46,11 @@ app.get("/cap.js", async ({ set }) => { app.get("/cap-floating.js", () => file("../../widget/src/src/cap-floating.js")); +app.get("/cap.css", async ({ set }) => { + set.headers = { "Content-Type": "text/css" }; + return processCSS(); +}); + app.post("/api/challenge", () => cap.createChallenge()); app.post("/api/redeem", async ({ body, set }) => { diff --git a/demo/basic-store/package.json b/demo/basic-store/package.json index 3463025..c61d703 100644 --- a/demo/basic-store/package.json +++ b/demo/basic-store/package.json @@ -12,6 +12,7 @@ }, "dependencies": { "@cap.js/server": "^1.0.13", - "elysia": "^1.3.4" + "elysia": "^1.3.4", + "lightningcss": "^1.31.1" } } diff --git a/demo/checkpoint/elysia/bun.lock b/demo/checkpoint/elysia/bun.lock deleted file mode 100644 index 7551e4f..0000000 --- a/demo/checkpoint/elysia/bun.lock +++ /dev/null @@ -1,54 +0,0 @@ -{ - "lockfileVersion": 1, - "workspaces": { - "": { - "dependencies": { - "@cap.js/server": "latest", - "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.29", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LNdjOkUDlU1RZb8e1kOIUpN1qQUlzGkEtbVNo53vbrwDg5om6oduhm4SiUaPW5ASTXhAiP0jInWG8Qx9fVlOeQ=="], - - "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=="], - } -} diff --git a/demo/checkpoint/elysia/index.js b/demo/checkpoint/elysia/index.js deleted file mode 100644 index 6a1a0ff..0000000 --- a/demo/checkpoint/elysia/index.js +++ /dev/null @@ -1,22 +0,0 @@ -import { Elysia, file } from "elysia"; -import { capMiddleware } from "../../../checkpoints/elysia/index"; - -// import { capMiddleware } from "@cap.js/middleware-elysia"; - -new Elysia() - .use( - capMiddleware({ - /* - token_validity_hours: 32, // how long the token is valid for - tokens_store_path: ".data/tokensList.json", - token_size: 16, // token size in bytes - verification_template_path: join( - dirname(fileURLToPath(import.meta.url)), - "./index.html" - ), - scoping: "scoped", // 'global' | 'scoped' - */ - }), - ) - .get("/", () => file("success.html")) - .listen(3000); diff --git a/demo/checkpoint/elysia/package.json b/demo/checkpoint/elysia/package.json deleted file mode 100644 index 6a48d75..0000000 --- a/demo/checkpoint/elysia/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "dependencies": { - "@cap.js/server": "latest", - "elysia": "^1.3.3" - } -} diff --git a/demo/checkpoint/elysia/success.html b/demo/checkpoint/elysia/success.html deleted file mode 100644 index 22c26e0..0000000 --- a/demo/checkpoint/elysia/success.html +++ /dev/null @@ -1,52 +0,0 @@ - - - - Success! - - - - -
-

🎉

-

You have successfully passed Cap's checkpoint.

- -
- - - diff --git a/demo/checkpoint/express/bun.lock b/demo/checkpoint/express/bun.lock deleted file mode 100644 index c6da6ef..0000000 --- a/demo/checkpoint/express/bun.lock +++ /dev/null @@ -1,157 +0,0 @@ -{ - "lockfileVersion": 1, - "workspaces": { - "": { - "dependencies": { - "@cap.js/server": "latest", - "cookie-parser": "^1.4.7", - "express": "^5.1.0", - }, - }, - }, - "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.29", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LNdjOkUDlU1RZb8e1kOIUpN1qQUlzGkEtbVNo53vbrwDg5om6oduhm4SiUaPW5ASTXhAiP0jInWG8Qx9fVlOeQ=="], - - "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], - - "body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="], - - "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], - - "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=="], - - "content-disposition": ["content-disposition@1.0.0", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg=="], - - "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], - - "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], - - "cookie-parser": ["cookie-parser@1.4.7", "", { "dependencies": { "cookie": "0.7.2", "cookie-signature": "1.0.6" } }, "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw=="], - - "cookie-signature": ["cookie-signature@1.0.6", "", {}, "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="], - - "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], - - "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], - - "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=="], - - "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - - "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], - - "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=="], - - "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], - - "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], - - "express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="], - - "finalhandler": ["finalhandler@2.1.0", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q=="], - - "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], - - "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], - - "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], - - "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-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], - - "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], - - "http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], - - "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], - - "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - - "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], - - "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], - - "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], - - "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], - - "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], - - "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], - - "mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="], - - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - - "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], - - "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], - - "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], - - "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], - - "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], - - "path-to-regexp": ["path-to-regexp@8.2.0", "", {}, "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ=="], - - "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], - - "qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="], - - "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], - - "raw-body": ["raw-body@3.0.0", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.6.3", "unpipe": "1.0.0" } }, "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g=="], - - "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], - - "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - - "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], - - "send": ["send@1.2.0", "", { "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.0", "mime-types": "^3.0.1", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw=="], - - "serve-static": ["serve-static@2.2.0", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ=="], - - "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], - - "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], - - "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], - - "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], - - "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], - - "statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], - - "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], - - "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], - - "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=="], - - "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], - - "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], - - "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], - - "express/cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], - } -} diff --git a/demo/checkpoint/express/index.js b/demo/checkpoint/express/index.js deleted file mode 100644 index fcb6356..0000000 --- a/demo/checkpoint/express/index.js +++ /dev/null @@ -1,32 +0,0 @@ -import path, { dirname } from "node:path"; -import { fileURLToPath } from "node:url"; -import cookieParser from "cookie-parser"; -import express from "express"; -import { capCheckpoint } from "../../../checkpoints/express/index"; - -// import { capCheckpoint } from "@cap.js/checkpoint-express"; - -const app = express(); -const __dirname = dirname(fileURLToPath(import.meta.url)); - -app.use(express.json()); -app.use(cookieParser()); - -app.use( - capCheckpoint({ - /* - token_validity_hours: 32, - tokens_store_path: ".data/tokensList.json", - token_size: 16, - verification_template_path: join(__dirname, "./index.html"), - */ - }), -); - -app.get("/", (_, res) => { - res.sendFile(path.join(__dirname, "success.html")); -}); - -app.listen(3000, () => { - console.log(`Server running on http://localhost:3000`); -}); diff --git a/demo/checkpoint/express/package.json b/demo/checkpoint/express/package.json deleted file mode 100644 index 6aa0461..0000000 --- a/demo/checkpoint/express/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "dependencies": { - "@cap.js/server": "latest", - "cookie-parser": "^1.4.7", - "express": "^5.1.0" - } -} diff --git a/demo/checkpoint/express/success.html b/demo/checkpoint/express/success.html deleted file mode 100644 index 22c26e0..0000000 --- a/demo/checkpoint/express/success.html +++ /dev/null @@ -1,52 +0,0 @@ - - - - Success! - - - - -
-

🎉

-

You have successfully passed Cap's checkpoint.

- -
- - - diff --git a/demo/checkpoint/hono/bun.lock b/demo/checkpoint/hono/bun.lock deleted file mode 100644 index b226c79..0000000 --- a/demo/checkpoint/hono/bun.lock +++ /dev/null @@ -1,22 +0,0 @@ -{ - "lockfileVersion": 1, - "workspaces": { - "": { - "dependencies": { - "@cap.js/server": "latest", - "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=="], - } -} diff --git a/demo/checkpoint/hono/index.js b/demo/checkpoint/hono/index.js deleted file mode 100644 index 6666046..0000000 --- a/demo/checkpoint/hono/index.js +++ /dev/null @@ -1,24 +0,0 @@ -import { Hono } from "hono"; -import { serveStatic } from "hono/bun"; -import { capCheckpoint } from "../../../checkpoints/hono/index"; -// import { capCheckpoint } from "@cap.js/checkpoint-hono"; - -const app = new Hono(); - -app.use( - "*", - capCheckpoint({ - /* - token_validity_hours: 32, // how long the token is valid for - tokens_store_path: ".data/tokensList.json", - token_size: 16, // token size in bytes - verification_template_path: join( - dirname(fileURLToPath(import.meta.url)), - "./index.html" - ) - */ - }) -); -app.get("/", serveStatic({ path: "./success.html" })); - -export default app; diff --git a/demo/checkpoint/hono/package.json b/demo/checkpoint/hono/package.json deleted file mode 100644 index 5f60a30..0000000 --- a/demo/checkpoint/hono/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "dependencies": { - "@cap.js/server": "latest", - "hono": "^4.7.10" - } -} diff --git a/demo/checkpoint/hono/success.html b/demo/checkpoint/hono/success.html deleted file mode 100644 index 22c26e0..0000000 --- a/demo/checkpoint/hono/success.html +++ /dev/null @@ -1,52 +0,0 @@ - - - - Success! - - - - -
-

🎉

-

You have successfully passed Cap's checkpoint.

- -
- - - diff --git a/demo/localized/bun.lock b/demo/localized/bun.lock index d66696c..ea4a1bc 100644 --- a/demo/localized/bun.lock +++ b/demo/localized/bun.lock @@ -1,11 +1,13 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "cap-demo", "dependencies": { "@cap.js/server": "^1.0.13", "elysia": "^1.3.4", + "lightningcss": "^1.31.1", }, }, }, @@ -24,6 +26,8 @@ "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + "elysia": ["elysia@1.3.4", "", { "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-kAfM3Zwovy3z255IZgTKVxBw91HbgKhYl3TqrGRdZqqr+Fd+4eKOfvxgaKij22+MZLczPzIHtscAmvfpI3+q/A=="], "exact-mirror": ["exact-mirror@0.1.2", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-wFCPCDLmHbKGUb8TOi/IS7jLsgR8WVDGtDK3CzcB4Guf/weq7G+I+DkXiRSZfbemBFOxOINKpraM6ml78vo8Zw=="], @@ -36,6 +40,30 @@ "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + "lightningcss": ["lightningcss@1.31.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.31.1", "lightningcss-darwin-arm64": "1.31.1", "lightningcss-darwin-x64": "1.31.1", "lightningcss-freebsd-x64": "1.31.1", "lightningcss-linux-arm-gnueabihf": "1.31.1", "lightningcss-linux-arm64-gnu": "1.31.1", "lightningcss-linux-arm64-musl": "1.31.1", "lightningcss-linux-x64-gnu": "1.31.1", "lightningcss-linux-x64-musl": "1.31.1", "lightningcss-win32-arm64-msvc": "1.31.1", "lightningcss-win32-x64-msvc": "1.31.1" } }, "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ=="], + + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.31.1", "", { "os": "android", "cpu": "arm64" }, "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.31.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.31.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.31.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.31.1", "", { "os": "linux", "cpu": "arm" }, "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.31.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.31.1", "", { "os": "win32", "cpu": "x64" }, "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], diff --git a/demo/localized/index.js b/demo/localized/index.js index 26731e5..6f2844d 100644 --- a/demo/localized/index.js +++ b/demo/localized/index.js @@ -3,9 +3,25 @@ import fs from "node:fs/promises"; import Cap from "@cap.js/server"; import { Elysia, file } from "elysia"; +import { transform } from "lightningcss"; + +const processCSS = async () => { + const raw = await fs.readFile("../../widget/src/src/cap.css", "utf-8"); + const { code } = transform({ + filename: "cap.css", + code: Buffer.from(raw), + minify: true, + targets: { + chrome: 90 << 16, + firefox: 90 << 16, + safari: 14 << 16, + }, + }); + return code.toString(); +}; const cap = new Cap({ - tokens_store_path: ".data/tokensList.json", + tokens_store_path: ".data/tokensList.json", }); const app = new Elysia(); @@ -13,41 +29,47 @@ const app = new Elysia(); app.get("/", () => file("./index.html")); app.get("/cap.js", async ({ set }) => { - // in the newest version, the worker is injected into the main file - // by a build script. since we don't have a build script here, - // we'll need to build it ourselves. + // in the newest version, the worker and CSS are injected into the main file + // by a build script. since we don't have a build script here, + // we'll need to build it ourselves. - const main = await fs.readFile("../../widget/src/src/cap.js", "utf-8"); - const worker = await fs.readFile("../../widget/src/src/worker.js", "utf-8"); + const main = await fs.readFile("../../widget/src/src/cap.js", "utf-8"); + const worker = await fs.readFile("../../widget/src/src/worker.js", "utf-8"); + const css = await processCSS(); - const bundle = main.replace("%%workerScript%%", worker); + const bundle = main.replace("%%workerScript%%", worker).replace("%%capCSS%%", css); - set.headers = { - "Content-Type": "application/javascript", - }; + set.headers = { + "Content-Type": "application/javascript", + }; - return bundle; + return bundle; }); app.get("/cap-floating.js", () => file("../../widget/src/src/cap-floating.js")); +app.get("/cap.css", async ({ set }) => { + set.headers = { "Content-Type": "text/css" }; + return processCSS(); +}); + app.post("/api/challenge", () => cap.createChallenge()); app.post("/api/redeem", async ({ body, set }) => { - const { token, solutions } = body; - if (!token || !solutions) { - set.status = 400; - return { success: false }; - } + const { token, solutions } = body; + if (!token || !solutions) { + set.status = 400; + return { success: false }; + } - const answer = await cap.redeemChallenge({ token, solutions }); + const answer = await cap.redeemChallenge({ token, solutions }); - console.log("new challenge redeemed", { - ...answer, - isValid: (await cap.validateToken(answer.token)).success, - }); + console.log("new challenge redeemed", { + ...answer, + isValid: (await cap.validateToken(answer.token)).success, + }); - return answer; + return answer; }); app.listen(3000); diff --git a/demo/localized/package.json b/demo/localized/package.json index 3463025..c61d703 100644 --- a/demo/localized/package.json +++ b/demo/localized/package.json @@ -12,6 +12,7 @@ }, "dependencies": { "@cap.js/server": "^1.0.13", - "elysia": "^1.3.4" + "elysia": "^1.3.4", + "lightningcss": "^1.31.1" } } diff --git a/demo/main/bun.lock b/demo/main/bun.lock index d66696c..ea4a1bc 100644 --- a/demo/main/bun.lock +++ b/demo/main/bun.lock @@ -1,11 +1,13 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "cap-demo", "dependencies": { "@cap.js/server": "^1.0.13", "elysia": "^1.3.4", + "lightningcss": "^1.31.1", }, }, }, @@ -24,6 +26,8 @@ "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + "elysia": ["elysia@1.3.4", "", { "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-kAfM3Zwovy3z255IZgTKVxBw91HbgKhYl3TqrGRdZqqr+Fd+4eKOfvxgaKij22+MZLczPzIHtscAmvfpI3+q/A=="], "exact-mirror": ["exact-mirror@0.1.2", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-wFCPCDLmHbKGUb8TOi/IS7jLsgR8WVDGtDK3CzcB4Guf/weq7G+I+DkXiRSZfbemBFOxOINKpraM6ml78vo8Zw=="], @@ -36,6 +40,30 @@ "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + "lightningcss": ["lightningcss@1.31.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.31.1", "lightningcss-darwin-arm64": "1.31.1", "lightningcss-darwin-x64": "1.31.1", "lightningcss-freebsd-x64": "1.31.1", "lightningcss-linux-arm-gnueabihf": "1.31.1", "lightningcss-linux-arm64-gnu": "1.31.1", "lightningcss-linux-arm64-musl": "1.31.1", "lightningcss-linux-x64-gnu": "1.31.1", "lightningcss-linux-x64-musl": "1.31.1", "lightningcss-win32-arm64-msvc": "1.31.1", "lightningcss-win32-x64-msvc": "1.31.1" } }, "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ=="], + + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.31.1", "", { "os": "android", "cpu": "arm64" }, "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.31.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.31.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.31.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.31.1", "", { "os": "linux", "cpu": "arm" }, "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.31.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.31.1", "", { "os": "win32", "cpu": "x64" }, "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], diff --git a/demo/main/index.js b/demo/main/index.js index 7522aa9..0a14777 100644 --- a/demo/main/index.js +++ b/demo/main/index.js @@ -1,8 +1,24 @@ import { Database } from "bun:sqlite"; import fs from "node:fs/promises"; import { Elysia, file } from "elysia"; +import { transform } from "lightningcss"; import Cap from "../../server/index.js"; +const processCSS = async () => { + const raw = await fs.readFile("../../widget/src/src/cap.css", "utf-8"); + const { code } = transform({ + filename: "cap.css", + code: Buffer.from(raw), + minify: true, + targets: { + chrome: 90 << 16, + firefox: 90 << 16, + safari: 14 << 16, + }, + }); + return code.toString(); +}; + const db = new Database("./.data/cap.db"); db.exec(` @@ -18,56 +34,51 @@ db.exec(` `); const cap = new Cap({ - storage: { - challenges: { - store: async (token, challengeData) => { - db.prepare( - "INSERT OR REPLACE INTO challenges (token, data, expires) VALUES (?, ?, ?)", - ).run( - token, - JSON.stringify(challengeData.challenge), - challengeData.expires, - ); - }, - read: async (token) => { - const row = db - .prepare( - "SELECT data, expires FROM challenges WHERE token = ? AND expires > ?", - ) - .get(token, Date.now()); + storage: { + challenges: { + store: async (token, challengeData) => { + db.prepare("INSERT OR REPLACE INTO challenges (token, data, expires) VALUES (?, ?, ?)").run( + token, + JSON.stringify(challengeData.challenge), + challengeData.expires, + ); + }, + read: async (token) => { + const row = db + .prepare("SELECT data, expires FROM challenges WHERE token = ? AND expires > ?") + .get(token, Date.now()); - return row - ? { challenge: JSON.parse(row.data), expires: row.expires } - : null; - }, - delete: async (token) => { - db.prepare("DELETE FROM challenges WHERE token = ?").run(token); - }, - deleteExpired: async () => { - db.prepare("DELETE FROM challenges WHERE expires <= ?").run(Date.now()); - }, - }, - tokens: { - store: async (tokenKey, expires) => { - db.prepare( - "INSERT OR REPLACE INTO tokens (key, expires) VALUES (?, ?)", - ).run(tokenKey, expires); - }, - get: async (tokenKey) => { - const row = db - .prepare("SELECT expires FROM tokens WHERE key = ? AND expires > ?") - .get(tokenKey, Date.now()); + return row ? { challenge: JSON.parse(row.data), expires: row.expires } : null; + }, + delete: async (token) => { + db.prepare("DELETE FROM challenges WHERE token = ?").run(token); + }, + deleteExpired: async () => { + db.prepare("DELETE FROM challenges WHERE expires <= ?").run(Date.now()); + }, + }, + tokens: { + store: async (tokenKey, expires) => { + db.prepare("INSERT OR REPLACE INTO tokens (key, expires) VALUES (?, ?)").run( + tokenKey, + expires, + ); + }, + get: async (tokenKey) => { + const row = db + .prepare("SELECT expires FROM tokens WHERE key = ? AND expires > ?") + .get(tokenKey, Date.now()); - return row ? row.expires : null; - }, - delete: async (tokenKey) => { - db.prepare("DELETE FROM tokens WHERE key = ?").run(tokenKey); - }, - deleteExpired: async () => { - db.prepare("DELETE FROM tokens WHERE expires <= ?").run(Date.now()); - }, - }, - }, + return row ? row.expires : null; + }, + delete: async (tokenKey) => { + db.prepare("DELETE FROM tokens WHERE key = ?").run(tokenKey); + }, + deleteExpired: async () => { + db.prepare("DELETE FROM tokens WHERE expires <= ?").run(Date.now()); + }, + }, + }, }); const app = new Elysia(); @@ -75,41 +86,47 @@ const app = new Elysia(); app.get("/", () => file("./index.html")); app.get("/cap.js", async ({ set }) => { - // in the newest version, the worker is injected into the main file - // by a build script. since we don't have a build script here, - // we'll need to run a minimal build ourselves. + // in the newest version, the worker and CSS are injected into the main file + // by a build script. since we don't have a build script here, + // we'll need to run a minimal build ourselves. - const main = await fs.readFile("../../widget/src/src/cap.js", "utf-8"); - const worker = await fs.readFile("../../widget/src/src/worker.js", "utf-8"); + const main = await fs.readFile("../../widget/src/src/cap.js", "utf-8"); + const worker = await fs.readFile("../../widget/src/src/worker.js", "utf-8"); + const css = await processCSS(); - const bundle = main.replace("%%workerScript%%", worker); + const bundle = main.replace("%%workerScript%%", worker).replace("%%capCSS%%", css); - set.headers = { - "Content-Type": "application/javascript", - }; + set.headers = { + "Content-Type": "application/javascript", + }; - return bundle; + return bundle; }); app.get("/cap-floating.js", () => file("../../widget/src/src/cap-floating.js")); +app.get("/cap.css", async ({ set }) => { + set.headers = { "Content-Type": "text/css" }; + return processCSS(); +}); + app.post("/api/challenge", () => cap.createChallenge()); app.post("/api/redeem", async ({ body, set }) => { - const { token, solutions } = body; - if (!token || !solutions) { - set.status = 400; - return { success: false }; - } + const { token, solutions } = body; + if (!token || !solutions) { + set.status = 400; + return { success: false }; + } - const answer = await cap.redeemChallenge({ token, solutions }); + const answer = await cap.redeemChallenge({ token, solutions }); - console.log("new challenge redeemed", { - ...answer, - isValid: (await cap.validateToken(answer.token)).success, - }); + console.log("new challenge redeemed", { + ...answer, + isValid: (await cap.validateToken(answer.token)).success, + }); - return answer; + return answer; }); app.listen(3000); diff --git a/demo/main/package.json b/demo/main/package.json index 3463025..c61d703 100644 --- a/demo/main/package.json +++ b/demo/main/package.json @@ -12,6 +12,7 @@ }, "dependencies": { "@cap.js/server": "^1.0.13", - "elysia": "^1.3.4" + "elysia": "^1.3.4", + "lightningcss": "^1.31.1" } } diff --git a/docs/.vitepress/config.mjs b/docs/.vitepress/config.mjs index 2790862..e195dae 100644 --- a/docs/.vitepress/config.mjs +++ b/docs/.vitepress/config.mjs @@ -128,7 +128,7 @@ export default withMermaid({ [ "script", { - src: "https://cdn.jsdelivr.net/npm/@cap.js/widget@0.1.35", + src: "https://cdn.jsdelivr.net/npm/@cap.js/widget@0.1.36", async: true, }, ], diff --git a/widget/build.js b/widget/build.js index 7643bc9..7f009d4 100644 --- a/widget/build.js +++ b/widget/build.js @@ -1,44 +1,61 @@ import fs from "node:fs/promises"; +import { transform } from "lightningcss"; import { chromium } from "playwright"; import { minify } from "terser"; -const min = async (input) => { - return ( - await minify(input, { - compress: { - drop_console: false, - dead_code: true, - reduce_vars: true, - }, - output: { - beautify: false, - comments: false, - }, - mangle: true, - }) - ).code - .split("\\n") - .map((e) => { - return e.trimStart(); - }) - .join("\\n"); +const minifyCSS = (input) => { + const { code } = transform({ + filename: "cap.css", + code: Buffer.from(input), + minify: true, + targets: { + chrome: 90 << 16, + firefox: 90 << 16, + safari: 14 << 16, + }, + }); + return code.toString(); +}; + +const minifyJS = async (input) => { + return ( + await minify(input, { + compress: { + drop_console: false, + dead_code: true, + reduce_vars: true, + }, + output: { + beautify: false, + comments: false, + }, + mangle: true, + }) + ).code + .split("\n") + .map((e) => { + return e.trimStart(); + }) + .join("\n"); }; console.time("build"); const rawMain = await fs.readFile("./src/src/cap.js", "utf-8"); -const minifiedWorker = await min( - await fs.readFile("./src/src/worker.js", "utf-8"), -); +const rawCSS = await fs.readFile("./src/src/cap.css", "utf-8"); +const minifiedWorker = await minifyJS(await fs.readFile("./src/src/worker.js", "utf-8")); +const minifiedCSS = minifyCSS(rawCSS); -const bundle = rawMain.replace("%%workerScript%%", minifiedWorker); +const bundle = rawMain + .replace("%%workerScript%%", minifiedWorker) + .replace("%%capCSS%%", minifiedCSS); await fs.writeFile("./src/cap.min.js", bundle); -await fs.writeFile("./src/cap.min.js", await min(bundle)); +await fs.writeFile("./src/cap.min.js", await minifyJS(bundle)); await fs.writeFile( - "./src/cap-floating.min.js", - await min(await fs.readFile("./src/src/cap-floating.js", "utf-8")), + "./src/cap-floating.min.js", + await minifyJS(await fs.readFile("./src/src/cap-floating.js", "utf-8")), ); console.timeEnd("build"); @@ -49,42 +66,42 @@ const browser = await chromium.launch({ headless: false }); const page = await browser.newPage(); const server = Bun.serve({ - port: 3006, - routes: { - "/": () => { - return new Response(Bun.file("./test.html")); - }, - "/widget.js": () => { - return new Response(Bun.file("./src/cap.min.js"), { - headers: { - "Content-Type": "application/javascript", - }, - }); - }, - "/failed": () => { - console.error("test failed, quitting"); - process.exit(1); - }, - "/solved": async () => { - await page.close(); - await browser.close(); + port: 3006, + routes: { + "/": () => { + return new Response(Bun.file("./test.html")); + }, + "/widget.js": () => { + return new Response(Bun.file("./src/cap.min.js"), { + headers: { + "Content-Type": "application/javascript", + }, + }); + }, + "/failed": () => { + console.error("test failed, quitting"); + process.exit(1); + }, + "/solved": async () => { + await page.close(); + await browser.close(); - console.timeEnd("test"); - server.stop(); + console.timeEnd("test"); + server.stop(); - const publish = prompt("\npublish package to npm? (y/N)"); + const publish = prompt("\npublish package to npm? (y/N)"); - if (publish === "y") { - Bun.spawn({ - cmd: ["bun", "publish", "--access", "public"], - cwd: "./src", - stdout: "inherit", - }); - } + if (publish === "y") { + Bun.spawn({ + cmd: ["bun", "publish", "--access", "public"], + cwd: "./src", + stdout: "inherit", + }); + } - process.exit(0); - }, - }, + process.exit(0); + }, + }, }); page.goto(`http://localhost:3006/`); diff --git a/widget/bun.lock b/widget/bun.lock index 210844f..87ee088 100644 --- a/widget/bun.lock +++ b/widget/bun.lock @@ -1,9 +1,11 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "widget", "dependencies": { + "lightningcss": "^1.31.1", "playwright": "^1.52.0", "terser": "^5.43.1", }, @@ -42,8 +44,34 @@ "commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + "fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], + "lightningcss": ["lightningcss@1.31.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.31.1", "lightningcss-darwin-arm64": "1.31.1", "lightningcss-darwin-x64": "1.31.1", "lightningcss-freebsd-x64": "1.31.1", "lightningcss-linux-arm-gnueabihf": "1.31.1", "lightningcss-linux-arm64-gnu": "1.31.1", "lightningcss-linux-arm64-musl": "1.31.1", "lightningcss-linux-x64-gnu": "1.31.1", "lightningcss-linux-x64-musl": "1.31.1", "lightningcss-win32-arm64-msvc": "1.31.1", "lightningcss-win32-x64-msvc": "1.31.1" } }, "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ=="], + + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.31.1", "", { "os": "android", "cpu": "arm64" }, "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.31.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.31.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.31.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.31.1", "", { "os": "linux", "cpu": "arm" }, "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.31.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.31.1", "", { "os": "win32", "cpu": "x64" }, "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw=="], + "playwright": ["playwright@1.52.0", "", { "dependencies": { "playwright-core": "1.52.0" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw=="], "playwright-core": ["playwright-core@1.52.0", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg=="], diff --git a/widget/package.json b/widget/package.json index 37eeb10..3f3db62 100644 --- a/widget/package.json +++ b/widget/package.json @@ -8,6 +8,7 @@ "typescript": "^5" }, "dependencies": { + "lightningcss": "^1.31.1", "playwright": "^1.52.0", "terser": "^5.43.1" } diff --git a/widget/src/cap.min.js b/widget/src/cap.min.js index 01b8fc1..65e2a29 100644 --- a/widget/src/cap.min.js +++ b/widget/src/cap.min.js @@ -1 +1 @@ -(()=>{const e="0.0.6";if("undefined"==typeof window)return;const t=(...e)=>window?.CAP_CUSTOM_FETCH?window.CAP_CUSTOM_FETCH(...e):fetch(...e);function r(e,t){let r=function(e){let t=2166136261;for(let r=0;r>>0}(e),s="";function i(){return r^=r<<13,r^=r>>>17,r^=r<<5,r>>>0}for(;s.length{const t=document.createElement("link");t.rel="prefetch",t.href=e,t.as=e.endsWith(".wasm")?"fetch":"script",document.head.appendChild(t)});class s extends HTMLElement{#e="";#t=null;#r=navigator.hardwareConcurrency||8;token=null;#s;#i;#a;#n=!1;#o;getI18nText(e,t){return this.getAttribute(`data-cap-i18n-${e}`)||t}static get observedAttributes(){return["onsolve","onprogress","onreset","onerror","data-cap-worker-count","data-cap-i18n-initial-state","[cap]"]}constructor(){super(),this.#o&&this.#o.forEach((e,t)=>{this.removeEventListener(t.slice(2),e)}),this.#o=new Map,this.boundHandleProgress=this.handleProgress.bind(this),this.boundHandleSolve=this.handleSolve.bind(this),this.boundHandleError=this.handleError.bind(this),this.boundHandleReset=this.handleReset.bind(this)}initialize(){this.#e=URL.createObjectURL(new Blob(['(()=>{const e=async({salt:e,target:t})=>{let r=0;const o=new TextEncoder,s=new Uint8Array(t.length/2);for(let e=0;ee({salt:t,target:r}));let t,r;self.onmessage=async({data:{salt:o,target:s,wasmUrl:n}})=>{let a;if(t===n||(t=n,await import(n).then(e=>e.default().then(t=>{r=(t?.exports?t.exports:e).solve_pow})).catch(t=>(console.error("[cap worker] using fallback solver due to error:",t),a=!0,e({salt:o,target:s}))),!a))try{const e=performance.now(),t=r(o,s),n=performance.now();self.postMessage({nonce:Number(t),found:!0,durationMs:(n-e).toFixed(2)})}catch(e){console.error("[cap worker]",e),self.postMessage({found:!1,error:e.message||String(e)})}},self.onerror=e=>{self.postMessage({found:!1,error:e})}})();'],{type:"application/javascript"}))}attributeChangedCallback(e,t,r){if(e.startsWith("on")){const t=e.slice(2),s=this.#o.get(e);if(s&&this.removeEventListener(t,s),r){const r=t=>{const r=this.getAttribute(e);"function"==typeof window[r]&&window[r].call(this,t)};this.#o.set(e,r),this.addEventListener(t,r)}}"data-cap-worker-count"===e&&this.setWorkersCount(parseInt(r,10)),"data-cap-i18n-initial-state"===e&&this.#i&&this.#i?.querySelector("p")?.innerText&&(this.#i.querySelector("p").innerText=this.getI18nText("initial-state","Verify you're human"))}async connectedCallback(){this.#a=this,this.#s=this.attachShadow({mode:"open"}),this.#i=document.createElement("div"),this.createUI(),this.addEventListeners(),this.initialize(),this.#i.removeAttribute("disabled");const e=this.getAttribute("data-cap-worker-count"),t=e?parseInt(e,10):null;this.setWorkersCount(t||navigator.hardwareConcurrency||8);const r=this.getAttribute("data-cap-hidden-field-name")||"cap-token";this.#a.innerHTML=``}async solve(){if(!this.#n)try{this.#n=!0,this.updateUI("verifying",this.getI18nText("verifying-label","Verifying..."),!0),this.#i.setAttribute("aria-label",this.getI18nText("verifying-aria-label","Verifying you're a human, please wait")),this.dispatchEvent("progress",{progress:0});try{let e=this.getAttribute("data-cap-api-endpoint");if(!e&&window?.CAP_CUSTOM_FETCH)e="/";else if(!e)throw new Error("Missing API endpoint. Either custom fetch or an API endpoint must be provided.");e.endsWith("/")||(e+="/");const{challenge:s,token:i}=await(await t(`${e}challenge`,{method:"POST"})).json();let a=s;if(!Array.isArray(a)){let e=0;a=Array.from({length:s.c},()=>(e+=1,[r(`${i}${e}`,s.s),r(`${i}${e}d`,s.d)]))}const n=await this.solveChallenges(a),o=await(await t(`${e}redeem`,{method:"POST",body:JSON.stringify({token:i,solutions:n}),headers:{"Content-Type":"application/json"}})).json();if(this.dispatchEvent("progress",{progress:100}),!o.success)throw new Error("Invalid solution");const d=this.getAttribute("data-cap-hidden-field-name")||"cap-token";this.querySelector(`input[name='${d}']`)&&(this.querySelector(`input[name='${d}']`).value=o.token),this.dispatchEvent("solve",{token:o.token}),this.token=o.token,this.#t&&clearTimeout(this.#t);const c=new Date(o.expires).getTime()-Date.now();return c>0&&c<864e5?this.#t=setTimeout(()=>this.reset(),c):this.error("Invalid expiration time"),this.#i.setAttribute("aria-label",this.getI18nText("verified-aria-label","We have verified you're a human, you may now continue")),{success:!0,token:this.token}}catch(e){throw this.#i.setAttribute("aria-label",this.getI18nText("error-aria-label","An error occurred, please try again")),this.error(e.message),e}}finally{this.#n=!1}}async solveChallenges(t){const r=t.length;let s=0;const i=Array(this.#r).fill(null).map(()=>{try{return new Worker(this.#e)}catch(e){throw console.error("[cap] Failed to create worker:",e),new Error("Worker creation failed")}}),a=([t,a],n)=>new Promise((o,d)=>{const c=i[n];if(c){if(c.onmessage=({data:e})=>{e.found&&(s++,this.dispatchEvent("progress",{progress:Math.round(s/r*100)}),o(e.nonce))},c.onerror=e=>{this.error(`Error in worker: ${e.message||e}`),d(e)},c.postMessage({salt:t,target:a,wasmUrl:window.CAP_CUSTOM_WASM_URL||`https://cdn.jsdelivr.net/npm/@cap.js/wasm@${e}/browser/cap_wasm.min.js`}),"object"!=typeof WebAssembly||"function"!=typeof WebAssembly?.instantiate){if(this.#s.querySelector(".warning"))return;const e=document.createElement("div");e.className="warning",e.style.cssText="width: var(--cap-widget-width, 230px);background: rgb(237, 56, 46);color: white;padding: 4px 6px;padding-bottom: calc(var(--cap-border-radius, 14px) + 5px);font-size: 10px;box-sizing: border-box;font-family: system-ui;border-top-left-radius: 8px;border-top-right-radius: 8px;text-align: center;padding-bottom:calc(var(--cap-border-radius,14px) + 5px);user-select:none;margin-bottom: -35.5px;opacity: 0;transition: margin-bottom .3s,opacity .3s;",e.innerText=this.getI18nText("wasm-disabled","Enable WASM for significantly faster solving"),this.#s.insertBefore(e,this.#s.firstChild),setTimeout(()=>{e.style.marginBottom="calc(-1 * var(--cap-border-radius, 14px))",e.style.opacity=1},10)}}else d(new Error("Worker not available"))}),n=[];try{for(let e=0;ea(e,t)));n.push(...s)}}finally{i.forEach(e=>{if(e)try{e.terminate()}catch(e){console.error("[cap] error terminating worker:",e)}})}return n}setWorkersCount(e){const t=parseInt(e,10),r=Math.min(navigator.hardwareConcurrency||8,16);this.#r=!Number.isNaN(t)&&t>0&&t<=r?t:navigator.hardwareConcurrency||8}createUI(){this.#i.classList.add("captcha"),this.#i.setAttribute("role","button"),this.#i.setAttribute("tabindex","0"),this.#i.setAttribute("aria-label",this.getI18nText("verify-aria-label","Click to verify you're a human")),this.#i.setAttribute("aria-live","polite"),this.#i.setAttribute("disabled","true"),this.#i.innerHTML=`

${this.getI18nText("initial-state","Verify you're human")}

Cap`,this.#s.innerHTML=`.captcha,.captcha * {box-sizing:border-box;}.captcha{background-color:var(--cap-background,#fdfdfd);border:1px solid var(--cap-border-color,#dddddd8f);border-radius:var(--cap-border-radius,14px);user-select:none;height:var(--cap-widget-height, 58px);width:var(--cap-widget-width, 260px);display:flex;align-items:center;padding:var(--cap-widget-padding,14px);gap:var(--cap-gap,15px);cursor:pointer;transition:filter .2s,transform .2s;position:relative;-webkit-tap-highlight-color:rgba(255,255,255,0);overflow:hidden;color:var(--cap-color,#212121)}.captcha:hover{filter:brightness(98%)}.checkbox{width:var(--cap-checkbox-size,25px);height:var(--cap-checkbox-size,25px);border:var(--cap-checkbox-border,1px solid #aaaaaad1);border-radius:var(--cap-checkbox-border-radius,6px);background-color:var(--cap-checkbox-background,#fafafa91);transition:opacity .2s;margin-top:var(--cap-checkbox-margin,2px);margin-bottom:var(--cap-checkbox-margin,2px)}.captcha *{font-family:var(--cap-font,system,-apple-system,"BlinkMacSystemFont",".SFNSText-Regular","San Francisco","Roboto","Segoe UI","Helvetica Neue","Lucida Grande","Ubuntu","arial",sans-serif)}.captcha p{margin:0;font-weight:500;font-size:15px;user-select:none;transition:opacity .2s}.checkbox .progress-ring{display:none;width:100%;height:100%;transform:rotate(-90deg)}.checkbox .progress-ring-bg{fill:none;stroke:var(--cap-spinner-background-color,#eee);stroke-width:var(--cap-spinner-thickness,3)}.checkbox .progress-ring-circle{fill:none;stroke:var(--cap-spinner-color,#000);stroke-width:var(--cap-spinner-thickness,3);stroke-linecap:round;stroke-dasharray:87.96;stroke-dashoffset:87.96;transition:stroke-dashoffset 0.3s ease}.captcha[data-state=verifying] .checkbox{background:none;display:flex;align-items:center;justify-content:center;transform:scale(1.1);border:none;border-radius:50%;background-color:transparent}.captcha[data-state=verifying] .checkbox .progress-ring{display:block}.captcha[data-state=done] .checkbox{border:1px solid transparent;background-image:var(--cap-checkmark,url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%3E%3Cstyle%3E%40keyframes%20anim%7B0%25%7Bstroke-dashoffset%3A23.21320343017578px%7Dto%7Bstroke-dashoffset%3A0%7D%7D%3C%2Fstyle%3E%3Cpath%20fill%3D%22none%22%20stroke%3D%22%2300a67d%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20stroke-width%3D%222%22%20d%3D%22m5%2012%205%205L20%207%22%20style%3D%22stroke-dashoffset%3A0%3Bstroke-dasharray%3A23.21320343017578px%3Banimation%3Aanim%20.5s%20ease%22%2F%3E%3C%2Fsvg%3E"));background-size:cover}.captcha[data-state=done] .checkbox .progress-ring{display:none}.captcha[data-state=error] .checkbox{border:1px solid transparent;background-image:var(--cap-error-cross,url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='96' height='96' viewBox='0 0 24 24'%3E%3Cpath fill='%23f55b50' d='M11 15h2v2h-2zm0-8h2v6h-2zm1-5C6.47 2 2 6.5 2 12a10 10 0 0 0 10 10a10 10 0 0 0 10-10A10 10 0 0 0 12 2m0 18a8 8 0 0 1-8-8a8 8 0 0 1 8-8a8 8 0 0 1 8 8a8 8 0 0 1-8 8'/%3E%3C/svg%3E"));background-size:cover}.captcha[data-state=error] .checkbox .progress-ring{display:none}.captcha[disabled]{cursor:not-allowed}.captcha[disabled][data-state=verifying]{cursor:progress}.captcha[disabled][data-state=done]{cursor:default}.captcha .credits{position:absolute;bottom:10px;right:10px;font-size:12px;color:var(--cap-color,#212121);opacity:0.8;text-underline-offset: 1.5px;}`,this.#s.appendChild(this.#i)}addEventListeners(){this.#i&&(this.#i.querySelector("a").addEventListener("click",e=>{e.stopPropagation(),e.preventDefault(),window.open("https://capjs.js.org","_blank")}),this.#i.addEventListener("click",()=>{this.#i.hasAttribute("disabled")||this.solve()}),this.#i.addEventListener("keydown",e=>{"Enter"!==e.key&&" "!==e.key||this.#i.hasAttribute("disabled")||(e.preventDefault(),e.stopPropagation(),this.solve())}),this.addEventListener("progress",this.boundHandleProgress),this.addEventListener("solve",this.boundHandleSolve),this.addEventListener("error",this.boundHandleError),this.addEventListener("reset",this.boundHandleReset))}updateUI(e,t,r=!1){this.#i&&(this.#i.setAttribute("data-state",e),this.#i.querySelector("p").innerText=t,r?this.#i.setAttribute("disabled","true"):this.#i.removeAttribute("disabled"))}handleProgress(e){if(!this.#i)return;const t=this.#i.querySelector("p"),r=this.#i.querySelector(".progress-ring-circle");if(t&&r){const s=2*Math.PI*14,i=s-e.detail.progress/100*s;r.style.strokeDashoffset=i,t.innerText=`${this.getI18nText("verifying-label","Verifying...")} ${e.detail.progress}%`}this.executeAttributeCode("onprogress",e)}handleSolve(e){this.updateUI("done",this.getI18nText("solved-label","You're a human"),!0),this.executeAttributeCode("onsolve",e)}handleError(e){this.updateUI("error",this.getI18nText("error-label","Error. Try again.")),this.executeAttributeCode("onerror",e)}handleReset(e){this.updateUI("",this.getI18nText("initial-state","I'm a human")),this.executeAttributeCode("onreset",e)}executeAttributeCode(e,t){const r=this.getAttribute(e);r&&new Function("event",r).call(this,t)}error(e="Unknown error"){console.error("[cap]",e),this.dispatchEvent("error",{isCap:!0,message:e})}dispatchEvent(e,t={}){const r=new CustomEvent(e,{bubbles:!0,composed:!0,detail:t});super.dispatchEvent(r)}reset(){this.#t&&(clearTimeout(this.#t),this.#t=null),this.dispatchEvent("reset"),this.token=null;const e=this.getAttribute("data-cap-hidden-field-name")||"cap-token";this.querySelector(`input[name='${e}']`)&&(this.querySelector(`input[name='${e}']`).value="")}get tokenValue(){return this.token}disconnectedCallback(){this.removeEventListener("progress",this.boundHandleProgress),this.removeEventListener("solve",this.boundHandleSolve),this.removeEventListener("error",this.boundHandleError),this.removeEventListener("reset",this.boundHandleReset),this.#o.forEach((e,t)=>{this.removeEventListener(t.slice(2),e)}),this.#o.clear(),this.#s&&(this.#s.innerHTML=""),this.reset(),this.cleanup()}cleanup(){this.#t&&(clearTimeout(this.#t),this.#t=null),this.#e&&(URL.revokeObjectURL(this.#e),this.#e="")}}class i{constructor(e={},t){const r=t||document.createElement("cap-widget");if(Object.entries(e).forEach(([e,t])=>{r.setAttribute(e,t)}),!e.apiEndpoint&&!window?.CAP_CUSTOM_FETCH)throw r.remove(),new Error("Missing API endpoint. Either custom fetch or an API endpoint must be provided.");e.apiEndpoint&&r.setAttribute("data-cap-api-endpoint",e.apiEndpoint),this.widget=r,this.solve=this.widget.solve.bind(this.widget),this.reset=this.widget.reset.bind(this.widget),this.addEventListener=this.widget.addEventListener.bind(this.widget),Object.defineProperty(this,"token",{get:()=>r.token,configurable:!0,enumerable:!0}),t||(r.style.display="none",document.documentElement.appendChild(r))}}window.Cap=i,customElements.get("cap-widget")||window?.CAP_DONT_SKIP_REDEFINE?console.warn("[cap] the cap-widget element has already been defined, skipping re-defining it.\nto prevent this, set window.CAP_DONT_SKIP_REDEFINE to true"):customElements.define("cap-widget",s),"object"==typeof exports&&"undefined"!=typeof module?module.exports=i:"function"==typeof define&&define.amd&&define([],()=>i),"undefined"!=typeof exports&&(exports.default=i)})(); \ No newline at end of file +(()=>{const e="0.0.6";if("undefined"==typeof window)return;const t=(t,r={})=>{const i={...r,headers:{...r.headers||{},"Cap-Stamp":btoa(String.fromCharCode(...[[Date.now()]].map(e=>[e>>24,e>>16,e>>8,e[0]].map(e=>255&e))[0])).replace(/=/g,""),"Cap-Solver":`0,${e}`}};return window?.CAP_CUSTOM_FETCH?window.CAP_CUSTOM_FETCH(t,i):fetch(t,i)};function r(e,t){let r=function(e){let t=2166136261;for(let r=0;r>>0}(e),i="";function a(){return r^=r<<13,r^=r>>>17,r^=r<<5,r>>>0}for(;i.length{if(i)return i;const t=window.CAP_CUSTOM_WASM_URL||`https://cdn.jsdelivr.net/npm/@cap.js/wasm@${e}/browser/cap_wasm_bg.wasm`;return i=fetch(t).then(e=>{if(!e.ok)throw new Error(`Failed to fetch wasm: ${e.status}`);return e.arrayBuffer()}).then(e=>WebAssembly.compile(e)).catch(e=>{throw i=null,e}),i};"object"==typeof WebAssembly&&"function"==typeof WebAssembly.compile&&a().catch(()=>{});class n extends HTMLElement{#e="";#t=null;#r=navigator.hardwareConcurrency||8;token=null;#i;#a;#n;#s=!1;#o;getI18nText(e,t){return this.getAttribute(`data-cap-i18n-${e}`)||t}static get observedAttributes(){return["onsolve","onprogress","onreset","onerror","data-cap-worker-count","data-cap-i18n-initial-state"]}constructor(){super(),this.#o&&this.#o.forEach((e,t)=>{this.removeEventListener(t.slice(2),e)}),this.#o=new Map,this.boundHandleProgress=this.handleProgress.bind(this),this.boundHandleSolve=this.handleSolve.bind(this),this.boundHandleError=this.handleError.bind(this),this.boundHandleReset=this.handleReset.bind(this)}initialize(){this.#e=URL.createObjectURL(new Blob(['(()=>{const e=async({salt:e,target:t})=>{let n=0;const r=new TextEncoder,o=new Uint8Array(t.length/2);for(let e=0;ee({salt:t,target:n}));let t=null;self.onmessage=async({data:{salt:n,target:r,wasmModule:o}})=>{if(o instanceof WebAssembly.Module&&null===t){const s=(e=>{try{let n,r=0,o=null;const s=()=>(null!==o&&0!==o.byteLength||(o=new Uint8Array(n.memory.buffer)),o),l=new TextEncoder,a=(e,t,n)=>{if(void 0===n){const n=l.encode(e),o=t(n.length,1)>>>0;return s().subarray(o,o+n.length).set(n),r=n.length,o}let o=e.length,a=t(o,1)>>>0;const c=s();let i=0;for(;i127)break;c[a+i]=t}if(i!==o){0!==i&&(e=e.slice(i)),a=n(a,o,o=i+3*e.length,1)>>>0;const t=s().subarray(a+i,a+o),{written:r}=l.encodeInto(e,t);i+=r,a=n(a,o,i,1)>>>0}return r=i,a},c={wbg:{}};c.wbg.__wbindgen_init_externref_table=()=>{const e=n.__wbindgen_export_0,t=e.grow(4);e.set(0,void 0),e.set(t+0,void 0),e.set(t+1,null),e.set(t+2,!0),e.set(t+3,!1)};const i=new WebAssembly.Instance(e,c);return n=i.exports,n.__wbindgen_start&&n.__wbindgen_start(),t=(e,t)=>{const o=a(e,n.__wbindgen_malloc,n.__wbindgen_realloc),s=r,l=a(t,n.__wbindgen_malloc,n.__wbindgen_realloc),c=r;return BigInt.asUintN(64,n.solve_pow(o,s,l,c))},!0}catch(e){return console.error("[cap worker] failed to init wasm from module:",e),!1}})(o);if(!s)return console.warn("[cap worker] wasm init failed, falling back to JS solver."),e({salt:n,target:r})}if(null===t)return console.warn("[cap worker] no wasm module provided, falling back to JS solver."),e({salt:n,target:r});try{const e=performance.now(),o=t(n,r),s=performance.now();self.postMessage({nonce:Number(o),found:!0,durationMs:(s-e).toFixed(2)})}catch(e){console.error("[cap worker]",e),self.postMessage({found:!1,error:e.message||String(e)})}},self.onerror=e=>{self.postMessage({found:!1,error:e})}})();'],{type:"application/javascript"}))}attributeChangedCallback(e,t,r){if(e.startsWith("on")){const t=e.slice(2),i=this.#o.get(e);if(i&&this.removeEventListener(t,i),r){const r=t=>{const r=this.getAttribute(e);"function"==typeof window[r]&&window[r].call(this,t)};this.#o.set(e,r),this.addEventListener(t,r)}}"data-cap-worker-count"===e&&this.setWorkersCount(parseInt(r,10)),"data-cap-i18n-initial-state"===e&&this.#a&&this.#a?.querySelector(".label.active")&&this.animateLabel(this.getI18nText("initial-state","Verify you're human"))}async connectedCallback(){this.#n=this,this.#i=this.attachShadow({mode:"open"}),this.#a=document.createElement("div"),this.createUI(),this.addEventListeners(),this.initialize(),this.#a.removeAttribute("disabled");const e=this.getAttribute("data-cap-worker-count"),t=e?parseInt(e,10):null;this.setWorkersCount(t||navigator.hardwareConcurrency||8);const r=this.getAttribute("data-cap-hidden-field-name")||"cap-token";this.#n.innerHTML=``}async solve(){if(!this.#s)try{this.#s=!0,this.updateUI("verifying",this.getI18nText("verifying-label","Verifying..."),!0),this.#a.setAttribute("aria-label",this.getI18nText("verifying-aria-label","Verifying you're a human, please wait")),this.dispatchEvent("progress",{progress:0});try{let e=this.getAttribute("data-cap-api-endpoint");if(!e&&window?.CAP_CUSTOM_FETCH)e="/";else if(!e)throw new Error("Missing API endpoint. Either custom fetch or an API endpoint must be provided.");e.endsWith("/")||(e+="/");const{challenge:i,token:a}=await(await t(`${e}challenge`,{method:"POST"})).json();let n=i;if(!Array.isArray(n)){let e=0;n=Array.from({length:i.c},()=>(e+=1,[r(`${a}${e}`,i.s),r(`${a}${e}d`,i.d)]))}const s=await this.solveChallenges(n),o=await(await t(`${e}redeem`,{method:"POST",body:JSON.stringify({token:a,solutions:s}),headers:{"Content-Type":"application/json"}})).json();if(this.dispatchEvent("progress",{progress:100}),!o.success)throw new Error("Invalid solution");const l=this.getAttribute("data-cap-hidden-field-name")||"cap-token";this.querySelector(`input[name='${l}']`)&&(this.querySelector(`input[name='${l}']`).value=o.token),this.dispatchEvent("solve",{token:o.token}),this.token=o.token,this.#t&&clearTimeout(this.#t);const c=new Date(o.expires).getTime()-Date.now();return c>0&&c<864e5?this.#t=setTimeout(()=>this.reset(),c):this.error("Invalid expiration time"),this.#a.setAttribute("aria-label",this.getI18nText("verified-aria-label","We have verified you're a human, you may now continue")),{success:!0,token:this.token}}catch(e){throw this.#a.setAttribute("aria-label",this.getI18nText("error-aria-label","An error occurred, please try again")),this.error(e.message),e}}finally{this.#s=!1}}async solveChallenges(e){const t=e.length;let r=0,i=null;const n="object"==typeof WebAssembly&&"function"==typeof WebAssembly.instantiate;if(n)try{i=await a()}catch(e){console.warn("[cap] wasm unavailable, falling back to JS solver:",e)}if(!n&&!this.#i.querySelector(".warning")){const e=document.createElement("div");e.className="warning",e.style.cssText="width:var(--cap-widget-width,230px);background:rgb(237,56,46);color:white;padding:4px 6px;padding-bottom:calc(var(--cap-border-radius,14px) + 5px);font-size:10px;box-sizing:border-box;font-family:system-ui;border-top-left-radius:8px;border-top-right-radius:8px;text-align:center;user-select:none;margin-bottom:-35.5px;opacity:0;transition:margin-bottom .3s,opacity .3s;",e.innerText=this.getI18nText("wasm-disabled","Enable WASM for significantly faster solving"),this.#i.insertBefore(e,this.#i.firstChild),setTimeout(()=>{e.style.marginBottom="calc(-1 * var(--cap-border-radius, 14px))",e.style.opacity=1},10)}const s=Array(this.#r).fill(null).map(()=>{try{return new Worker(this.#e)}catch(e){throw console.error("[cap] Failed to create worker:",e),new Error("Worker creation failed")}}),o=([e,a],n)=>new Promise((o,l)=>{const c=s[n];c?(c.onmessage=({data:e})=>{e.found&&(r++,this.dispatchEvent("progress",{progress:Math.round(r/t*100)}),o(e.nonce))},c.onerror=e=>{this.error(`Error in worker: ${e.message||e}`),l(e)},i?c.postMessage({salt:e,target:a,wasmModule:i},[]):c.postMessage({salt:e,target:a})):l(new Error("Worker not available"))}),l=[];try{for(let t=0;to(e,t)));l.push(...i)}}finally{s.forEach(e=>{if(e)try{e.terminate()}catch(e){console.error("[cap] error terminating worker:",e)}})}return l}setWorkersCount(e){const t=parseInt(e,10),r=Math.min(navigator.hardwareConcurrency||8,16);this.#r=!Number.isNaN(t)&&t>0&&t<=r?t:navigator.hardwareConcurrency||8}createUI(){this.#a.classList.add("captcha"),this.#a.setAttribute("role","button"),this.#a.setAttribute("tabindex","0"),this.#a.setAttribute("aria-label",this.getI18nText("verify-aria-label","Click to verify you're a human")),this.#a.setAttribute("aria-live","polite"),this.#a.setAttribute("disabled","true"),this.#a.innerHTML=`

${this.getI18nText("initial-state","Verify you're human")}

Cap`;this.#i.innerHTML=`@media (prefers-reduced-motion:reduce){.captcha,.captcha *{transition:none!important;animation:none!important}.label{filter:none!important;opacity:1!important;transition:none!important;transform:none!important}.label:not(.active),.label.exit{display:none!important}}.captcha,.captcha *{box-sizing:border-box}.captcha{background-color:var(--cap-background,#fdfdfd);border:1px solid var(--cap-border-color,#dddddd8f);border-radius:var(--cap-border-radius,14px);-webkit-user-select:none;user-select:none;height:var(--cap-widget-height,58px);width:var(--cap-widget-width,260px);padding:var(--cap-widget-padding,14px);align-items:center;gap:var(--cap-gap,15px);cursor:pointer;-webkit-tap-highlight-color:#fff0;color:var(--cap-color,#212121);transition:filter .2s,transform .2s;display:flex;position:relative;overflow:hidden}.captcha:hover{filter:brightness(98%)}.captcha *{font-family:var(--cap-font,system, -apple-system, "BlinkMacSystemFont", ".SFNSText-Regular", "San Francisco", "Roboto", "Segoe UI", "Helvetica Neue", "Lucida Grande", "Ubuntu", "arial", sans-serif)}.checkbox{width:var(--cap-checkbox-size,25px);height:var(--cap-checkbox-size,25px);border:var(--cap-checkbox-border,1px solid #aaaaaad1);border-radius:var(--cap-checkbox-border-radius,6px);background-color:var(--cap-checkbox-background,#fafafa91);margin-top:var(--cap-checkbox-margin,2px);margin-bottom:var(--cap-checkbox-margin,2px);transition:opacity .2s}.captcha p{-webkit-user-select:none;user-select:none;margin:0;font-size:15px;font-weight:500}.label-wrapper{flex:1;align-items:center;min-width:0;height:2em;display:flex;position:relative;overflow:hidden}.label-wrapper:before,.label-wrapper:after{content:"";pointer-events:none;z-index:1;width:100%;height:.6em;position:absolute;left:0}.label-wrapper:before{background:linear-gradient(to bottom, var(--cap-background,#fdfdfd), transparent);top:0}.label-wrapper:after{background:linear-gradient(to top, var(--cap-background,#fdfdfd), transparent);bottom:0}.label{white-space:nowrap;opacity:0;filter:blur(2px);transition:transform .5s cubic-bezier(.25,1,.5,1),opacity .5s,filter .5s;position:absolute;transform:translateY(100%)}.label.active{opacity:1;filter:none;transform:translateY(0)}.label.exit{opacity:0;filter:blur(2px);transform:translateY(-100%)}.checkbox .progress-ring{width:100%;height:100%;display:none;transform:rotate(-90deg)}.checkbox .progress-ring-bg{fill:none;stroke:var(--cap-spinner-background-color,#eee);stroke-width:var(--cap-spinner-thickness,3)}.checkbox .progress-ring-circle{fill:none;stroke:var(--cap-spinner-color,#000);stroke-width:var(--cap-spinner-thickness,3);stroke-linecap:round;stroke-dasharray:87.96;stroke-dashoffset:87.96px;transition:stroke-dashoffset .3s}.captcha[data-state=verifying] .checkbox{background:0 0;border:none;border-radius:50%;justify-content:center;align-items:center;display:flex;transform:scale(1.1)}.captcha[data-state=verifying] .checkbox .progress-ring{display:block}.captcha[data-state=done] .checkbox{background-image:var(--cap-checkmark,url(data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%3E%3Cstyle%3E%40keyframes%20anim%7B0%25%7Bstroke-dashoffset%3A23.21320343017578px%7Dto%7Bstroke-dashoffset%3A0%7D%7D%3C%2Fstyle%3E%3Cpath%20fill%3D%22none%22%20stroke%3D%22%2300a67d%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20stroke-width%3D%222%22%20d%3D%22m5%2012%205%205L20%207%22%20style%3D%22stroke-dashoffset%3A0%3Bstroke-dasharray%3A23.21320343017578px%3Banimation%3Aanim%20.5s%20ease%22%2F%3E%3C%2Fsvg%3E));background-size:cover;border:1px solid #0000}.captcha[data-state=done] .checkbox .progress-ring{display:none}.captcha[data-state=error] .checkbox{background-image:var(--cap-error-cross,url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='96' height='96' viewBox='0 0 24 24'%3E%3Cpath fill='%23f55b50' d='M11 15h2v2h-2zm0-8h2v6h-2zm1-5C6.47 2 2 6.5 2 12a10 10 0 0 0 10 10a10 10 0 0 0 10-10A10 10 0 0 0 12 2m0 18a8 8 0 0 1-8-8a8 8 0 0 1 8-8a8 8 0 0 1 8 8a8 8 0 0 1-8 8'/%3E%3C%2Fsvg%3E"));background-size:cover;border:1px solid #0000}.captcha[data-state=error] .checkbox .progress-ring{display:none}.captcha[disabled]{cursor:not-allowed}.captcha[disabled][data-state=verifying]{cursor:progress}.captcha[disabled][data-state=done]{cursor:default}.captcha .credits{color:var(--cap-color,#212121);opacity:.8;text-underline-offset:.18em;z-index:6;font-size:12px;text-decoration-thickness:.08em;position:absolute;bottom:10px;right:10px}`,this.#i.appendChild(this.#a)}addEventListeners(){this.#a&&(this.#a.querySelector("a").addEventListener("click",e=>{e.stopPropagation(),e.preventDefault(),window.open("https://capjs.js.org","_blank")}),this.#a.addEventListener("click",()=>{this.#a.hasAttribute("disabled")||this.solve()}),this.#a.addEventListener("keydown",e=>{"Enter"!==e.key&&" "!==e.key||this.#a.hasAttribute("disabled")||(e.preventDefault(),e.stopPropagation(),this.solve())}),this.addEventListener("progress",this.boundHandleProgress),this.addEventListener("solve",this.boundHandleSolve),this.addEventListener("error",this.boundHandleError),this.addEventListener("reset",this.boundHandleReset))}animateLabel(e){if(!this.#a)return;const t=this.#a.querySelector(".label-wrapper");if(!t)return;if(window.matchMedia?.("(prefers-reduced-motion: reduce)").matches){const r=t.querySelector(".label.active");if(r)r.textContent=e;else{const r=document.createElement("span");r.className="label active",r.textContent=e,t.appendChild(r)}return}const r=t.querySelector(".label.active"),i=document.createElement("span");i.className="label",i.textContent=e,t.appendChild(i),i.offsetWidth,i.classList.add("active"),r&&(r.classList.remove("active"),r.classList.add("exit"),r.addEventListener("transitionend",()=>r.remove(),{once:!0}))}updateUI(e,t,r=!1){this.#a&&(this.#a.setAttribute("data-state",e),this.animateLabel(t),r?this.#a.setAttribute("disabled","true"):this.#a.removeAttribute("disabled"))}handleProgress(e){if(!this.#a)return;const t=this.#a.querySelector(".progress-ring-circle");if(t){const r=2*Math.PI*14,i=r-e.detail.progress/100*r;t.style.strokeDashoffset=i}const r=this.#a.querySelector(".label-wrapper");if(r){const t=r.querySelector(".label.active");t&&(t.textContent=`${this.getI18nText("verifying-label","Verifying...")} ${e.detail.progress}%`)}this.executeAttributeCode("onprogress",e)}handleSolve(e){this.updateUI("done",this.getI18nText("solved-label","You're a human"),!0),this.executeAttributeCode("onsolve",e)}handleError(e){this.updateUI("error",this.getI18nText("error-label","Error. Try again.")),this.executeAttributeCode("onerror",e)}handleReset(e){this.updateUI("",this.getI18nText("initial-state","I'm a human")),this.executeAttributeCode("onreset",e)}executeAttributeCode(e,t){const r=this.getAttribute(e);r&&new Function("event",r).call(this,t)}error(e="Unknown error"){console.error("[cap]",e),this.dispatchEvent("error",{isCap:!0,message:e})}dispatchEvent(e,t={}){const r=new CustomEvent(e,{bubbles:!0,composed:!0,detail:t});super.dispatchEvent(r)}reset(){this.#t&&(clearTimeout(this.#t),this.#t=null),this.dispatchEvent("reset"),this.token=null;const e=this.getAttribute("data-cap-hidden-field-name")||"cap-token";this.querySelector(`input[name='${e}']`)&&(this.querySelector(`input[name='${e}']`).value="")}get tokenValue(){return this.token}disconnectedCallback(){this.removeEventListener("progress",this.boundHandleProgress),this.removeEventListener("solve",this.boundHandleSolve),this.removeEventListener("error",this.boundHandleError),this.removeEventListener("reset",this.boundHandleReset),this.#o.forEach((e,t)=>{this.removeEventListener(t.slice(2),e)}),this.#o.clear(),this.#i&&(this.#i.innerHTML=""),this.reset(),this.cleanup()}cleanup(){this.#t&&(clearTimeout(this.#t),this.#t=null),this.#e&&(URL.revokeObjectURL(this.#e),this.#e="")}}class s{constructor(e={},t){const r=t||document.createElement("cap-widget");if(Object.entries(e).forEach(([e,t])=>{r.setAttribute(e,t)}),!e.apiEndpoint&&!window?.CAP_CUSTOM_FETCH)throw r.remove(),new Error("Missing API endpoint. Either custom fetch or an API endpoint must be provided.");e.apiEndpoint&&r.setAttribute("data-cap-api-endpoint",e.apiEndpoint),this.widget=r,this.solve=this.widget.solve.bind(this.widget),this.reset=this.widget.reset.bind(this.widget),this.addEventListener=this.widget.addEventListener.bind(this.widget),Object.defineProperty(this,"token",{get:()=>r.token,configurable:!0,enumerable:!0}),t||(r.style.display="none",document.documentElement.appendChild(r))}}window.Cap=s,customElements.get("cap-widget")||window?.CAP_DONT_SKIP_REDEFINE?customElements.get("cap-widget")&&console.warn("[cap] the cap-widget element has already been defined, skipping re-defining it.\nto prevent this, set window.CAP_DONT_SKIP_REDEFINE to true"):customElements.define("cap-widget",n),"object"==typeof exports&&"undefined"!=typeof module?module.exports=s:"function"==typeof define&&define.amd&&define([],()=>s),"undefined"!=typeof exports&&(exports.default=s)})(); \ No newline at end of file diff --git a/widget/src/package.json b/widget/src/package.json index 66ed837..403a3de 100644 --- a/widget/src/package.json +++ b/widget/src/package.json @@ -1,6 +1,6 @@ { "name": "@cap.js/widget", - "version": "0.1.35", + "version": "0.1.36", "description": "Client-side widget for Cap, a lightweight, modern open-source CAPTCHA alternative designed using SHA-256 PoW.", "keywords": [ "security", diff --git a/widget/src/src/cap.css b/widget/src/src/cap.css new file mode 100644 index 0000000..0365417 --- /dev/null +++ b/widget/src/src/cap.css @@ -0,0 +1,231 @@ +@media (prefers-reduced-motion: reduce) { + .captcha, + .captcha * { + transition: none !important; + animation: none !important; + } + + .label { + transition: none !important; + transform: none !important; + filter: none !important; + opacity: 1 !important; + } + + .label:not(.active) { + display: none !important; + } + + .label.exit { + display: none !important; + } +} + +.captcha, +.captcha * { + box-sizing: border-box; +} + +.captcha { + background-color: var(--cap-background, #fdfdfd); + border: 1px solid var(--cap-border-color, #dddddd8f); + border-radius: var(--cap-border-radius, 14px); + user-select: none; + height: var(--cap-widget-height, 58px); + width: var(--cap-widget-width, 260px); + display: flex; + align-items: center; + padding: var(--cap-widget-padding, 14px); + gap: var(--cap-gap, 15px); + cursor: pointer; + transition: + filter 0.2s, + transform 0.2s; + position: relative; + -webkit-tap-highlight-color: rgba(255, 255, 255, 0); + overflow: hidden; + color: var(--cap-color, #212121); +} + +.captcha:hover { + filter: brightness(98%); +} + +.captcha * { + font-family: var( + --cap-font, + system, + -apple-system, + "BlinkMacSystemFont", + ".SFNSText-Regular", + "San Francisco", + "Roboto", + "Segoe UI", + "Helvetica Neue", + "Lucida Grande", + "Ubuntu", + "arial", + sans-serif + ); +} + +.checkbox { + width: var(--cap-checkbox-size, 25px); + height: var(--cap-checkbox-size, 25px); + border: var(--cap-checkbox-border, 1px solid #aaaaaad1); + border-radius: var(--cap-checkbox-border-radius, 6px); + background-color: var(--cap-checkbox-background, #fafafa91); + transition: opacity 0.2s; + margin-top: var(--cap-checkbox-margin, 2px); + margin-bottom: var(--cap-checkbox-margin, 2px); +} + +.captcha p { + margin: 0; + font-weight: 500; + font-size: 15px; + user-select: none; +} + +.label-wrapper { + position: relative; + height: 2em; + overflow: hidden; + flex: 1; + min-width: 0; + display: flex; + align-items: center; +} + +.label-wrapper::before, +.label-wrapper::after { + content: ""; + pointer-events: none; + position: absolute; + left: 0; + width: 100%; + height: 0.6em; + z-index: 1; +} + +.label-wrapper::before { + top: 0; + background: linear-gradient(to bottom, var(--cap-background, #fdfdfd), transparent); +} + +.label-wrapper::after { + bottom: 0; + background: linear-gradient(to top, var(--cap-background, #fdfdfd), transparent); +} + +.label { + position: absolute; + white-space: nowrap; + opacity: 0; + transform: translateY(100%); + transition: + transform 0.5s cubic-bezier(0.25, 1, 0.5, 1), + opacity 0.5s ease, + filter 0.5s ease; + filter: blur(2px); +} + +.label.active { + opacity: 1; + transform: translateY(0); + filter: none; +} + +.label.exit { + opacity: 0; + transform: translateY(-100%); + filter: blur(2px); +} + +.checkbox .progress-ring { + display: none; + width: 100%; + height: 100%; + transform: rotate(-90deg); +} + +.checkbox .progress-ring-bg { + fill: none; + stroke: var(--cap-spinner-background-color, #eee); + stroke-width: var(--cap-spinner-thickness, 3); +} + +.checkbox .progress-ring-circle { + fill: none; + stroke: var(--cap-spinner-color, #000); + stroke-width: var(--cap-spinner-thickness, 3); + stroke-linecap: round; + stroke-dasharray: 87.96; + stroke-dashoffset: 87.96; + transition: stroke-dashoffset 0.3s ease; +} + +.captcha[data-state="verifying"] .checkbox { + background: none; + display: flex; + align-items: center; + justify-content: center; + transform: scale(1.1); + border: none; + border-radius: 50%; + background-color: transparent; +} + +.captcha[data-state="verifying"] .checkbox .progress-ring { + display: block; +} + +.captcha[data-state="done"] .checkbox { + border: 1px solid transparent; + background-image: var( + --cap-checkmark, + url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%3E%3Cstyle%3E%40keyframes%20anim%7B0%25%7Bstroke-dashoffset%3A23.21320343017578px%7Dto%7Bstroke-dashoffset%3A0%7D%7D%3C%2Fstyle%3E%3Cpath%20fill%3D%22none%22%20stroke%3D%22%2300a67d%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20stroke-width%3D%222%22%20d%3D%22m5%2012%205%205L20%207%22%20style%3D%22stroke-dashoffset%3A0%3Bstroke-dasharray%3A23.21320343017578px%3Banimation%3Aanim%20.5s%20ease%22%2F%3E%3C%2Fsvg%3E") + ); + background-size: cover; +} + +.captcha[data-state="done"] .checkbox .progress-ring { + display: none; +} + +.captcha[data-state="error"] .checkbox { + border: 1px solid transparent; + background-image: var( + --cap-error-cross, + url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='96' height='96' viewBox='0 0 24 24'%3E%3Cpath fill='%23f55b50' d='M11 15h2v2h-2zm0-8h2v6h-2zm1-5C6.47 2 2 6.5 2 12a10 10 0 0 0 10 10a10 10 0 0 0 10-10A10 10 0 0 0 12 2m0 18a8 8 0 0 1-8-8a8 8 0 0 1 8-8a8 8 0 0 1 8 8a8 8 0 0 1-8 8'/%3E%3C%2Fsvg%3E") + ); + background-size: cover; +} + +.captcha[data-state="error"] .checkbox .progress-ring { + display: none; +} + +.captcha[disabled] { + cursor: not-allowed; +} + +.captcha[disabled][data-state="verifying"] { + cursor: progress; +} + +.captcha[disabled][data-state="done"] { + cursor: default; +} + +.captcha .credits { + position: absolute; + bottom: 10px; + right: 10px; + font-size: 12px; + color: var(--cap-color, #212121); + opacity: 0.8; + text-underline-offset: 0.18em; + text-decoration-thickness: 0.08em; + z-index: 6; +} diff --git a/widget/src/src/cap.js b/widget/src/src/cap.js index 4243b5f..0f3e0d7 100644 --- a/widget/src/src/cap.js +++ b/widget/src/src/cap.js @@ -1,613 +1,657 @@ (() => { - const WASM_VERSION = "0.0.6"; + const WASM_VERSION = "0.0.6"; - if (typeof window === "undefined") { + if (typeof window === "undefined") { return; } - const capFetch = (...args) => { - if (window?.CAP_CUSTOM_FETCH) { - return window.CAP_CUSTOM_FETCH(...args); - } - return fetch(...args); - }; + const capFetch = (u, _conf = {}) => { + const conf = { + ..._conf, + headers: { + ...(_conf.headers || {}), + "Cap-Stamp": btoa( + String.fromCharCode( + ...[[Date.now()]].map((n) => [n >> 24, n >> 16, n >> 8, n[0]].map((x) => x & 255))[0], + ), + ).replace(/=/g, ""), + "Cap-Solver": `0,${WASM_VERSION}`, + }, + }; - function prng(seed, length) { - function fnv1a(str) { - let hash = 2166136261; - for (let i = 0; i < str.length; i++) { - hash ^= str.charCodeAt(i); - hash += - (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24); - } - return hash >>> 0; - } + if (window?.CAP_CUSTOM_FETCH) { + return window.CAP_CUSTOM_FETCH(u, conf); + } - let state = fnv1a(seed); - let result = ""; + return fetch(u, conf); + }; - function next() { - state ^= state << 13; - state ^= state >>> 17; - state ^= state << 5; - return state >>> 0; - } + function prng(seed, length) { + function fnv1a(str) { + let hash = 2166136261; + for (let i = 0; i < str.length; i++) { + hash ^= str.charCodeAt(i); + hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24); + } + return hash >>> 0; + } - while (result.length < length) { - const rnd = next(); - result += rnd.toString(16).padStart(8, "0"); - } + let state = fnv1a(seed); + let result = ""; - return result.substring(0, length); - } + function next() { + state ^= state << 13; + state ^= state >>> 17; + state ^= state << 5; + return state >>> 0; + } - if (!window.CAP_CUSTOM_WASM_URL) { - // preloads the wasm files to save up time on solve - [ - `https://cdn.jsdelivr.net/npm/@cap.js/wasm@${WASM_VERSION}/browser/cap_wasm.min.js`, - `https://cdn.jsdelivr.net/npm/@cap.js/wasm@${WASM_VERSION}/browser/cap_wasm_bg.wasm`, - ].forEach((url) => { - const link = document.createElement("link"); - link.rel = "prefetch"; - link.href = url; - link.as = url.endsWith(".wasm") ? "fetch" : "script"; - document.head.appendChild(link); - }); - } + while (result.length < length) { + const rnd = next(); + result += rnd.toString(16).padStart(8, "0"); + } - class CapWidget extends HTMLElement { - #workerUrl = ""; - #resetTimer = null; - #workersCount = navigator.hardwareConcurrency || 8; - token = null; - #shadow; - #div; - #host; - #solving = false; - #eventHandlers; + return result.substring(0, length); + } - getI18nText(key, defaultValue) { - return this.getAttribute(`data-cap-i18n-${key}`) || defaultValue; - } + let wasmModulePromise = null; - static get observedAttributes() { - return [ - "onsolve", - "onprogress", - "onreset", - "onerror", - "data-cap-worker-count", - "data-cap-i18n-initial-state", - "[cap]", - ]; - } + const getWasmModule = () => { + if (wasmModulePromise) return wasmModulePromise; - constructor() { - super(); - if (this.#eventHandlers) { - this.#eventHandlers.forEach((handler, eventName) => { - this.removeEventListener(eventName.slice(2), handler); - }); - } + const wasmUrl = + window.CAP_CUSTOM_WASM_URL || + `https://cdn.jsdelivr.net/npm/@cap.js/wasm@${WASM_VERSION}/browser/cap_wasm_bg.wasm`; - this.#eventHandlers = new Map(); - this.boundHandleProgress = this.handleProgress.bind(this); - this.boundHandleSolve = this.handleSolve.bind(this); - this.boundHandleError = this.handleError.bind(this); - this.boundHandleReset = this.handleReset.bind(this); - } + wasmModulePromise = fetch(wasmUrl) + .then((r) => { + if (!r.ok) throw new Error(`Failed to fetch wasm: ${r.status}`); + return r.arrayBuffer(); + }) + .then((buf) => WebAssembly.compile(buf)) + .catch((e) => { + wasmModulePromise = null; + throw e; + }); - initialize() { - this.#workerUrl = URL.createObjectURL( - // this placeholder will be replaced with the actual worker by the build script + return wasmModulePromise; + }; - new Blob([`%%workerScript%%`], { - type: "application/javascript", - }), - ); - } + if (typeof WebAssembly === "object" && typeof WebAssembly.compile === "function") { + getWasmModule().catch(() => {}); + } - attributeChangedCallback(name, _, value) { - if (name.startsWith("on")) { - const eventName = name.slice(2); - const oldHandler = this.#eventHandlers.get(name); - if (oldHandler) { - this.removeEventListener(eventName, oldHandler); - } + const prefersReducedMotion = () => + window.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false; - if (value) { - const handler = (event) => { - const callback = this.getAttribute(name); - if (typeof window[callback] === "function") { - window[callback].call(this, event); - } - }; - this.#eventHandlers.set(name, handler); - this.addEventListener(eventName, handler); - } - } + class CapWidget extends HTMLElement { + #workerUrl = ""; + #resetTimer = null; + #workersCount = navigator.hardwareConcurrency || 8; + token = null; + #shadow; + #div; + #host; + #solving = false; + #eventHandlers; - if (name === "data-cap-worker-count") { - this.setWorkersCount(parseInt(value, 10)); - } + getI18nText(key, defaultValue) { + return this.getAttribute(`data-cap-i18n-${key}`) || defaultValue; + } - if ( - name === "data-cap-i18n-initial-state" && - this.#div && - this.#div?.querySelector("p")?.innerText - ) { - this.#div.querySelector("p").innerText = this.getI18nText( - "initial-state", - "Verify you're human", - ); - } - } + static get observedAttributes() { + return [ + "onsolve", + "onprogress", + "onreset", + "onerror", + "data-cap-worker-count", + "data-cap-i18n-initial-state", + ]; + } - async connectedCallback() { - this.#host = this; - this.#shadow = this.attachShadow({ mode: "open" }); - this.#div = document.createElement("div"); - this.createUI(); - this.addEventListeners(); - this.initialize(); - this.#div.removeAttribute("disabled"); + constructor() { + super(); + if (this.#eventHandlers) { + this.#eventHandlers.forEach((handler, eventName) => { + this.removeEventListener(eventName.slice(2), handler); + }); + } - const workers = this.getAttribute("data-cap-worker-count"); - const parsedWorkers = workers ? parseInt(workers, 10) : null; - this.setWorkersCount(parsedWorkers || navigator.hardwareConcurrency || 8); - const fieldName = - this.getAttribute("data-cap-hidden-field-name") || "cap-token"; - this.#host.innerHTML = ``; - } + this.#eventHandlers = new Map(); + this.boundHandleProgress = this.handleProgress.bind(this); + this.boundHandleSolve = this.handleSolve.bind(this); + this.boundHandleError = this.handleError.bind(this); + this.boundHandleReset = this.handleReset.bind(this); + } - async solve() { - if (this.#solving) { - return; - } + initialize() { + this.#workerUrl = URL.createObjectURL( + // this placeholder will be replaced with the actual worker by the build script - try { - this.#solving = true; - this.updateUI( - "verifying", - this.getI18nText("verifying-label", "Verifying..."), - true, - ); + new Blob([`%%workerScript%%`], { + type: "application/javascript", + }), + ); + } - this.#div.setAttribute( - "aria-label", - this.getI18nText( - "verifying-aria-label", - "Verifying you're a human, please wait", - ), - ); + attributeChangedCallback(name, _, value) { + if (name.startsWith("on")) { + const eventName = name.slice(2); + const oldHandler = this.#eventHandlers.get(name); + if (oldHandler) { + this.removeEventListener(eventName, oldHandler); + } - this.dispatchEvent("progress", { progress: 0 }); + if (value) { + const handler = (event) => { + const callback = this.getAttribute(name); + if (typeof window[callback] === "function") { + window[callback].call(this, event); + } + }; + this.#eventHandlers.set(name, handler); + this.addEventListener(eventName, handler); + } + } - try { - let apiEndpoint = this.getAttribute("data-cap-api-endpoint"); + if (name === "data-cap-worker-count") { + this.setWorkersCount(parseInt(value, 10)); + } - if (!apiEndpoint && window?.CAP_CUSTOM_FETCH) { - apiEndpoint = "/"; - } else if (!apiEndpoint) - throw new Error( - "Missing API endpoint. Either custom fetch or an API endpoint must be provided.", - ); + if ( + name === "data-cap-i18n-initial-state" && + this.#div && + this.#div?.querySelector(".label.active") + ) { + this.animateLabel(this.getI18nText("initial-state", "Verify you're human")); + } + } - if (!apiEndpoint.endsWith("/")) { + async connectedCallback() { + this.#host = this; + this.#shadow = this.attachShadow({ mode: "open" }); + this.#div = document.createElement("div"); + this.createUI(); + this.addEventListeners(); + this.initialize(); + this.#div.removeAttribute("disabled"); + + const workers = this.getAttribute("data-cap-worker-count"); + const parsedWorkers = workers ? parseInt(workers, 10) : null; + this.setWorkersCount(parsedWorkers || navigator.hardwareConcurrency || 8); + const fieldName = this.getAttribute("data-cap-hidden-field-name") || "cap-token"; + this.#host.innerHTML = ``; + } + + async solve() { + if (this.#solving) { + return; + } + + try { + this.#solving = true; + this.updateUI("verifying", this.getI18nText("verifying-label", "Verifying..."), true); + + this.#div.setAttribute( + "aria-label", + this.getI18nText("verifying-aria-label", "Verifying you're a human, please wait"), + ); + + this.dispatchEvent("progress", { progress: 0 }); + + try { + let apiEndpoint = this.getAttribute("data-cap-api-endpoint"); + + if (!apiEndpoint && window?.CAP_CUSTOM_FETCH) { + apiEndpoint = "/"; + } else if (!apiEndpoint) + throw new Error( + "Missing API endpoint. Either custom fetch or an API endpoint must be provided.", + ); + + if (!apiEndpoint.endsWith("/")) { apiEndpoint += "/"; } - const { challenge, token } = await ( - await capFetch(`${apiEndpoint}challenge`, { - method: "POST", - }) - ).json(); - - let challenges = challenge; - - if (!Array.isArray(challenges)) { - let i = 0; - - challenges = Array.from({ length: challenge.c }, () => { - i = i + 1; - - return [ - prng(`${token}${i}`, challenge.s), - prng(`${token}${i}d`, challenge.d), - ]; - }); - } - - const solutions = await this.solveChallenges(challenges); - - const resp = await ( - await capFetch(`${apiEndpoint}redeem`, { - method: "POST", - body: JSON.stringify({ token, solutions }), - headers: { "Content-Type": "application/json" }, - }) - ).json(); - - this.dispatchEvent("progress", { progress: 100 }); - - if (!resp.success) throw new 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; - } - - this.dispatchEvent("solve", { token: resp.token }); - this.token = resp.token; - - if (this.#resetTimer) clearTimeout(this.#resetTimer); - const expiresIn = new Date(resp.expires).getTime() - Date.now(); - if (expiresIn > 0 && expiresIn < 24 * 60 * 60 * 1000) { - this.#resetTimer = setTimeout(() => this.reset(), expiresIn); - } else { - this.error("Invalid expiration time"); - } - - this.#div.setAttribute( - "aria-label", - this.getI18nText( - "verified-aria-label", - "We have verified you're a human, you may now continue", - ), - ); - - return { success: true, token: this.token }; - } catch (err) { - this.#div.setAttribute( - "aria-label", - this.getI18nText( - "error-aria-label", - "An error occurred, please try again", - ), - ); - this.error(err.message); - throw err; - } - } finally { - this.#solving = false; - } - } - - async solveChallenges(challenge) { - const total = challenge.length; - let completed = 0; - - const workers = Array(this.#workersCount) - .fill(null) - .map(() => { - try { - return new Worker(this.#workerUrl); - } catch (error) { - console.error("[cap] Failed to create worker:", error); - throw new Error("Worker creation failed"); - } - }); - - const solveSingleChallenge = ([salt, target], workerId) => - new Promise((resolve, reject) => { - const worker = workers[workerId]; - if (!worker) { - reject(new Error("Worker not available")); - return; - } - - worker.onmessage = ({ data }) => { - if (!data.found) return; - - completed++; - this.dispatchEvent("progress", { - progress: Math.round((completed / total) * 100), - }); - - resolve(data.nonce); - }; - - worker.onerror = (err) => { - this.error(`Error in worker: ${err.message || err}`); - reject(err); - }; - - worker.postMessage({ - salt, - target, - wasmUrl: - window.CAP_CUSTOM_WASM_URL || - `https://cdn.jsdelivr.net/npm/@cap.js/wasm@${WASM_VERSION}/browser/cap_wasm.min.js`, - }); - - if ( - typeof WebAssembly !== "object" || - typeof WebAssembly?.instantiate !== "function" - ) { - if (this.#shadow.querySelector(".warning")) return; - const warningEl = document.createElement("div"); - warningEl.className = "warning"; - warningEl.style.cssText = `width: var(--cap-widget-width, 230px);background: rgb(237, 56, 46);color: white;padding: 4px 6px;padding-bottom: calc(var(--cap-border-radius, 14px) + 5px);font-size: 10px;box-sizing: border-box;font-family: system-ui;border-top-left-radius: 8px;border-top-right-radius: 8px;text-align: center;padding-bottom:calc(var(--cap-border-radius,14px) + 5px);user-select:none;margin-bottom: -35.5px;opacity: 0;transition: margin-bottom .3s,opacity .3s;`; - warningEl.innerText = - this.getI18nText("wasm-disabled", "Enable WASM for significantly faster solving"); - this.#shadow.insertBefore(warningEl, this.#shadow.firstChild); - - setTimeout(() => { - warningEl.style.marginBottom = `calc(-1 * var(--cap-border-radius, 14px))` - warningEl.style.opacity = 1; - }, 10); - } - }); - - const results = []; - try { - for (let i = 0; i < challenge.length; i += this.#workersCount) { - const chunk = challenge.slice( - i, - Math.min(i + this.#workersCount, challenge.length), - ); - const chunkResults = await Promise.all( - chunk.map((c, idx) => solveSingleChallenge(c, idx)), - ); - results.push(...chunkResults); - } - } finally { - workers.forEach((w) => { - if (w) { - try { - w.terminate(); - } catch (error) { - console.error("[cap] error terminating worker:", error); - } - } - }); - } - - return results; - } - - setWorkersCount(workers) { - const parsedWorkers = parseInt(workers, 10); - const maxWorkers = Math.min(navigator.hardwareConcurrency || 8, 16); - this.#workersCount = - !Number.isNaN(parsedWorkers) && - parsedWorkers > 0 && - parsedWorkers <= maxWorkers - ? parsedWorkers - : navigator.hardwareConcurrency || 8; - } - - createUI() { - this.#div.classList.add("captcha"); - this.#div.setAttribute("role", "button"); - this.#div.setAttribute("tabindex", "0"); - this.#div.setAttribute( - "aria-label", - this.getI18nText("verify-aria-label", "Click to verify you're a human"), - ); - this.#div.setAttribute("aria-live", "polite"); - this.#div.setAttribute("disabled", "true"); - this.#div.innerHTML = `

${this.getI18nText( - "initial-state", - "Verify you're human", - )}

Cap`; - - this.#shadow.innerHTML = `.captcha,.captcha * {box-sizing:border-box;}.captcha{background-color:var(--cap-background,#fdfdfd);border:1px solid var(--cap-border-color,#dddddd8f);border-radius:var(--cap-border-radius,14px);user-select:none;height:var(--cap-widget-height, 58px);width:var(--cap-widget-width, 260px);display:flex;align-items:center;padding:var(--cap-widget-padding,14px);gap:var(--cap-gap,15px);cursor:pointer;transition:filter .2s,transform .2s;position:relative;-webkit-tap-highlight-color:rgba(255,255,255,0);overflow:hidden;color:var(--cap-color,#212121)}.captcha:hover{filter:brightness(98%)}.checkbox{width:var(--cap-checkbox-size,25px);height:var(--cap-checkbox-size,25px);border:var(--cap-checkbox-border,1px solid #aaaaaad1);border-radius:var(--cap-checkbox-border-radius,6px);background-color:var(--cap-checkbox-background,#fafafa91);transition:opacity .2s;margin-top:var(--cap-checkbox-margin,2px);margin-bottom:var(--cap-checkbox-margin,2px)}.captcha *{font-family:var(--cap-font,system,-apple-system,"BlinkMacSystemFont",".SFNSText-Regular","San Francisco","Roboto","Segoe UI","Helvetica Neue","Lucida Grande","Ubuntu","arial",sans-serif)}.captcha p{margin:0;font-weight:500;font-size:15px;user-select:none;transition:opacity .2s}.checkbox .progress-ring{display:none;width:100%;height:100%;transform:rotate(-90deg)}.checkbox .progress-ring-bg{fill:none;stroke:var(--cap-spinner-background-color,#eee);stroke-width:var(--cap-spinner-thickness,3)}.checkbox .progress-ring-circle{fill:none;stroke:var(--cap-spinner-color,#000);stroke-width:var(--cap-spinner-thickness,3);stroke-linecap:round;stroke-dasharray:87.96;stroke-dashoffset:87.96;transition:stroke-dashoffset 0.3s ease}.captcha[data-state=verifying] .checkbox{background:none;display:flex;align-items:center;justify-content:center;transform:scale(1.1);border:none;border-radius:50%;background-color:transparent}.captcha[data-state=verifying] .checkbox .progress-ring{display:block}.captcha[data-state=done] .checkbox{border:1px solid transparent;background-image:var(--cap-checkmark,url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%3E%3Cstyle%3E%40keyframes%20anim%7B0%25%7Bstroke-dashoffset%3A23.21320343017578px%7Dto%7Bstroke-dashoffset%3A0%7D%7D%3C%2Fstyle%3E%3Cpath%20fill%3D%22none%22%20stroke%3D%22%2300a67d%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20stroke-width%3D%222%22%20d%3D%22m5%2012%205%205L20%207%22%20style%3D%22stroke-dashoffset%3A0%3Bstroke-dasharray%3A23.21320343017578px%3Banimation%3Aanim%20.5s%20ease%22%2F%3E%3C%2Fsvg%3E"));background-size:cover}.captcha[data-state=done] .checkbox .progress-ring{display:none}.captcha[data-state=error] .checkbox{border:1px solid transparent;background-image:var(--cap-error-cross,url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='96' height='96' viewBox='0 0 24 24'%3E%3Cpath fill='%23f55b50' d='M11 15h2v2h-2zm0-8h2v6h-2zm1-5C6.47 2 2 6.5 2 12a10 10 0 0 0 10 10a10 10 0 0 0 10-10A10 10 0 0 0 12 2m0 18a8 8 0 0 1-8-8a8 8 0 0 1 8-8a8 8 0 0 1 8 8a8 8 0 0 1-8 8'/%3E%3C/svg%3E"));background-size:cover}.captcha[data-state=error] .checkbox .progress-ring{display:none}.captcha[disabled]{cursor:not-allowed}.captcha[disabled][data-state=verifying]{cursor:progress}.captcha[disabled][data-state=done]{cursor:default}.captcha .credits{position:absolute;bottom:10px;right:10px;font-size:12px;color:var(--cap-color,#212121);opacity:0.8;text-underline-offset: 1.5px;}`; - - this.#shadow.appendChild(this.#div); - } - - addEventListeners() { - if (!this.#div) return; - - this.#div.querySelector("a").addEventListener("click", (e) => { - e.stopPropagation(); - e.preventDefault(); - window.open("https://capjs.js.org", "_blank"); - }); - - this.#div.addEventListener("click", () => { - if (!this.#div.hasAttribute("disabled")) this.solve(); - }); - - this.#div.addEventListener("keydown", (e) => { - if ( - (e.key === "Enter" || e.key === " ") && - !this.#div.hasAttribute("disabled") - ) { - e.preventDefault(); - e.stopPropagation(); - this.solve(); - } - }); - - this.addEventListener("progress", this.boundHandleProgress); - this.addEventListener("solve", this.boundHandleSolve); - this.addEventListener("error", this.boundHandleError); - this.addEventListener("reset", this.boundHandleReset); - } - - updateUI(state, text, disabled = false) { - if (!this.#div) return; - - this.#div.setAttribute("data-state", state); - - this.#div.querySelector("p").innerText = text; - - if (disabled) { - this.#div.setAttribute("disabled", "true"); - } else { - this.#div.removeAttribute("disabled"); - } - } - - handleProgress(event) { - if (!this.#div) return; - - const progressElement = this.#div.querySelector("p"); - const progressCircle = this.#div.querySelector(".progress-ring-circle"); - - if (progressElement && progressCircle) { - const circumference = 2 * Math.PI * 14; - const offset = circumference - (event.detail.progress / 100) * circumference; - progressCircle.style.strokeDashoffset = offset; - progressElement.innerText = `${this.getI18nText( - "verifying-label", - "Verifying...", - )} ${event.detail.progress}%`; - } - this.executeAttributeCode("onprogress", event); - } - - handleSolve(event) { - this.updateUI( - "done", - this.getI18nText("solved-label", "You're a human"), - true, - ); - this.executeAttributeCode("onsolve", event); - } - - handleError(event) { - this.updateUI( - "error", - this.getI18nText("error-label", "Error. Try again."), - ); - this.executeAttributeCode("onerror", event); - } - - handleReset(event) { - this.updateUI("", this.getI18nText("initial-state", "I'm a human")); - this.executeAttributeCode("onreset", event); - } - - executeAttributeCode(attributeName, event) { - const code = this.getAttribute(attributeName); - if (!code) { - return; - } - - new Function("event", code).call(this, event); - } - - error(message = "Unknown error") { - console.error("[cap]", message); - this.dispatchEvent("error", { isCap: true, message }); - } - - dispatchEvent(eventName, detail = {}) { - const event = new CustomEvent(eventName, { - bubbles: true, - composed: true, - detail, - }); - super.dispatchEvent(event); - } - - reset() { - if (this.#resetTimer) { - clearTimeout(this.#resetTimer); - this.#resetTimer = null; - } - this.dispatchEvent("reset"); - this.token = null; - const fieldName = - this.getAttribute("data-cap-hidden-field-name") || "cap-token"; - if (this.querySelector(`input[name='${fieldName}']`)) { - this.querySelector(`input[name='${fieldName}']`).value = ""; - } - } - - get tokenValue() { - return this.token; - } - - disconnectedCallback() { - this.removeEventListener("progress", this.boundHandleProgress); - this.removeEventListener("solve", this.boundHandleSolve); - this.removeEventListener("error", this.boundHandleError); - this.removeEventListener("reset", this.boundHandleReset); - - this.#eventHandlers.forEach((handler, eventName) => { - this.removeEventListener(eventName.slice(2), handler); - }); - this.#eventHandlers.clear(); - - if (this.#shadow) { - this.#shadow.innerHTML = ""; - } - - this.reset(); - this.cleanup(); - } - - cleanup() { - if (this.#resetTimer) { - clearTimeout(this.#resetTimer); - this.#resetTimer = null; - } - - if (this.#workerUrl) { - URL.revokeObjectURL(this.#workerUrl); - this.#workerUrl = ""; - } - } - } - - // MARK: Invisible - class Cap { - constructor(config = {}, el) { - const widget = el || document.createElement("cap-widget"); - - Object.entries(config).forEach(([a, b]) => { - widget.setAttribute(a, b); - }); - - if (!config.apiEndpoint && !window?.CAP_CUSTOM_FETCH) { - widget.remove(); - throw new Error( - "Missing API endpoint. Either custom fetch or an API endpoint must be provided.", - ); - } - - if (config.apiEndpoint) { - widget.setAttribute("data-cap-api-endpoint", config.apiEndpoint); - } - - this.widget = widget; - this.solve = this.widget.solve.bind(this.widget); - this.reset = this.widget.reset.bind(this.widget); - this.addEventListener = this.widget.addEventListener.bind(this.widget); - - Object.defineProperty(this, "token", { - get: () => widget.token, - configurable: true, - enumerable: true, - }); - - if (!el) { - widget.style.display = "none"; - document.documentElement.appendChild(widget); - } - } - } - window.Cap = Cap; - - if (!customElements.get("cap-widget") && !window?.CAP_DONT_SKIP_REDEFINE) { - customElements.define("cap-widget", CapWidget); - } else { - console.warn( - "[cap] the cap-widget element has already been defined, skipping re-defining it.\nto prevent this, set window.CAP_DONT_SKIP_REDEFINE to true", - ); - } - - if (typeof exports === "object" && typeof module !== "undefined") { - module.exports = Cap; - } else if (typeof define === "function" && define.amd) { - define([], () => Cap); - } - - if (typeof exports !== "undefined") { - exports.default = Cap; - } + const { challenge, token } = await ( + await capFetch(`${apiEndpoint}challenge`, { + method: "POST", + }) + ).json(); + + let challenges = challenge; + + if (!Array.isArray(challenges)) { + let i = 0; + + challenges = Array.from({ length: challenge.c }, () => { + i = i + 1; + + return [prng(`${token}${i}`, challenge.s), prng(`${token}${i}d`, challenge.d)]; + }); + } + + const solutions = await this.solveChallenges(challenges); + + const resp = await ( + await capFetch(`${apiEndpoint}redeem`, { + method: "POST", + body: JSON.stringify({ token, solutions }), + headers: { "Content-Type": "application/json" }, + }) + ).json(); + + this.dispatchEvent("progress", { progress: 100 }); + + if (!resp.success) throw new 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; + } + + this.dispatchEvent("solve", { token: resp.token }); + this.token = resp.token; + + if (this.#resetTimer) clearTimeout(this.#resetTimer); + const expiresIn = new Date(resp.expires).getTime() - Date.now(); + if (expiresIn > 0 && expiresIn < 24 * 60 * 60 * 1000) { + this.#resetTimer = setTimeout(() => this.reset(), expiresIn); + } else { + this.error("Invalid expiration time"); + } + + this.#div.setAttribute( + "aria-label", + this.getI18nText( + "verified-aria-label", + "We have verified you're a human, you may now continue", + ), + ); + + return { success: true, token: this.token }; + } catch (err) { + this.#div.setAttribute( + "aria-label", + this.getI18nText("error-aria-label", "An error occurred, please try again"), + ); + this.error(err.message); + throw err; + } + } finally { + this.#solving = false; + } + } + + async solveChallenges(challenges) { + const total = challenges.length; + let completed = 0; + + let wasmModule = null; + const wasmSupported = + typeof WebAssembly === "object" && typeof WebAssembly.instantiate === "function"; + + if (wasmSupported) { + try { + wasmModule = await getWasmModule(); + } catch (e) { + console.warn("[cap] wasm unavailable, falling back to JS solver:", e); + } + } + + if (!wasmSupported) { + if (!this.#shadow.querySelector(".warning")) { + const warningEl = document.createElement("div"); + warningEl.className = "warning"; + warningEl.style.cssText = `width:var(--cap-widget-width,230px);background:rgb(237,56,46);color:white;padding:4px 6px;padding-bottom:calc(var(--cap-border-radius,14px) + 5px);font-size:10px;box-sizing:border-box;font-family:system-ui;border-top-left-radius:8px;border-top-right-radius:8px;text-align:center;user-select:none;margin-bottom:-35.5px;opacity:0;transition:margin-bottom .3s,opacity .3s;`; + warningEl.innerText = this.getI18nText( + "wasm-disabled", + "Enable WASM for significantly faster solving", + ); + this.#shadow.insertBefore(warningEl, this.#shadow.firstChild); + setTimeout(() => { + warningEl.style.marginBottom = `calc(-1 * var(--cap-border-radius, 14px))`; + warningEl.style.opacity = 1; + }, 10); + } + } + + const workers = Array(this.#workersCount) + .fill(null) + .map(() => { + try { + return new Worker(this.#workerUrl); + } catch (error) { + console.error("[cap] Failed to create worker:", error); + throw new Error("Worker creation failed"); + } + }); + + const solveSingleChallenge = ([salt, target], workerId) => + new Promise((resolve, reject) => { + const worker = workers[workerId]; + if (!worker) { + reject(new Error("Worker not available")); + return; + } + + worker.onmessage = ({ data }) => { + if (!data.found) return; + + completed++; + this.dispatchEvent("progress", { + progress: Math.round((completed / total) * 100), + }); + + resolve(data.nonce); + }; + + worker.onerror = (err) => { + this.error(`Error in worker: ${err.message || err}`); + reject(err); + }; + + if (wasmModule) { + worker.postMessage({ salt, target, wasmModule }, []); + } else { + worker.postMessage({ salt, target }); + } + }); + + const results = []; + try { + for (let i = 0; i < challenges.length; i += this.#workersCount) { + const chunk = challenges.slice(i, Math.min(i + this.#workersCount, challenges.length)); + const chunkResults = await Promise.all( + chunk.map((c, idx) => solveSingleChallenge(c, idx)), + ); + results.push(...chunkResults); + } + } finally { + workers.forEach((w) => { + if (w) { + try { + w.terminate(); + } catch (error) { + console.error("[cap] error terminating worker:", error); + } + } + }); + } + + return results; + } + + setWorkersCount(workers) { + const parsedWorkers = parseInt(workers, 10); + const maxWorkers = Math.min(navigator.hardwareConcurrency || 8, 16); + this.#workersCount = + !Number.isNaN(parsedWorkers) && parsedWorkers > 0 && parsedWorkers <= maxWorkers + ? parsedWorkers + : navigator.hardwareConcurrency || 8; + } + + createUI() { + this.#div.classList.add("captcha"); + this.#div.setAttribute("role", "button"); + this.#div.setAttribute("tabindex", "0"); + this.#div.setAttribute( + "aria-label", + this.getI18nText("verify-aria-label", "Click to verify you're a human"), + ); + this.#div.setAttribute("aria-live", "polite"); + this.#div.setAttribute("disabled", "true"); + this.#div.innerHTML = `

${this.getI18nText( + "initial-state", + "Verify you're human", + )}

Cap`; + + const css = `%%capCSS%%`; + + this.#shadow.innerHTML = `${css}`; + + this.#shadow.appendChild(this.#div); + } + + addEventListeners() { + if (!this.#div) return; + + this.#div.querySelector("a").addEventListener("click", (e) => { + e.stopPropagation(); + e.preventDefault(); + window.open("https://capjs.js.org", "_blank"); + }); + + this.#div.addEventListener("click", () => { + if (!this.#div.hasAttribute("disabled")) this.solve(); + }); + + this.#div.addEventListener("keydown", (e) => { + if ((e.key === "Enter" || e.key === " ") && !this.#div.hasAttribute("disabled")) { + e.preventDefault(); + e.stopPropagation(); + this.solve(); + } + }); + + this.addEventListener("progress", this.boundHandleProgress); + this.addEventListener("solve", this.boundHandleSolve); + this.addEventListener("error", this.boundHandleError); + this.addEventListener("reset", this.boundHandleReset); + } + + animateLabel(text) { + if (!this.#div) return; + const wrapper = this.#div.querySelector(".label-wrapper"); + if (!wrapper) return; + + if (prefersReducedMotion()) { + const current = wrapper.querySelector(".label.active"); + if (current) { + current.textContent = text; + } else { + const span = document.createElement("span"); + span.className = "label active"; + span.textContent = text; + wrapper.appendChild(span); + } + return; + } + + const current = wrapper.querySelector(".label.active"); + + const next = document.createElement("span"); + next.className = "label"; + next.textContent = text; + 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 }); + } + } + + updateUI(state, text, disabled = false) { + if (!this.#div) return; + + this.#div.setAttribute("data-state", state); + + this.animateLabel(text); + + if (disabled) { + this.#div.setAttribute("disabled", "true"); + } else { + this.#div.removeAttribute("disabled"); + } + } + + handleProgress(event) { + if (!this.#div) return; + + const progressCircle = this.#div.querySelector(".progress-ring-circle"); + + if (progressCircle) { + const circumference = 2 * Math.PI * 14; + const offset = circumference - (event.detail.progress / 100) * circumference; + progressCircle.style.strokeDashoffset = offset; + } + + const wrapper = this.#div.querySelector(".label-wrapper"); + if (wrapper) { + const activeLabel = wrapper.querySelector(".label.active"); + if (activeLabel) { + activeLabel.textContent = `${this.getI18nText("verifying-label", "Verifying...")} ${event.detail.progress}%`; + } + } + + this.executeAttributeCode("onprogress", event); + } + + handleSolve(event) { + this.updateUI("done", this.getI18nText("solved-label", "You're a human"), true); + this.executeAttributeCode("onsolve", event); + } + + handleError(event) { + this.updateUI("error", this.getI18nText("error-label", "Error. Try again.")); + this.executeAttributeCode("onerror", event); + } + + handleReset(event) { + this.updateUI("", this.getI18nText("initial-state", "I'm a human")); + this.executeAttributeCode("onreset", event); + } + + executeAttributeCode(attributeName, event) { + const code = this.getAttribute(attributeName); + if (!code) { + return; + } + + new Function("event", code).call(this, event); + } + + error(message = "Unknown error") { + console.error("[cap]", message); + this.dispatchEvent("error", { isCap: true, message }); + } + + dispatchEvent(eventName, detail = {}) { + const event = new CustomEvent(eventName, { + bubbles: true, + composed: true, + detail, + }); + super.dispatchEvent(event); + } + + reset() { + if (this.#resetTimer) { + clearTimeout(this.#resetTimer); + this.#resetTimer = null; + } + this.dispatchEvent("reset"); + this.token = null; + const fieldName = this.getAttribute("data-cap-hidden-field-name") || "cap-token"; + if (this.querySelector(`input[name='${fieldName}']`)) { + this.querySelector(`input[name='${fieldName}']`).value = ""; + } + } + + get tokenValue() { + return this.token; + } + + disconnectedCallback() { + this.removeEventListener("progress", this.boundHandleProgress); + this.removeEventListener("solve", this.boundHandleSolve); + this.removeEventListener("error", this.boundHandleError); + this.removeEventListener("reset", this.boundHandleReset); + + this.#eventHandlers.forEach((handler, eventName) => { + this.removeEventListener(eventName.slice(2), handler); + }); + this.#eventHandlers.clear(); + + if (this.#shadow) { + this.#shadow.innerHTML = ""; + } + + this.reset(); + this.cleanup(); + } + + cleanup() { + if (this.#resetTimer) { + clearTimeout(this.#resetTimer); + this.#resetTimer = null; + } + + if (this.#workerUrl) { + URL.revokeObjectURL(this.#workerUrl); + this.#workerUrl = ""; + } + } + } + + class Cap { + constructor(config = {}, el) { + const widget = el || document.createElement("cap-widget"); + + Object.entries(config).forEach(([a, b]) => { + widget.setAttribute(a, b); + }); + + if (!config.apiEndpoint && !window?.CAP_CUSTOM_FETCH) { + widget.remove(); + throw new Error( + "Missing API endpoint. Either custom fetch or an API endpoint must be provided.", + ); + } + + if (config.apiEndpoint) { + widget.setAttribute("data-cap-api-endpoint", config.apiEndpoint); + } + + this.widget = widget; + this.solve = this.widget.solve.bind(this.widget); + this.reset = this.widget.reset.bind(this.widget); + this.addEventListener = this.widget.addEventListener.bind(this.widget); + + Object.defineProperty(this, "token", { + get: () => widget.token, + configurable: true, + enumerable: true, + }); + + if (!el) { + widget.style.display = "none"; + document.documentElement.appendChild(widget); + } + } + } + + window.Cap = Cap; + + if (!customElements.get("cap-widget") && !window?.CAP_DONT_SKIP_REDEFINE) { + customElements.define("cap-widget", CapWidget); + } else if (customElements.get("cap-widget")) { + console.warn( + "[cap] the cap-widget element has already been defined, skipping re-defining it.\nto prevent this, set window.CAP_DONT_SKIP_REDEFINE to true", + ); + } + + if (typeof exports === "object" && typeof module !== "undefined") { + module.exports = Cap; + } else if (typeof define === "function" && define.amd) { + define([], () => Cap); + } + + if (typeof exports !== "undefined") { + exports.default = Cap; + } })(); diff --git a/widget/src/src/worker.js b/widget/src/src/worker.js index 34662d6..17a8fb7 100644 --- a/widget/src/src/worker.js +++ b/widget/src/src/worker.js @@ -1,115 +1,184 @@ (() => { - const solveFallback = async ({ salt, target }) => { - let nonce = 0; - const batchSize = 50000; - const encoder = new TextEncoder(); + const solveFallback = async ({ salt, target }) => { + let nonce = 0; + 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 targetBytesLength = targetBytes.length; + 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 targetBytesLength = targetBytes.length; - while (true) { - try { - for (let i = 0; i < batchSize; i++) { - const inputString = salt + nonce; - const inputBytes = encoder.encode(inputString); + while (true) { + try { + for (let i = 0; i < batchSize; i++) { + const inputString = salt + nonce; + const inputBytes = encoder.encode(inputString); - const hashBuffer = await crypto.subtle.digest("SHA-256", inputBytes); + const hashBuffer = await crypto.subtle.digest("SHA-256", inputBytes); - const hashBytes = new Uint8Array(hashBuffer, 0, targetBytesLength); + const hashBytes = new Uint8Array(hashBuffer, 0, targetBytesLength); - let matches = true; - for (let k = 0; k < targetBytesLength; k++) { - if (hashBytes[k] !== targetBytes[k]) { - matches = false; - break; - } - } + let matches = true; + for (let k = 0; k < targetBytesLength; k++) { + if (hashBytes[k] !== targetBytes[k]) { + matches = false; + break; + } + } - if (matches) { - self.postMessage({ nonce, found: true }); - return; - } + if (matches) { + self.postMessage({ nonce, found: true }); + return; + } - nonce++; - } - } catch (error) { - console.error("[cap worker]", error); - self.postMessage({ - found: false, - error: error.message, - }); - return; - } - } - }; + nonce++; + } + } catch (error) { + console.error("[cap worker]", error); + self.postMessage({ + found: false, + error: error.message, + }); + return; + } + } + }; - if ( - typeof WebAssembly !== "object" || - typeof WebAssembly?.instantiate !== "function" - ) { - console.warn( - "[cap worker] wasm not supported, falling back to alternative solver. this will be significantly slower.", - ); + if (typeof WebAssembly !== "object" || typeof WebAssembly?.instantiate !== "function") { + console.warn( + "[cap worker] wasm not supported, falling back to alternative solver. this will be significantly slower.", + ); - self.onmessage = async ({ data: { salt, target } }) => { - return solveFallback({ salt, target }); - }; + self.onmessage = async ({ data: { salt, target } }) => { + return solveFallback({ salt, target }); + }; - return; - } + return; + } - let wasmCacheUrl, solve_pow_function; + let solve_pow_function = null; - self.onmessage = async ({ data: { salt, target, wasmUrl } }) => { - let fb; + const initFromModule = (wasmModule) => { + try { + let wasm; + let WASM_VECTOR_LEN = 0; + let cachedUint8ArrayMemory = null; - if (wasmCacheUrl !== wasmUrl) { - wasmCacheUrl = wasmUrl; - await import(wasmUrl) - .then((wasmModule) => { - return wasmModule.default().then((instance) => { - solve_pow_function = ( - instance?.exports ? instance.exports : wasmModule - ).solve_pow; - }); - }) - .catch((e) => { - console.error("[cap worker] using fallback solver due to error:", e); - fb = true; + const getMemory = () => { + if (cachedUint8ArrayMemory === null || cachedUint8ArrayMemory.byteLength === 0) { + cachedUint8ArrayMemory = new Uint8Array(wasm.memory.buffer); + } + return cachedUint8ArrayMemory; + }; - return solveFallback({ salt, target }); - }); + const encoder = new TextEncoder(); - if (fb) return; - } + const passStringToWasm = (str, malloc, realloc) => { + if (realloc === undefined) { + const encoded = encoder.encode(str); + const ptr = malloc(encoded.length, 1) >>> 0; + getMemory() + .subarray(ptr, ptr + encoded.length) + .set(encoded); + WASM_VECTOR_LEN = encoded.length; + return ptr; + } - try { - const startTime = performance.now(); - const nonce = solve_pow_function(salt, target); - const endTime = performance.now(); + let len = str.length; + let ptr = malloc(len, 1) >>> 0; + const mem = getMemory(); + let offset = 0; - self.postMessage({ - nonce: Number(nonce), - found: true, - durationMs: (endTime - startTime).toFixed(2), - }); - } catch (error) { - console.error("[cap worker]", error); - - self.postMessage({ - found: false, - error: error.message || String(error), - }); - } - }; + for (; offset < len; offset++) { + const code = str.charCodeAt(offset); + if (code > 127) break; + mem[ptr + offset] = code; + } - self.onerror = (error) => { - self.postMessage({ - found: false, - error, - }); - }; + if (offset !== len) { + if (offset !== 0) str = str.slice(offset); + ptr = realloc(ptr, len, (len = offset + str.length * 3), 1) >>> 0; + const subarray = getMemory().subarray(ptr + offset, ptr + len); + const { written } = encoder.encodeInto(str, subarray); + offset += written; + ptr = realloc(ptr, len, offset, 1) >>> 0; + } + + WASM_VECTOR_LEN = offset; + return ptr; + }; + + const imports = { wbg: {} }; + imports.wbg.__wbindgen_init_externref_table = () => { + const table = wasm.__wbindgen_export_0; + const offset = table.grow(4); + table.set(0, undefined); + table.set(offset + 0, undefined); + table.set(offset + 1, null); + table.set(offset + 2, true); + table.set(offset + 3, false); + }; + + const instance = new WebAssembly.Instance(wasmModule, imports); + wasm = instance.exports; + + if (wasm.__wbindgen_start) wasm.__wbindgen_start(); + + solve_pow_function = (salt, target) => { + const saltPtr = passStringToWasm(salt, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const saltLen = WASM_VECTOR_LEN; + const targetPtr = passStringToWasm(target, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const targetLen = WASM_VECTOR_LEN; + return BigInt.asUintN(64, wasm.solve_pow(saltPtr, saltLen, targetPtr, targetLen)); + }; + + return true; + } catch (e) { + console.error("[cap worker] failed to init wasm from module:", e); + return false; + } + }; + + self.onmessage = async ({ data: { salt, target, wasmModule } }) => { + if (wasmModule instanceof WebAssembly.Module && solve_pow_function === null) { + const ok = initFromModule(wasmModule); + if (!ok) { + console.warn("[cap worker] wasm init failed, falling back to JS solver."); + return solveFallback({ salt, target }); + } + } + + if (solve_pow_function === null) { + console.warn("[cap worker] no wasm module provided, falling back to JS solver."); + return solveFallback({ salt, target }); + } + + try { + const startTime = performance.now(); + const nonce = solve_pow_function(salt, target); + const endTime = performance.now(); + + self.postMessage({ + nonce: Number(nonce), + found: true, + durationMs: (endTime - startTime).toFixed(2), + }); + } catch (error) { + console.error("[cap worker]", error); + + self.postMessage({ + found: false, + error: error.message || String(error), + }); + } + }; + + self.onerror = (error) => { + self.postMessage({ + found: false, + error, + }); + }; })(); diff --git a/widget/src/wasm-hashes.min.js b/widget/src/wasm-hashes.min.js index 3ea2b9e..931f559 100644 --- a/widget/src/wasm-hashes.min.js +++ b/widget/src/wasm-hashes.min.js @@ -359,26 +359,6 @@ }); }); -// ==================================================== - -// this is a now unused library which an old version of -// cap depended on. to avoid breaking things as much as -// possible, we've kept it here. - -try { - // lets us know how many people are actually using this - // so we can know when it's safe to deprecate. - - // server source code: - // https://gist.github.com/tiagozip/ffb39489f5af91e6e45f94f1bfa14ab2 - fetch( - `https://api.tiagorangel.com/cap-legacy-widget?h=${self.location?.hostname || "unknown"}`, - { - mode: "no-cors", - }, - ).catch(() => {}); -} catch {} - console.warn( `[cap] %cYou're using a deprecated version of Cap's widget that still relies on this file. @@ -387,9 +367,4 @@ It may continue to work for now, but could break at any time since this dependen Please update Cap to fix this.`, "font-size:15px;background-image:url('https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fpreview.colorkit.co%2Fcolor%2FEEEEEE.png%3Ftype%3Darticle-preview-logo%26size%3Dsocial%26colorname%3DSuper%2520Silver&f=1&nofb=1&ipt=49845e9195461b7c779182793c2ebf7834102eaf5561c15fa2cbb55494b77a9b');background-size:10px", -); - -console.log( - `%cTo help us understand how many people are using this legacy version and when it's safe to deprecate it, we've sent a small, privacy-friendly beacon to our servers. All related code, including the telemetry server, is open source.\n\nYou might have gotten a CSP error before this, it should be safe to ignore. Server source code: https://gist.github.com/tiagozip/ffb39489f5af91e6e45f94f1bfa14ab2`, - "font-size:11px;color:#b7b7b7", -); +); \ No newline at end of file