import { useState, useCallback } from "react"; import CryptoJS from "crypto-js"; import * as React from "react"; import * as jsxRuntime from "react/jsx-runtime"; import type { CryptoLockerModule } from "./types"; export type CryptoLockerStatus = "idle" | "loading" | "success" | "error"; /** * Hook that provides logic for AES decryption of dynamic modules. * UI, password handling, and validation are fully delegated to the developer. * * @param dependencies Optional dictionary of external dependencies to provide to the encrypted module. */ export function useCryptoLocker(dependencies: Record = {}) { const [status, setStatus] = React.useState("idle"); const [decryptedData, setDecryptedData] = React.useState(null); const [error, setError] = React.useState(null); /** * Decrypts the dynamically loaded module and evaluates its code. * * @param fetchData A function that calls dynamic `import()` for the secret module. * @param password The AES password. */ const unlock = useCallback( async ( // eslint-disable-next-line @typescript-eslint/no-explicit-any fetchData: () => Promise, password: string ): Promise => { setStatus("loading"); setError(null); try { const mod = (await fetchData()) as CryptoLockerModule; const ciphertext = mod.encryptedData; if (typeof ciphertext !== "string") { throw new Error("Module does not export 'encryptedData'."); } if (mod.type !== "component") { throw new Error("Expected a 'component' module, but received: " + mod.type); } const bytes = CryptoJS.AES.decrypt(ciphertext, password); const plaintextCode = bytes.toString(CryptoJS.enc.Utf8); if (!plaintextCode) { throw new Error("Invalid password or empty decryption result."); } // Evaluate the decrypted CommonJS code // eslint-disable-next-line @typescript-eslint/no-explicit-any const exportsObj: any = {}; const moduleObj = { exports: exportsObj }; // Provide dependencies to the evaluated module const requireFn = (id: string) => { if (id === "react") return React; if (id === "react/jsx-runtime") return jsxRuntime; if (id === "react-dom") return (window as any).ReactDOM || {}; if (/\.(css|scss|less)$/i.test(id)) return {}; // Ignore CSS imports inside secret modules if (id in dependencies) return dependencies[id]; throw new Error(`Cannot require '${id}' in encrypted module.`); }; const fn = new Function( "require", "exports", "module", "React", plaintextCode ); fn(requireFn, exportsObj, moduleObj, React); const parsed = (moduleObj.exports.default ?? moduleObj.exports) as T; setDecryptedData(() => parsed); setStatus("success"); return parsed; } catch (err: unknown) { console.error("CryptoLocker decryption failed:", err); const errMsg = err instanceof Error ? err.message : "Decryption failed"; setError(errMsg); setStatus("error"); setDecryptedData(null); throw err; } }, [] ); /** * Resets the hook state to idle, clearing any decrypted data and errors. */ const reset = useCallback(() => { setStatus("idle"); setDecryptedData(null); setError(null); }, []); return { unlock, reset, status, decryptedData, error }; }