// Low-level bridge to the Emscripten-generated WASM module. // Handles all pointer/length marshalling so higher layers never touch raw memory. export interface StartProvisioningResult { cpim: Uint8Array; session: number; } export interface RequestOtpResult { otp: Uint8Array; machineId: Uint8Array; } export class WasmBridge { // eslint-disable-next-line @typescript-eslint/no-explicit-any private m: any; // eslint-disable-next-line @typescript-eslint/no-explicit-any constructor(wasmModule: any) { this.m = wasmModule; } // ---- memory helpers ---- private allocBytes(bytes: Uint8Array): number { const ptr = this.m._malloc(bytes.length) as number; this.m.HEAPU8.set(bytes, ptr); return ptr; } private allocCString(value: string | null | undefined): number { if (!value) return 0; const size = (this.m.lengthBytesUTF8(value) as number) + 1; const ptr = this.m._malloc(size) as number; this.m.stringToUTF8(value, ptr, size); return ptr; } private readBytes(ptr: number, len: number): Uint8Array { if (!ptr || !len) return new Uint8Array(0); return (this.m.HEAPU8 as Uint8Array).slice(ptr, ptr + len); } private free(ptr: number): void { if (ptr) this.m._free(ptr); } // ---- error handling ---- getLastError(): string { const ptr = this.m._anisette_last_error_ptr() as number; const len = this.m._anisette_last_error_len() as number; if (!ptr || !len) return ""; const bytes = (this.m.HEAPU8 as Uint8Array).subarray(ptr, ptr + len); return new TextDecoder("utf-8").decode(bytes); } private check(result: number, context: string): void { if (result !== 0) { const msg = this.getLastError(); throw new Error(`${context}: ${msg || "unknown error"}`); } } // ---- public API ---- /** * Initialize ADI from in-memory library blobs. */ initFromBlobs( storeservices: Uint8Array, coreadi: Uint8Array, libraryPath: string, provisioningPath?: string, identifier?: string ): void { const ssPtr = this.allocBytes(storeservices); const caPtr = this.allocBytes(coreadi); const libPtr = this.allocCString(libraryPath); const provPtr = this.allocCString(provisioningPath ?? null); const idPtr = this.allocCString(identifier ?? null); try { const result = this.m._anisette_init_from_blobs( ssPtr, storeservices.length, caPtr, coreadi.length, libPtr, provPtr, idPtr ) as number; this.check(result, "anisette_init_from_blobs"); } finally { this.free(ssPtr); this.free(caPtr); this.free(libPtr); this.free(provPtr); this.free(idPtr); } } /** * Read a file from the WASM virtual filesystem. */ readVirtualFile(filePath: string): Uint8Array { const pathPtr = this.allocCString(filePath); try { const result = this.m._anisette_fs_read_file(pathPtr) as number; this.check(result, `anisette_fs_read_file(${filePath})`); } finally { this.free(pathPtr); } const ptr = this.m._anisette_fs_read_ptr() as number; const len = this.m._anisette_fs_read_len() as number; return this.readBytes(ptr, len); } /** * Write a file into the WASM virtual filesystem. */ writeVirtualFile(filePath: string, data: Uint8Array): void { const pathPtr = this.allocCString(filePath); const dataPtr = this.allocBytes(data); try { const result = this.m._anisette_fs_write_file( pathPtr, dataPtr, data.length ) as number; this.check(result, `anisette_fs_write_file(${filePath})`); } finally { this.free(pathPtr); this.free(dataPtr); } } /** * Returns 1 if provisioned, 0 if not, throws on error. */ isMachineProvisioned(dsid: bigint): boolean { const result = this.m._anisette_is_machine_provisioned(dsid) as number; if (result < 0) { throw new Error( `anisette_is_machine_provisioned: ${this.getLastError()}` ); } return result === 1; } /** * Start provisioning — returns CPIM bytes and session handle. */ startProvisioning( dsid: bigint, spim: Uint8Array ): StartProvisioningResult { const spimPtr = this.allocBytes(spim); try { const result = this.m._anisette_start_provisioning( dsid, spimPtr, spim.length ) as number; this.check(result, "anisette_start_provisioning"); } finally { this.free(spimPtr); } const cpimPtr = this.m._anisette_get_cpim_ptr() as number; const cpimLen = this.m._anisette_get_cpim_len() as number; const session = this.m._anisette_get_session() as number; return { cpim: this.readBytes(cpimPtr, cpimLen), session, }; } /** * Finish provisioning with PTM and TK from Apple servers. */ endProvisioning(session: number, ptm: Uint8Array, tk: Uint8Array): void { const ptmPtr = this.allocBytes(ptm); const tkPtr = this.allocBytes(tk); try { const result = this.m._anisette_end_provisioning( session, ptmPtr, ptm.length, tkPtr, tk.length ) as number; this.check(result, "anisette_end_provisioning"); } finally { this.free(ptmPtr); this.free(tkPtr); } } /** * Request OTP — returns OTP bytes and machine ID bytes. */ requestOtp(dsid: bigint): RequestOtpResult { const result = this.m._anisette_request_otp(dsid) as number; this.check(result, "anisette_request_otp"); const otpPtr = this.m._anisette_get_otp_ptr() as number; const otpLen = this.m._anisette_get_otp_len() as number; const midPtr = this.m._anisette_get_mid_ptr() as number; const midLen = this.m._anisette_get_mid_len() as number; return { otp: this.readBytes(otpPtr, otpLen), machineId: this.readBytes(midPtr, midLen), }; } /** * Check if IDBFS is available (browser environment only). */ isIdbfsAvailable(): boolean { try { return !!(this.m.FS && this.m.FS.filesystems?.IDBFS); } catch { return false; } } /** * Initialize IDBFS for browser persistence. * Only works in browser environments with IDBFS available. */ initIdbfs(path: string): void { // Check if FS and IDBFS are available (browser only) if (!this.isIdbfsAvailable()) { return; // Node.js or environment without IDBFS } const normalizedPath = this.normalizeMountPath(path); // Create directory structure if (normalizedPath !== "/") { try { this.m.FS.mkdirTree(normalizedPath); } catch { // Directory already exists, ignore } } // Mount IDBFS try { this.m.FS.mount(this.m.FS.filesystems.IDBFS, {}, normalizedPath); } catch { // Already mounted, ignore } } /** * Sync IDBFS from IndexedDB to memory (async). * Must be called after initIdbfs to load existing data from IndexedDB. * Only works in browser environments with IDBFS available. */ async syncIdbfsFromStorage(): Promise { if (!this.isIdbfsAvailable()) { return; // IDBFS not available, skip silently } return new Promise((resolve, reject) => { this.m.FS.syncfs(true, (err: Error | null) => { if (err) { console.error("[anisette] IDBFS sync from storage failed:", err); reject(err); } else { resolve(); } }); }); } /** * Sync IDBFS from memory to IndexedDB (async). * Must be called after modifying files to persist them. * Only works in browser environments with IDBFS available. */ async syncIdbfsToStorage(): Promise { if (!this.isIdbfsAvailable()) { return; // IDBFS not available, skip silently } return new Promise((resolve, reject) => { this.m.FS.syncfs(false, (err: Error | null) => { if (err) { console.error("[anisette] IDBFS sync to storage failed:", err); reject(err); } else { resolve(); } }); }); } private normalizeMountPath(path: string): string { const trimmed = path.trim(); const noSlash = trimmed.replace(/\/+$/, ""); const noDot = noSlash.startsWith("./") ? noSlash.slice(2) : noSlash; if (!noDot || noDot === ".") { return "/"; } else if (noDot.startsWith("/")) { return noDot; } else { return "/" + noDot; } } }