altcha 3.0.0-beta.2 → 3.0.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,6 +14,42 @@ class BasePlugin {
14
14
  async onVerify(value) {
15
15
  }
16
16
  }
17
+ function getDigest(algorithm) {
18
+ switch (algorithm) {
19
+ case "PBKDF2/SHA-512":
20
+ return "SHA-512";
21
+ case "PBKDF2/SHA-384":
22
+ return "SHA-384";
23
+ case "PBKDF2/SHA-256":
24
+ default:
25
+ return "SHA-256";
26
+ }
27
+ }
28
+ async function deriveKey(parameters, salt, password) {
29
+ const { algorithm, cost, keyLength = 32 } = parameters;
30
+ const passwordKey = await crypto.subtle.importKey(
31
+ "raw",
32
+ password,
33
+ { name: "PBKDF2" },
34
+ false,
35
+ ["deriveKey"]
36
+ );
37
+ const derivedKey = await crypto.subtle.deriveKey(
38
+ {
39
+ name: "PBKDF2",
40
+ salt,
41
+ iterations: cost,
42
+ hash: getDigest(algorithm)
43
+ },
44
+ passwordKey,
45
+ { name: "AES-GCM", length: keyLength * 8 },
46
+ true,
47
+ ["encrypt"]
48
+ );
49
+ return {
50
+ derivedKey: new Uint8Array(await crypto.subtle.exportKey("raw", derivedKey))
51
+ };
52
+ }
17
53
  function bufferStartsWith(buffer, prefix) {
18
54
  if (prefix.length > buffer.length) {
19
55
  return false;
@@ -94,7 +130,7 @@ async function solveChallenge(options) {
94
130
  counterMode = "uint32",
95
131
  counterStart = 0,
96
132
  counterStep = 1,
97
- deriveKey,
133
+ deriveKey: deriveKey2,
98
134
  timeout = 9e4
99
135
  } = options;
100
136
  const { nonce, keyPrefix, salt } = challenge.parameters;
@@ -110,7 +146,7 @@ async function solveChallenge(options) {
110
146
  if (controller?.signal.aborted || timeout && counter % 10 === 0 && performance.now() - start > timeout) {
111
147
  return null;
112
148
  }
113
- const { derivedKey } = await deriveKey(
149
+ const { derivedKey } = await deriveKey2(
114
150
  challenge.parameters,
115
151
  saltBuf,
116
152
  password.setCounter(counter)
@@ -138,7 +174,8 @@ async function solveChallengeWorkers(options) {
138
174
  controller = new AbortController(),
139
175
  createWorker,
140
176
  onOutOfMemory = (c) => c > 1 ? Math.floor(c / 2) : 0,
141
- counterMode
177
+ counterMode,
178
+ timeout = 9e4
142
179
  } = options;
143
180
  const workersConcurrency = Math.min(16, Math.max(1, concurrency));
144
181
  const workersInstances = [];
@@ -179,6 +216,7 @@ async function solveChallengeWorkers(options) {
179
216
  counterMode,
180
217
  counterStart: i,
181
218
  counterStep: workersConcurrency,
219
+ timeout,
182
220
  type: "work"
183
221
  });
184
222
  });
@@ -211,7 +249,11 @@ async function solveChallengeWorkers(options) {
211
249
  return solution || null;
212
250
  }
213
251
  async function deobfuscate(obfuscatedData, options = {}) {
214
- const { concurrency = navigator.hardwareConcurrency, deriveKey: deriveKey2 } = options;
252
+ let {
253
+ concurrency = Math.max(1, Math.min(4, navigator.hardwareConcurrency)),
254
+ createWorker,
255
+ deriveKey: deriveKey$1 = deriveKey
256
+ } = options;
215
257
  let challenge = null;
216
258
  try {
217
259
  challenge = JSON.parse(atob(obfuscatedData));
@@ -223,21 +265,20 @@ async function deobfuscate(obfuscatedData, options = {}) {
223
265
  }
224
266
  const cipher = challenge.cipher;
225
267
  let solution = null;
226
- if (deriveKey2) {
227
- solution = await solveChallenge({
228
- challenge,
229
- deriveKey: deriveKey2
230
- });
231
- } else {
232
- const createWorker = globalThis.$altcha.algorithms.get(challenge.parameters.algorithm);
233
- if (!createWorker) {
234
- throw new Error(`Unsupported algorithm ${challenge.parameters.algorithm}.`);
235
- }
268
+ if (!createWorker && "$altcha" in globalThis) {
269
+ createWorker = globalThis.$altcha.algorithms.get(challenge.parameters.algorithm);
270
+ }
271
+ if (createWorker) {
236
272
  solution = await solveChallengeWorkers({
237
273
  challenge,
238
274
  concurrency,
239
275
  createWorker
240
276
  });
277
+ } else {
278
+ solution = await solveChallenge({
279
+ challenge,
280
+ deriveKey: deriveKey$1
281
+ });
241
282
  }
242
283
  if (!solution) {
243
284
  throw new Error("Unable to find solution.");
@@ -1 +1 @@
1
- class e{constructor(e){this.host=e}static register(e){"$altcha"in globalThis&&!globalThis.$altcha.plugins.has(e)&&globalThis.$altcha.plugins.add(e)}async onFetchChallenge(e){}async onRequestServerVerification(e,t){}async onVerify(e){}}function t(e,t){if(t.length>e.length)return!1;for(let r=0;r<t.length;r++)if(e[r]!==t[r])return!1;return!0}function r(e){return Array.from(new Uint8Array(e)).map(e=>e.toString(16).padStart(2,"0")).join("")}function n(e){if(e.length%2!=0)throw new Error(`Hex string must have an even length. Got: ${e}`);const t=new ArrayBuffer(e.length/2),r=new DataView(t);for(let t=0;t<e.length;t+=2){const n=e.substring(t,t+2),a=parseInt(n,16);r.setUint8(t/2,a)}return new Uint8Array(t)}async function a(e){await new Promise(t=>setTimeout(t,e))}function o(e){return Math.floor(10*(performance.now()-e))/10}var i=(e=>(e.CODE="code",e.ERROR="error",e.VERIFIED="verified",e.VERIFYING="verifying",e.UNVERIFIED="unverified",e.EXPIRED="expired",e))(i||{});class s{constructor(e,t="uint32"){this.nonce=e,this.mode=t,this.buffer=new Uint8Array(this.nonce.length+this.COUNTER_BYTES),this.buffer.set(this.nonce,0),this.dataView=new DataView(this.buffer.buffer)}COUNTER_BYTES=4;buffer;dataView;encoder=new TextEncoder;setCounter(e){return"string"===this.mode?function(e,t){const r=new Uint8Array(e.length+t.length);return r.set(e,0),r.set(t,e.length),r}(this.nonce,this.encoder.encode(e.toString())):(this.dataView.setUint32(this.nonce.length,e,!1),this.buffer)}}async function c(e){const{challenge:t,concurrency:r=navigator.hardwareConcurrency,controller:n=new AbortController,createWorker:a,onOutOfMemory:o=e=>e>1?Math.floor(e/2):0,counterMode:i}=e,s=Math.min(16,Math.max(1,r)),l=[],h=()=>{for(const e of l)e.terminate()};for(let e=0;e<s;e++)l.push(await a(t.parameters.algorithm));let u=null;try{u=await Promise.race(l.map((e,r)=>(n.signal.addEventListener("abort",()=>{e.postMessage({type:"abort"})}),new Promise((n,a)=>{e.addEventListener("error",e=>{a(e)}),e.addEventListener("message",t=>{if(t.data){for(const t of l)t!==e&&t.postMessage({type:"abort"});if(t.data.error)return a(new Error(t.data.error))}n(t.data)}),e.postMessage({challenge:t,counterMode:i,counterStart:r,counterStep:s,type:"work"})}))))}catch(r){if(r instanceof Error&&!!r?.message?.includes("Out of memory")&&o){h();const r=o(s);if(r)return c({...e,challenge:t,controller:n,concurrency:r,createWorker:a})}throw r}finally{h()}return n.signal.aborted?null:u||null}async function l(e,i={}){const{concurrency:l=navigator.hardwareConcurrency,deriveKey:h}=i;let u=null;try{u=JSON.parse(atob(e))}catch{throw new Error("Unable to parse obfuscated data.")}if(!u||"object"!=typeof u||!("parameters"in u)||!("cipher"in u))throw new Error("Invalid obfuscated data format.");const f=u.cipher;let g=null;if(h)g=await async function(e){const{challenge:i,controller:c,counterMode:l="uint32",counterStart:h=0,counterStep:u=1,deriveKey:f,timeout:g=9e4}=e,{nonce:d,keyPrefix:w,salt:m}=i.parameters,y=n(d),p=n(m),E=w.length%2==0?n(w):null,b=new s(y,l),T=performance.now();let v=h,S="",C=T;for(;;){if(c?.signal.aborted||g&&v%10==0&&performance.now()-T>g)return null;const{derivedKey:e}=await f(i.parameters,p,b.setCounter(v));if(v%10==0&&performance.now()-C>200&&(await a(0),C=performance.now()),E?t(e,E):r(e).startsWith(w)){S=r(e);break}v+=u}return{counter:v,derivedKey:S,time:o(T)}}({challenge:u,deriveKey:h});else{const e=globalThis.$altcha.algorithms.get(u.parameters.algorithm);if(!e)throw new Error(`Unsupported algorithm ${u.parameters.algorithm}.`);g=await c({challenge:u,concurrency:l,createWorker:e})}if(!g)throw new Error("Unable to find solution.");const d=await crypto.subtle.importKey("raw",n(g.derivedKey),{name:"AES-GCM"},!1,["decrypt"]),w=await crypto.subtle.decrypt({name:"AES-GCM",iv:n(f.iv)},d,n(f.data));return(new TextDecoder).decode(w)}e.register(class extends e{elTrigger=null;activate(){this.elTrigger=this.host.querySelector("button"),this.elTrigger&&(this.elTrigger.addEventListener("click",this.onTriggerClick.bind(this)),this.host.configure({floatingAnchor:this.elTrigger}))}destroy(){}async onVerify(e){const{minDuration:t=500}=e,r=performance.now(),n=this.host.getAttribute("data-obfuscated");if(n){this.host.reset(i.VERIFYING);try{const e=await l(n);await this.#e(Math.max(0,t-(performance.now()-r))),this.#t(e)}catch(e){this.host.setState(i.ERROR,String(e))}finally{this.host.setState(i.VERIFIED)}return null}}onTriggerClick(e){e.preventDefault(),this.host.show(),this.host.verify().then(()=>{this.host.hide()})}#t(e){let t;if(e.match(/^(mailto|tel|sms|https?):/)){const[r]=e.slice(e.indexOf(":")+1).replace(/^\/\//,"").split("?");t=document.createElement("a"),t.href=e,t.innerText=r}else t=document.createTextNode(e);this.elTrigger&&t&&(this.elTrigger.after(t),this.elTrigger.parentElement?.removeChild(this.elTrigger))}async#e(e){await new Promise(t=>setTimeout(t,e))}});
1
+ class e{constructor(e){this.host=e}static register(e){"$altcha"in globalThis&&!globalThis.$altcha.plugins.has(e)&&globalThis.$altcha.plugins.add(e)}async onFetchChallenge(e){}async onRequestServerVerification(e,t){}async onVerify(e){}}function t(e){switch(e){case"PBKDF2/SHA-512":return"SHA-512";case"PBKDF2/SHA-384":return"SHA-384";default:return"SHA-256"}}async function r(e,r,n){const{algorithm:a,cost:o,keyLength:i=32}=e,s=await crypto.subtle.importKey("raw",n,{name:"PBKDF2"},!1,["deriveKey"]),c=await crypto.subtle.deriveKey({name:"PBKDF2",salt:r,iterations:o,hash:t(a)},s,{name:"AES-GCM",length:8*i},!0,["encrypt"]);return{derivedKey:new Uint8Array(await crypto.subtle.exportKey("raw",c))}}function n(e,t){if(t.length>e.length)return!1;for(let r=0;r<t.length;r++)if(e[r]!==t[r])return!1;return!0}function a(e){return Array.from(new Uint8Array(e)).map(e=>e.toString(16).padStart(2,"0")).join("")}function o(e){if(e.length%2!=0)throw new Error(`Hex string must have an even length. Got: ${e}`);const t=new ArrayBuffer(e.length/2),r=new DataView(t);for(let t=0;t<e.length;t+=2){const n=e.substring(t,t+2),a=parseInt(n,16);r.setUint8(t/2,a)}return new Uint8Array(t)}async function i(e){await new Promise(t=>setTimeout(t,e))}function s(e){return Math.floor(10*(performance.now()-e))/10}var c=(e=>(e.CODE="code",e.ERROR="error",e.VERIFIED="verified",e.VERIFYING="verifying",e.UNVERIFIED="unverified",e.EXPIRED="expired",e))(c||{});class l{constructor(e,t="uint32"){this.nonce=e,this.mode=t,this.buffer=new Uint8Array(this.nonce.length+this.COUNTER_BYTES),this.buffer.set(this.nonce,0),this.dataView=new DataView(this.buffer.buffer)}COUNTER_BYTES=4;buffer;dataView;encoder=new TextEncoder;setCounter(e){return"string"===this.mode?function(e,t){const r=new Uint8Array(e.length+t.length);return r.set(e,0),r.set(t,e.length),r}(this.nonce,this.encoder.encode(e.toString())):(this.dataView.setUint32(this.nonce.length,e,!1),this.buffer)}}async function h(e){const{challenge:t,concurrency:r=navigator.hardwareConcurrency,controller:n=new AbortController,createWorker:a,onOutOfMemory:o=e=>e>1?Math.floor(e/2):0,counterMode:i,timeout:s=9e4}=e,c=Math.min(16,Math.max(1,r)),l=[],u=()=>{for(const e of l)e.terminate()};for(let e=0;e<c;e++)l.push(await a(t.parameters.algorithm));let f=null;try{f=await Promise.race(l.map((e,r)=>(n.signal.addEventListener("abort",()=>{e.postMessage({type:"abort"})}),new Promise((n,a)=>{e.addEventListener("error",e=>{a(e)}),e.addEventListener("message",t=>{if(t.data){for(const t of l)t!==e&&t.postMessage({type:"abort"});if(t.data.error)return a(new Error(t.data.error))}n(t.data)}),e.postMessage({challenge:t,counterMode:i,counterStart:r,counterStep:c,timeout:s,type:"work"})}))))}catch(r){if(r instanceof Error&&!!r?.message?.includes("Out of memory")&&o){u();const r=o(c);if(r)return h({...e,challenge:t,controller:n,concurrency:r,createWorker:a})}throw r}finally{u()}return n.signal.aborted?null:f||null}async function u(e,t={}){let{concurrency:c=Math.max(1,Math.min(4,navigator.hardwareConcurrency)),createWorker:u,deriveKey:f=r}=t,g=null;try{g=JSON.parse(atob(e))}catch{throw new Error("Unable to parse obfuscated data.")}if(!g||"object"!=typeof g||!("parameters"in g)||!("cipher"in g))throw new Error("Invalid obfuscated data format.");const d=g.cipher;let w=null;if(!u&&"$altcha"in globalThis&&(u=globalThis.$altcha.algorithms.get(g.parameters.algorithm)),w=u?await h({challenge:g,concurrency:c,createWorker:u}):await async function(e){const{challenge:t,controller:r,counterMode:c="uint32",counterStart:h=0,counterStep:u=1,deriveKey:f,timeout:g=9e4}=e,{nonce:d,keyPrefix:w,salt:y}=t.parameters,m=o(d),p=o(y),b=w.length%2==0?o(w):null,E=new l(m,c),v=performance.now();let T=h,S="",A=v;for(;;){if(r?.signal.aborted||g&&T%10==0&&performance.now()-v>g)return null;const{derivedKey:e}=await f(t.parameters,p,E.setCounter(T));if(T%10==0&&performance.now()-A>200&&(await i(0),A=performance.now()),b?n(e,b):a(e).startsWith(w)){S=a(e);break}T+=u}return{counter:T,derivedKey:S,time:s(v)}}({challenge:g,deriveKey:f}),!w)throw new Error("Unable to find solution.");const y=await crypto.subtle.importKey("raw",o(w.derivedKey),{name:"AES-GCM"},!1,["decrypt"]),m=await crypto.subtle.decrypt({name:"AES-GCM",iv:o(d.iv)},y,o(d.data));return(new TextDecoder).decode(m)}e.register(class extends e{elTrigger=null;activate(){this.elTrigger=this.host.querySelector("button"),this.elTrigger&&(this.elTrigger.addEventListener("click",this.onTriggerClick.bind(this)),this.host.configure({floatingAnchor:this.elTrigger}))}destroy(){}async onVerify(e){const{minDuration:t=500}=e,r=performance.now(),n=this.host.getAttribute("data-obfuscated");if(n){this.host.reset(c.VERIFYING);try{const e=await u(n);await this.#e(Math.max(0,t-(performance.now()-r))),this.#t(e)}catch(e){this.host.setState(c.ERROR,String(e))}finally{this.host.setState(c.VERIFIED)}return null}}onTriggerClick(e){e.preventDefault(),this.host.show(),this.host.verify().then(()=>{this.host.hide()})}#t(e){let t;if(e.match(/^(mailto|tel|sms|https?):/)){const[r]=e.slice(e.indexOf(":")+1).replace(/^\/\//,"").split("?");t=document.createElement("a"),t.href=e,t.innerText=r}else t=document.createTextNode(e);this.elTrigger&&t&&(this.elTrigger.after(t),this.elTrigger.parentElement?.removeChild(this.elTrigger))}async#e(e){await new Promise(t=>setTimeout(t,e))}});
@@ -18,6 +18,42 @@
18
18
  async onVerify(value) {
19
19
  }
20
20
  }
21
+ function getDigest(algorithm) {
22
+ switch (algorithm) {
23
+ case "PBKDF2/SHA-512":
24
+ return "SHA-512";
25
+ case "PBKDF2/SHA-384":
26
+ return "SHA-384";
27
+ case "PBKDF2/SHA-256":
28
+ default:
29
+ return "SHA-256";
30
+ }
31
+ }
32
+ async function deriveKey(parameters, salt, password) {
33
+ const { algorithm, cost, keyLength = 32 } = parameters;
34
+ const passwordKey = await crypto.subtle.importKey(
35
+ "raw",
36
+ password,
37
+ { name: "PBKDF2" },
38
+ false,
39
+ ["deriveKey"]
40
+ );
41
+ const derivedKey = await crypto.subtle.deriveKey(
42
+ {
43
+ name: "PBKDF2",
44
+ salt,
45
+ iterations: cost,
46
+ hash: getDigest(algorithm)
47
+ },
48
+ passwordKey,
49
+ { name: "AES-GCM", length: keyLength * 8 },
50
+ true,
51
+ ["encrypt"]
52
+ );
53
+ return {
54
+ derivedKey: new Uint8Array(await crypto.subtle.exportKey("raw", derivedKey))
55
+ };
56
+ }
21
57
  function bufferStartsWith(buffer, prefix) {
22
58
  if (prefix.length > buffer.length) {
23
59
  return false;
@@ -98,7 +134,7 @@
98
134
  counterMode = "uint32",
99
135
  counterStart = 0,
100
136
  counterStep = 1,
101
- deriveKey,
137
+ deriveKey: deriveKey2,
102
138
  timeout = 9e4
103
139
  } = options;
104
140
  const { nonce, keyPrefix, salt } = challenge.parameters;
@@ -114,7 +150,7 @@
114
150
  if (controller?.signal.aborted || timeout && counter % 10 === 0 && performance.now() - start > timeout) {
115
151
  return null;
116
152
  }
117
- const { derivedKey } = await deriveKey(
153
+ const { derivedKey } = await deriveKey2(
118
154
  challenge.parameters,
119
155
  saltBuf,
120
156
  password.setCounter(counter)
@@ -142,7 +178,8 @@
142
178
  controller = new AbortController(),
143
179
  createWorker,
144
180
  onOutOfMemory = (c) => c > 1 ? Math.floor(c / 2) : 0,
145
- counterMode
181
+ counterMode,
182
+ timeout = 9e4
146
183
  } = options;
147
184
  const workersConcurrency = Math.min(16, Math.max(1, concurrency));
148
185
  const workersInstances = [];
@@ -183,6 +220,7 @@
183
220
  counterMode,
184
221
  counterStart: i,
185
222
  counterStep: workersConcurrency,
223
+ timeout,
186
224
  type: "work"
187
225
  });
188
226
  });
@@ -215,7 +253,11 @@
215
253
  return solution || null;
216
254
  }
217
255
  async function deobfuscate(obfuscatedData, options = {}) {
218
- const { concurrency = navigator.hardwareConcurrency, deriveKey: deriveKey2 } = options;
256
+ let {
257
+ concurrency = Math.max(1, Math.min(4, navigator.hardwareConcurrency)),
258
+ createWorker,
259
+ deriveKey: deriveKey$1 = deriveKey
260
+ } = options;
219
261
  let challenge = null;
220
262
  try {
221
263
  challenge = JSON.parse(atob(obfuscatedData));
@@ -227,21 +269,20 @@
227
269
  }
228
270
  const cipher = challenge.cipher;
229
271
  let solution = null;
230
- if (deriveKey2) {
231
- solution = await solveChallenge({
232
- challenge,
233
- deriveKey: deriveKey2
234
- });
235
- } else {
236
- const createWorker = globalThis.$altcha.algorithms.get(challenge.parameters.algorithm);
237
- if (!createWorker) {
238
- throw new Error(`Unsupported algorithm ${challenge.parameters.algorithm}.`);
239
- }
272
+ if (!createWorker && "$altcha" in globalThis) {
273
+ createWorker = globalThis.$altcha.algorithms.get(challenge.parameters.algorithm);
274
+ }
275
+ if (createWorker) {
240
276
  solution = await solveChallengeWorkers({
241
277
  challenge,
242
278
  concurrency,
243
279
  createWorker
244
280
  });
281
+ } else {
282
+ solution = await solveChallenge({
283
+ challenge,
284
+ deriveKey: deriveKey$1
285
+ });
245
286
  }
246
287
  if (!solution) {
247
288
  throw new Error("Unable to find solution.");
@@ -1 +1 @@
1
- !function(e){"function"==typeof define&&define.amd?define(e):e()}(function(){"use strict";class e{constructor(e){this.host=e}static register(e){"$altcha"in globalThis&&!globalThis.$altcha.plugins.has(e)&&globalThis.$altcha.plugins.add(e)}async onFetchChallenge(e){}async onRequestServerVerification(e,t){}async onVerify(e){}}function t(e,t){if(t.length>e.length)return!1;for(let r=0;r<t.length;r++)if(e[r]!==t[r])return!1;return!0}function r(e){return Array.from(new Uint8Array(e)).map(e=>e.toString(16).padStart(2,"0")).join("")}function n(e){if(e.length%2!=0)throw new Error(`Hex string must have an even length. Got: ${e}`);const t=new ArrayBuffer(e.length/2),r=new DataView(t);for(let t=0;t<e.length;t+=2){const n=e.substring(t,t+2),a=parseInt(n,16);r.setUint8(t/2,a)}return new Uint8Array(t)}async function a(e){await new Promise(t=>setTimeout(t,e))}function o(e){return Math.floor(10*(performance.now()-e))/10}var i=(e=>(e.CODE="code",e.ERROR="error",e.VERIFIED="verified",e.VERIFYING="verifying",e.UNVERIFIED="unverified",e.EXPIRED="expired",e))(i||{});class s{constructor(e,t="uint32"){this.nonce=e,this.mode=t,this.buffer=new Uint8Array(this.nonce.length+this.COUNTER_BYTES),this.buffer.set(this.nonce,0),this.dataView=new DataView(this.buffer.buffer)}COUNTER_BYTES=4;buffer;dataView;encoder=new TextEncoder;setCounter(e){return"string"===this.mode?function(e,t){const r=new Uint8Array(e.length+t.length);return r.set(e,0),r.set(t,e.length),r}(this.nonce,this.encoder.encode(e.toString())):(this.dataView.setUint32(this.nonce.length,e,!1),this.buffer)}}async function c(e){const{challenge:t,concurrency:r=navigator.hardwareConcurrency,controller:n=new AbortController,createWorker:a,onOutOfMemory:o=e=>e>1?Math.floor(e/2):0,counterMode:i}=e,s=Math.min(16,Math.max(1,r)),l=[],h=()=>{for(const e of l)e.terminate()};for(let e=0;e<s;e++)l.push(await a(t.parameters.algorithm));let u=null;try{u=await Promise.race(l.map((e,r)=>(n.signal.addEventListener("abort",()=>{e.postMessage({type:"abort"})}),new Promise((n,a)=>{e.addEventListener("error",e=>{a(e)}),e.addEventListener("message",t=>{if(t.data){for(const t of l)t!==e&&t.postMessage({type:"abort"});if(t.data.error)return a(new Error(t.data.error))}n(t.data)}),e.postMessage({challenge:t,counterMode:i,counterStart:r,counterStep:s,type:"work"})}))))}catch(r){if(r instanceof Error&&!!r?.message?.includes("Out of memory")&&o){h();const r=o(s);if(r)return c({...e,challenge:t,controller:n,concurrency:r,createWorker:a})}throw r}finally{h()}return n.signal.aborted?null:u||null}async function l(e,i={}){const{concurrency:l=navigator.hardwareConcurrency,deriveKey:h}=i;let u=null;try{u=JSON.parse(atob(e))}catch{throw new Error("Unable to parse obfuscated data.")}if(!u||"object"!=typeof u||!("parameters"in u)||!("cipher"in u))throw new Error("Invalid obfuscated data format.");const f=u.cipher;let g=null;if(h)g=await async function(e){const{challenge:i,controller:c,counterMode:l="uint32",counterStart:h=0,counterStep:u=1,deriveKey:f,timeout:g=9e4}=e,{nonce:d,keyPrefix:w,salt:m}=i.parameters,y=n(d),p=n(m),E=w.length%2==0?n(w):null,b=new s(y,l),T=performance.now();let v=h,S="",C=T;for(;;){if(c?.signal.aborted||g&&v%10==0&&performance.now()-T>g)return null;const{derivedKey:e}=await f(i.parameters,p,b.setCounter(v));if(v%10==0&&performance.now()-C>200&&(await a(0),C=performance.now()),E?t(e,E):r(e).startsWith(w)){S=r(e);break}v+=u}return{counter:v,derivedKey:S,time:o(T)}}({challenge:u,deriveKey:h});else{const e=globalThis.$altcha.algorithms.get(u.parameters.algorithm);if(!e)throw new Error(`Unsupported algorithm ${u.parameters.algorithm}.`);g=await c({challenge:u,concurrency:l,createWorker:e})}if(!g)throw new Error("Unable to find solution.");const d=await crypto.subtle.importKey("raw",n(g.derivedKey),{name:"AES-GCM"},!1,["decrypt"]),w=await crypto.subtle.decrypt({name:"AES-GCM",iv:n(f.iv)},d,n(f.data));return(new TextDecoder).decode(w)}e.register(class extends e{elTrigger=null;activate(){this.elTrigger=this.host.querySelector("button"),this.elTrigger&&(this.elTrigger.addEventListener("click",this.onTriggerClick.bind(this)),this.host.configure({floatingAnchor:this.elTrigger}))}destroy(){}async onVerify(e){const{minDuration:t=500}=e,r=performance.now(),n=this.host.getAttribute("data-obfuscated");if(n){this.host.reset(i.VERIFYING);try{const e=await l(n);await this.#e(Math.max(0,t-(performance.now()-r))),this.#t(e)}catch(e){this.host.setState(i.ERROR,String(e))}finally{this.host.setState(i.VERIFIED)}return null}}onTriggerClick(e){e.preventDefault(),this.host.show(),this.host.verify().then(()=>{this.host.hide()})}#t(e){let t;if(e.match(/^(mailto|tel|sms|https?):/)){const[r]=e.slice(e.indexOf(":")+1).replace(/^\/\//,"").split("?");t=document.createElement("a"),t.href=e,t.innerText=r}else t=document.createTextNode(e);this.elTrigger&&t&&(this.elTrigger.after(t),this.elTrigger.parentElement?.removeChild(this.elTrigger))}async#e(e){await new Promise(t=>setTimeout(t,e))}})});
1
+ !function(e){"function"==typeof define&&define.amd?define(e):e()}(function(){"use strict";class e{constructor(e){this.host=e}static register(e){"$altcha"in globalThis&&!globalThis.$altcha.plugins.has(e)&&globalThis.$altcha.plugins.add(e)}async onFetchChallenge(e){}async onRequestServerVerification(e,t){}async onVerify(e){}}function t(e){switch(e){case"PBKDF2/SHA-512":return"SHA-512";case"PBKDF2/SHA-384":return"SHA-384";default:return"SHA-256"}}async function r(e,r,n){const{algorithm:a,cost:i,keyLength:o=32}=e,s=await crypto.subtle.importKey("raw",n,{name:"PBKDF2"},!1,["deriveKey"]),c=await crypto.subtle.deriveKey({name:"PBKDF2",salt:r,iterations:i,hash:t(a)},s,{name:"AES-GCM",length:8*o},!0,["encrypt"]);return{derivedKey:new Uint8Array(await crypto.subtle.exportKey("raw",c))}}function n(e,t){if(t.length>e.length)return!1;for(let r=0;r<t.length;r++)if(e[r]!==t[r])return!1;return!0}function a(e){return Array.from(new Uint8Array(e)).map(e=>e.toString(16).padStart(2,"0")).join("")}function i(e){if(e.length%2!=0)throw new Error(`Hex string must have an even length. Got: ${e}`);const t=new ArrayBuffer(e.length/2),r=new DataView(t);for(let t=0;t<e.length;t+=2){const n=e.substring(t,t+2),a=parseInt(n,16);r.setUint8(t/2,a)}return new Uint8Array(t)}async function o(e){await new Promise(t=>setTimeout(t,e))}function s(e){return Math.floor(10*(performance.now()-e))/10}var c=(e=>(e.CODE="code",e.ERROR="error",e.VERIFIED="verified",e.VERIFYING="verifying",e.UNVERIFIED="unverified",e.EXPIRED="expired",e))(c||{});class l{constructor(e,t="uint32"){this.nonce=e,this.mode=t,this.buffer=new Uint8Array(this.nonce.length+this.COUNTER_BYTES),this.buffer.set(this.nonce,0),this.dataView=new DataView(this.buffer.buffer)}COUNTER_BYTES=4;buffer;dataView;encoder=new TextEncoder;setCounter(e){return"string"===this.mode?function(e,t){const r=new Uint8Array(e.length+t.length);return r.set(e,0),r.set(t,e.length),r}(this.nonce,this.encoder.encode(e.toString())):(this.dataView.setUint32(this.nonce.length,e,!1),this.buffer)}}async function h(e){const{challenge:t,concurrency:r=navigator.hardwareConcurrency,controller:n=new AbortController,createWorker:a,onOutOfMemory:i=e=>e>1?Math.floor(e/2):0,counterMode:o,timeout:s=9e4}=e,c=Math.min(16,Math.max(1,r)),l=[],u=()=>{for(const e of l)e.terminate()};for(let e=0;e<c;e++)l.push(await a(t.parameters.algorithm));let f=null;try{f=await Promise.race(l.map((e,r)=>(n.signal.addEventListener("abort",()=>{e.postMessage({type:"abort"})}),new Promise((n,a)=>{e.addEventListener("error",e=>{a(e)}),e.addEventListener("message",t=>{if(t.data){for(const t of l)t!==e&&t.postMessage({type:"abort"});if(t.data.error)return a(new Error(t.data.error))}n(t.data)}),e.postMessage({challenge:t,counterMode:o,counterStart:r,counterStep:c,timeout:s,type:"work"})}))))}catch(r){if(r instanceof Error&&!!r?.message?.includes("Out of memory")&&i){u();const r=i(c);if(r)return h({...e,challenge:t,controller:n,concurrency:r,createWorker:a})}throw r}finally{u()}return n.signal.aborted?null:f||null}async function u(e,t={}){let{concurrency:c=Math.max(1,Math.min(4,navigator.hardwareConcurrency)),createWorker:u,deriveKey:f=r}=t,d=null;try{d=JSON.parse(atob(e))}catch{throw new Error("Unable to parse obfuscated data.")}if(!d||"object"!=typeof d||!("parameters"in d)||!("cipher"in d))throw new Error("Invalid obfuscated data format.");const g=d.cipher;let y=null;if(!u&&"$altcha"in globalThis&&(u=globalThis.$altcha.algorithms.get(d.parameters.algorithm)),y=u?await h({challenge:d,concurrency:c,createWorker:u}):await async function(e){const{challenge:t,controller:r,counterMode:c="uint32",counterStart:h=0,counterStep:u=1,deriveKey:f,timeout:d=9e4}=e,{nonce:g,keyPrefix:y,salt:w}=t.parameters,m=i(g),p=i(w),b=y.length%2==0?i(y):null,E=new l(m,c),v=performance.now();let T=h,S="",A=v;for(;;){if(r?.signal.aborted||d&&T%10==0&&performance.now()-v>d)return null;const{derivedKey:e}=await f(t.parameters,p,E.setCounter(T));if(T%10==0&&performance.now()-A>200&&(await o(0),A=performance.now()),b?n(e,b):a(e).startsWith(y)){S=a(e);break}T+=u}return{counter:T,derivedKey:S,time:s(v)}}({challenge:d,deriveKey:f}),!y)throw new Error("Unable to find solution.");const w=await crypto.subtle.importKey("raw",i(y.derivedKey),{name:"AES-GCM"},!1,["decrypt"]),m=await crypto.subtle.decrypt({name:"AES-GCM",iv:i(g.iv)},w,i(g.data));return(new TextDecoder).decode(m)}e.register(class extends e{elTrigger=null;activate(){this.elTrigger=this.host.querySelector("button"),this.elTrigger&&(this.elTrigger.addEventListener("click",this.onTriggerClick.bind(this)),this.host.configure({floatingAnchor:this.elTrigger}))}destroy(){}async onVerify(e){const{minDuration:t=500}=e,r=performance.now(),n=this.host.getAttribute("data-obfuscated");if(n){this.host.reset(c.VERIFYING);try{const e=await u(n);await this.#e(Math.max(0,t-(performance.now()-r))),this.#t(e)}catch(e){this.host.setState(c.ERROR,String(e))}finally{this.host.setState(c.VERIFIED)}return null}}onTriggerClick(e){e.preventDefault(),this.host.show(),this.host.verify().then(()=>{this.host.hide()})}#t(e){let t;if(e.match(/^(mailto|tel|sms|https?):/)){const[r]=e.slice(e.indexOf(":")+1).replace(/^\/\//,"").split("?");t=document.createElement("a"),t.href=e,t.innerText=r}else t=document.createTextNode(e);this.elTrigger&&t&&(this.elTrigger.after(t),this.elTrigger.parentElement?.removeChild(this.elTrigger))}async#e(e){await new Promise(t=>setTimeout(t,e))}})});
@@ -4,15 +4,18 @@ export {};
4
4
  export declare class AltchaWidgetElement extends HTMLElement {
5
5
  auto?: Configuration['auto'];
6
6
  challenge?: string;
7
+ configuration?: string;
7
8
  display?: Configuration['display'];
8
9
  language?: string;
9
10
  name?: string;
11
+ theme?: string;
10
12
  type?: Configuration['type'];
13
+ workers?: number;
11
14
  configure: (config: Partial<Configuration>) => Promise<void>;
12
15
  getConfiguration: () => Configuration;
13
16
  getState: () => State;
14
17
  hide: () => void;
15
- reset: (newState: State) => void;
18
+ reset: (newState?: State, err?: string | null) => void;
16
19
  setState: (newState: State, err?: string | null) => void;
17
20
  show: () => void;
18
21
  updateUI: () => void;
@@ -46,11 +46,6 @@ export interface Configuration {
46
46
  * Enables verbose logging in the browser console for debugging.
47
47
  */
48
48
  debug: boolean;
49
- /**
50
- * The minimum verification time in milliseconds (adds an artificial delay if the PoW is faster).
51
- * @default 500
52
- */
53
- minDuration: number;
54
49
  /**
55
50
  * Prevents the code-challenge modal from stealing focus when opened.
56
51
  */
@@ -89,10 +84,19 @@ export interface Configuration {
89
84
  * Hides the ALTCHA logo icon.
90
85
  */
91
86
  hideLogo: boolean;
87
+ /**
88
+ * Enables the collection of pointer and scroll events for ALTCHA HIS mechanism (defaults to true).
89
+ */
90
+ humanInteractionSignature: boolean;
92
91
  /**
93
92
  * The ISO alpha-2 language code for localization (requires corresponding i18n file).
94
93
  */
95
94
  language: string;
95
+ /**
96
+ * The minimum verification time in milliseconds (adds an artificial delay if the PoW is faster).
97
+ * @default 500
98
+ */
99
+ minDuration: number;
96
100
  /**
97
101
  * Forces the widget into a failed state with a mock error for UI testing.
98
102
  */
@@ -106,6 +110,10 @@ export interface Configuration {
106
110
  * CSS selector for an element to be mirrored inside the overlay modal.
107
111
  */
108
112
  overlayContent: string | null;
113
+ /**
114
+ * Configures the placement (top / bottom) for popovers. Defaults to 'auto'.
115
+ */
116
+ popoverPlacement: 'auto' | 'bottom' | 'top';
109
117
  /**
110
118
  * Automatically attempts to restart verification with fewer workers if the browser runs out of memory (Argon2 and Scrypt only).
111
119
  */
@@ -126,6 +134,10 @@ export interface Configuration {
126
134
  * Mocks a successful verification. Useful for testing environments without network access.
127
135
  */
128
136
  test: boolean;
137
+ /**
138
+ * PoW verification timeout in milliseconds. Defaults to 90_000 ms.
139
+ */
140
+ timeout: number;
129
141
  /**
130
142
  * The visual style of the interaction element.
131
143
  */
@@ -384,7 +396,7 @@ export interface WidgetMethods {
384
396
  getConfiguration: () => Configuration;
385
397
  getState: () => State;
386
398
  hide: () => void;
387
- reset: (newState: State) => void;
399
+ reset: (newState?: State, err?: string | null) => void;
388
400
  setState: (newState: State, err?: string | null) => void;
389
401
  show: () => void;
390
402
  updateUI: () => void;
@@ -112,7 +112,7 @@
112
112
  const { deriveKey: deriveKey2 } = options;
113
113
  let controller = void 0;
114
114
  self.onmessage = async (message) => {
115
- const { challenge, counterMode, counterStart, counterStep, type } = message.data;
115
+ const { challenge, counterMode, counterStart, counterStep, timeout, type } = message.data;
116
116
  if (type === "abort") {
117
117
  controller?.abort();
118
118
  } else if (type === "work") {
@@ -125,7 +125,8 @@
125
125
  counterStart,
126
126
  counterStep,
127
127
  deriveKey: deriveKey2,
128
- counterMode
128
+ counterMode,
129
+ timeout
129
130
  });
130
131
  } catch (err) {
131
132
  return self.postMessage({ error: err });
@@ -112,7 +112,7 @@
112
112
  const { deriveKey: deriveKey2 } = options;
113
113
  let controller = void 0;
114
114
  self.onmessage = async (message) => {
115
- const { challenge, counterMode, counterStart, counterStep, type } = message.data;
115
+ const { challenge, counterMode, counterStart, counterStep, timeout, type } = message.data;
116
116
  if (type === "abort") {
117
117
  controller?.abort();
118
118
  } else if (type === "work") {
@@ -125,7 +125,8 @@
125
125
  counterStart,
126
126
  counterStep,
127
127
  deriveKey: deriveKey2,
128
- counterMode
128
+ counterMode,
129
+ timeout
129
130
  });
130
131
  } catch (err) {
131
132
  return self.postMessage({ error: err });
@@ -112,7 +112,7 @@
112
112
  const { deriveKey: deriveKey2 } = options;
113
113
  let controller = void 0;
114
114
  self.onmessage = async (message) => {
115
- const { challenge, counterMode, counterStart, counterStep, type } = message.data;
115
+ const { challenge, counterMode, counterStart, counterStep, timeout, type } = message.data;
116
116
  if (type === "abort") {
117
117
  controller?.abort();
118
118
  } else if (type === "work") {
@@ -125,7 +125,8 @@
125
125
  counterStart,
126
126
  counterStep,
127
127
  deriveKey: deriveKey2,
128
- counterMode
128
+ counterMode,
129
+ timeout
129
130
  });
130
131
  } catch (err) {
131
132
  return self.postMessage({ error: err });
@@ -112,7 +112,7 @@
112
112
  const { deriveKey: deriveKey2 } = options;
113
113
  let controller = void 0;
114
114
  self.onmessage = async (message) => {
115
- const { challenge, counterMode, counterStart, counterStep, type } = message.data;
115
+ const { challenge, counterMode, counterStart, counterStep, timeout, type } = message.data;
116
116
  if (type === "abort") {
117
117
  controller?.abort();
118
118
  } else if (type === "work") {
@@ -125,7 +125,8 @@
125
125
  counterStart,
126
126
  counterStep,
127
127
  deriveKey: deriveKey2,
128
- counterMode
128
+ counterMode,
129
+ timeout
129
130
  });
130
131
  } catch (err) {
131
132
  return self.postMessage({ error: err });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "altcha",
3
3
  "description": "Privacy-first CAPTCHA widget, compliant with global regulations (GDPR/HIPAA/CCPA/LGDP/DPDPA/PIPL) and WCAG accessible. No tracking, self-verifying.",
4
- "version": "3.0.0-beta.2",
4
+ "version": "3.0.0-beta.3",
5
5
  "license": "MIT",
6
6
  "author": {
7
7
  "name": "Daniel Regeci",
@@ -50,7 +50,7 @@
50
50
  "require": "./dist/external/altcha.css"
51
51
  },
52
52
  "./external": {
53
- "types": "./dist/external/index.d.ts",
53
+ "types": "./dist/types/generic.d.ts",
54
54
  "import": "./dist/external/altcha.js",
55
55
  "require": "./dist/external/altcha.umd.cjs"
56
56
  },
@@ -1 +0,0 @@
1
- declare module 'altcha/external'; export {};