(function(factory) {
  typeof define === "function" && define.amd ? define(factory) : factory();
})((function() {
  "use strict";
  const noop = () => {
  };
  function safe_not_equal(a, b) {
    return a != a ? b == b : a !== b || a !== null && typeof a === "object" || typeof a === "function";
  }
  function subscribe_to_store(store2, run, invalidate) {
    if (store2 == null) {
      run(void 0);
      return noop;
    }
    const unsub = untrack(
      () => store2.subscribe(
        run,
        // @ts-expect-error
        invalidate
      )
    );
    return unsub.unsubscribe ? () => unsub.unsubscribe() : unsub;
  }
  const subscriber_queue = [];
  function writable(value, start = noop) {
    let stop = null;
    const subscribers = /* @__PURE__ */ new Set();
    function set(new_value) {
      if (safe_not_equal(value, new_value)) {
        value = new_value;
        if (stop) {
          const run_queue = !subscriber_queue.length;
          for (const subscriber of subscribers) {
            subscriber[1]();
            subscriber_queue.push(subscriber, value);
          }
          if (run_queue) {
            for (let i = 0; i < subscriber_queue.length; i += 2) {
              subscriber_queue[i][0](subscriber_queue[i + 1]);
            }
            subscriber_queue.length = 0;
          }
        }
      }
    }
    function update(fn) {
      set(fn(
        /** @type {T} */
        value
      ));
    }
    function subscribe(run, invalidate = noop) {
      const subscriber = [run, invalidate];
      subscribers.add(subscriber);
      if (subscribers.size === 1) {
        stop = start(set, update) || noop;
      }
      run(
        /** @type {T} */
        value
      );
      return () => {
        subscribers.delete(subscriber);
        if (subscribers.size === 0 && stop) {
          stop();
          stop = null;
        }
      };
    }
    return { set, update, subscribe };
  }
  function get(store2) {
    let value;
    subscribe_to_store(store2, (_) => value = _)();
    return value;
  }
  let untracking = false;
  function untrack(fn) {
    var previous_untracking = untracking;
    try {
      untracking = true;
      return fn();
    } finally {
      untracking = previous_untracking;
    }
  }
  function store(defaultValue) {
    const scope = {
      get: (name) => {
        return get(scope.store)[name];
      },
      set: (name, value) => {
        if (typeof name === "string") {
          Object.assign(get(scope.store), {
            [name]: value
          });
        } else {
          Object.assign(get(scope.store), name);
        }
        scope.store.set(get(scope.store));
      },
      store: writable(defaultValue)
    };
    return scope;
  }
  globalThis.$altcha = globalThis.$altcha || {
    algorithms: /* @__PURE__ */ new Map(),
    defaults: store({}),
    i18n: store({}),
    instances: /* @__PURE__ */ new Set(),
    plugins: /* @__PURE__ */ new Set()
  };
  class BasePlugin {
    constructor(host) {
      this.host = host;
    }
    host;
    static register(pluginClass) {
      if ("$altcha" in globalThis && !globalThis.$altcha.plugins.has(pluginClass)) {
        globalThis.$altcha.plugins.add(pluginClass);
      }
    }
    async onFetchChallenge(source) {
    }
    async onRequestServerVerification(payload, code) {
    }
    async onVerify(value) {
    }
  }
  function getDigest(algorithm) {
    switch (algorithm) {
      case "PBKDF2/SHA-512":
        return "SHA-512";
      case "PBKDF2/SHA-384":
        return "SHA-384";
      case "PBKDF2/SHA-256":
      default:
        return "SHA-256";
    }
  }
  async function deriveKey(parameters, salt, password) {
    const { algorithm, cost, keyLength = 32 } = parameters;
    const passwordKey = await crypto.subtle.importKey(
      "raw",
      password,
      { name: "PBKDF2" },
      false,
      ["deriveKey"]
    );
    const derivedKey = await crypto.subtle.deriveKey(
      {
        name: "PBKDF2",
        salt,
        iterations: cost,
        hash: getDigest(algorithm)
      },
      passwordKey,
      { name: "AES-GCM", length: keyLength * 8 },
      true,
      ["encrypt"]
    );
    return {
      derivedKey: new Uint8Array(await crypto.subtle.exportKey("raw", derivedKey))
    };
  }
  function bufferStartsWith(buffer, prefix) {
    if (prefix.length > buffer.length) {
      return false;
    }
    for (let i = 0; i < prefix.length; i++) {
      if (buffer[i] !== prefix[i]) {
        return false;
      }
    }
    return true;
  }
  function bufferToHex(buffer) {
    return Array.from(new Uint8Array(buffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
  }
  function concatBuffers(a, b) {
    const out = new Uint8Array(a.length + b.length);
    out.set(a, 0);
    out.set(b, a.length);
    return out;
  }
  function hexToBuffer(hex) {
    if (hex.length % 2 !== 0) {
      throw new Error(`Hex string must have an even length. Got: ${hex}`);
    }
    const buffer = new ArrayBuffer(hex.length / 2);
    const view = new DataView(buffer);
    for (let i = 0; i < hex.length; i += 2) {
      const byteString = hex.substring(i, i + 2);
      const byteValue = parseInt(byteString, 16);
      view.setUint8(i / 2, byteValue);
    }
    return new Uint8Array(buffer);
  }
  async function delay(ms) {
    await new Promise((resolve) => setTimeout(resolve, ms));
  }
  async function hmac(algorithm, data, keyStr) {
    const key = await crypto.subtle.importKey(
      "raw",
      new TextEncoder().encode(keyStr),
      {
        name: "HMAC",
        hash: { name: algorithm }
      },
      false,
      ["sign", "verify"]
    );
    const signature = await crypto.subtle.sign(
      "HMAC",
      key,
      typeof data === "string" ? new TextEncoder().encode(data) : data
    );
    return new Uint8Array(signature);
  }
  function sortKeys(obj) {
    if (typeof obj !== "object" || obj === null || Array.isArray(obj)) {
      return obj;
    }
    return Object.keys(obj).sort().reduce((acc, key) => {
      const value = obj[key];
      if (value !== void 0) {
        acc[key] = sortKeys(value);
      }
      return acc;
    }, {});
  }
  function timeDuration(start) {
    return Math.floor((performance.now() - start) * 10) / 10;
  }
  var HmacAlgorithm = /* @__PURE__ */ ((HmacAlgorithm2) => {
    HmacAlgorithm2["SHA_256"] = "SHA-256";
    HmacAlgorithm2["SHA_384"] = "SHA-384";
    HmacAlgorithm2["SHA_512"] = "SHA-512";
    return HmacAlgorithm2;
  })(HmacAlgorithm || {});
  var State = /* @__PURE__ */ ((State2) => {
    State2["CODE"] = "code";
    State2["ERROR"] = "error";
    State2["VERIFIED"] = "verified";
    State2["VERIFYING"] = "verifying";
    State2["UNVERIFIED"] = "unverified";
    State2["EXPIRED"] = "expired";
    return State2;
  })(State || {});
  class PasswordBuffer {
    constructor(nonce, mode = "uint32") {
      this.nonce = nonce;
      this.mode = mode;
      this.buffer = new Uint8Array(this.nonce.length + this.COUNTER_BYTES);
      this.buffer.set(this.nonce, 0);
      this.dataView = new DataView(this.buffer.buffer);
    }
    nonce;
    mode;
    COUNTER_BYTES = 4;
    buffer;
    dataView;
    encoder = new TextEncoder();
    /**
     * Appends the counter to the nonce buffer.
     * In 'string' mode, encodes the counter as a UTF-8 string.
     * In 'uint32' mode, writes the counter as a big-endian 32-bit integer.
     */
    setCounter(n) {
      if (this.mode === "string") {
        return concatBuffers(this.nonce, this.encoder.encode(n.toString()));
      }
      this.dataView.setUint32(this.nonce.length, n, false);
      return this.buffer;
    }
  }
  async function createChallenge(options) {
    const {
      algorithm,
      counter,
      counterMode = "uint32",
      cost,
      deriveKey: deriveKey2,
      data,
      expiresAt,
      hmacAlgorithm = HmacAlgorithm.SHA_256,
      hmacKeySignatureSecret,
      hmacSignatureSecret,
      keyLength = 32,
      keyPrefix = "00",
      keyPrefixLength = keyLength / 2,
      memoryCost,
      parallelism
    } = options;
    const parameters = {
      algorithm,
      nonce: bufferToHex(crypto.getRandomValues(new Uint8Array(16))),
      salt: bufferToHex(crypto.getRandomValues(new Uint8Array(16))),
      cost,
      keyLength,
      memoryCost,
      parallelism,
      keyPrefix,
      expiresAt: expiresAt instanceof Date ? Math.floor(expiresAt.getTime() / 1e3) : expiresAt,
      data
    };
    let deriveKeyResult = null;
    if (counter !== void 0) {
      const nonceBuf = hexToBuffer(parameters.nonce);
      deriveKeyResult = await deriveKey2(
        parameters,
        hexToBuffer(parameters.salt),
        new PasswordBuffer(nonceBuf, counterMode).setCounter(counter)
      );
      if (deriveKeyResult.parameters) {
        Object.assign(parameters, deriveKeyResult.parameters);
      }
      parameters.keyPrefix = bufferToHex(deriveKeyResult.derivedKey.slice(0, keyPrefixLength));
    }
    if (!hmacSignatureSecret) {
      return {
        parameters: sortKeys(parameters)
      };
    }
    return signChallenge(
      hmacAlgorithm,
      parameters,
      deriveKeyResult?.derivedKey,
      hmacSignatureSecret,
      hmacKeySignatureSecret
    );
  }
  async function solveChallenge(options) {
    const {
      challenge,
      controller,
      counterMode = "uint32",
      counterStart = 0,
      counterStep = 1,
      deriveKey: deriveKey2,
      timeout = 9e4
    } = options;
    const { nonce, keyPrefix, salt } = challenge.parameters;
    const nonceBuf = hexToBuffer(nonce);
    const saltBuf = hexToBuffer(salt);
    const keyPrefixBuf = keyPrefix.length % 2 === 0 ? hexToBuffer(keyPrefix) : null;
    const password = new PasswordBuffer(nonceBuf, counterMode);
    const start = performance.now();
    let counter = counterStart;
    let iterations = 0;
    let derivedKeyHex = "";
    let lastYield = start;
    while (true) {
      if (controller?.signal.aborted || timeout && iterations % 10 === 0 && performance.now() - start > timeout) {
        return null;
      }
      const { derivedKey } = await deriveKey2(
        challenge.parameters,
        saltBuf,
        password.setCounter(counter)
      );
      if (iterations % 10 === 0 && performance.now() - lastYield > 200) {
        await delay(0);
        lastYield = performance.now();
      }
      if (keyPrefixBuf ? bufferStartsWith(derivedKey, keyPrefixBuf) : bufferToHex(derivedKey).startsWith(keyPrefix)) {
        derivedKeyHex = bufferToHex(derivedKey);
        break;
      }
      counter = counter + counterStep;
      iterations = iterations + 1;
    }
    return {
      counter,
      derivedKey: derivedKeyHex,
      time: timeDuration(start)
    };
  }
  async function solveChallengeWorkers(options) {
    const {
      challenge,
      concurrency = navigator.hardwareConcurrency,
      controller = new AbortController(),
      createWorker,
      onOutOfMemory = (c) => c > 1 ? Math.floor(c / 2) : 0,
      counterMode,
      timeout = 9e4
    } = options;
    const workersConcurrency = Math.min(16, Math.max(1, concurrency));
    const workersInstances = [];
    const terminate = () => {
      for (const worker of workersInstances) {
        worker.terminate();
      }
    };
    for (let i = 0; i < workersConcurrency; i++) {
      workersInstances.push(await createWorker(challenge.parameters.algorithm));
    }
    let solution = null;
    try {
      solution = await Promise.race(
        workersInstances.map((worker, i) => {
          controller.signal.addEventListener("abort", () => {
            worker.postMessage({ type: "abort" });
          });
          return new Promise((resolve, reject) => {
            worker.addEventListener("error", (err) => {
              reject(err);
            });
            worker.addEventListener("message", (message) => {
              if (message.data) {
                for (const w of workersInstances) {
                  if (w !== worker) {
                    w.postMessage({ type: "abort" });
                  }
                }
                if (message.data.error) {
                  return reject(new Error(message.data.error));
                }
              }
              resolve(message.data);
            });
            worker.postMessage({
              challenge,
              counterMode,
              counterStart: i,
              counterStep: workersConcurrency,
              timeout,
              type: "work"
            });
          });
        })
      );
    } catch (err) {
      const isOOM = err instanceof Error && !!err?.message?.includes("Out of memory");
      if (isOOM) {
        if (onOutOfMemory) {
          terminate();
          const retryConcurrency = onOutOfMemory(workersConcurrency);
          if (retryConcurrency) {
            return solveChallengeWorkers({
              ...options,
              challenge,
              controller,
              concurrency: retryConcurrency,
              createWorker
            });
          }
        }
      }
      throw err;
    } finally {
      terminate();
    }
    if (controller.signal.aborted) {
      return null;
    }
    return solution || null;
  }
  async function signChallenge(algorithm, parameters, derivedKey, hmacSignatureSecret, hmacKeySignatureSecret) {
    if (derivedKey && hmacKeySignatureSecret) {
      parameters.keySignature = bufferToHex(
        await hmac(algorithm, derivedKey, hmacKeySignatureSecret)
      );
    }
    parameters = sortKeys(parameters);
    return {
      parameters,
      signature: bufferToHex(await hmac(algorithm, JSON.stringify(parameters), hmacSignatureSecret))
    };
  }
  async function deobfuscate(obfuscatedData, options = {}) {
    let {
      concurrency = Math.max(1, Math.min(4, navigator.hardwareConcurrency)),
      createWorker,
      deriveKey: deriveKey$1 = deriveKey
    } = options;
    let challenge = null;
    try {
      challenge = JSON.parse(atob(obfuscatedData));
    } catch {
      throw new Error(`Unable to parse obfuscated data.`);
    }
    if (!challenge || typeof challenge !== "object" || !("parameters" in challenge) || !("cipher" in challenge)) {
      throw new Error(`Invalid obfuscated data format.`);
    }
    const cipher = challenge.cipher;
    let solution = null;
    if (!createWorker && "$altcha" in globalThis) {
      createWorker = globalThis.$altcha.algorithms.get(challenge.parameters.algorithm);
    }
    if (createWorker) {
      solution = await solveChallengeWorkers({
        challenge,
        concurrency,
        createWorker
      });
    } else {
      solution = await solveChallenge({
        challenge,
        deriveKey: deriveKey$1
      });
    }
    if (!solution) {
      throw new Error("Unable to find solution.");
    }
    const key = await crypto.subtle.importKey(
      "raw",
      hexToBuffer(solution.derivedKey),
      { name: "AES-GCM" },
      false,
      ["decrypt"]
    );
    const result = await crypto.subtle.decrypt(
      {
        name: "AES-GCM",
        iv: hexToBuffer(cipher.iv)
      },
      key,
      hexToBuffer(cipher.data)
    );
    return new TextDecoder().decode(result);
  }
  async function obfuscate(str, options = {}) {
    const { deriveKey: deriveKey$1 = deriveKey } = options;
    const counterMin = options?.counterMin || 20;
    const counterMax = options?.counterMax || 200;
    const { parameters } = await createChallenge({
      algorithm: "PBKDF2/SHA-256",
      cost: 5e3,
      deriveKey: deriveKey$1,
      counter: Math.floor(Math.random() * (counterMax - counterMin + 1)) + counterMin,
      keyPrefixLength: 32,
      ...options
    });
    const key = await crypto.subtle.importKey(
      "raw",
      hexToBuffer(parameters.keyPrefix),
      { name: "AES-GCM" },
      false,
      ["encrypt"]
    );
    const iv = crypto.getRandomValues(new Uint8Array(12));
    const data = await crypto.subtle.encrypt(
      { name: "AES-GCM", iv },
      key,
      new TextEncoder().encode(str)
    );
    return btoa(
      JSON.stringify({
        parameters: {
          ...parameters,
          // Return only half the derived key
          keyPrefix: parameters.keyPrefix.slice(0, parameters.keyLength || 32)
        },
        cipher: {
          iv: bufferToHex(iv),
          data: bufferToHex(data)
        }
      })
    );
  }
  class ObfuscationPlugin extends BasePlugin {
    static deobfuscate = deobfuscate;
    static obfuscate = obfuscate;
    elTrigger = null;
    activate() {
      this.elTrigger = this.host.querySelector("button");
      if (this.elTrigger) {
        this.elTrigger.addEventListener("click", this.onTriggerClick.bind(this));
        this.host.configure({
          floatingAnchor: this.elTrigger
        });
      }
    }
    destroy() {
    }
    async onVerify(options) {
      const { minDuration = 500 } = options;
      const start = performance.now();
      const obfuscated = this.host.getAttribute("data-obfuscated");
      if (obfuscated) {
        this.host.reset(State.VERIFYING);
        try {
          const text = await deobfuscate(obfuscated);
          await this.#wait(Math.max(0, minDuration - (performance.now() - start)));
          this.#renderClearText(text);
        } catch (err) {
          this.host.setState(State.ERROR, String(err));
        } finally {
          this.host.setState(State.VERIFIED);
        }
        return null;
      }
    }
    onTriggerClick(ev) {
      ev.preventDefault();
      this.host.show();
      this.host.verify().then(() => {
        this.host.hide();
      });
    }
    #renderClearText(clearText) {
      const match = clearText.match(/^(mailto|tel|sms|https?):/);
      let el;
      if (match) {
        const [contact] = clearText.slice(clearText.indexOf(":") + 1).replace(/^\/\//, "").split("?");
        el = document.createElement("a");
        el.href = clearText;
        el.innerText = contact;
      } else {
        el = document.createTextNode(clearText);
      }
      if (this.elTrigger && el) {
        this.elTrigger.after(el);
        this.elTrigger.parentElement?.removeChild(this.elTrigger);
      }
    }
    async #wait(ms) {
      await new Promise((resolve) => setTimeout(resolve, ms));
    }
  }
  BasePlugin.register(ObfuscationPlugin);
}));
