🎉
-You have successfully passed Cap's checkpoint.
- -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 @@ - - -
-You have successfully passed Cap's checkpoint.
- -You have successfully passed Cap's checkpoint.
- -You have successfully passed Cap's checkpoint.
- -${this.getI18nText("initial-state","Verify you're human")}
Cap`,this.#s.innerHTML=``,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${this.getI18nText("initial-state","Verify you're human")}
Cap`;this.#i.innerHTML=``,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 = ``; - - 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 = ``; + + 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