altcha 3.0.0-beta.2 → 3.0.0-beta.4

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.
@@ -2,6 +2,114 @@
2
2
  typeof define === "function" && define.amd ? define(factory) : factory();
3
3
  })((function() {
4
4
  "use strict";
5
+ const noop = () => {
6
+ };
7
+ function safe_not_equal(a, b) {
8
+ return a != a ? b == b : a !== b || a !== null && typeof a === "object" || typeof a === "function";
9
+ }
10
+ function subscribe_to_store(store2, run, invalidate) {
11
+ if (store2 == null) {
12
+ run(void 0);
13
+ return noop;
14
+ }
15
+ const unsub = untrack(
16
+ () => store2.subscribe(
17
+ run,
18
+ // @ts-expect-error
19
+ invalidate
20
+ )
21
+ );
22
+ return unsub.unsubscribe ? () => unsub.unsubscribe() : unsub;
23
+ }
24
+ const subscriber_queue = [];
25
+ function writable(value, start = noop) {
26
+ let stop = null;
27
+ const subscribers = /* @__PURE__ */ new Set();
28
+ function set(new_value) {
29
+ if (safe_not_equal(value, new_value)) {
30
+ value = new_value;
31
+ if (stop) {
32
+ const run_queue = !subscriber_queue.length;
33
+ for (const subscriber of subscribers) {
34
+ subscriber[1]();
35
+ subscriber_queue.push(subscriber, value);
36
+ }
37
+ if (run_queue) {
38
+ for (let i = 0; i < subscriber_queue.length; i += 2) {
39
+ subscriber_queue[i][0](subscriber_queue[i + 1]);
40
+ }
41
+ subscriber_queue.length = 0;
42
+ }
43
+ }
44
+ }
45
+ }
46
+ function update(fn) {
47
+ set(fn(
48
+ /** @type {T} */
49
+ value
50
+ ));
51
+ }
52
+ function subscribe(run, invalidate = noop) {
53
+ const subscriber = [run, invalidate];
54
+ subscribers.add(subscriber);
55
+ if (subscribers.size === 1) {
56
+ stop = start(set, update) || noop;
57
+ }
58
+ run(
59
+ /** @type {T} */
60
+ value
61
+ );
62
+ return () => {
63
+ subscribers.delete(subscriber);
64
+ if (subscribers.size === 0 && stop) {
65
+ stop();
66
+ stop = null;
67
+ }
68
+ };
69
+ }
70
+ return { set, update, subscribe };
71
+ }
72
+ function get(store2) {
73
+ let value;
74
+ subscribe_to_store(store2, (_) => value = _)();
75
+ return value;
76
+ }
77
+ let untracking = false;
78
+ function untrack(fn) {
79
+ var previous_untracking = untracking;
80
+ try {
81
+ untracking = true;
82
+ return fn();
83
+ } finally {
84
+ untracking = previous_untracking;
85
+ }
86
+ }
87
+ function store(defaultValue) {
88
+ const scope = {
89
+ get: (name) => {
90
+ return get(scope.store)[name];
91
+ },
92
+ set: (name, value) => {
93
+ if (typeof name === "string") {
94
+ Object.assign(get(scope.store), {
95
+ [name]: value
96
+ });
97
+ } else {
98
+ Object.assign(get(scope.store), name);
99
+ }
100
+ scope.store.set(get(scope.store));
101
+ },
102
+ store: writable(defaultValue)
103
+ };
104
+ return scope;
105
+ }
106
+ globalThis.$altcha = globalThis.$altcha || {
107
+ algorithms: /* @__PURE__ */ new Map(),
108
+ defaults: store({}),
109
+ i18n: store({}),
110
+ instances: /* @__PURE__ */ new Set(),
111
+ plugins: /* @__PURE__ */ new Set()
112
+ };
5
113
  class BasePlugin {
6
114
  constructor(host) {
7
115
  this.host = host;
@@ -18,6 +126,42 @@
18
126
  async onVerify(value) {
19
127
  }
20
128
  }
129
+ function getDigest(algorithm) {
130
+ switch (algorithm) {
131
+ case "PBKDF2/SHA-512":
132
+ return "SHA-512";
133
+ case "PBKDF2/SHA-384":
134
+ return "SHA-384";
135
+ case "PBKDF2/SHA-256":
136
+ default:
137
+ return "SHA-256";
138
+ }
139
+ }
140
+ async function deriveKey(parameters, salt, password) {
141
+ const { algorithm, cost, keyLength = 32 } = parameters;
142
+ const passwordKey = await crypto.subtle.importKey(
143
+ "raw",
144
+ password,
145
+ { name: "PBKDF2" },
146
+ false,
147
+ ["deriveKey"]
148
+ );
149
+ const derivedKey = await crypto.subtle.deriveKey(
150
+ {
151
+ name: "PBKDF2",
152
+ salt,
153
+ iterations: cost,
154
+ hash: getDigest(algorithm)
155
+ },
156
+ passwordKey,
157
+ { name: "AES-GCM", length: keyLength * 8 },
158
+ true,
159
+ ["encrypt"]
160
+ );
161
+ return {
162
+ derivedKey: new Uint8Array(await crypto.subtle.exportKey("raw", derivedKey))
163
+ };
164
+ }
21
165
  function bufferStartsWith(buffer, prefix) {
22
166
  if (prefix.length > buffer.length) {
23
167
  return false;
@@ -54,9 +198,45 @@
54
198
  async function delay(ms) {
55
199
  await new Promise((resolve) => setTimeout(resolve, ms));
56
200
  }
201
+ async function hmac(algorithm, data, keyStr) {
202
+ const key = await crypto.subtle.importKey(
203
+ "raw",
204
+ new TextEncoder().encode(keyStr),
205
+ {
206
+ name: "HMAC",
207
+ hash: { name: algorithm }
208
+ },
209
+ false,
210
+ ["sign", "verify"]
211
+ );
212
+ const signature = await crypto.subtle.sign(
213
+ "HMAC",
214
+ key,
215
+ typeof data === "string" ? new TextEncoder().encode(data) : data
216
+ );
217
+ return new Uint8Array(signature);
218
+ }
219
+ function sortKeys(obj) {
220
+ if (typeof obj !== "object" || obj === null || Array.isArray(obj)) {
221
+ return obj;
222
+ }
223
+ return Object.keys(obj).sort().reduce((acc, key) => {
224
+ const value = obj[key];
225
+ if (value !== void 0) {
226
+ acc[key] = sortKeys(value);
227
+ }
228
+ return acc;
229
+ }, {});
230
+ }
57
231
  function timeDuration(start) {
58
232
  return Math.floor((performance.now() - start) * 10) / 10;
59
233
  }
234
+ var HmacAlgorithm = /* @__PURE__ */ ((HmacAlgorithm2) => {
235
+ HmacAlgorithm2["SHA_256"] = "SHA-256";
236
+ HmacAlgorithm2["SHA_384"] = "SHA-384";
237
+ HmacAlgorithm2["SHA_512"] = "SHA-512";
238
+ return HmacAlgorithm2;
239
+ })(HmacAlgorithm || {});
60
240
  var State = /* @__PURE__ */ ((State2) => {
61
241
  State2["CODE"] = "code";
62
242
  State2["ERROR"] = "error";
@@ -91,6 +271,62 @@
91
271
  return this.buffer;
92
272
  }
93
273
  }
274
+ async function createChallenge(options) {
275
+ const {
276
+ algorithm,
277
+ counter,
278
+ counterMode = "uint32",
279
+ cost,
280
+ deriveKey: deriveKey2,
281
+ data,
282
+ expiresAt,
283
+ hmacAlgorithm = HmacAlgorithm.SHA_256,
284
+ hmacKeySignatureSecret,
285
+ hmacSignatureSecret,
286
+ keyLength = 32,
287
+ keyPrefix = "00",
288
+ keyPrefixLength = keyLength / 2,
289
+ memoryCost,
290
+ parallelism
291
+ } = options;
292
+ const parameters = {
293
+ algorithm,
294
+ nonce: bufferToHex(crypto.getRandomValues(new Uint8Array(16))),
295
+ salt: bufferToHex(crypto.getRandomValues(new Uint8Array(16))),
296
+ cost,
297
+ keyLength,
298
+ memoryCost,
299
+ parallelism,
300
+ keyPrefix,
301
+ expiresAt: expiresAt instanceof Date ? Math.floor(expiresAt.getTime() / 1e3) : expiresAt,
302
+ data
303
+ };
304
+ let deriveKeyResult = null;
305
+ if (counter !== void 0) {
306
+ const nonceBuf = hexToBuffer(parameters.nonce);
307
+ deriveKeyResult = await deriveKey2(
308
+ parameters,
309
+ hexToBuffer(parameters.salt),
310
+ new PasswordBuffer(nonceBuf, counterMode).setCounter(counter)
311
+ );
312
+ if (deriveKeyResult.parameters) {
313
+ Object.assign(parameters, deriveKeyResult.parameters);
314
+ }
315
+ parameters.keyPrefix = bufferToHex(deriveKeyResult.derivedKey.slice(0, keyPrefixLength));
316
+ }
317
+ if (!hmacSignatureSecret) {
318
+ return {
319
+ parameters: sortKeys(parameters)
320
+ };
321
+ }
322
+ return signChallenge(
323
+ hmacAlgorithm,
324
+ parameters,
325
+ deriveKeyResult?.derivedKey,
326
+ hmacSignatureSecret,
327
+ hmacKeySignatureSecret
328
+ );
329
+ }
94
330
  async function solveChallenge(options) {
95
331
  const {
96
332
  challenge,
@@ -98,7 +334,7 @@
98
334
  counterMode = "uint32",
99
335
  counterStart = 0,
100
336
  counterStep = 1,
101
- deriveKey,
337
+ deriveKey: deriveKey2,
102
338
  timeout = 9e4
103
339
  } = options;
104
340
  const { nonce, keyPrefix, salt } = challenge.parameters;
@@ -114,7 +350,7 @@
114
350
  if (controller?.signal.aborted || timeout && counter % 10 === 0 && performance.now() - start > timeout) {
115
351
  return null;
116
352
  }
117
- const { derivedKey } = await deriveKey(
353
+ const { derivedKey } = await deriveKey2(
118
354
  challenge.parameters,
119
355
  saltBuf,
120
356
  password.setCounter(counter)
@@ -142,7 +378,8 @@
142
378
  controller = new AbortController(),
143
379
  createWorker,
144
380
  onOutOfMemory = (c) => c > 1 ? Math.floor(c / 2) : 0,
145
- counterMode
381
+ counterMode,
382
+ timeout = 9e4
146
383
  } = options;
147
384
  const workersConcurrency = Math.min(16, Math.max(1, concurrency));
148
385
  const workersInstances = [];
@@ -183,6 +420,7 @@
183
420
  counterMode,
184
421
  counterStart: i,
185
422
  counterStep: workersConcurrency,
423
+ timeout,
186
424
  type: "work"
187
425
  });
188
426
  });
@@ -214,8 +452,24 @@
214
452
  }
215
453
  return solution || null;
216
454
  }
455
+ async function signChallenge(algorithm, parameters, derivedKey, hmacSignatureSecret, hmacKeySignatureSecret) {
456
+ if (derivedKey && hmacKeySignatureSecret) {
457
+ parameters.keySignature = bufferToHex(
458
+ await hmac(algorithm, derivedKey, hmacKeySignatureSecret)
459
+ );
460
+ }
461
+ parameters = sortKeys(parameters);
462
+ return {
463
+ parameters,
464
+ signature: bufferToHex(await hmac(algorithm, JSON.stringify(parameters), hmacSignatureSecret))
465
+ };
466
+ }
217
467
  async function deobfuscate(obfuscatedData, options = {}) {
218
- const { concurrency = navigator.hardwareConcurrency, deriveKey: deriveKey2 } = options;
468
+ let {
469
+ concurrency = Math.max(1, Math.min(4, navigator.hardwareConcurrency)),
470
+ createWorker,
471
+ deriveKey: deriveKey$1 = deriveKey
472
+ } = options;
219
473
  let challenge = null;
220
474
  try {
221
475
  challenge = JSON.parse(atob(obfuscatedData));
@@ -227,21 +481,20 @@
227
481
  }
228
482
  const cipher = challenge.cipher;
229
483
  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
- }
484
+ if (!createWorker && "$altcha" in globalThis) {
485
+ createWorker = globalThis.$altcha.algorithms.get(challenge.parameters.algorithm);
486
+ }
487
+ if (createWorker) {
240
488
  solution = await solveChallengeWorkers({
241
489
  challenge,
242
490
  concurrency,
243
491
  createWorker
244
492
  });
493
+ } else {
494
+ solution = await solveChallenge({
495
+ challenge,
496
+ deriveKey: deriveKey$1
497
+ });
245
498
  }
246
499
  if (!solution) {
247
500
  throw new Error("Unable to find solution.");
@@ -263,7 +516,48 @@
263
516
  );
264
517
  return new TextDecoder().decode(result);
265
518
  }
519
+ async function obfuscate(str, options = {}) {
520
+ const { deriveKey: deriveKey$1 = deriveKey } = options;
521
+ const counterMin = options?.counterMin || 20;
522
+ const counterMax = options?.counterMax || 200;
523
+ const { parameters } = await createChallenge({
524
+ algorithm: "PBKDF2/SHA-256",
525
+ cost: 5e3,
526
+ deriveKey: deriveKey$1,
527
+ counter: Math.floor(Math.random() * (counterMax - counterMin + 1)) + counterMin,
528
+ keyPrefixLength: 32,
529
+ ...options
530
+ });
531
+ const key = await crypto.subtle.importKey(
532
+ "raw",
533
+ hexToBuffer(parameters.keyPrefix),
534
+ { name: "AES-GCM" },
535
+ false,
536
+ ["encrypt"]
537
+ );
538
+ const iv = crypto.getRandomValues(new Uint8Array(12));
539
+ const data = await crypto.subtle.encrypt(
540
+ { name: "AES-GCM", iv },
541
+ key,
542
+ new TextEncoder().encode(str)
543
+ );
544
+ return btoa(
545
+ JSON.stringify({
546
+ parameters: {
547
+ ...parameters,
548
+ // Return only half the derived key
549
+ keyPrefix: parameters.keyPrefix.slice(0, parameters.keyLength || 32)
550
+ },
551
+ cipher: {
552
+ iv: bufferToHex(iv),
553
+ data: bufferToHex(data)
554
+ }
555
+ })
556
+ );
557
+ }
266
558
  class ObfuscationPlugin extends BasePlugin {
559
+ static deobfuscate = deobfuscate;
560
+ static obfuscate = obfuscate;
267
561
  elTrigger = null;
268
562
  activate() {
269
563
  this.elTrigger = this.host.querySelector("button");
@@ -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";const e=()=>{};function t(t,r,n){if(null==t)return r(void 0),e;const a=function(e){var t=i;try{return i=!0,e()}finally{i=t}}(()=>t.subscribe(r,n));return a.unsubscribe?()=>a.unsubscribe():a}const r=[];function n(t,n=e){let a=null;const i=new Set;function o(e){if(o=e,((n=t)!=n?o==o:n!==o||null!==n&&"object"==typeof n||"function"==typeof n)&&(t=e,a)){const e=!r.length;for(const e of i)e[1](),r.push(e,t);if(e){for(let e=0;e<r.length;e+=2)r[e][0](r[e+1]);r.length=0}}var n,o}function s(e){o(e(t))}return{set:o,update:s,subscribe:function(r,c=e){const l=[r,c];return i.add(l),1===i.size&&(a=n(o,s)||e),r(t),()=>{i.delete(l),0===i.size&&a&&(a(),a=null)}}}}function a(e){let r;return t(e,e=>r=e)(),r}let i=!1;function o(e){const t={get:e=>a(t.store)[e],set:(e,r)=>{"string"==typeof e?Object.assign(a(t.store),{[e]:r}):Object.assign(a(t.store),e),t.store.set(a(t.store))},store:n(e)};return t}globalThis.$altcha=globalThis.$altcha||{algorithms:new Map,defaults:o({}),i18n:o({}),instances:new Set,plugins:new Set};class s{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 c(e){switch(e){case"PBKDF2/SHA-512":return"SHA-512";case"PBKDF2/SHA-384":return"SHA-384";default:return"SHA-256"}}async function l(e,t,r){const{algorithm:n,cost:a,keyLength:i=32}=e,o=await crypto.subtle.importKey("raw",r,{name:"PBKDF2"},!1,["deriveKey"]),s=await crypto.subtle.deriveKey({name:"PBKDF2",salt:t,iterations:a,hash:c(n)},o,{name:"AES-GCM",length:8*i},!0,["encrypt"]);return{derivedKey:new Uint8Array(await crypto.subtle.exportKey("raw",s))}}function u(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 h(e){return Array.from(new Uint8Array(e)).map(e=>e.toString(16).padStart(2,"0")).join("")}function f(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 y(e){await new Promise(t=>setTimeout(t,e))}async function g(e,t,r){const n=await crypto.subtle.importKey("raw",(new TextEncoder).encode(r),{name:"HMAC",hash:{name:e}},!1,["sign","verify"]),a=await crypto.subtle.sign("HMAC",n,"string"==typeof t?(new TextEncoder).encode(t):t);return new Uint8Array(a)}function d(e){return"object"!=typeof e||null===e||Array.isArray(e)?e:Object.keys(e).sort().reduce((t,r)=>{const n=e[r];return void 0!==n&&(t[r]=d(n)),t},{})}function m(e){return Math.floor(10*(performance.now()-e))/10}var w=(e=>(e.SHA_256="SHA-256",e.SHA_384="SHA-384",e.SHA_512="SHA-512",e))(w||{}),p=(e=>(e.CODE="code",e.ERROR="error",e.VERIFIED="verified",e.VERIFYING="verifying",e.UNVERIFIED="unverified",e.EXPIRED="expired",e))(p||{});class b{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 S(e){const{algorithm:t,counter:r,counterMode:n="uint32",cost:a,deriveKey:i,data:o,expiresAt:s,hmacAlgorithm:c=w.SHA_256,hmacKeySignatureSecret:l,hmacSignatureSecret:u,keyLength:y=32,keyPrefix:m="00",keyPrefixLength:p=y/2,memoryCost:S,parallelism:v}=e,A={algorithm:t,nonce:h(crypto.getRandomValues(new Uint8Array(16))),salt:h(crypto.getRandomValues(new Uint8Array(16))),cost:a,keyLength:y,memoryCost:S,parallelism:v,keyPrefix:m,expiresAt:s instanceof Date?Math.floor(s.getTime()/1e3):s,data:o};let E=null;if(void 0!==r){const e=f(A.nonce);E=await i(A,f(A.salt),new b(e,n).setCounter(r)),E.parameters&&Object.assign(A,E.parameters),A.keyPrefix=h(E.derivedKey.slice(0,p))}return u?async function(e,t,r,n,a){r&&a&&(t.keySignature=h(await g(e,r,a)));return t=d(t),{parameters:t,signature:h(await g(e,JSON.stringify(t),n))}}(c,A,E?.derivedKey,u,l):{parameters:d(A)}}async function v(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 h=null;try{h=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 v({...e,challenge:t,controller:n,concurrency:r,createWorker:a})}throw r}finally{u()}return n.signal.aborted?null:h||null}async function A(e,t={}){let{concurrency:r=Math.max(1,Math.min(4,navigator.hardwareConcurrency)),createWorker:n,deriveKey:a=l}=t,i=null;try{i=JSON.parse(atob(e))}catch{throw new Error("Unable to parse obfuscated data.")}if(!i||"object"!=typeof i||!("parameters"in i)||!("cipher"in i))throw new Error("Invalid obfuscated data format.");const o=i.cipher;let s=null;if(!n&&"$altcha"in globalThis&&(n=globalThis.$altcha.algorithms.get(i.parameters.algorithm)),s=n?await v({challenge:i,concurrency:r,createWorker:n}):await async function(e){const{challenge:t,controller:r,counterMode:n="uint32",counterStart:a=0,counterStep:i=1,deriveKey:o,timeout:s=9e4}=e,{nonce:c,keyPrefix:l,salt:g}=t.parameters,d=f(c),w=f(g),p=l.length%2==0?f(l):null,S=new b(d,n),v=performance.now();let A=a,E="",T=v;for(;;){if(r?.signal.aborted||s&&A%10==0&&performance.now()-v>s)return null;const{derivedKey:e}=await o(t.parameters,w,S.setCounter(A));if(A%10==0&&performance.now()-T>200&&(await y(0),T=performance.now()),p?u(e,p):h(e).startsWith(l)){E=h(e);break}A+=i}return{counter:A,derivedKey:E,time:m(v)}}({challenge:i,deriveKey:a}),!s)throw new Error("Unable to find solution.");const c=await crypto.subtle.importKey("raw",f(s.derivedKey),{name:"AES-GCM"},!1,["decrypt"]),g=await crypto.subtle.decrypt({name:"AES-GCM",iv:f(o.iv)},c,f(o.data));return(new TextDecoder).decode(g)}async function E(e,t={}){const{deriveKey:r=l}=t,n=t?.counterMin||20,a=t?.counterMax||200,{parameters:i}=await S({algorithm:"PBKDF2/SHA-256",cost:5e3,deriveKey:r,counter:Math.floor(Math.random()*(a-n+1))+n,keyPrefixLength:32,...t}),o=await crypto.subtle.importKey("raw",f(i.keyPrefix),{name:"AES-GCM"},!1,["encrypt"]),s=crypto.getRandomValues(new Uint8Array(12)),c=await crypto.subtle.encrypt({name:"AES-GCM",iv:s},o,(new TextEncoder).encode(e));return btoa(JSON.stringify({parameters:{...i,keyPrefix:i.keyPrefix.slice(0,i.keyLength||32)},cipher:{iv:h(s),data:h(c)}}))}s.register(class extends s{static deobfuscate=A;static obfuscate=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(p.VERIFYING);try{const e=await A(n);await this.#e(Math.max(0,t-(performance.now()-r))),this.#t(e)}catch(e){this.host.setState(p.ERROR,String(e))}finally{this.host.setState(p.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.4",
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 {};