This commit is contained in:
Daniel Regeci
2024-08-09 19:05:28 -03:00
parent 9158c592c0
commit bddbad588f
17 changed files with 2099 additions and 1372 deletions

View File

@@ -2,14 +2,15 @@
ALTCHA uses a proof-of-work mechanism to protect your website, APIs, and online services from spam and abuse. Unlike other solutions, ALTCHA is self-hosted, does not use cookies nor fingerprinting, does not track users, and is fully compliant with GDPR.
https://altcha.org
Visit [ALTCHA](https://altcha.org) for more information.
## Benefits
## Features
- __Friction-less__ - Using PoW instead of visual puzzles.
- __Cookie-less__ - GDPR compliant by design.
- __Self-hosted__ - Without reliance on external providers in self-hosted mode.
- __SaaS available__ - Visit [altcha.org](https://altcha.org/docs/api) to get started with the SaaS API.
- **Frictionless CAPTCHA Alternative** - Employs proof-of-work (PoW) instead of visual puzzles.
- **Data Obfuscation** - Safeguards your email address from scraping.
- **Cookie-less** - Designed to be GDPR compliant by default.
- **Self-hosted** - Operates independently without relying on external providers.
- **SaaS Available** - Get started with the SaaS API at [altcha.org/docs/api](https://altcha.org/docs/api).
## Examples
@@ -105,8 +106,9 @@ Additional options:
- __hidelogo__ - Hide the ALTCHA logo.
- __maxnumber__ - The max. number to iterate to (defaults to 1,000,000).
- __name__ - The name of the hidden field containing the payload (defaults to "altcha").
- __obfuscated__ - The [obfuscated data](https://altcha.org/docs/obfuscation) provided as a base64-encoded string. Use only without `challengeurl`/`challengejson`.
- __spamfilter__ - Enable [Spam Filter](#spam-filter).
- __strings__ - JSON-encoded translation strings. Refer to [customization](/docs/widget-customization).
- __strings__ - JSON-encoded translation strings. Refer to [customization](https://altcha.org/docs/widget-customization).
- __refetchonexpire__ - Automatically re-fetch and re-validate when the challenge expires (defaults to true).
- __verifyurl__ - URL for server-side verification requests. This option is automatically configured when the `spamfilter` option is used. Override this setting only if you are using a custom server implementation.
- __workers__ - The number of workers to utilize for PoW (defaults to `navigator.hardwareConcurrency || 8`, max. value `16`).
@@ -163,6 +165,7 @@ export interface Configure {
maxnumber?: number;
mockerror?: boolean;
name?: string;
obfuscated?: string;
refetchonexpire?: boolean;
spamfilter?: boolean | 'ipAddress' | SpamFilter;
strings?: {

16
dist/altcha.d.ts vendored
View File

@@ -16,7 +16,7 @@ declare global {
interface AltchaServerVerificationEvent extends CustomEvent<Record<string, unknown>> {}
interface AltchaWidget {
interface AltchaWidgetOptions {
analytics?: boolean | string;
auto?: 'onfocus' | 'onload' | 'onsubmit';
beaconurl?: string;
@@ -31,9 +31,10 @@ declare global {
floatingoffset?: number;
hidefooter?: boolean;
hidelogo?: boolean;
name?: string;
maxnumber?: number;
mockerror?: boolean;
name?: string;
obfuscated?: string;
refetchonexpire?: boolean;
spamfilter?: boolean | 'ipAddress';
strings?: string;
@@ -43,6 +44,16 @@ declare global {
workerurl?: string;
}
interface AltchaWidgetMethods {
configure: (options: AltchaWidgetOptions) => void;
clarify: () => Promise<void>;
reset: (newState: AltchaState = 'unverified', err: string | null = null) => void;
verify: () => Promise<void>;
}
interface AltchaWidget extends AltchaWidgetOptions extends AltchaWidgetMethods {
}
declare namespace svelteHTML {
interface IntrinsicElements {
'altcha-widget': AltchaWidgetSvelte;
@@ -74,6 +85,7 @@ declare global {
}
interface AltchaWidgetReact extends AltchaWidget extends React.HTMLAttributes<HTMLElement> {
children?: React.ReactNode;
ref?: React.RefObject<HTMLElement>;
style?: AltchaWidgetCSSProperties;
}

1546
dist/altcha.js vendored

File diff suppressed because it is too large Load Diff

4
dist/altcha.umd.cjs vendored

File diff suppressed because one or more lines are too long

View File

@@ -16,7 +16,7 @@ declare global {
interface AltchaServerVerificationEvent extends CustomEvent<Record<string, unknown>> {}
interface AltchaWidget {
interface AltchaWidgetOptions {
analytics?: boolean | string;
auto?: 'onfocus' | 'onload' | 'onsubmit';
beaconurl?: string;
@@ -31,9 +31,10 @@ declare global {
floatingoffset?: number;
hidefooter?: boolean;
hidelogo?: boolean;
name?: string;
maxnumber?: number;
mockerror?: boolean;
name?: string;
obfuscated?: string;
refetchonexpire?: boolean;
spamfilter?: boolean | 'ipAddress';
strings?: string;
@@ -43,6 +44,16 @@ declare global {
workerurl?: string;
}
interface AltchaWidgetMethods {
configure: (options: AltchaWidgetOptions) => void;
clarify: () => Promise<void>;
reset: (newState: AltchaState = 'unverified', err: string | null = null) => void;
verify: () => Promise<void>;
}
interface AltchaWidget extends AltchaWidgetOptions extends AltchaWidgetMethods {
}
declare namespace svelteHTML {
interface IntrinsicElements {
'altcha-widget': AltchaWidgetSvelte;
@@ -74,6 +85,7 @@ declare global {
}
interface AltchaWidgetReact extends AltchaWidget extends React.HTMLAttributes<HTMLElement> {
children?: React.ReactNode;
ref?: React.RefObject<HTMLElement>;
style?: AltchaWidgetCSSProperties;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
(function(){"use strict";const f=new TextEncoder;function d(e){return[...new Uint8Array(e)].map(t=>t.toString(16).padStart(2,"0")).join("")}async function p(e,t,o){return d(await crypto.subtle.digest(o.toUpperCase(),f.encode(e+t)))}function g(e,t,o="SHA-256",l=1e6,c=0){const a=new AbortController,i=Date.now();return{promise:(async()=>{for(let n=c;n<=l;n+=1){if(a.signal.aborted)return null;if(await p(t,n,o)===e)return{number:n,took:Date.now()-i}}return null})(),controller:a}}let r;onmessage=async e=>{const{type:t,payload:o}=e.data;if(t==="abort")r==null||r.abort(),r=void 0;else if(t==="work"){const{alg:l,challenge:c,max:a,salt:i,start:u}=o||{},n=g(c,i,l,a,u);r=n.controller,n.promise.then(s=>{self.postMessage(s&&{...s,worker:!0})})}}})();
(function(){"use strict";const f=new TextEncoder;function p(e){return[...new Uint8Array(e)].map(t=>t.toString(16).padStart(2,"0")).join("")}async function w(e,t,r){return p(await crypto.subtle.digest(r.toUpperCase(),f.encode(e+t)))}function b(e,t,r="SHA-256",n=1e6,s=0){const o=new AbortController,a=Date.now();return{promise:(async()=>{for(let c=s;c<=n;c+=1){if(o.signal.aborted)return null;if(await w(t,c,r)===e)return{number:c,took:Date.now()-a}}return null})(),controller:o}}function h(e){const t=atob(e),r=new Uint8Array(t.length);for(let n=0;n<t.length;n++)r[n]=t.charCodeAt(n);return r}function g(e,t=12){const r=new Uint8Array(t);for(let n=0;n<t;n++)r[n]=e%256,e=Math.floor(e/256);return r}async function m(e,t="",r=1e6,n=0){const s="AES-GCM",o=new AbortController,a=Date.now(),l=async()=>{for(let u=n;u<=r;u+=1){if(o.signal.aborted||!c||!y)return null;try{const d=await crypto.subtle.decrypt({name:s,iv:g(u)},c,y);if(d)return{clearText:new TextDecoder().decode(d),took:Date.now()-a}}catch{}}return null};let c=null,y=null;try{y=h(e);const u=await crypto.subtle.digest("SHA-256",f.encode(t));c=await crypto.subtle.importKey("raw",u,s,!1,["decrypt"])}catch{return{promise:Promise.reject(),controller:o}}return{promise:l(),controller:o}}let i;onmessage=async e=>{const{type:t,payload:r,start:n,max:s}=e.data;let o=null;if(t==="abort")i==null||i.abort(),i=void 0;else if(t==="work"){if("obfuscated"in r){const{key:a,obfuscated:l}=r||{};o=await m(l,a,s,n)}else{const{algorithm:a,challenge:l,salt:c}=r||{};o=b(l,c,a,s,n)}i=o.controller,o.promise.then(a=>{self.postMessage(a&&{...a,worker:!0})})}}})();

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "altcha",
"version": "0.7.0",
"version": "0.8.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "altcha",
"version": "0.7.0",
"version": "0.8.0",
"license": "MIT",
"devDependencies": {
"@playwright/test": "^1.44.1",

View File

@@ -1,7 +1,7 @@
{
"name": "altcha",
"description": "GDPR compliant, self-hosted CAPTCHA alternative.",
"version": "0.7.0",
"version": "0.8.0",
"license": "MIT",
"author": {
"name": "Daniel Regeci",

64
scripts/obfuscate.ts Normal file
View File

@@ -0,0 +1,64 @@
/**
* Run this script with Bun:
*
* bun run scripts/obfuscate.ts "mailto:..."
*
* or with Node:
*
* npx tsx scripts/obfuscate.ts "mailto:..."
*/
const MAX_NUMBER = parseInt(process.env.MAX_NUMBER || '10000', 10);
const NUMBER = process.env.NUMBER ? parseInt(process.env.NUMBER || '0', 10) : undefined;
const KEY = process.env.KEY || '';
function randomInt(max: number) {
const ab = new Uint32Array(1);
crypto.getRandomValues(ab);
const randomNumber = ab[0] / (0xffffffff + 1);
return Math.floor(randomNumber * max + 1);
}
async function uInt8ArrayToBase64(ua: Uint8Array) {
return Buffer.from(ua).toString('base64');
}
function numberToUint8Array(num: number, len: number = 12) {
const ua = new Uint8Array(len);
for (let i = 0; i < len; i++) {
ua[i] = num % 256;
num = Math.floor(num / 256);
}
return ua;
}
async function obfuscateData(
raw: string,
key: string = '',
number: number = NUMBER || randomInt(MAX_NUMBER),
) {
const encoder = new TextEncoder();
const encodedData = encoder.encode(raw);
const algorithm = { name: 'AES-GCM', iv: numberToUint8Array(number) };
const keyHash = await crypto.subtle.digest(
'SHA-256',
encoder.encode(key)
);
const keyData = await crypto.subtle.importKey(
'raw',
keyHash,
algorithm,
false,
['encrypt']
);
const encryptedData = await crypto.subtle.encrypt(
algorithm,
keyData,
encodedData
);
return uInt8ArrayToBase64(new Uint8Array(encryptedData));
}
console.log(await obfuscateData(process.argv[process.argv.length - 1], KEY));
export {};

View File

@@ -7,7 +7,12 @@
<script lang="ts">
import { createEventDispatcher, onDestroy, onMount, tick } from 'svelte';
import { solveChallenge, createTestChallenge, getTimeZone } from './helpers';
import {
solveChallenge,
createTestChallenge,
getTimeZone,
clarifyData,
} from './helpers';
import { Session } from './session';
import { State } from './types';
import type {
@@ -17,11 +22,13 @@
Solution,
SpamFilter,
ServerVerificationPayload,
Obfuscated,
ClarifySolution,
} from './types';
export let analytics: boolean = false;
export let auto: 'onfocus' | 'onload' | 'onsubmit' | undefined = undefined;
export let beaconurl: string | undefined = undefined
export let beaconurl: string | undefined = undefined;
export let blockspam: boolean | undefined = undefined;
export let challengeurl: string | undefined = undefined;
export let challengejson: string | undefined = undefined;
@@ -43,6 +50,7 @@
export let name: string = 'altcha';
export let maxnumber: number = 1e6;
export let mockerror: boolean = false;
export let obfuscated: string | undefined = undefined;
export let refetchonexpire: boolean = true;
export let spamfilter: boolean | 'ipAddress' | SpamFilter = false;
export let strings: string | undefined = undefined;
@@ -58,10 +66,12 @@
const documentLocale = document.documentElement.lang?.split('-')?.[0];
let checked: boolean = false;
let clarifiedData: string | null = null;
let el: HTMLElement;
let elAnchorArrow: HTMLElement | null = null;
let elFloatingAnchor: HTMLElement | null = null;
let elForm: HTMLFormElement | null = null;
let elClarifyButton: HTMLElement | null = null;
let error: string | null = null;
let expireTimeout: ReturnType<typeof setTimeout> | null = null;
let payload: string | null = null;
@@ -87,9 +97,13 @@
waitAlert: 'Verifying... please wait.',
...parsedStrings,
};
$: dispatch('statechange', { payload, state });
$: dispatch(
'statechange',
clarifiedData ? { clarifiedData, state } : { payload, state }
);
$: onErrorChange(error);
$: onStateChange(state);
$: onClarifiedDataChange(clarifiedData);
onDestroy(() => {
if (elForm) {
@@ -98,6 +112,9 @@
elForm.removeEventListener('focusin', onFormFocusIn);
elForm = null;
}
if (elClarifyButton) {
el.removeEventListener('click', onClarifyClick);
}
if (session) {
session.destroy();
}
@@ -135,12 +152,22 @@
elForm.addEventListener('focusin', onFormFocusIn);
}
}
elClarifyButton =
el.parentElement?.querySelector('[data-clarify-button]') ||
(el.parentElement?.querySelector('button, a') as HTMLElement | null);
if (elClarifyButton) {
elClarifyButton.addEventListener('click', onClarifyClick);
}
if (analytics) {
enableAnalytics();
}
if (auto === 'onload') {
if (obfuscated) {
clarify();
} else {
verify();
}
}
if (isFreeSaaS && (hidefooter || hidelogo)) {
log(
'Attributes hidefooter and hidelogo ignored because usage with free API Keys require attribution.'
@@ -154,6 +181,13 @@
}
}
function onClarifyClick(ev: Event) {
ev.preventDefault();
if (state === State.UNVERIFIED) {
clarify();
}
}
function onFormFocusIn(ev: FocusEvent) {
if (state === State.UNVERIFIED) {
verify();
@@ -247,9 +281,11 @@
}
log('fetching challenge from', challengeurl);
const resp = await fetch(challengeurl, {
headers: !!spamfilter ? {
headers: !!spamfilter
? {
'x-altcha-spam-filter': '1',
} : {},
}
: {},
});
if (resp.status !== 200) {
throw new Error(`Server responded with ${resp.status}.`);
@@ -296,7 +332,7 @@
}
}
function enableAnalytics () {
function enableAnalytics() {
if (session) {
// already enabled
return;
@@ -325,28 +361,35 @@
}
}
async function run(
data: Challenge
): Promise<{ data: Challenge; solution: Solution | null }> {
let solution: Solution | null = null;
async function run(data: Challenge | Obfuscated): Promise<{
data: Challenge | Obfuscated;
solution: Solution | ClarifySolution | null;
}> {
let solution: Solution | ClarifySolution | null = null;
if ('Worker' in window) {
try {
solution = await runWorker(
data.challenge,
data.salt,
data.algorithm,
data.maxnumber
);
solution = await runWorker(data, data.maxnumber);
} catch (err) {
log(err);
}
if (solution?.number !== undefined) {
if (solution?.number !== undefined || 'obfuscated' in data) {
return {
data,
solution,
};
}
}
if ('obfuscated' in data) {
const solution = await clarifyData(
data.obfuscated,
data.key,
data.maxnumber
);
return {
data,
solution: await solution.promise,
};
}
return {
data,
solution: await solveChallenge(
@@ -359,9 +402,7 @@
}
async function runWorker(
challenge: string,
salt: string,
alg?: string,
challenge: Challenge | Obfuscated,
max: number = typeof test === 'number' ? test : maxnumber,
concurrency: number = Math.ceil(workers)
): Promise<Solution | null> {
@@ -386,13 +427,9 @@
resolve(message.data);
});
worker.postMessage({
payload: {
alg,
challenge,
payload: challenge,
max: start + step,
salt,
start,
},
type: 'work',
});
}) as Promise<Solution | null>;
@@ -408,6 +445,8 @@
if ([State.UNVERIFIED, State.ERROR, State.EXPIRED].includes(state)) {
if (spamfilter && elForm?.reportValidity() === false) {
checked = false;
} else if (obfuscated) {
clarify();
} else {
verify();
}
@@ -440,6 +479,28 @@
}
}
function onClarifiedDataChange(_: typeof clarifiedData) {
if (clarifiedData) {
const match = clarifiedData.match(/^(mailto|tel|sms|https?):/);
let el: HTMLAnchorElement | Text;
if (match) {
const [contact] = clarifiedData
.slice(clarifiedData.indexOf(':') + 1)
.replace(/^\/\//, '')
.split('?');
el = document.createElement('a');
el.href = clarifiedData;
el.innerHTML = contact;
} else {
el = document.createTextNode(clarifiedData);
}
if (elClarifyButton && el) {
elClarifyButton.after(el);
elClarifyButton.parentElement?.removeChild(elClarifyButton);
}
}
}
function onErrorChange(_: typeof error) {
if (session) {
session.trackError(error);
@@ -622,7 +683,9 @@
? document.querySelector(floatinganchor)
: elForm?.querySelector(
'input[type="submit"], button[type="submit"], button:not([type="button"]):not([type="reset"])'
)) || elForm;
)) ||
elForm ||
elClarifyButton;
}
if (elFloatingAnchor) {
// @ts-expect-error
@@ -673,12 +736,19 @@
analytics = options.analytics;
enableAnalytics();
}
if (options.obfuscated !== undefined) {
obfuscated = options.obfuscated;
}
if (options.auto !== undefined) {
auto = options.auto;
if (auto === 'onload') {
if (obfuscated) {
clarify();
} else {
verify();
}
}
}
if (options.beaconurl) {
beaconurl = options.beaconurl;
if (session) {
@@ -777,6 +847,7 @@
})
.then(({ data, solution }) => {
log('solution', solution);
if ('challenge' in data && solution && !('clearText' in solution)) {
if (solution?.number !== undefined) {
if (verifyurl) {
return requestServerVerification(
@@ -792,6 +863,7 @@
);
throw new Error('Unexpected result returned.');
}
}
})
.then(() => {
tick().then(() => {
@@ -808,8 +880,45 @@
error = err.message;
});
}
export async function clarify() {
if (!obfuscated) {
state = State.ERROR;
return;
}
reset(State.VERIFYING);
await new Promise((resolve) => setTimeout(resolve, delay || 0));
const [data, params] = obfuscated.split('?');
const parsedParams = new URLSearchParams(params || '');
let key = parsedParams.get('key') || undefined;
if (key) {
const match = key.match(/^\(prompt:?(.*)\)$/);
if (match) {
key = prompt(match[1] || 'Enter Key:') || undefined;
}
}
const { solution } = await run({
obfuscated: data,
key,
maxnumber,
});
if (solution && 'clearText' in solution) {
clarifiedData = solution.clearText;
state = State.VERIFIED;
checked = true;
if (floating && el) {
el.style.display = 'none';
}
} else {
state = State.ERROR;
checked = false;
error = 'Unable to decrypt data.';
}
}
</script>
<slot />
<div bind:this={el} class="altcha" data-state={state} data-floating={floating}>
<div class="altcha-main">
{#if state === State.VERIFYING}

View File

@@ -15,6 +15,7 @@
let mockerror: boolean = false;
let altcha: Altcha;
let altchaObfuscated: Altcha;
onMount(() => {
location.hash = '';
@@ -100,6 +101,20 @@
<button type="reset">Reset</button>
</div>
</form>
<div>
Email:
<Altcha
bind:this={altchaObfuscated}
obfuscated="14tZkC2tFAQSrksIcD3OTD0u4ZWE4VkePJ5d0oVyoGmABDyW9YvNTA=="
on:statechange={(ev) => console.log('Event: statechange:', ev.detail)}
delay={1500}
name="email"
floating
>
<a href="#">(click to reveal)</a>
</Altcha>
</div>
</main>
<style lang="scss">

16
src/declarations.d.ts vendored
View File

@@ -16,7 +16,7 @@ declare global {
interface AltchaServerVerificationEvent extends CustomEvent<Record<string, unknown>> {}
interface AltchaWidget {
interface AltchaWidgetOptions {
analytics?: boolean | string;
auto?: 'onfocus' | 'onload' | 'onsubmit';
beaconurl?: string;
@@ -31,9 +31,10 @@ declare global {
floatingoffset?: number;
hidefooter?: boolean;
hidelogo?: boolean;
name?: string;
maxnumber?: number;
mockerror?: boolean;
name?: string;
obfuscated?: string;
refetchonexpire?: boolean;
spamfilter?: boolean | 'ipAddress';
strings?: string;
@@ -43,6 +44,16 @@ declare global {
workerurl?: string;
}
interface AltchaWidgetMethods {
configure: (options: AltchaWidgetOptions) => void;
clarify: () => Promise<void>;
reset: (newState: AltchaState = 'unverified', err: string | null = null) => void;
verify: () => Promise<void>;
}
interface AltchaWidget extends AltchaWidgetOptions extends AltchaWidgetMethods {
}
declare namespace svelteHTML {
interface IntrinsicElements {
'altcha-widget': AltchaWidgetSvelte;
@@ -74,6 +85,7 @@ declare global {
}
interface AltchaWidgetReact extends AltchaWidget extends React.HTMLAttributes<HTMLElement> {
children?: React.ReactNode;
ref?: React.RefObject<HTMLElement>;
style?: AltchaWidgetCSSProperties;
}

View File

@@ -62,7 +62,7 @@ export function solveChallenge(
}
}
return null;
}
};
return {
promise: fn(),
controller,
@@ -78,3 +78,83 @@ export function getTimeZone() {
return undefined;
}
export function base64ToUint8Array(encoded: string) {
const str = atob(encoded);
const ua = new Uint8Array(str.length);
for (let i = 0; i < str.length; i++) {
ua[i] = str.charCodeAt(i);
}
return ua;
}
export function numberToUint8Array(num: number, len: number = 12) {
const ua = new Uint8Array(len);
for (let i = 0; i < len; i++) {
ua[i] = num % 256;
num = Math.floor(num / 256);
}
return ua;
}
export async function clarifyData(
encrypted: string,
key: string = '',
max: number = 1e6,
start: number = 0
) {
const algorithm = 'AES-GCM';
const controller = new AbortController();
const startTime = Date.now();
const fn = async () => {
for (let n = start; n <= max; n += 1) {
if (controller.signal.aborted || !cryptoKey || !encryptedData) {
return null;
}
try {
const decryptedData = await crypto.subtle.decrypt(
{
name: algorithm,
iv: numberToUint8Array(n),
},
cryptoKey,
encryptedData,
);
if (decryptedData) {
return {
clearText: new TextDecoder().decode(decryptedData),
took: Date.now() - startTime,
};
}
} catch {
// noop
}
}
return null;
};
let cryptoKey: CryptoKey | null = null;
let encryptedData: Uint8Array | null = null;
try {
encryptedData = base64ToUint8Array(encrypted);
const keyHash = await crypto.subtle.digest(
'SHA-256',
encoder.encode(key)
);
cryptoKey = await crypto.subtle.importKey(
'raw',
keyHash,
algorithm,
false,
['decrypt']
);
} catch {
return {
promise: Promise.reject(),
controller,
};
}
return {
promise: fn(),
controller,
};
}

View File

@@ -26,6 +26,7 @@ export interface Configure {
maxnumber?: number;
mockerror?: boolean;
name?: string;
obfuscated?: string;
refetchonexpire?: boolean;
spamfilter?: boolean | 'ipAddress' | SpamFilter;
strings?: Partial<Strings>;
@@ -86,6 +87,18 @@ export interface Payload {
took: number;
}
export interface Obfuscated {
obfuscated: string;
key?: string;
maxnumber?: number;
}
export interface ClarifySolution {
clearText: string;
took: number;
worker?: boolean;
}
export enum State {
ERROR = 'error',
VERIFIED = 'verified',

View File

@@ -1,15 +1,22 @@
import { solveChallenge } from './helpers';
import { solveChallenge, clarifyData } from './helpers';
let controller: AbortController | undefined = undefined;
onmessage = async (message) => {
const { type, payload } = message.data;
const { type, payload, start, max } = message.data;
let result: ReturnType<typeof solveChallenge> | Awaited<ReturnType<typeof clarifyData>> | null = null;
if (type === 'abort') {
controller?.abort();
controller = undefined;
} else if (type === 'work') {
const { alg, challenge, max, salt, start } = payload || {};
const result = solveChallenge(challenge, salt, alg, max, start);
if ('obfuscated' in payload) {
const { key, obfuscated } = payload || {};
result = await clarifyData(obfuscated, key, max, start);
} else {
const { algorithm, challenge, salt } = payload || {};
result = solveChallenge(challenge, salt, algorithm, max, start);
}
controller = result.controller;
result.promise.then((solution) => {
self.postMessage(solution ? { ...solution, worker: true } : solution);