├── docs ├── .nojekyll ├── CNAME ├── robots.txt ├── openapi_spec.md ├── _sidebar.md ├── index.html ├── README.md ├── theme.md ├── csp.md ├── automated_testing.md ├── legacy_endpoint.md ├── verification_api.md ├── browser_support.md ├── eu_endpoint.md ├── flutter.md ├── installation.md ├── changelog.md ├── swagger.json ├── widget_api.md └── openapiv3.json ├── .prettierrc ├── src ├── declaration.d.ts ├── index.ts ├── main.ts ├── headless.ts ├── types.ts ├── html │ ├── polyfill.html │ ├── manual-polyfill.html │ └── index.html ├── puzzle.ts ├── styles.css ├── worker.ts ├── workergroup.ts ├── dom.ts ├── captcha.ts ├── polyfills.min.js └── localization.ts ├── .gitignore ├── terser.json ├── babel.fa.config.js ├── tsconfig.json ├── rollup.worker.config.ts ├── .github └── FUNDING.yml ├── babel.config.js ├── README.md ├── LICENSE.md ├── .eslintrc.cjs ├── rollup.library.config.ts ├── rollup.widget.config.ts └── package.json /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | docs.friendlycaptcha.com -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120 3 | } -------------------------------------------------------------------------------- /docs/robots.txt: -------------------------------------------------------------------------------- 1 | User-Agent: * 2 | Allow: / 3 | -------------------------------------------------------------------------------- /src/declaration.d.ts: -------------------------------------------------------------------------------- 1 | declare type u8 = number; 2 | declare type u32 = number; 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | coverage/ 4 | build 5 | tests_output 6 | 7 | .DS_Store 8 | .DS_Store? 9 | Thumbs.db 10 | *.log 11 | 12 | src/html/csp.html 13 | -------------------------------------------------------------------------------- /terser.json: -------------------------------------------------------------------------------- 1 | { 2 | "compress": { 3 | "passes": 4, 4 | "keep_fargs": false 5 | }, 6 | "output": { 7 | "comments": false, 8 | "beautify": false 9 | } 10 | } -------------------------------------------------------------------------------- /docs/openapi_spec.md: -------------------------------------------------------------------------------- 1 | # OpenAPI Specification 2 | 3 | For advanced usage of our public API, please refer to our OpenAPI spec: 4 | - [v2](/swagger.json ':ignore') 5 | - [v3](/openapiv3.json ':ignore') 6 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { WidgetInstance } from "./captcha"; 2 | export type { WidgetInstanceOptions } from "./captcha"; 3 | export { localizations } from "./localization"; 4 | export type { Localization } from "./localization"; 5 | -------------------------------------------------------------------------------- /babel.fa.config.js: -------------------------------------------------------------------------------- 1 | // Only applies fast-async 2 | export default { 3 | "plugins": [ 4 | ["module:fast-async", { 5 | "compiler": { 6 | "promises": true, 7 | "generators": true 8 | }, 9 | "useRuntimeModule": false 10 | }] 11 | ], 12 | "ignore": ["/node_modules\/(?!friendly-pow)/"] 13 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ESNext", 4 | "target": "es2017", 5 | "types": [ 6 | "node" 7 | ], 8 | "strict": true, 9 | "preserveConstEnums": false, 10 | "moduleResolution": "node", 11 | "sourceMap": true, 12 | "esModuleInterop": true, 13 | }, 14 | "include": [ 15 | "./src/**/*.ts", 16 | ], 17 | "exclude": [ 18 | "node_modules/", 19 | "dist/" 20 | ] 21 | } -------------------------------------------------------------------------------- /rollup.worker.config.ts: -------------------------------------------------------------------------------- 1 | import resolve from '@rollup/plugin-node-resolve'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import typescript from 'rollup-plugin-typescript2'; 4 | 5 | export default 6 | { 7 | input: `src/worker.ts`, 8 | output: [ 9 | {file: 'dist/worker.js', format: 'iife'} 10 | ], 11 | plugins: [ 12 | typescript({ 13 | include: [ 14 | './**/*.ts', 15 | ], 16 | useTsconfigDeclarationDir: true 17 | }), 18 | resolve(), 19 | commonjs(), 20 | ] 21 | } 22 | ; -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: FriendlyCaptcha 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | presets: [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "exclude": [ 7 | "transform-regenerator", "transform-async-to-generator" 8 | ], 9 | "targets": { 10 | "browsers": ["since 2013", "not dead", "not ie <= 10", "not ie_mob <= 11"] 11 | }, 12 | "modules": "auto", 13 | "useBuiltIns": "entry", 14 | "corejs": 3, 15 | } 16 | ] 17 | ], 18 | plugins: [ 19 | ["module:fast-async", { 20 | "compiler": { 21 | "promises": true, 22 | "generators": true 23 | }, 24 | "useRuntimeModule": false 25 | }], 26 | 27 | ], 28 | ignore: [/node_modules\/(?!friendly-pow)/] 29 | } -------------------------------------------------------------------------------- /docs/_sidebar.md: -------------------------------------------------------------------------------- 1 | * [Introduction](/) 2 | * [Installation](/installation.md) 3 | * [Widget API](/widget_api.md) 4 | * [Verification API](/verification_api.md) 5 | * **Advanced** 6 | * [Browser Support](/browser_support.md) 7 | * [Themes & Dark mode](/theme.md) 8 | * [📱 Use in Flutter](/flutter.md) 9 | * [🇪🇺 EU-only Endpoint](/eu_endpoint.md) 10 | * [🛡️ CSP](/csp.md) 11 | * [⚙️ OpenAPI](/openapi_spec.md) 12 | * [🤖 Automated Testing](/automated_testing.md) 13 | * [Changelog](/changelog.md) 14 | * **Links** 15 | * [ 📘 Friendly Captcha website](https://friendlycaptcha.com) 16 | * [ 🗂️ Github repository](https://github.com/friendlycaptcha/friendly-challenge) 17 | * [ 📦 NPM package](https://www.npmjs.com/package/friendly-challenge) 18 | * [ ⚡️ V2 Documentation](https://developer.friendlycaptcha.com/docs/v2) 19 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Friendly Captcha Documentation 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🛡️ Friendly Challenge 2 | 3 | [![NPM badge](https://img.shields.io/npm/v/friendly-challenge)](https://www.npmjs.com/package/friendly-challenge) [![Documentation](https://img.shields.io/badge/Read%20the-documentation-1abc9c.svg)](https://docs.friendlycaptcha.com) 4 | 5 | ![Friendly Captcha widget solving screenshot](https://i.imgur.com/BNRdsxS.png) ![Friendly Captcha widget finished screenshot](https://i.imgur.com/HlMY7QM.png) 6 | 7 | This repository contains the widget code and documentation for Friendly Captcha, here is a [demo](https://friendlycaptcha.com/demo) of the widget in a real form. 8 | 9 | Friendly Captcha is a proof-of-work based CAPTCHA alternative that respects the user's privacy, see the [**Friendly Captcha website**](https://friendlycaptcha.com). 10 | 11 | ## Getting started 12 | Read the [**documentation**](https://docs.friendlycaptcha.com). 13 | 14 | ## Changelog 15 | Check the [**changelog**](https://docs.friendlycaptcha.com/#/changelog). 16 | 17 | ## License 18 | [**MIT**](./LICENSE.md) -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { findCaptchaElements } from "./dom"; 2 | import { WidgetInstance } from "./captcha"; 3 | 4 | declare global { 5 | interface Window { 6 | friendlyChallenge: any; 7 | } 8 | } 9 | 10 | window.friendlyChallenge = { 11 | WidgetInstance: WidgetInstance, 12 | }; 13 | 14 | function setup() { 15 | let autoWidget = window.friendlyChallenge.autoWidget; 16 | 17 | const elements = findCaptchaElements(); 18 | for (let index = 0; index < elements.length; index++) { 19 | const hElement = elements[index] as HTMLElement; 20 | if (hElement && !hElement.dataset["attached"]) { 21 | autoWidget = new WidgetInstance(hElement); 22 | // We set the "data-attached" attribute so we don't attach to the same element twice. 23 | hElement.dataset["attached"] = "1"; 24 | } 25 | } 26 | window.friendlyChallenge.autoWidget = autoWidget; 27 | } 28 | 29 | if (document.readyState !== "loading") { 30 | setup(); 31 | } else { 32 | document.addEventListener("DOMContentLoaded", setup); 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2020-2023 Friendly Captcha GmbH 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Friendly Captcha Documentation 2 | 3 | > This is documentation for Friendly Captcha **v1**. 4 | > 5 | > You can [learn more](https://developer.friendlycaptcha.com/docs/v2/versions) about v1 and v2, or you can switch to the [Friendly Captcha v2 Developer Hub](https://developer.friendlycaptcha.com). 6 | 7 | Friendly Captcha is a system for preventing spam on your website. You can add the **Friendly Captcha widget** to your web app to fight spam, with little impact to the user experience. 8 | 9 | Friendly Captcha sends the user a cryptographic puzzle that takes the user's device a few seconds to solve, the user doesn't have to do anything. 10 | 11 | This documentation will show you how to integrate Friendly Captcha into your website. If you wish to learn more about Friendly Captcha, visit the [**Friendly Captcha website**](https://friendlycaptcha.com). 12 | 13 | **Screenshots of the widget in action** 14 | ![Friendly Captcha widget solving screenshot](https://i.imgur.com/BNRdsxS.png) ![Friendly Captcha widget finished screenshot](https://i.imgur.com/HlMY7QM.png) 15 | 16 | ## Next steps 17 | * The [**installation guide**](/installation) shows you how to add Friendly Captcha to your website in 3 steps. 18 | * The [**Javascript API**](/widget_api) allows you to customize the integration with your website. 19 | -------------------------------------------------------------------------------- /src/headless.ts: -------------------------------------------------------------------------------- 1 | // Defensive init to make it easier to integrate with Gatsby, NextJS, and friends. 2 | let nav: Navigator; 3 | let ua: string; 4 | if (typeof navigator !== "undefined" && typeof navigator.userAgent === "string") { 5 | nav = navigator; 6 | ua = nav.userAgent.toLowerCase(); 7 | } 8 | 9 | /** 10 | * Headless browser detection on the clientside is imperfect. One can modify any clientside code to disable or change this check, 11 | * and one can spoof whatever is checked here. However, that doesn't make it worthless: it's yet another hurdle for spammers and 12 | * it stops unsophisticated scripters from making any request whatsoever. 13 | */ 14 | export function isHeadless() { 15 | return ( 16 | //tell-tale bot signs 17 | ua.indexOf("headless") !== -1 || 18 | nav.appVersion.indexOf("Headless") !== -1 || 19 | ua.indexOf("bot") !== -1 || // http://www.useragentstring.com/pages/useragentstring.php?typ=Browser 20 | ua.indexOf("crawl") !== -1 || // Only IE5 has two distributions that has this on windows NT.. so yeah. 21 | nav.webdriver === true || 22 | !nav.language || 23 | (nav.languages !== undefined && !nav.languages.length) // IE 11 does not support NavigatorLanguage.languages https://developer.mozilla.org/en-US/docs/Web/API/NavigatorLanguage/languages 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /docs/theme.md: -------------------------------------------------------------------------------- 1 | # Customizing the look 2 | 3 | The styling of Friendly Captcha is done in plain CSS, and you can change it however you want. A stylesheet gets injected into the `head` of your HTML document when the first widget is loaded. 4 | 5 | ## Dark mode 6 | 7 | ![Friendly Captcha widget finished screenshot](https://i.imgur.com/HlMY7QM.png) ![Friendly Captcha widget finished screenshot dark mode](https://i.imgur.com/UgqOJaB.png) 8 | 9 | Friendly Captcha ships with two built-in themes, by adding the `dark` class to your `frc-captcha` element you can enable dark mode: 10 | 11 | ```html 12 | 13 |
14 | 15 | 16 |
17 | ``` 18 | 19 | 20 | ## Using your own stylesheet 21 | You can create your own stylesheet for the Friendly Captcha widget. The [existing css file](https://github.com/FriendlyCaptcha/friendly-challenge/blob/master/src/styles.css) is probably a good start. 22 | 23 | If any HTML element with id `frc-style` is present on the HTML document, the original styles will not be injected. So to use your own custom theme you could add the following: 24 | 25 | ```html 26 | 27 | ``` 28 | 29 | Alternatively, if you are using the library approach, you can use the `skipStyleInjection` option to prevent a stylesheet from getting injected. -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "parser": "@typescript-eslint/parser", 8 | "parserOptions": { 9 | "project": "tsconfig.json", 10 | "sourceType": "module" 11 | }, 12 | "plugins": [ 13 | "@typescript-eslint" 14 | ], 15 | "rules": { 16 | "@typescript-eslint/member-delimiter-style": [ 17 | "error", 18 | { 19 | "multiline": { 20 | "delimiter": "semi", 21 | "requireLast": true 22 | }, 23 | "singleline": { 24 | "delimiter": "semi", 25 | "requireLast": false 26 | } 27 | } 28 | ], 29 | "@typescript-eslint/semi": [ 30 | "error", 31 | "always" 32 | ], 33 | "@typescript-eslint/no-use-before-define": "off", 34 | "@typescript-eslint/explicit-function-return-type": "off", 35 | "@typescript-eslint/no-explicit-any": "off", 36 | "@typescript-eslint/no-unused-vars": [ 37 | "warn", 38 | { 39 | "argsIgnorePattern": "^_", 40 | "varsIgnorePattern": "^_" 41 | } 42 | ], 43 | }, 44 | "extends": [ 45 | "eslint:recommended", 46 | "plugin:@typescript-eslint/eslint-recommended", 47 | "plugin:@typescript-eslint/recommended" 48 | ] 49 | }; -------------------------------------------------------------------------------- /docs/csp.md: -------------------------------------------------------------------------------- 1 | # Content Security Policy (CSP) 2 | 3 | Content Security Policy is a way to secure your website from cross-site scripting (XSS). The HTTP Content-Security-Policy response header allows website administrators to control resources the user agent is allowed to load for a given page. 4 | 5 | ## Configuring your CSP for Friendly Captcha 6 | 7 | If you are using a CSP for your website you will have to configure it to allow Friendly Captcha's script to be loaded. There are three ways this can be achieved: 8 | 9 | ### Source-based approach (Easiest) 10 | 11 | You will only need to add the following directives: 12 | 13 | - If you're loading the widget from the CDN: 14 | 15 | `script-src 'wasm-unsafe-eval' https://cdn.jsdelivr.net/npm/; worker-src blob:; child-src blob:` 16 | 17 | > note: as an extra precaution you can use the full path to our CDN hosted widget by replacing `https://cdn.jsdelivr.net/npm/` above with `https://cdn.jsdelivr.net/npm/friendly-challenge@0.9.19/widget.module.min.js https://cdn.jsdelivr.net/npm/friendly-challenge@0.9.19/widget.min.js` (making sure the widget versions are correct) 18 | 19 | - If you're loading the widget from your own bundle: 20 | 21 | `script-src 'wasm-unsafe-eval' 'self'; worker-src blob:; child-src blob:` 22 | 23 | ### Nonce-based approach (Recommended) 24 | 25 | See [this guide](https://content-security-policy.com/nonce/) and include your nonce in the `widget.min.js` and `widget.module.min.js` script tag. 26 | 27 | ### Hash-based approach 28 | 29 | See [this guide](https://content-security-policy.com/hash/) 30 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type Solver = (puzzleBuffer: Uint8Array, threshold: number, n?: number) => Uint8Array[]; 2 | 3 | export type MessageFromWorker = ReadyMessage | StartedMessage | DonePartMessage | ErrorMessage; 4 | export type MessageToWorker = StartMessage | SolverMessage; 5 | 6 | export interface StartMessage { 7 | type: "start"; 8 | puzzleSolverInput: Uint8Array; 9 | threshold: number; 10 | puzzleIndex: number; 11 | puzzleNumber: number; 12 | } 13 | 14 | export interface SolverMessage { 15 | type: "solver"; 16 | forceJS: boolean; 17 | } 18 | 19 | export interface ReadyMessage { 20 | type: "ready"; 21 | solver: 1 | 2; 22 | } 23 | 24 | export interface StartedMessage { 25 | type: "started"; 26 | } 27 | 28 | export interface ErrorMessage { 29 | type: "error"; 30 | message: string; 31 | } 32 | 33 | export interface ProgressMessage { 34 | /** 35 | * Number of solutions to be found in total 36 | */ 37 | n: number; 38 | /** 39 | * Number of all hashes calculated 40 | */ 41 | h: number; 42 | /** 43 | * Time this solution took in seconds 44 | */ 45 | t: number; 46 | /** 47 | * This is the i'th solution. 48 | */ 49 | i: number; 50 | } 51 | 52 | export interface DonePartMessage { 53 | type: "done"; 54 | solution: Uint8Array; 55 | puzzleIndex: number; 56 | puzzleNumber: number; 57 | /** 58 | * Hashes attempted for this solution 59 | */ 60 | h: number; 61 | } 62 | 63 | export interface DoneMessage { 64 | solution: Uint8Array; 65 | /** 66 | * Total number of hashes that were required 67 | */ 68 | h: number; 69 | /** 70 | * Total time it took in seconds 71 | */ 72 | t: number; 73 | diagnostics: Uint8Array; 74 | solver: 1 | 2; 75 | } 76 | -------------------------------------------------------------------------------- /src/html/polyfill.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | FriendlyCaptcha 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |
17 | 18 | 19 | 20 | 21 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /rollup.library.config.ts: -------------------------------------------------------------------------------- 1 | import resolve from '@rollup/plugin-node-resolve'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import typescript from 'rollup-plugin-typescript2'; 4 | import {string} from 'rollup-plugin-string'; 5 | import replace from '@rollup/plugin-replace'; 6 | 7 | import dts from "rollup-plugin-dts"; 8 | 9 | const CleanCSS = require('clean-css'); 10 | 11 | // Inline plugin to load css as minified string 12 | const css = () => {return { 13 | name: "css", 14 | transform(code, id) { 15 | if (id.endsWith(".css")) { 16 | const minified = new CleanCSS({level: 2}).minify(code); 17 | return `export default '${minified.styles}'`; 18 | } 19 | } 20 | }} 21 | 22 | const ts = () => { 23 | return typescript({ 24 | useTsconfigDeclarationDir: true, 25 | include: [ 26 | './src/**/*.ts', 27 | ], 28 | }) 29 | } 30 | 31 | export default [ 32 | { 33 | input: `src/index.ts`, 34 | output: { 35 | file: 'dist/index.js', 36 | format: 'es', 37 | sourcemap: true, 38 | }, 39 | plugins: [ 40 | ts(), 41 | commonjs(), 42 | resolve(), 43 | string({ 44 | include: ["dist/worker.min.js"], 45 | }), 46 | css(), 47 | ], 48 | }, 49 | { 50 | input: `src/index.ts`, 51 | output: [ 52 | { 53 | file: 'dist/compat/index.js', 54 | format: 'es', 55 | sourcemap: true, 56 | }, 57 | ], 58 | plugins: [ 59 | ts(), 60 | replace({"dist/worker.min.js": "dist/worker.compat.min.js"}), 61 | commonjs(), 62 | resolve(), 63 | string({ 64 | include: ["dist/worker.compat.min.js"], 65 | }), 66 | css(), 67 | ], 68 | }, 69 | { 70 | input: `src/index.ts`, 71 | output: [{file: "dist/friendly-challenge.d.ts", format: "es"}], 72 | plugins: [dts()], 73 | } 74 | ]; -------------------------------------------------------------------------------- /rollup.widget.config.ts: -------------------------------------------------------------------------------- 1 | import resolve from '@rollup/plugin-node-resolve'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import typescript from 'rollup-plugin-typescript2'; 4 | import {string} from 'rollup-plugin-string'; 5 | import replace from '@rollup/plugin-replace'; 6 | 7 | const CleanCSS = require('clean-css'); 8 | 9 | // Inline plugin to load css as minified string 10 | const css = () => {return { 11 | name: "css", 12 | transform(code, id) { 13 | if (id.endsWith(".css")) { 14 | const minified = new CleanCSS({level: 2}).minify(code); 15 | return `export default '${minified.styles}'`; 16 | } 17 | } 18 | }} 19 | 20 | const ts = () => { 21 | return typescript({ 22 | useTsconfigDeclarationDir: true, 23 | include: [ 24 | './src/**/*.ts', 25 | ], 26 | }) 27 | } 28 | 29 | export default [ 30 | { 31 | input: `src/main.ts`, 32 | output: { 33 | file: 'dist/widget.module.js', 34 | format: 'es', 35 | sourcemap: false, 36 | name: "friendlyChallenge", 37 | }, 38 | watch: { 39 | include: 'src/**', 40 | }, 41 | plugins: [ 42 | ts(), 43 | commonjs(), 44 | resolve(), 45 | string({ 46 | include: ["dist/worker.min.js"], 47 | }), 48 | css(), 49 | ], 50 | }, 51 | { 52 | input: `src/main.ts`, 53 | output: [ 54 | { 55 | file: 'dist/widget-pre-babel.js', 56 | format: 'iife', 57 | sourcemap: false, 58 | name: "friendlyChallenge", 59 | }, 60 | ], 61 | watch: { 62 | include: 'src/**', 63 | }, 64 | plugins: [ 65 | ts(), 66 | replace({"dist/worker.min.js": "dist/worker.compat.min.js"}), 67 | commonjs(), 68 | resolve(), 69 | string({ 70 | include: ["dist/worker.compat.min.js"], 71 | }), 72 | css(), 73 | ], 74 | } 75 | ]; -------------------------------------------------------------------------------- /docs/automated_testing.md: -------------------------------------------------------------------------------- 1 | # Automated Testing 2 | 3 | Testing can be at odds with a captcha you add to your website or app. Perhaps you use automated testing tools like [Cypress](https://www.cypress.io/), [Selenium](https://www.selenium.dev/), or [Puppeteer](https://github.com/puppeteer/puppeteer). On this page you will find some tips and approaches that will help work out a testing story for pages that include a captcha widget. 4 | 5 | ## Testing tips 6 | 7 | ### Mocking out the API 8 | The easiest and most recommended approach is to mock out the API in the backend. Instead of calling our API and getting the JSON response, you instead always use `{success: true}` when running in test mode. With this approach you can keep all other code in your application the same. 9 | 10 | ### IP or password-based gating 11 | If you perform automated end-to-end tests in production you may want to conditionally disable the captcha check for your test-runner. 12 | * If you know the IPs of the machines that run these tests you could whitelist those and skip the captcha check for those (or use the mocking approach above). 13 | * If you do not know the IP beforehand, you could have your test-runner put a secret in the form submission that you check for. In other words, you can specify a password that bypasses the captcha. 14 | 15 | ### Dynamic button enabling 16 | Many websites that have a form protected using Friendly Captcha will only enable the submit button after the captcha widget is finished to prevent users from submitting without a valid captcha solution. When you are using a browser automation tool you may want to enable the button despite the captcha not being completed, we advice you achieve this by executing a snippet of Javascript (which is something you can do in all browser testing automation tools). 17 | 18 | 19 | Here's an example such snippet: 20 | ```javascript 21 | const button = document.querySelector("#my-button"); 22 | button.disabled = false; 23 | ``` 24 | -------------------------------------------------------------------------------- /docs/legacy_endpoint.md: -------------------------------------------------------------------------------- 1 | # Updating from the legacy endpoint 2 | 3 | Since April 2021 we offer our global API on `api.friendlycaptcha.com/api/`. 4 | 5 | Before then our documentation and widget distributions would default to `friendlycaptcha.com/api/`. 6 | 7 | Only a small subset of our users are still using the old domain. We are starting to actively reach out to those that have not updated since, asking them to make the switch to the `api.friendlycaptcha.com` subdomain. 8 | 9 | We are planning to sunset the API endpoints on the old domain entirely. Therefore, action on your part is needed. 10 | 11 | ## Why? 12 | 13 | Serving our API from a separate subdomain allows for better reliability and better privacy preservation. 14 | 15 | This change also offers us more flexibility in how we serve our website and apps. It simplifies moving away from a non-EU CDN provider. 16 | 17 | --- 18 | 19 | ## Upgrade instructions 20 | 21 | Please perform the following updates. Upgrading should only take a few minutes, there have been no breaking changes for either the widget or the siteverify endpoints. 22 | 23 | ### Widget and the `friendly-challenge` library 24 | 25 | If you are using version `0.8.3` or below, please update to the latest version as soon as possible. If you are using a more recent version still we advise to upgrade to the latest version (`0.9.19`). 26 | 27 | The changelog can be found [here](https://github.com/FriendlyCaptcha/friendly-challenge/blob/master/docs/changelog.md). 28 | 29 | ### If you are using the WordPress plugin 30 | 31 | Please upgrade to the latest version. Versions of the plugin before `1.2.0` are affected (released April 2021). 32 | 33 | ### Backend code 34 | 35 | Please replace the following in your backend code: 36 | 37 | `https://friendlycaptcha.com/api/v1/siteverify` 38 | 39 | Replace it with 40 | 41 | `https://api.friendlycaptcha.com/api/v1/siteverify` 42 | 43 | > Note: If you are using the 🇪🇺 EU endpoint for siteverify, no changes are required in the backend code. 44 | -------------------------------------------------------------------------------- /src/html/manual-polyfill.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | FriendlyCaptcha 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 | 23 | 24 | 25 | 26 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/puzzle.ts: -------------------------------------------------------------------------------- 1 | import { decode } from "friendly-pow/base64"; 2 | import { difficultyToThreshold } from "friendly-pow/encoding"; 3 | import { NUMBER_OF_PUZZLES_OFFSET, PUZZLE_DIFFICULTY_OFFSET, PUZZLE_EXPIRY_OFFSET } from "friendly-pow/puzzle"; 4 | import { Localization } from "./localization"; 5 | 6 | export interface Puzzle { 7 | signature: string; 8 | base64: string; 9 | buffer: Uint8Array; // input puzzle 10 | threshold: number; // Related to difficulty 11 | n: number; // Amount of puzzles to solve 12 | expiry: number; // Expiry in milliseconds from now 13 | } 14 | 15 | export function decodeBase64Puzzle(base64Puzzle: string): Puzzle { 16 | const parts = base64Puzzle.split("."); 17 | const puzzle = parts[1]; 18 | const arr = decode(puzzle); 19 | return { 20 | signature: parts[0], 21 | base64: puzzle, 22 | buffer: arr, 23 | n: arr[NUMBER_OF_PUZZLES_OFFSET], 24 | threshold: difficultyToThreshold(arr[PUZZLE_DIFFICULTY_OFFSET]), 25 | expiry: arr[PUZZLE_EXPIRY_OFFSET] * 300000, 26 | }; 27 | } 28 | 29 | export async function getPuzzle(urlsSeparatedByComma: string, siteKey: string, lang: Localization): Promise { 30 | const urls = urlsSeparatedByComma.split(","); 31 | for (let i = 0; i < urls.length; i++) { 32 | try { 33 | const response = await fetchAndRetryWithBackoff( 34 | urls[i] + "?sitekey=" + siteKey, 35 | { headers: [["x-frc-client", "js-0.9.19"]], mode: "cors" }, 36 | 2 37 | ); 38 | if (response.ok) { 39 | const json = await response.json(); 40 | return json.data.puzzle; 41 | } else { 42 | let json; 43 | try { 44 | json = await response.json(); 45 | } catch (e) { 46 | /* Do nothing, the error is not valid JSON */ 47 | } 48 | 49 | if (json && json.errors && json.errors[0] === "endpoint_not_enabled") { 50 | throw Error(`Endpoint not allowed (${response.status})`); 51 | } 52 | 53 | if (i === urls.length - 1) { 54 | throw Error(`Response status ${response.status} ${response.statusText} ${json ? json.errors : ""}`); 55 | } 56 | } 57 | } catch (e) { 58 | console.error("[FRC Fetch]:", e); 59 | const err = new Error(`${lang.text_fetch_error} ${urls[i]}`); 60 | (err as any).rawError = e; 61 | throw err; 62 | } 63 | } 64 | // This code should never be reached. 65 | throw Error(`Internal error`); 66 | } 67 | 68 | /** 69 | * Retries given request with exponential backoff (starting with 1000ms delay, multiplying by 4 every time) 70 | * @param url Request (can be string url) to fetch 71 | * @param opts Options for fetch 72 | * @param n Number of times to attempt before giving up. 73 | */ 74 | export async function fetchAndRetryWithBackoff(url: RequestInfo, opts: RequestInit, n: number): Promise { 75 | let time = 1000; 76 | return fetch(url, opts).catch(async (error) => { 77 | if (n === 0) throw error; 78 | await new Promise((r) => setTimeout(r, time)); 79 | time *= 4; 80 | return fetchAndRetryWithBackoff(url, opts, n - 1); 81 | }); 82 | } 83 | -------------------------------------------------------------------------------- /docs/verification_api.md: -------------------------------------------------------------------------------- 1 | # Verification API 2 | 3 | You will need an API key to prove it's you, you can create one on the [**API Keys page**](https://app.friendlycaptcha.eu/dashboard) in the dashboard. 4 | 5 | To verify the CAPTCHA solution, make a POST request to `https://api.friendlycaptcha.com/api/v1/siteverify` with the following parameters: 6 | 7 | | POST Parameter | Description | 8 | |----------------|-----------------------------------------------------| 9 | | `solution` | The solution value that the user submitted in the `frc-captcha-solution` field | 10 | | `secret` | An API key that proves it's you, create one on the Friendly Captcha website | 11 | | `sitekey` | **Optional:** the sitekey that you want to make sure the puzzle was generated from. | 12 | 13 | You can pass these parameters in a JSON body, or as formdata. 14 | 15 | > If your account is on the **Advanced** or **Enterprise** plan your server can also make a request [to our EU endpoint](./eu_endpoint). 16 | 17 | ### The verification response 18 | 19 | The response will tell you whether the CAPTCHA solution is valid and hasn't been used before. The response body is a JSON object: 20 | 21 | ```JSON 22 | { 23 | "success": true|false, 24 | "errors": [...] // optional 25 | } 26 | ``` 27 | 28 | If `success` is false, `errors` will be a list containing at least one of the following error codes below. **If you are seeing status code 400 or 401 your server code is probably not configured correctly.** 29 | 30 | 31 | | Error code | Status |Description | 32 | |----------------|----------|-------------------------------------------| 33 | | `secret_missing` | 400 | You forgot to add the secret (=API key) parameter. | 34 | | `secret_invalid` | 401 | The API key you provided was invalid. | 35 | | `solution_missing` | 400 | You forgot to add the solution parameter. | 36 | | `bad_request` | 400 | Something else is wrong with your request, e.g. your request body is empty. | 37 | | `solution_invalid` | 200 | The solution you provided was invalid (perhaps the user tried to tamper with the puzzle). | 38 | | `solution_timeout_or_duplicate` | 200 | The puzzle that the solution was for has expired or has already been used. | 39 | 40 | 41 | > ⚠️ Status code 200 does not mean the solution was valid, it just means the verification was performed succesfully. Use the `success` field. 42 | 43 | A solution can be invalid for a number of reasons, perhaps the user submitted before the CAPTCHA was completed or they tried to change the puzzle to make it easier. The first case can be prevented by disabling the submit button until the CAPTCHA has been completed succesfully. 44 | 45 | ### Verification Best practices 46 | If you receive a response code other than 200 in production, you should probably accept the user's form despite not having been able to verify the CAPTCHA solution. 47 | 48 | Maybe your server is misconfigured or the Friendly Captcha servers are down. While we try to make sure that never happens, it is a good idea to assume one day disaster will strike. 49 | 50 | An example: you are using Friendly Captcha for a sign up form and you can't verify the solution. It is better to trust the user and let them sign up anyway, because otherwise no signup will be possible at all. Do send an alert to yourself! 51 | 52 | -------------------------------------------------------------------------------- /docs/browser_support.md: -------------------------------------------------------------------------------- 1 | # Browser Support 2 | 3 | All modern browsers are supported, on both mobile and desktop, all releases up to at least 8 years old. That includes Safari, Edge, Chrome, Firefox, and Opera. Internet Explorer 11 also works, with some sidenotes (see the section below). See the targeted [**browserlist compatible browsers**](https://browserslist.dev/?q=c2luY2UgMjAxMywgbm90IGRlYWQsIG5vdCBpZSA8PTEwLCBub3QgaWVfbW9iIDw9IDEx). 4 | 5 | ## Polyfills 6 | 7 | If you want to support browsers over 5 years old, you will need some polyfills (`fetch`, `Promise`, `URL` and `Object.assign`). 8 | 9 | You can use the build that includes the polyfills: 10 | 11 | ```html 12 | 18 | 24 | ``` 25 | 26 | Or you can include the polyfills manually: 27 | 28 | ```html 29 | 30 | 31 | 32 | 33 | ``` 34 | 35 | If you find any compatibility issues please create a [**Github issue**](https://github.com/FriendlyCaptcha/friendly-challenge/issues). 36 | 37 | ## Compatibility mode for the library 38 | 39 | If you are importing _friendly-challenge_ into your own bundle and want to support old browsers (those that don't support ES2017) you should change your imports to be from `friendly-challenge/compat`. For example: 40 | 41 | ``` 42 | import {WidgetInstance} from 'friendly-challenge' 43 | // change to 44 | import {WidgetInstance} from 'friendly-challenge/compat' 45 | ``` 46 | 47 | Both imports are ES2017, use a tool like Babel to transpile it to ES5 or lower. The difference between these two imports is the webworker script which is included as a string. In the _compat_ build it is ES5 compatible and includes necessary polyfills (at the cost of slighlty worse performance and an extra 3KB bundle size). 48 | 49 | ### Old browser speed 50 | 51 | The Javascript engine in old browsers is generally slower than modern ones, the CAPTCHA may take a minute to solve on very old browsers (>5 years old). 52 | 53 | ## NoScript 54 | 55 | Users need to have Javascript enabled to solve the CAPTCHA. We recommend you add a note for users that have Javascript disabled by default: 56 | 57 | ```html 58 | 59 | ``` 60 | 61 | This will only be visible to users without Javascript enabled. 62 | 63 | ## Internet Explorer 64 | 65 | Internet Explorer 11 is supported out of the box, but take note that **the Javascript engine is very slow in Internet Explorer leading to a poor user experience**: the CAPTCHA will likely take more than a minute to solve. 66 | 67 | Consider displaying a message to IE users that they should use a different browser. You can use this Javascript snippet to display a note after the widget in Internet Explorer only: 68 | 69 | ```javascript 70 | if (!!document.documentMode) { 71 | // Only true in Internet Explorer 72 | Array.prototype.slice.call(document.querySelectorAll(".frc-captcha")).forEach(function (element) { 73 | var messageElement = document.createElement("p"); 74 | messageElement.innerHTML = 75 | "The anti-robot check works better and faster in modern browsers such as Edge, Firefox, or Chrome. Please consider updating your browser"; 76 | element.parentNode.insertBefore(messageElement, element.nextSibling); 77 | }); 78 | } 79 | ``` 80 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | .frc-captcha * { 2 | /* Mostly a CSS reset so existing website styles don't clash */ 3 | margin: 0; 4 | padding: 0; 5 | border: 0; 6 | text-align: initial; 7 | border-radius: 0; 8 | filter: none !important; 9 | transition: none !important; 10 | font-weight: normal; 11 | font-size: 14px; 12 | line-height: 1.2; 13 | text-decoration: none; 14 | background-color: initial; 15 | color: #222; 16 | } 17 | 18 | .frc-captcha { 19 | position: relative; 20 | min-width: 250px; 21 | max-width: 312px; 22 | border: 1px solid #f4f4f4; 23 | padding-bottom: 12px; 24 | background-color: #fff; 25 | } 26 | 27 | .frc-captcha b { 28 | font-weight: bold; 29 | } 30 | 31 | .frc-container { 32 | display: flex; 33 | align-items: center; 34 | min-height: 52px; 35 | } 36 | 37 | .frc-icon { 38 | fill: #222; 39 | stroke: #222; 40 | flex-shrink: 0; 41 | margin: 8px 8px 0 8px; 42 | } 43 | 44 | .frc-icon.frc-warning { 45 | fill: #C00; 46 | } 47 | 48 | .frc-success .frc-icon { 49 | animation: frc-fade-in 1s both ease-in; 50 | } 51 | 52 | .frc-content { 53 | white-space: nowrap; 54 | display: flex; 55 | flex-direction: column; 56 | margin: 4px 6px 0 0; 57 | overflow-x: auto; 58 | flex-grow: 1; 59 | } 60 | 61 | .frc-banner { 62 | position: absolute; 63 | bottom: 0px; 64 | right: 6px; 65 | line-height: 1; 66 | } 67 | 68 | .frc-banner * { 69 | font-size: 10px; 70 | opacity: 0.8; 71 | text-decoration: none; 72 | } 73 | 74 | .frc-progress { 75 | -webkit-appearance: none; 76 | -moz-appearance: none; 77 | appearance: none; 78 | margin: 3px 0; 79 | height: 4px; 80 | border: none; 81 | background-color: #eee; 82 | color: #222; 83 | width: 100%; 84 | transition: all 0.5s linear; 85 | } 86 | 87 | .frc-progress::-webkit-progress-bar { 88 | background: #eee; 89 | } 90 | 91 | .frc-progress::-webkit-progress-value { 92 | background: #222; 93 | } 94 | 95 | .frc-progress::-moz-progress-bar { 96 | background: #222; 97 | } 98 | 99 | .frc-button { 100 | cursor: pointer; 101 | padding: 2px 6px; 102 | background-color: #f1f1f1; 103 | border: 1px solid transparent; 104 | text-align: center; 105 | font-weight: 600; 106 | text-transform: none; 107 | } 108 | 109 | .frc-button:focus { 110 | border: 1px solid #333; 111 | } 112 | 113 | .frc-button:hover { 114 | background-color: #ddd; 115 | } 116 | 117 | .frc-captcha-solution { 118 | display: none; 119 | } 120 | 121 | .frc-err-url { 122 | text-decoration: underline; 123 | font-size: 0.9em; 124 | } 125 | 126 | /* RTL support */ 127 | 128 | .frc-rtl { 129 | direction: rtl; 130 | } 131 | 132 | .frc-rtl .frc-content { 133 | margin: 4px 0 0 6px; 134 | } 135 | 136 | .frc-banner.frc-rtl { 137 | left: 6px; 138 | right: auto; 139 | } 140 | 141 | /* Dark theme */ 142 | 143 | .dark.frc-captcha { 144 | color: #fff; 145 | background-color: #222; 146 | border-color: #333; 147 | } 148 | 149 | .dark.frc-captcha * { 150 | color: #fff; 151 | } 152 | 153 | .dark.frc-captcha button { 154 | background-color: #444; 155 | } 156 | 157 | .dark .frc-icon { 158 | fill: #fff; 159 | stroke: #fff; 160 | } 161 | 162 | .dark .frc-progress { 163 | background-color: #444; 164 | } 165 | 166 | .dark .frc-progress::-webkit-progress-bar { 167 | background: #444; 168 | } 169 | 170 | .dark .frc-progress::-webkit-progress-value { 171 | background: #ddd; 172 | } 173 | 174 | .dark .frc-progress::-moz-progress-bar { 175 | background: #ddd; 176 | } 177 | 178 | @keyframes frc-fade-in { 179 | from { 180 | opacity: 0; 181 | } 182 | to { 183 | opacity: 1; 184 | } 185 | } -------------------------------------------------------------------------------- /docs/eu_endpoint.md: -------------------------------------------------------------------------------- 1 | # 🇪🇺 Dedicated EU Endpoint 2 | 3 | By default the FriendlyCaptcha widget talks to our global service served from all over the world to retrieve CAPTCHA puzzles. Depending on your user's geography this request may be served from outside the EU. 4 | 5 | As a premium feature we offer a dedicated forwarding endpoint hosted in Germany as an additional guarantee that the personal information (i.e. visitor IP addresses) never leave the EU. 6 | 7 | > Note: Using this service requires a **Friendly Captcha Advanced** or **Enterprise** plan. 8 | 9 | ## Enabling the EU endpoint 10 | Open your [account page](https://app.friendlycaptcha.eu/dashboard/accounts") and click **Manage** on the app you want to enable the EU endpoint for. 11 | 12 | In the **Puzzle Endpoints** section you are able to enable and disable the endpoints you allow your visitors to fetch puzzles from. 13 | 14 | > We advise you to enable both for now. Later when you confirm everything is working you can disable the global endpoint. 15 | 16 | ## Configuring the widget 17 | The widget talks to the global endpoint by default, you need to tell it to use the EU endpoint instead. 18 | 19 | You can use the `data-puzzle-endpoint` HTML attribute: 20 | 21 | ```html 22 | 23 |
24 | ``` 25 | 26 | Or if you are using the [Javascript widget API](http://docs.friendlycaptcha.com/#/widget_api?id=javascript-api) you can specify it in the options passed in the constructor: 27 | ```javascript 28 | import { WidgetInstance } from "friendly-challenge"; 29 | 30 | const element = document.querySelector("#my-widget"); 31 | const options = { 32 | puzzleEndpoint: "https://eu-api.friendlycaptcha.eu/api/v1/puzzle", 33 | /* ... other options */ 34 | } 35 | const widget = new WidgetInstance(element, options); 36 | ``` 37 | 38 | And with this the widget will only ever make requests to our EU endpoint. No changes are required for the verification of submitted solutions. 39 | 40 | ## Fallback to global service 41 | Although we work hard to make sure it never happens, disaster may one day strike (e.g. a meteor strike to our German data center). In case our EU endpoint service goes down you can instruct your widget to use the global service as a fallback. 42 | 43 | You can do this by specifying both endpoints separated with a comma (`,`) in order of preference: 44 | ```html 45 | 46 |
47 | ``` 48 | 49 | ## EU Verification endpoint 50 | Your servers can also use our EU endpoint for the verification of submitted puzzles. 51 | 52 | Instead of the [usual verification endpoint](./verification_api) your server makes the POST request to `https://eu-api.friendlycaptcha.eu/api/v1/siteverify`. 53 | 54 | ## Reference 55 | For reference, these are the puzzle and siteverify endpoints. 56 | 57 | ### Puzzle (used in the widget configuration) 58 | 59 | | Endpoint Name | URL | 60 | |----------------|----------| 61 | | 🌍 Global | https://api.friendlycaptcha.com/api/v1/puzzle 62 | | 🇪🇺 EU | https://eu-api.friendlycaptcha.eu/api/v1/puzzle 63 | 64 | ### Siteverify (used in your backend server) 65 | 66 | | Endpoint Name | URL | 67 | |----------------|----------| 68 | | 🌍 Global | https://api.friendlycaptcha.com/api/v1/siteverify 69 | | 🇪🇺 EU | https://eu-api.friendlycaptcha.eu/api/v1/siteverify 70 | 71 | 72 | ## Troubleshooting 73 | If your widget or the browser console shows **Endpoint not allowed** or **403 Forbidden**, double-check that the you enabled the configured endpoint for the given sitekey. 74 | 75 | If you run into any other issues you can of course always reach out. 76 | -------------------------------------------------------------------------------- /src/worker.ts: -------------------------------------------------------------------------------- 1 | import { decode } from "friendly-pow/base64"; 2 | import { base64 } from "friendly-pow/wasm/optimized.wrap"; 3 | import { getWasmSolver } from "friendly-pow/api/wasm"; 4 | import { getJSSolver } from "friendly-pow/api/js"; 5 | import { SOLVER_TYPE_JS, SOLVER_TYPE_WASM } from "friendly-pow/constants"; 6 | import { Solver, DonePartMessage, MessageToWorker } from "./types"; 7 | 8 | if (!Uint8Array.prototype.slice) { 9 | Object.defineProperty(Uint8Array.prototype, "slice", { 10 | value: function (begin: number, end: number) { 11 | return new Uint8Array(Array.prototype.slice.call(this, begin, end)); 12 | }, 13 | }); 14 | } 15 | 16 | // Not technically correct, but it makes TS happy.. 17 | // eslint-disable-next-line @typescript-eslint/ban-ts-ignore 18 | // @ts-ignore 19 | declare let self: Worker; 20 | 21 | (self as any).ASC_TARGET = 0; 22 | 23 | // 1 for JS, 2 for WASM 24 | let solverType: 1 | 2; 25 | 26 | // Puzzle consisting of zeroes 27 | let setSolver: (s: Solver) => void; 28 | const solver: Promise = new Promise((resolve) => (setSolver = resolve)); 29 | 30 | self.onerror = (evt: any) => { 31 | self.postMessage({ 32 | type: "error", 33 | message: JSON.stringify(evt), 34 | }); 35 | }; 36 | 37 | self.onmessage = async (evt: any) => { 38 | const data: MessageToWorker = evt.data; 39 | try { 40 | /** 41 | * Compile the WASM and setup the solver. 42 | * If WASM support is not present, it uses the JS version instead. 43 | */ 44 | if (data.type === "solver") { 45 | if (data.forceJS) { 46 | solverType = SOLVER_TYPE_JS; 47 | const s = await getJSSolver(); 48 | setSolver(s); 49 | } else { 50 | try { 51 | solverType = SOLVER_TYPE_WASM; 52 | const module = WebAssembly.compile(decode(base64)); 53 | const s = await getWasmSolver(await module); 54 | setSolver(s); 55 | } catch (e: any) { 56 | console.log( 57 | "FriendlyCaptcha failed to initialize WebAssembly, falling back to Javascript solver: " + e.toString() 58 | ); 59 | solverType = SOLVER_TYPE_JS; 60 | const s = await getJSSolver(); 61 | setSolver(s); 62 | } 63 | } 64 | 65 | self.postMessage({ 66 | type: "ready", 67 | solver: solverType, 68 | }); 69 | } else if (data.type === "start") { 70 | const solve = await solver; 71 | 72 | self.postMessage({ 73 | type: "started", 74 | }); 75 | 76 | let totalH = 0; 77 | let solution!: Uint8Array; 78 | 79 | // We loop over a uint32 to find as solution, it is technically possible (but extremely unlikely - only possible with very high difficulty) that 80 | // there is no solution, here we loop over one byte further up too in case that happens. 81 | for (let b = 0; b < 256; b++) { 82 | data.puzzleSolverInput[123] = b; 83 | const [s, hash] = solve(data.puzzleSolverInput, data.threshold); 84 | if (hash.length === 0) { 85 | // This means 2^32 puzzles were evaluated, which takes a while in a browser! 86 | // As we try 256 times, this is not fatal 87 | console.warn("FC: Internal error or no solution found"); 88 | totalH += Math.pow(2, 32) - 1; 89 | continue; 90 | } 91 | solution = s; 92 | break; 93 | } 94 | 95 | const view = new DataView(solution.slice(-4).buffer); 96 | totalH += view.getUint32(0, true); 97 | 98 | self.postMessage({ 99 | type: "done", 100 | solution: solution.slice(-8), // The last 8 bytes are the solution nonce 101 | h: totalH, 102 | puzzleIndex: data.puzzleIndex, 103 | puzzleNumber: data.puzzleNumber, 104 | } as DonePartMessage); 105 | } 106 | } catch (e) { 107 | setTimeout(() => { 108 | throw e; 109 | }); 110 | } 111 | }; 112 | -------------------------------------------------------------------------------- /src/html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | FriendlyCaptcha 6 | 7 | 8 | 9 | 10 | 11 | 12 | 21 | 22 | 23 | 24 |

Localization tests

25 |
26 |
27 |
28 |
29 |
30 |

This one has an invalid data-lang, it should fall back to English.

31 |
32 |

Custom field name

33 |
34 | 35 |

No hidden form field

36 |
37 | 38 |

Invalid sitekey

39 |
40 | 41 |

Invalid puzzle endpoint

42 |
43 | 44 |

Completely invalid endpoint

45 |
46 | 47 | 48 |

Not allowed EU endpoint

49 |
50 |
51 | 52 |

With fallback endpoint

53 |
54 | 55 |
56 |
57 |
58 |
59 | 60 | 61 | 62 | 63 | 64 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "friendly-challenge", 3 | "version": "0.9.19", 4 | "description": "The client code used for FriendlyCaptcha (widget script, html, styling and webworker solvers)", 5 | "keywords": [ 6 | "captcha", 7 | "anti-bot", 8 | "widget", 9 | "friendlycaptcha", 10 | "privacy", 11 | "friendly" 12 | ], 13 | "license": "MIT", 14 | "repository": "https://github.com/FriendlyCaptcha/friendly-challenge", 15 | "scripts": { 16 | "prebuild": "rimraf dist", 17 | "lint": "eslint src/**/*.ts", 18 | "build:html": "cp src/html/*.html dist/", 19 | "build:worker": "npm run build:worker:ts && npm run build:worker:compat && npm run build:worker:addpolyfill && npm run build:worker:minify", 20 | "build:worker:ts": "rollup -c rollup.worker.config.ts", 21 | "build:worker:compat": "babel dist/worker.js -o dist/worker.compat.js --config-file ./babel.fa.config.js && babel dist/worker.compat.js -o dist/worker.compat.js", 22 | "build:worker:addpolyfill": "cat node_modules/promis/promise.js dist/worker.compat.js > dist/worker.compat.poly.js && mv dist/worker.compat.poly.js dist/worker.compat.js && sed -i.bak 's/window/self/g' dist/worker.compat.js && rm dist/worker.compat.js.bak", 23 | "build:worker:minify": "terser dist/worker.js -o dist/worker.min.js --config-file terser.json --ecma 2017 --toplevel && terser dist/worker.compat.js -o dist/worker.compat.min.js --config-file terser.json --safari10 --toplevel --wrap frcWorker", 24 | "build:widget": "npm run build:widget:ts && npm run build:widget:compat && npm run build:widget:minify && npm run build:widget:compat:polyfilled", 25 | "build:widget:ts": "rollup -c rollup.widget.config.ts", 26 | "build:widget:compat": "babel dist/widget-pre-babel.js -o dist/widget.js --config-file ./babel.fa.config.js && babel dist/widget.js -o dist/widget.js && rm dist/widget-pre-babel.js", 27 | "build:widget:minify": "terser dist/widget.module.js -o dist/widget.module.min.js --config-file terser.json --ecma 2017 --module && terser dist/widget.js -o dist/widget.min.js --config-file terser.json --safari10 --toplevel --wrap friendlyChallenge", 28 | "build:widget:compat:polyfilled": "cat src/polyfills.min.js dist/widget.min.js > dist/widget.polyfilled.min.js", 29 | "build:library": "rollup -c rollup.library.config.ts && cp dist/friendly-challenge.d.ts dist/compat/index.d.ts", 30 | "build:tests": "tsc", 31 | "build": "npm run build:worker && npm run build:widget && npm run build:library && npm run build:html && cp package.json dist/package.json", 32 | "start": "rollup -c rollup.widget.config.ts -w", 33 | "dist": "npm i && npm run build && cd dist && npm publish --ignore-scripts", 34 | "prepublishOnly": "echo \"Error: Don't run 'npm publish' in root. Use 'npm run dist' instead.\" && exit 1" 35 | }, 36 | "types": "friendly-challenge.d.ts", 37 | "type": "module", 38 | "main": "index.js", 39 | "author": "Friendly Captcha GmbH ", 40 | "devDependencies": { 41 | "@babel/cli": "^7.24.7", 42 | "@babel/core": "^7.10.4", 43 | "@babel/plugin-transform-block-scoped-functions": "^7.10.4", 44 | "@babel/plugin-transform-class-properties": "^7.24.7", 45 | "@babel/plugin-transform-runtime": "^7.10.4", 46 | "@babel/preset-env": "^7.10.4", 47 | "@rollup/plugin-commonjs": "^13.0.0", 48 | "@rollup/plugin-node-resolve": "^8.0.0", 49 | "@rollup/plugin-replace": "^2.3.3", 50 | "@types/node": "^14.0.9", 51 | "@typescript-eslint/eslint-plugin": "^5.30.0", 52 | "@typescript-eslint/parser": "^5.30.0", 53 | "babel-plugin-class-properties": "^1.0.0", 54 | "babel-plugin-transform-es2015-block-scoped-functions": "^6.22.0", 55 | "babel-plugin-transform-hoist-nested-functions": "^1.2.0", 56 | "babel-plugin-transform-runtime": "^6.23.0", 57 | "clean-css": "^4.2.3", 58 | "eslint": "^8.18.0", 59 | "fast-async": "^7.0.6", 60 | "rimraf": "^3.0.2", 61 | "rollup": "^2.16.1", 62 | "rollup-plugin-dts": "^4.2.2", 63 | "rollup-plugin-sourcemaps": "^0.6.2", 64 | "rollup-plugin-string": "^3.0.0", 65 | "rollup-plugin-typescript2": "^0.32.1", 66 | "terser": "^5.13.1", 67 | "typescript": "^4.4.4" 68 | }, 69 | "dependencies": { 70 | "core-js": "^3.41.0", 71 | "friendly-pow": "0.2.2", 72 | "object-assign-polyfill": "^0.1.0", 73 | "promis": "^1.1.4", 74 | "url-polyfill": "^1.1.10", 75 | "whatwg-fetch": "^3.4.1" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/workergroup.ts: -------------------------------------------------------------------------------- 1 | import { Puzzle } from "./puzzle"; 2 | import { getPuzzleSolverInputs } from "friendly-pow/puzzle"; 3 | import { createDiagnosticsBuffer } from "friendly-pow/diagnostics"; 4 | import { DoneMessage, ProgressMessage, MessageFromWorker, SolverMessage, StartMessage } from "./types"; 5 | // @ts-ignore 6 | import workerString from "../dist/worker.min.js"; 7 | 8 | // Defensive init to make it easier to integrate with Gatsby and friends. 9 | let URL: any; 10 | if (typeof window !== "undefined") { 11 | URL = window.URL || window.webkitURL; 12 | } 13 | 14 | export class WorkerGroup { 15 | private workers: Worker[] = []; 16 | 17 | private puzzleNumber = 0; 18 | 19 | private numPuzzles = 0; 20 | private threshold = 0; 21 | private startTime = 0; 22 | private progress = 0; 23 | private totalHashes = 0; 24 | private puzzleSolverInputs: Uint8Array[] = []; 25 | // The index of the next puzzle 26 | private puzzleIndex = 0; 27 | private solutionBuffer: Uint8Array = new Uint8Array(0); 28 | // initialize some value, so ts is happy 29 | private solverType: 1 | 2 = 1; 30 | 31 | private readyPromise: Promise = new Promise(() => {}); 32 | private readyCount = 0; 33 | private startCount = 0; 34 | 35 | public progressCallback: (p: ProgressMessage) => any = () => 0; 36 | public readyCallback: () => any = () => 0; 37 | public startedCallback: () => any = () => 0; 38 | public doneCallback: (d: DoneMessage) => any = () => 0; 39 | public errorCallback: (e: any) => any = () => 0; 40 | 41 | public init() { 42 | this.terminateWorkers(); 43 | 44 | this.progress = 0; 45 | this.totalHashes = 0; 46 | 47 | let setReady: () => void; 48 | this.readyPromise = new Promise((resolve) => (setReady = resolve)); 49 | 50 | this.readyCount = 0; 51 | this.startCount = 0; 52 | 53 | // Setup four workers for now - later we could calculate this depending on the device 54 | this.workers = new Array(4); 55 | const workerBlob = new Blob([workerString] as any, { type: "text/javascript" }); 56 | 57 | for (let i = 0; i < this.workers.length; i++) { 58 | this.workers[i] = new Worker(URL.createObjectURL(workerBlob)); 59 | this.workers[i].onerror = (e: ErrorEvent) => this.errorCallback(e); 60 | 61 | this.workers[i].onmessage = (e: any) => { 62 | const data: MessageFromWorker = e.data; 63 | if (!data) return; 64 | if (data.type === "ready") { 65 | this.readyCount++; 66 | this.solverType = data.solver; 67 | // We are ready, when all workers are ready 68 | if (this.readyCount == this.workers.length) { 69 | setReady(); 70 | this.readyCallback(); 71 | } 72 | } else if (data.type === "started") { 73 | this.startCount++; 74 | // We started, when the first worker starts working 75 | if (this.startCount == 1) { 76 | this.startTime = Date.now(); 77 | this.startedCallback(); 78 | } 79 | } else if (data.type === "done") { 80 | if (data.puzzleNumber !== this.puzzleNumber) return; // solution belongs to a previous puzzle 81 | 82 | if (this.puzzleIndex < this.puzzleSolverInputs.length) { 83 | this.workers[i].postMessage({ 84 | type: "start", 85 | puzzleSolverInput: this.puzzleSolverInputs[this.puzzleIndex], 86 | threshold: this.threshold, 87 | puzzleIndex: this.puzzleIndex, 88 | puzzleNumber: this.puzzleNumber, 89 | } as StartMessage); 90 | this.puzzleIndex++; 91 | } 92 | 93 | this.progress++; 94 | this.totalHashes += data.h; 95 | 96 | this.progressCallback({ 97 | n: this.numPuzzles, 98 | h: this.totalHashes, 99 | t: (Date.now() - this.startTime) / 1000, 100 | i: this.progress, 101 | }); 102 | 103 | this.solutionBuffer.set(data.solution, data.puzzleIndex * 8); 104 | // We are done, when all puzzles have been solved 105 | if (this.progress == this.numPuzzles) { 106 | const totalTime = (Date.now() - this.startTime) / 1000; 107 | this.doneCallback({ 108 | solution: this.solutionBuffer, 109 | h: this.totalHashes, 110 | t: totalTime, 111 | diagnostics: createDiagnosticsBuffer(this.solverType, totalTime), 112 | solver: this.solverType, 113 | }); 114 | } 115 | } else if (data.type === "error") { 116 | this.errorCallback(data); 117 | } 118 | }; 119 | } 120 | } 121 | 122 | public setupSolver(forceJS = false) { 123 | const msg: SolverMessage = { type: "solver", forceJS: forceJS }; 124 | for (let i = 0; i < this.workers.length; i++) { 125 | this.workers[i].postMessage(msg); 126 | } 127 | } 128 | 129 | async start(puzzle: Puzzle) { 130 | await this.readyPromise; 131 | 132 | this.puzzleSolverInputs = getPuzzleSolverInputs(puzzle.buffer, puzzle.n); 133 | this.solutionBuffer = new Uint8Array(8 * puzzle.n); 134 | this.numPuzzles = puzzle.n; 135 | this.threshold = puzzle.threshold; 136 | this.puzzleIndex = 0; 137 | this.puzzleNumber++; 138 | 139 | for (let i = 0; i < this.workers.length; i++) { 140 | if (this.puzzleIndex === this.puzzleSolverInputs.length) break; 141 | 142 | this.workers[i].postMessage({ 143 | type: "start", 144 | puzzleSolverInput: this.puzzleSolverInputs[i], 145 | threshold: this.threshold, 146 | puzzleIndex: this.puzzleIndex, 147 | puzzleNumber: this.puzzleNumber, 148 | } as StartMessage); 149 | this.puzzleIndex++; 150 | } 151 | } 152 | 153 | public terminateWorkers() { 154 | if (this.workers.length == 0) return; 155 | for (let i = 0; i < this.workers.length; i++) { 156 | this.workers[i].terminate(); 157 | } 158 | this.workers = []; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/dom.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import css from "./styles.css"; 3 | import { ProgressMessage, DoneMessage } from "./types"; 4 | import { SOLVER_TYPE_JS } from "friendly-pow/constants"; 5 | import { Localization } from "./localization"; 6 | 7 | const loaderSVG = ``; 8 | const errorSVG = ``; 9 | 10 | /** 11 | * Base template used for all widget states 12 | * The reason we use raw string interpolation here is so we don't have to ship something like lit-html. 13 | */ 14 | function getTemplate( 15 | fieldName: string, 16 | rtl: boolean | undefined, 17 | svgContent: string, 18 | svgAriaHidden: boolean, 19 | textContent: string, 20 | solutionString: string, 21 | buttonText?: string, 22 | progress = false, 23 | debugData?: string, 24 | additionalContainerClasses?: string 25 | ) { 26 | return `
27 | 30 |
31 | ${textContent} 32 | ${buttonText ? `` : ""} 33 | ${progress ? `0%` : ""} 34 |
35 |
FriendlyCaptcha ⇗ 36 | ${fieldName === "-" ? "":``}`; 37 | } 38 | 39 | /** 40 | * Used when the widget is ready to start solving. 41 | */ 42 | export function getReadyHTML(fieldName: string, l: Localization) { 43 | return getTemplate( 44 | fieldName, 45 | l.rtl, 46 | ``, 47 | true, 48 | l.text_ready, 49 | ".UNSTARTED", 50 | l.button_start, 51 | false 52 | ); 53 | } 54 | 55 | /** 56 | * Used when the widget is retrieving a puzzle 57 | */ 58 | export function getFetchingHTML(fieldName: string, l: Localization) { 59 | return getTemplate(fieldName, l.rtl, loaderSVG, true, l.text_fetching, ".FETCHING", undefined, true); 60 | } 61 | 62 | /** 63 | * Used when the solver is running, displays a progress bar. 64 | */ 65 | export function getRunningHTML(fieldName: string, l: Localization) { 66 | return getTemplate(fieldName, l.rtl, loaderSVG, true, l.text_solving, ".UNFINISHED", undefined, true); 67 | } 68 | 69 | export function getDoneHTML(fieldName: string, l: Localization, solution: string, data: DoneMessage) { 70 | const timeData = `${data.t.toFixed(0)}s (${((data.h / data.t) * 0.001).toFixed(0)}K/s)${ 71 | data.solver === SOLVER_TYPE_JS ? " JS Fallback" : "" 72 | }`; 73 | return getTemplate( 74 | fieldName, 75 | l.rtl, 76 | `${l.text_completed_sr}`, 77 | false, 78 | l.text_completed, 79 | solution, 80 | undefined, 81 | false, 82 | timeData, 83 | "frc-success" 84 | ); 85 | } 86 | 87 | export function getExpiredHTML(fieldName: string, l: Localization) { 88 | return getTemplate(fieldName, l.rtl, errorSVG, true, l.text_expired, ".EXPIRED", l.button_restart); 89 | } 90 | 91 | export function getErrorHTML( 92 | fieldName: string, 93 | l: Localization, 94 | errorDescription: string, 95 | recoverable = true, 96 | headless = false 97 | ) { 98 | return getTemplate( 99 | fieldName, 100 | l.rtl, 101 | errorSVG, 102 | true, 103 | `${l.text_error}
${errorDescription}`, 104 | headless ? ".HEADLESS_ERROR" : ".ERROR", 105 | recoverable ? l.button_retry : undefined 106 | ); 107 | } 108 | 109 | export function findCaptchaElements() { 110 | const elements = document.querySelectorAll(".frc-captcha"); 111 | if (elements.length === 0) { 112 | console.warn("FriendlyCaptcha: No div was found with .frc-captcha class"); 113 | } 114 | return elements; 115 | } 116 | 117 | /** 118 | * Injects the style if no #frc-style element is already present 119 | * (to support custom stylesheets) 120 | */ 121 | export function injectStyle(styleNonce: string | null = null) { 122 | if (!document.querySelector("#frc-style")) { 123 | const styleSheet = document.createElement("style"); 124 | styleSheet.id = "frc-style"; 125 | styleSheet.innerHTML = css; 126 | 127 | if (styleNonce) { 128 | styleSheet.setAttribute('nonce', styleNonce); 129 | } 130 | 131 | document.head.appendChild(styleSheet); 132 | } 133 | } 134 | 135 | /** 136 | * @param element parent element of friendlycaptcha 137 | * @param progress value between 0 and 1 138 | */ 139 | export function updateProgressBar(element: HTMLElement, data: ProgressMessage) { 140 | const p = element.querySelector(".frc-progress") as HTMLProgressElement; 141 | const perc = (data.i + 1) / data.n; 142 | if (p) { 143 | p.value = perc; 144 | p.innerText = (perc*100).toFixed(1) + "%"; 145 | p.title = data.i + 1 + "/" + data.n + " (" + ((data.h / data.t) * 0.001).toFixed(0) + "K/s)"; 146 | } 147 | } 148 | 149 | /** 150 | * Traverses parent nodes until a
is found, returns null if not found. 151 | */ 152 | export function findParentFormElement(element: HTMLElement) { 153 | while (element.tagName !== "FORM") { 154 | element = element.parentElement as HTMLElement; 155 | if (!element) { 156 | return null; 157 | } 158 | } 159 | return element; 160 | } 161 | 162 | /** 163 | * Add listener to specified element that will only fire once on focus. 164 | */ 165 | export function executeOnceOnFocusInEvent(element: HTMLElement, listener: (this: HTMLElement, fe: FocusEvent) => any) { 166 | element.addEventListener("focusin", listener, { once: true, passive: true }); 167 | } 168 | -------------------------------------------------------------------------------- /docs/flutter.md: -------------------------------------------------------------------------------- 1 | # 📱 Flutter 2 | 3 | You can use the Friendly Captcha widget in your [Flutter](https://flutter.dev/) apps. 4 | 5 | It works by opening an embedded WebView that displays the Friendly Captcha widget. When this widget has completed the anti-robot check, the solution string becomes available in your Flutter app code. You then send this solution to your backend server for verification. 6 | 7 | Here we will run you through the steps to get it working. 8 | 9 | > We are not Flutter experts, this integration will be improved over time. We welcome any suggestions to the example code below. _Contributions are welcome!_ 10 | 11 | ### 1. Setup 12 | 13 | - You will need to target at least Android SDK version 17 (edit `minSdkVersion` in `android/app/build.gradle`). 14 | - Add `flutter_inappwebview` to your dependencies in `pubspec.yaml`: 15 | ```yaml 16 | dependencies: 17 | flutter: 18 | sdk: flutter 19 | flutter_inappwebview: 5.3.2 20 | ``` 21 | 22 | ### 2. Define a Friendly Captcha widget in Flutter 23 | 24 | Create a file `friendlycaptcha.dart` (or some other filename) and paste the following code: 25 | 26 | ```dart 27 | import 'dart:async'; 28 | 29 | import 'package:flutter/material.dart'; 30 | import 'package:flutter_inappwebview/flutter_inappwebview.dart'; 31 | 32 | String buildPageContent({String sitekey, String theme = "", String start = "auto", String lang = "", String puzzleEndpoint = ""}) { 33 | return """ 34 | 35 | 36 | 37 | 38 | Friendly Captcha Verification 39 | 40 | 41 | 42 | 43 | 55 | 56 | 57 | 58 |
59 |
60 | 70 | 71 | 72 | """; 73 | } 74 | class FriendlyCaptcha extends StatefulWidget { 75 | Function(String solution) callback; 76 | 77 | String sitekey; 78 | String theme; 79 | String start; 80 | String lang; 81 | String puzzleEndpoint; 82 | 83 | FriendlyCaptcha({ 84 | @required this.sitekey, 85 | @required this.callback, 86 | this.theme = "", 87 | this.start = "auto", 88 | this.lang = "", 89 | this.puzzleEndpoint = ""} 90 | ) {} 91 | 92 | @override 93 | State createState() { 94 | return CaptchaState(); 95 | } 96 | } 97 | 98 | class CaptchaState extends State { 99 | InAppWebViewController webViewController; 100 | 101 | @override 102 | initState() { 103 | super.initState(); 104 | } 105 | 106 | @override 107 | Widget build(BuildContext context) { 108 | var htmlSource = buildPageContent( 109 | sitekey: widget.sitekey, 110 | lang: widget.lang, 111 | puzzleEndpoint: widget.puzzleEndpoint, 112 | theme: widget.theme, 113 | start: widget.start, 114 | ); 115 | 116 | return ConstrainedBox( 117 | constraints: BoxConstraints( 118 | maxHeight: 100, // Empirically determined to fit the widget.. to be improved 119 | ), 120 | child: Container( 121 | child: InAppWebView( 122 | initialData: InAppWebViewInitialData( 123 | data: htmlSource 124 | ), 125 | initialOptions: InAppWebViewGroupOptions( 126 | crossPlatform: InAppWebViewOptions( 127 | useShouldOverrideUrlLoading: true, 128 | disableContextMenu: true, 129 | clearCache: true, 130 | incognito: true, 131 | applicationNameForUserAgent: "FriendlyCaptchaFlutter" 132 | ), 133 | android: AndroidInAppWebViewOptions( 134 | useHybridComposition: true, 135 | ) 136 | ), 137 | onConsoleMessage: (controller, consoleMessage) { 138 | print(consoleMessage); // Useful for debugging, this prints (error) messages from the webview. 139 | }, 140 | shouldOverrideUrlLoading: (controller, navigationAction) async { 141 | // We deny any navigation away (which could be caused by the user clicking a link) 142 | return NavigationActionPolicy.CANCEL; 143 | }, 144 | onWebViewCreated: (InAppWebViewController w) { 145 | webViewController = w; 146 | w.addJavaScriptHandler(handlerName: 'solutionCallback', callback: (args) { 147 | widget.callback(args[0]["solution"]); 148 | }); 149 | }, 150 | ) 151 | ) 152 | ); 153 | } 154 | } 155 | ``` 156 | 157 | ### 3. Use the widget anywhere in your app 158 | 159 | The widget takes two required arguments: a `callback` that is called when the widget is completed, and your `sitekey`. 160 | 161 | Optionally you can also pass `lang`, `puzzleEndpoint`, `start` (defaults to `"auto"`), `theme` (`"dark"` is the only theme built-in). See the [`data-attributes` documentation](#/widget_api?id=data-start-attribute). 162 | 163 | ```dart 164 | // Creates a German widget 165 | FriendlyCaptcha( 166 | callback: mySolutionCallback, 167 | sitekey: "", 168 | lang: "de", 169 | ) 170 | ``` 171 | 172 | **Notes:** 173 | 174 | - Usually you would store the `solution` that gets passed to the callback in your app's state. 175 | - When the user performs the action you want to require the captcha for (e.g. user signup), you would send along this solution to your server. 176 | - In your backend server you then talk to our [verification API](#/verification_api) to check whether the captcha was valid. 177 | 178 | **Possible improvements (contributions welcome!):** 179 | 180 | - If the user has no network connection we currently don't display an error. 181 | - Widget reset functionality (a captcha solution can only be used once, currently you would recreate the widget entirely if your user can submit multiple times). 182 | - Publish the above code as a Dart package and embed the widget's Javascript code so that we don't make any request to a CDN. 183 | - Currently there is some space around the widget. You can edit the body's CSS to have it match your app's background color. 184 | In the future the embedded webpage communicate its size so that the embedded WebView's size can exactly match the widget. 185 | 186 | ## Example app 187 | 188 | We created an example app which you can view [here](https://github.com/FriendlyCaptcha/friendly-captcha-flutter-example). The relevant source file is [here](https://github.com/FriendlyCaptcha/friendly-captcha-flutter-example/blob/main/friendly_captcha_flutter_app/lib/main.dart). 189 | 190 | #### Example App screenshot 191 | 192 | ![Example App Screenshot](https://i.imgur.com/GJxlpZ6.png) 193 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | **There are three steps to adding Friendly Captcha to your website:** 4 | 5 | 1. Create an account on the [**Friendly Captcha website**](https://friendlycaptcha.com/signup) (it's free) and generate a `sitekey`. 6 | 2. Add the Friendly Captcha widget to your website 7 | 3. Change your server code to verify the CAPTCHA solutions 8 | 9 | Let's go! 10 | 11 | ## 1. Generating a sitekey 12 | 13 | Log in to your Friendly Captcha account and head to the [Applications page](https://app.friendlycaptcha.eu/dashboard). 14 | 15 | Click `Create New Application` and enter the necessary details. Once you have completed this, take note of the `sitekey` value under the application name, we will need it in the next step. 16 | 17 | A sitekey always starts with the characters `FC`. 18 | 19 | > If you don't have an account yet, you can create one [here](https://friendlycaptcha.com/signup). 20 | 21 | ## 2. Adding the widget 22 | 23 | ### Adding the widget script 24 | 25 | The **friendly-challenge** library contains the code for CAPTCHA widget. You have two options on how to add this to your website, either you can use a script tag to load the widget from any CDN that hosts NPM packages, or you can import the code into your own Javascript bundle. 26 | 27 | #### Option A: Using a script tag 28 | 29 | ```html 30 | 36 | 37 | ``` 38 | 39 | > Make sure to always import a specific version (e.g. `friendly-challenge@0.9.19`), then you can be sure that the script you import and integrate with your website doesn't change unexpectedly. 40 | 41 | It is recommended that you include the `async` and `defer` attributes like in the examples above, they make sure that the browser does not wait to load these scripts to show your website. The size of the scripts is 18KB (8.5KB compressed) for modern browsers, and 24KB (10KB compressed) for old browsers. 42 | 43 | > If you want to support old browsers, you can instead use a polyfill build, see the [**browser support**](browser_support?id=polyfills) page. 44 | 45 | ##### Download and self-host the widget library (releases, recommended by GDPR) 46 | 47 | Instead of using a CDN (e.g. for GDPR reasons) you can of course also download the library .js files and host them on your server. 48 | Simply download the latest release from one of the CDN's mentioned above, like: https://cdn.jsdelivr.net/npm/friendly-challenge/ 49 | 50 | - The module widget.module.min.js: https://cdn.jsdelivr.net/npm/friendly-challenge/widget.module.min.js 51 | - The nomodule widget.min.js: https://cdn.jsdelivr.net/npm/friendly-challenge/widget.min.js 52 | 53 | **Please remember to update to the latest release from time to time.** 54 | 55 | #### Option B: Import the library into your Javascript code 56 | 57 | Alternatively, you can install the **friendly-challenge** library using a package manager such as npm: 58 | 59 | ```bash 60 | npm install --save friendly-challenge 61 | ``` 62 | 63 | You can then import it into your app: 64 | 65 | ```javascript 66 | import "friendly-challenge/widget"; 67 | ``` 68 | 69 | > It is also possible to create and interact with the widget using the Javascript API. In this tutorial we will consider the simple case in which you want to secure a simple HTML form. If you are making a single page application (using e.g. React) you will probably want to use the API instead. See the [API documentation page](/widget_api). 70 | 71 | ### Adding the widget itself 72 | 73 | The friendly-challenge code you added won't do anything unless there is a special HTML element present that tells it where to create the widget. It will check for this widget once when it gets loaded, you can programmatically make it check for the element again. 74 | 75 | Where you want to add a Friendly Captcha widget, add 76 | 77 | ```html 78 |
79 | ``` 80 | 81 | Replace `` with the sitekey that you created in step 1. The widget will be rendered where you include this element, this should be inside the `
` you want to protect. 82 | 83 | A hidden input field with the CAPTCHA solution will be added automatically, this will be included in the form data sent to your server when the user submits the form. 84 | 85 | ## 3. Verifying the CAPTCHA solution on the server 86 | 87 | > The verification is almost the same as Google's ReCAPTCHA, so it should be easy to switch between the two (either direction). 88 | 89 | In the form data sent to the server, there will be an extra text field called `frc-captcha-solution`. We will send this string to the Friendly Captcha servers to verify that the CAPTCHA was completed successfully. 90 | 91 | ### Creating a verification request 92 | 93 | You will need an API key to prove it's you, you can create one on the [**API Keys page**](https://app.friendlycaptcha.eu/dashboard) in the dashboard. 94 | 95 | To verify the CAPTCHA solution, make a POST request to `https://api.friendlycaptcha.com/api/v1/siteverify` with the following parameters: 96 | 97 | | POST Parameter | Description | 98 | | -------------- | ----------------------------------------------------------------------------------- | 99 | | `solution` | The solution value that the user submitted in the `frc-captcha-solution` field | 100 | | `secret` | An API key that proves it's you, create one on the Friendly Captcha website | 101 | | `sitekey` | **Optional:** the sitekey that you want to make sure the puzzle was generated from. | 102 | 103 | You can pass these parameters in a JSON body, or as formdata. 104 | 105 | > If your account is on the **Advanced** or **Enterprise** plan your server can also make a request [to our EU endpoint](./eu_endpoint). 106 | 107 | ### The verification response 108 | 109 | The response will tell you whether the CAPTCHA solution is valid and hasn't been used before. The response body is a JSON object: 110 | 111 | ```JSON 112 | { 113 | "success": true|false, 114 | "errors": [...] // optional 115 | } 116 | ``` 117 | 118 | If `success` is false, `errors` will be a list containing at least one of the following error codes below. **If you are seeing status code 400 or 401 your server code is probably not configured correctly.** 119 | 120 | | Error code | Status | Description | 121 | | ------------------------------- | ------ | ----------------------------------------------------------------------------------------- | 122 | | `secret_missing` | 400 | You forgot to add the secret (=API key) parameter. | 123 | | `secret_invalid` | 401 | The API key you provided was invalid. | 124 | | `solution_missing` | 400 | You forgot to add the solution parameter. | 125 | | `bad_request` | 400 | Something else is wrong with your request, e.g. your request body is empty. | 126 | | `solution_invalid` | 200 | The solution you provided was invalid (perhaps the user tried to tamper with the puzzle). | 127 | | `solution_timeout_or_duplicate` | 200 | The puzzle that the solution was for has expired or has already been used. | 128 | 129 | > ⚠️ Status code 200 does not mean the solution was valid, it just means the verification was performed succesfully. Use the `success` field. 130 | 131 | A solution can be invalid for a number of reasons, perhaps the user submitted before the CAPTCHA was completed or they tried to change the puzzle to make it easier. The first case can be prevented by disabling the submit button until the CAPTCHA has been completed succesfully. 132 | 133 | ### Verification Best practices 134 | 135 | If you receive a response code other than 200 in production, you should probably accept the user's form despite not having been able to verify the CAPTCHA solution. 136 | 137 | Maybe your server is misconfigured or the Friendly Captcha servers are down. While we try to make sure that never happens, it is a good idea to assume one day disaster will strike. 138 | 139 | An example: you are using Friendly Captcha for a sign up form and you can't verify the solution, it is better to trust the user and let them sign up anyway, because otherwise no signup will be possible at all. Do send an alert to yourself! 140 | 141 | ### Checking your integration 142 | 143 | To check your integration works as it should, you can follow this [guide](https://support.friendlycaptcha.com/en/article/how-can-i-test-if-my-integration-is-working-15lbbb7/). 144 | -------------------------------------------------------------------------------- /docs/changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.9.19 4 | 5 | - Update dependencies (`core-js`), no functional changes. 6 | 7 | ## 0.9.18 8 | 9 | - Added Arabic (`"ar"`) localization (thank you @achaabni). 10 | 11 | ## 0.9.17 12 | 13 | - Fixed order of `start` and `ready` events 14 | - Fixed incorrect unit in CSS 15 | - Upgraded NPM dependencies to fix minor security issues 16 | 17 | ## 0.9.16 18 | 19 | - Added Korean (`"kr"`) localization (thank you @dimitriBouteille!). 20 | - Fixed the percentage text for the progress bar. 21 | - Added support for a `styleNonce` parameter when creating widgets programmatically, which is useful for specific CSP setups (thank you @rience!). 22 | 23 | ## 0.9.15 24 | 25 | - Added fallback support for supplying RFC 1766 language codes such as `en-GB` or `fr-FR` (thank you @julianwachholz!). 26 | - Fix for some Node-based server-side rendering setups (where `navigator` is undefined). 27 | 28 | ## 0.9.14 29 | 30 | - Small tweak to the French (`"fr"`) localization. 31 | - Added Hebrew (`"he"`) localization (thank you @tinytim84!). 32 | - Added Thai (`"th"`) localization (thank you @samlaukinoon!). 33 | 34 | ## 0.9.13 35 | 36 | - Improvement to the French (`"fr"`) localization. 37 | - Added feature for not including the hidden form field at all by passing `"-"` as custom field name. 38 | 39 | ## 0.9.12 40 | 41 | - No longer uses the title attribute for debug information during solving. Some screen readers would read this title as it updates. 42 | - Localization fix for Vietnamese (`"vi"`). 43 | - The widget now exposes a `loadLanguage` function that allows you to programmatically change the language of the widget. 44 | 45 | ## 0.9.11 46 | 47 | - Improvements to localizations, fix for Romanian (`"ro"`) localization (thank you @zcserei!). 48 | 49 | ## 0.9.10 50 | 51 | - Fix for false positive headless browser check in rare cases on Windows devices (`"Browser check failed, try a different browser"`). 52 | - Improved French (`"fr"`) localization (thank you @mikejpr!). 53 | 54 | ## 0.9.9 55 | 56 | - Fix for NextJS 13 production builds. 57 | - Added Chinese (Traditional) (`"zh_TW"`) localization (thank you @jhihyulin!). 58 | - Added Vietnamese (`"vi"`) localization (thank you @duy13!). 59 | 60 | ## 0.9.8 61 | 62 | - Fix for false positive headless errors in Chromium browsers when having certain plugins installed (`"Browser check failed, try a different browser"`). 63 | 64 | ## 0.9.7 65 | 66 | - When an error is thrown by fetch (e.g. because of connection errors), the error of the fetch request can now be accessed under `error.rawError` in the object passed in the `onErrorCallback`. 67 | 68 | ## 0.9.6 69 | 70 | - Added Chinese (Simplified) (`"zh"`) localization (thank you @shyn!). 71 | - Added `"nb"` as an alias for Norwegian language (`"no"`). 72 | - Improved accessibility by hiding visual-only SVG icons by adding `aria-hidden="true"`. 73 | - Errors are now logged with `console.error` instead of only appearing in the widget. 74 | 75 | ## 0.9.5 76 | 77 | - Added localizations `"el"`, `"uk"`, `"bg"`, `"cs"`, `"sk"`, `"no"`, `"fi"`, `"lt"`, `"lt"`, `"pl"`, `"et"`, `"hr"`, `"sr"`, `"sl"`, `"hu"`, and `"ro"` (Greek, Ukrainian, Bulgarian, Czech, Slovak, Norwegian, Finnish, Latvian, Lithuanian, Polish, Estonian, Croatian, Serbian, Slovenian, Hungarian, and Romanian), a big thank you to @Tubilopto! 78 | - Added `type: "module"` to `package.json` (see #117) to help fix some issues in Javascript build pipelines. This may require some reconfiguring of your build pipeline. 79 | - Build pipeline updates and upgrades (updated build dependencies, explicitly support IE11 in browser targets). 80 | 81 | ## 0.9.4 82 | 83 | - Fixed the retry button not working after expiration. 84 | - Added `skipStyleInjection` option to the config object. When true is passed the ` 21 | ``` 22 | 23 | The callback specified here should be defined in the global scope (i.e. on the `window` object). The callback will get called with one string argument: the `solution` that should be sent to the server as proof that the CAPTCHA was completed. You can use this to enable a submit button on a form when the CAPTCHA is complete. 24 | 25 | ### data-start attribute 26 | 27 | You can specify when the widget should start solving a puzzle, you can specify the `data-start` attribute with one of the following values: 28 | 29 | - `auto`: the solver will start as soon as possible. This is recommended if the user will definitely be submitting the CAPTCHA solution (e.g. there is only one form on the page), this has the best user experience. 30 | - `focus`: as soon as the form the widget is in fires the `focusin` event the solver starts, or when the user presses the start button in the widget. This is recommended for webpages where only few users will actually submit the form. **This is the default.** 31 | - `none`: the solver only starts when the user presses the button or it is programatically started by calling `start()`. 32 | 33 | Example: 34 | 35 | ```html 36 |
37 | ``` 38 | 39 | ### data-lang attribute 40 | 41 | FriendlyCaptcha ships with built-in translations, valid values for this attribute are `"en"`, `"fr"`, `"de"`, `"it"`, `"nl"`, `"pt"`, `"es"`, `"ca"`, `"da"`, `"ja"`, `"ru"`, `"sv"`, `"el"`, `"uk"`, `"bg"`, `"cs"`, `"sk"`, `"no"`, `"fi"`, `"lv"`, `"lt"`, `"pl"`, `"et"`, `"hr"`, `"sr"`, `"sl"`, `"hu"`, `"ro"`, `"zh"`, `"zh_TW"`, `"vi"`, `"he"`, `"th"`, `"kr"`, and `"ar"` for English, French, German, Italian, Dutch, Portuguese, Spanish, Catalan, Danish, Japanese, Russian, Swedish, Greek, Ukrainian, Bulgarian, Czech, Slovak, Norwegian, Finnish, Latvian, Lithuanian, Polish, Estonian, Croatian, Serbian, Slovenian, Hungarian, Romanian, Chinese (Simplified), Chinese (Traditional), Vietnamese, Hebrew, Thai, Korean, and Arabic respectively. 42 | 43 | > Are you a native speaker and want to add your language? 44 | > Please make an issue [here](https://github.com/FriendlyCaptcha/friendly-challenge/issues). 45 | > The translations we need are detailed [here](https://github.com/FriendlyCaptcha/friendly-challenge/blob/master/src/localization.ts), there's only a dozen values or so. 46 | 47 | Example: 48 | 49 | ```html 50 | 51 |
52 | ``` 53 | 54 | ### data-solution-field-name 55 | 56 | By default a hidden form field with name `frc-captcha-solution` is created. You can change the name of this field by setting this attribute, which can be useful for integrations with certain frameworks and content management systems. 57 | 58 | Example: 59 | 60 | ```html 61 |
62 | ``` 63 | 64 | You can completely omit this hidden form field by setting this value to `"-"`. 65 | 66 | ### data-puzzle-endpoint 67 | 68 | _Only relevant if you are using our [dedicated EU endpoint service](/#/eu_endpoint)_. 69 | By default the widget fetches puzzles from `https://api.friendlycaptcha.com/api/v1/puzzle`, which serves puzzles globally from over 200 data centers. As a premium service we offer an alternative endpoint that serves requests from datacenters in Germany only. 70 | 71 | Example: 72 | 73 | ```html 74 | 75 |
80 | ``` 81 | 82 | ## Javascript API 83 | 84 | For more advanced integrations you can use the **friendly-challenge** Javascript API. 85 | 86 | ### If you are using the widget script tag 87 | 88 | If you added widget script tag to your website, a global variable `friendlyChallenge` will be present on the window object. 89 | 90 | **Example** 91 | 92 | ```javascript 93 | // Creating a new widget programmatically 94 | const element = document.querySelector("#my-widget"); 95 | const myCustomWidget = new friendlyChallenge.WidgetInstance(element, { 96 | /* opts, more details in next section */ 97 | }); 98 | 99 | // Or to get the widget that was created on script load (null if no widget instance was created) 100 | const defaultWidget = friendlyChallenge.autoWidget; 101 | ``` 102 | 103 | ### If you are using the friendly-challenge library 104 | 105 | **Example** 106 | 107 | ```javascript 108 | import { WidgetInstance } from "friendly-challenge"; 109 | 110 | function doneCallback(solution) { 111 | console.log("CAPTCHA completed succesfully, solution:", solution); 112 | // ... Do something with the solution, maybe use it in a request 113 | } 114 | 115 | // This element should contain the `frc-captcha` class for correct styling 116 | const element = document.querySelector("#my-widget"); 117 | const options = { 118 | doneCallback: doneCallback, 119 | sitekey: "", 120 | }; 121 | const widget = new WidgetInstance(element, options); 122 | 123 | // this makes the widget fetch a puzzle and start solving it. 124 | widget.start(); 125 | ``` 126 | 127 | The options object takes the following fields, they are all optional: 128 | 129 | - **`startMode`**: string, default `"focus"`. Can be `"auto"`, `"focus"` or `"none"`. See documentation above (start mode) for the meaning of these. 130 | - **`sitekey`**: string. Your sitekey. 131 | - **`readyCallback`**: function, called when the solver is done initializing and is ready to start. 132 | - **`startedCallback`**: function, called when the solver has started. 133 | - **`doneCallback`**: function, called when the CAPTCHA has been completed. One argument will be passed: the solution string that should be sent to the server. 134 | - **`errorCallback`**: function, called when an internal error occurs. The error is passed as an object, the fields and values of this object are still to be documented may change in the future. Consider this experimental. In case of a fetch error (e.g. network connection loss or firewall block), the raw error of the fetch request can be retrieved under the `error.rawError` member variable. 135 | - **`language`**: string or object, the same values as the `data-lang` attribute can be provided, or a custom translation object for your language. See [here](https://github.com/FriendlyCaptcha/friendly-challenge/blob/master/src/localization.ts) for what this object should look like. 136 | - **`solutionFieldName`**: string, default `"frc-captcha-solution"`. The solution to the CAPTCHA will be put in a hidden form field with this name. If you set this value to `"-"`, no hidden form field is injected at all. 137 | - **`styleNonce`**: string, default `null`. If you're using Content Security Policy (CSP) headers, this allows you to load inline stylesheets, as inline style tags are blocked by CSP by default. 138 | 139 | - **`puzzleEndpoint`**: string, the URL the widget should retrieve its puzzle from. This defaults to Friendly Captcha's endpoint, you will only ever need to change this if you are creating your own puzzles or are using our dedicated EU endpoint service. 140 | - **`skipStyleInjection`**: boolean, if this is set to true the Friendly Captcha widget CSS will no longer be automatically injected into your webpage. You will be responsible for styling the element yourself. 141 | 142 | ### Resetting the widget 143 | 144 | If you are building a single page application (SPA), chances are the page will not refresh after the captcha is submitted. As a solved captcha can only be used once, you will have to reset the widget yourself (e.g. on submission). You can call the `reset()` function on the widget instance to achieve this. 145 | 146 | For example, if you are using the automatically created widget: 147 | 148 | ```javascript 149 | friendlyChallenge.autoWidget.reset(); 150 | ``` 151 | 152 | ### Destroying the widget 153 | 154 | To properly clean up the widget, you can use the `destroy()` function. It removes any DOM element and terminates any background workers. 155 | 156 | ### Full example in React (with React Hooks) 157 | 158 | _Contributed by @S-u-m-u-n, thank you!_ 159 | The following example presents a way to embed the Friendly Captcha widget in a React component: 160 | 161 | ```javascript 162 | import { useEffect, useRef } from "react"; 163 | import { WidgetInstance } from "friendly-challenge"; 164 | 165 | const FriendlyCaptcha = () => { 166 | const container = useRef(); 167 | const widget = useRef(); 168 | 169 | const doneCallback = (solution) => { 170 | console.log("Captcha was solved. The form can be submitted."); 171 | console.log(solution); 172 | }; 173 | 174 | const errorCallback = (err) => { 175 | console.log("There was an error when trying to solve the Captcha."); 176 | console.log(err); 177 | }; 178 | 179 | useEffect(() => { 180 | if (!widget.current && container.current) { 181 | widget.current = new WidgetInstance(container.current, { 182 | startMode: "auto", 183 | doneCallback: doneCallback, 184 | errorCallback: errorCallback, 185 | }); 186 | } 187 | 188 | return () => { 189 | if (widget.current != undefined) widget.current.reset(); 190 | }; 191 | }, [container]); 192 | 193 | return
; 194 | }; 195 | 196 | export default FriendlyCaptcha; 197 | ``` 198 | 199 | ### Full example in Vue (with Composition API) 200 | 201 | The following example presents a way to embed the Friendly Captcha widget in a Vue component: 202 | 203 | ```html 204 | 207 | 208 | 241 | ``` 242 | 243 | ### Full example in Svelte (with Typescript) 244 | 245 | The following example presents a way to embed the Friendly Captcha widget in a Svelte component: 246 | 247 | ```svelte 248 | 274 | 275 |
276 | ``` 277 | 278 | ### Full example in Angular 279 | 280 | ```html 281 |
287 | ``` 288 | 289 | ```typescript 290 | import { WidgetInstance } from 'friendly-challenge' 291 | 292 | ... 293 | 294 | @ViewChild('frccaptcha', { static: false }) 295 | friendlyCaptcha: ElementRef; 296 | 297 | ... 298 | ngAfterViewInit() { 299 | const widget = new WidgetInstance(this.friendlyCaptcha.nativeElement, { 300 | doneCallback: (a) => { 301 | this.myForm.get('captcha')?.patchValue(true) 302 | console.log('DONE: ', a); 303 | }, 304 | errorCallback: (b) => { 305 | console.log('FAILED', b); 306 | }, 307 | }) 308 | 309 | widget.start() 310 | } 311 | ``` 312 | 313 | ## Questions or issues 314 | 315 | If you have any questions about the API or run into problems, the best place to get help is the _issues_ page on the [github repository](https://github.com/FriendlyCaptcha/friendly-challenge/issues). 316 | -------------------------------------------------------------------------------- /src/polyfills.min.js: -------------------------------------------------------------------------------- 1 | /* Promise polyfill (promis@1.1.4) */ 2 | (function(){'use strict';var f,g=[];function l(a){g.push(a);1==g.length&&f()}function m(){for(;g.length;)g[0](),g.shift()}f=function(){setTimeout(m)};function n(a){this.a=p;this.b=void 0;this.f=[];var b=this;try{a(function(a){q(b,a)},function(a){r(b,a)})}catch(c){r(b,c)}}var p=2;function t(a){return new n(function(b,c){c(a)})}function u(a){return new n(function(b){b(a)})}function q(a,b){if(a.a==p){if(b==a)throw new TypeError;var c=!1;try{var d=b&&b.then;if(null!=b&&"object"==typeof b&&"function"==typeof d){d.call(b,function(b){c||q(a,b);c=!0},function(b){c||r(a,b);c=!0});return}}catch(e){c||r(a,e);return}a.a=0;a.b=b;v(a)}} 3 | function r(a,b){if(a.a==p){if(b==a)throw new TypeError;a.a=1;a.b=b;v(a)}}function v(a){l(function(){if(a.a!=p)for(;a.f.length;){var b=a.f.shift(),c=b[0],d=b[1],e=b[2],b=b[3];try{0==a.a?"function"==typeof c?e(c.call(void 0,a.b)):e(a.b):1==a.a&&("function"==typeof d?e(d.call(void 0,a.b)):b(a.b))}catch(h){b(h)}}})}n.prototype.g=function(a){return this.c(void 0,a)};n.prototype.c=function(a,b){var c=this;return new n(function(d,e){c.f.push([a,b,d,e]);v(c)})}; 4 | function w(a){return new n(function(b,c){function d(c){return function(d){h[c]=d;e+=1;e==a.length&&b(h)}}var e=0,h=[];0==a.length&&b(h);for(var k=0;kt[0]){return+1}else{return 0}});if(r._entries){r._entries={}}for(var e=0;e1?o(i[1]):"")}}})}})(typeof global!=="undefined"?global:typeof window!=="undefined"?window:typeof self!=="undefined"?self:this);(function(u){var e=function(){try{var e=new u.URL("b","http://a");e.pathname="c d";return e.href==="http://a/c%20d"&&e.searchParams}catch(e){return false}};var t=function(){var t=u.URL;var e=function(e,t){if(typeof e!=="string")e=String(e);var r=document,n;if(t&&(u.location===void 0||t!==u.location.href)){r=document.implementation.createHTMLDocument("");n=r.createElement("base");n.href=t;r.head.appendChild(n);try{if(n.href.indexOf(t)!==0)throw new Error(n.href)}catch(e){throw new Error("URL unable to set base "+t+" due to "+e)}}var i=r.createElement("a");i.href=e;if(n){r.body.appendChild(i);i.href=i.href}var o=r.createElement("input");o.type="url";o.value=e;if(i.protocol===":"||!/:/.test(i.href)||!o.checkValidity()&&!t){throw new TypeError("Invalid URL")}Object.defineProperty(this,"_anchorElement",{value:i});var a=new u.URLSearchParams(this.search);var s=true;var c=true;var f=this;["append","delete","set"].forEach(function(e){var t=a[e];a[e]=function(){t.apply(a,arguments);if(s){c=false;f.search=a.toString();c=true}}});Object.defineProperty(this,"searchParams",{value:a,enumerable:true});var h=void 0;Object.defineProperty(this,"_updateSearchParams",{enumerable:false,configurable:false,writable:false,value:function(){if(this.search!==h){h=this.search;if(c){s=false;this.searchParams._fromString(this.search);s=true}}}})};var r=e.prototype;var n=function(t){Object.defineProperty(r,t,{get:function(){return this._anchorElement[t]},set:function(e){this._anchorElement[t]=e},enumerable:true})};["hash","host","hostname","port","protocol"].forEach(function(e){n(e)});Object.defineProperty(r,"search",{get:function(){return this._anchorElement["search"]},set:function(e){this._anchorElement["search"]=e;this._updateSearchParams()},enumerable:true});Object.defineProperties(r,{toString:{get:function(){var e=this;return function(){return e.href}}},href:{get:function(){return this._anchorElement.href.replace(/\?$/,"")},set:function(e){this._anchorElement.href=e;this._updateSearchParams()},enumerable:true},pathname:{get:function(){return this._anchorElement.pathname.replace(/(^\/?)/,"/")},set:function(e){this._anchorElement.pathname=e},enumerable:true},origin:{get:function(){var e={"http:":80,"https:":443,"ftp:":21}[this._anchorElement.protocol];var t=this._anchorElement.port!=e&&this._anchorElement.port!=="";return this._anchorElement.protocol+"//"+this._anchorElement.hostname+(t?":"+this._anchorElement.port:"")},enumerable:true},password:{get:function(){return""},set:function(e){},enumerable:true},username:{get:function(){return""},set:function(e){},enumerable:true}});e.createObjectURL=function(e){return t.createObjectURL.apply(t,arguments)};e.revokeObjectURL=function(e){return t.revokeObjectURL.apply(t,arguments)};u.URL=e};if(!e()){t()}if(u.location!==void 0&&!("origin"in u.location)){var r=function(){return u.location.protocol+"//"+u.location.hostname+(u.location.port?":"+u.location.port:"")};try{Object.defineProperty(u.location,"origin",{get:r,enumerable:true})}catch(e){setInterval(function(){u.location.origin=r()},100)}}})(typeof global!=="undefined"?global:typeof window!=="undefined"?window:typeof self!=="undefined"?self:this); 7 | /* fetch polyfill (whatwg-fetch@3.1.0) */ 8 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e(t.WHATWGFetch={})}(this,function(t){"use strict";var e={searchParams:"URLSearchParams"in self,iterable:"Symbol"in self&&"iterator"in Symbol,blob:"FileReader"in self&&"Blob"in self&&function(){try{return new Blob,!0}catch(t){return!1}}(),formData:"FormData"in self,arrayBuffer:"ArrayBuffer"in self};if(e.arrayBuffer)var r=["[object Int8Array]","[object Uint8Array]","[object Uint8ClampedArray]","[object Int16Array]","[object Uint16Array]","[object Int32Array]","[object Uint32Array]","[object Float32Array]","[object Float64Array]"],o=ArrayBuffer.isView||function(t){return t&&r.indexOf(Object.prototype.toString.call(t))>-1};function n(t){if("string"!=typeof t&&(t=String(t)),/[^a-z0-9\-#$%&'*+.^_`|~!]/i.test(t)||""===t)throw new TypeError("Invalid character in header field name");return t.toLowerCase()}function i(t){return"string"!=typeof t&&(t=String(t)),t}function s(t){var r={next:function(){var e=t.shift();return{done:void 0===e,value:e}}};return e.iterable&&(r[Symbol.iterator]=function(){return r}),r}function a(t){this.map={},t instanceof a?t.forEach(function(t,e){this.append(e,t)},this):Array.isArray(t)?t.forEach(function(t){this.append(t[0],t[1])},this):t&&Object.getOwnPropertyNames(t).forEach(function(e){this.append(e,t[e])},this)}function f(t){if(t.bodyUsed)return Promise.reject(new TypeError("Already read"));t.bodyUsed=!0}function h(t){return new Promise(function(e,r){t.onload=function(){e(t.result)},t.onerror=function(){r(t.error)}})}function u(t){var e=new FileReader,r=h(e);return e.readAsArrayBuffer(t),r}function d(t){if(t.slice)return t.slice(0);var e=new Uint8Array(t.byteLength);return e.set(new Uint8Array(t)),e.buffer}function c(){return this.bodyUsed=!1,this._initBody=function(t){var r;this.bodyUsed=this.bodyUsed,this._bodyInit=t,t?"string"==typeof t?this._bodyText=t:e.blob&&Blob.prototype.isPrototypeOf(t)?this._bodyBlob=t:e.formData&&FormData.prototype.isPrototypeOf(t)?this._bodyFormData=t:e.searchParams&&URLSearchParams.prototype.isPrototypeOf(t)?this._bodyText=t.toString():e.arrayBuffer&&e.blob&&((r=t)&&DataView.prototype.isPrototypeOf(r))?(this._bodyArrayBuffer=d(t.buffer),this._bodyInit=new Blob([this._bodyArrayBuffer])):e.arrayBuffer&&(ArrayBuffer.prototype.isPrototypeOf(t)||o(t))?this._bodyArrayBuffer=d(t):this._bodyText=t=Object.prototype.toString.call(t):this._bodyText="",this.headers.get("content-type")||("string"==typeof t?this.headers.set("content-type","text/plain;charset=UTF-8"):this._bodyBlob&&this._bodyBlob.type?this.headers.set("content-type",this._bodyBlob.type):e.searchParams&&URLSearchParams.prototype.isPrototypeOf(t)&&this.headers.set("content-type","application/x-www-form-urlencoded;charset=UTF-8"))},e.blob&&(this.blob=function(){var t=f(this);if(t)return t;if(this._bodyBlob)return Promise.resolve(this._bodyBlob);if(this._bodyArrayBuffer)return Promise.resolve(new Blob([this._bodyArrayBuffer]));if(this._bodyFormData)throw new Error("could not read FormData body as blob");return Promise.resolve(new Blob([this._bodyText]))},this.arrayBuffer=function(){return this._bodyArrayBuffer?f(this)||Promise.resolve(this._bodyArrayBuffer):this.blob().then(u)}),this.text=function(){var t,e,r,o=f(this);if(o)return o;if(this._bodyBlob)return t=this._bodyBlob,e=new FileReader,r=h(e),e.readAsText(t),r;if(this._bodyArrayBuffer)return Promise.resolve(function(t){for(var e=new Uint8Array(t),r=new Array(e.length),o=0;o-1?o:r),this.mode=e.mode||this.mode||null,this.signal=e.signal||this.signal,this.referrer=null,("GET"===this.method||"HEAD"===this.method)&&n)throw new TypeError("Body not allowed for GET or HEAD requests");this._initBody(n)}function p(t){var e=new FormData;return t.trim().split("&").forEach(function(t){if(t){var r=t.split("="),o=r.shift().replace(/\+/g," "),n=r.join("=").replace(/\+/g," ");e.append(decodeURIComponent(o),decodeURIComponent(n))}}),e}function b(t,e){e||(e={}),this.type="default",this.status=void 0===e.status?200:e.status,this.ok=this.status>=200&&this.status<300,this.statusText="statusText"in e?e.statusText:"",this.headers=new a(e.headers),this.url=e.url||"",this._initBody(t)}l.prototype.clone=function(){return new l(this,{body:this._bodyInit})},c.call(l.prototype),c.call(b.prototype),b.prototype.clone=function(){return new b(this._bodyInit,{status:this.status,statusText:this.statusText,headers:new a(this.headers),url:this.url})},b.error=function(){var t=new b(null,{status:0,statusText:""});return t.type="error",t};var m=[301,302,303,307,308];b.redirect=function(t,e){if(-1===m.indexOf(e))throw new RangeError("Invalid status code");return new b(null,{status:e,headers:{location:t}})},t.DOMException=self.DOMException;try{new t.DOMException}catch(e){t.DOMException=function(t,e){this.message=t,this.name=e;var r=Error(t);this.stack=r.stack},t.DOMException.prototype=Object.create(Error.prototype),t.DOMException.prototype.constructor=t.DOMException}function w(r,o){return new Promise(function(n,i){var s=new l(r,o);if(s.signal&&s.signal.aborted)return i(new t.DOMException("Aborted","AbortError"));var f=new XMLHttpRequest;function h(){f.abort()}f.onload=function(){var t,e,r={status:f.status,statusText:f.statusText,headers:(t=f.getAllResponseHeaders()||"",e=new a,t.replace(/\r?\n[\t ]+/g," ").split(/\r?\n/).forEach(function(t){var r=t.split(":"),o=r.shift().trim();if(o){var n=r.join(":").trim();e.append(o,n)}}),e)};r.url="responseURL"in f?f.responseURL:r.headers.get("X-Request-URL");var o="response"in f?f.response:f.responseText;setTimeout(function(){n(new b(o,r))},0)},f.onerror=function(){setTimeout(function(){i(new TypeError("Network request failed"))},0)},f.ontimeout=function(){setTimeout(function(){i(new TypeError("Network request failed"))},0)},f.onabort=function(){setTimeout(function(){i(new t.DOMException("Aborted","AbortError"))},0)},f.open(s.method,function(t){try{return""===t&&self.location.href?self.location.href:t}catch(e){return t}}(s.url),!0),"include"===s.credentials?f.withCredentials=!0:"omit"===s.credentials&&(f.withCredentials=!1),"responseType"in f&&(e.blob?f.responseType="blob":e.arrayBuffer&&-1!==s.headers.get("Content-Type").indexOf("application/octet-stream")&&(f.responseType="arraybuffer")),s.headers.forEach(function(t,e){f.setRequestHeader(e,t)}),s.signal&&(s.signal.addEventListener("abort",h),f.onreadystatechange=function(){4===f.readyState&&s.signal.removeEventListener("abort",h)}),f.send(void 0===s._bodyInit?null:s._bodyInit)})}w.polyfill=!0,self.fetch||(self.fetch=w,self.Headers=a,self.Request=l,self.Response=b),t.Headers=a,t.Request=l,t.Response=b,t.fetch=w,Object.defineProperty(t,"__esModule",{value:!0})}); 9 | /* Object.assign() polyfill (object-assign-polyfill@0.1.0) */ 10 | "use strict";"function"!=typeof Object.assign&&(Object.assign=function(n){if(null==n)throw new TypeError("Cannot convert undefined or null to object");for(var r=Object(n),t=1;t