// Main Anisette class — the public-facing API import type { AnisetteHeaders, InitOptions } from "./types.js"; import type { HttpClient } from "./http.js"; import { WasmBridge } from "./wasm-bridge.js"; import { Device } from "./device.js"; import { LibraryStore } from "./library.js"; import { ProvisioningSession } from "./provisioning.js"; import { toBase64, toAppleClientTime, detectLocale, encodeUtf8, decodeUtf8, } from "./utils.js"; const DEFAULT_DSID = BigInt(-2); const DEFAULT_LIBRARY_PATH = "./anisette/"; const MD_RINFO = "17106176"; export interface AnisetteOptions { /** Override the HTTP client (useful for testing or custom proxy) */ httpClient?: HttpClient; /** DSID to use when requesting OTP (default: -2) */ dsid?: bigint; /** Options passed to WASM init */ init?: InitOptions; } export class Anisette { private bridge: WasmBridge; private device: Device; private provisioning: ProvisioningSession; private dsid: bigint; private provisioningPath: string; private libraryPath: string; private libs: LibraryStore; // eslint-disable-next-line @typescript-eslint/no-explicit-any private wasmModule: any; private identifier: string; private httpClient: HttpClient | undefined; private constructor( bridge: WasmBridge, device: Device, provisioning: ProvisioningSession, dsid: bigint, provisioningPath: string, libraryPath: string, libs: LibraryStore, // eslint-disable-next-line @typescript-eslint/no-explicit-any wasmModule: any, identifier: string, httpClient: HttpClient | undefined, ) { this.bridge = bridge; this.device = device; this.provisioning = provisioning; this.dsid = dsid; this.provisioningPath = provisioningPath; this.libraryPath = libraryPath; this.libs = libs; this.wasmModule = wasmModule; this.identifier = identifier; this.httpClient = httpClient; } // ---- factory methods ---- /** * Initialize from the two Android .so library files. * @param storeservicescore - bytes of libstoreservicescore.so * @param coreadi - bytes of libCoreADI.so */ static async fromSo( storeservicescore: Uint8Array, coreadi: Uint8Array, // eslint-disable-next-line @typescript-eslint/no-explicit-any wasmModule: any, options: AnisetteOptions = {} ): Promise { const libs = LibraryStore.fromBlobs(storeservicescore, coreadi); return Anisette._init(libs, wasmModule, options); } // eslint-disable-next-line @typescript-eslint/no-explicit-any private static async _init( libs: LibraryStore, // eslint-disable-next-line @typescript-eslint/no-explicit-any wasmModule: any, options: AnisetteOptions ): Promise { const bridge = new WasmBridge(wasmModule); const initOpts = options.init ?? {}; const libraryPath = normalizeAdiPath(initOpts.libraryPath ?? DEFAULT_LIBRARY_PATH); const provisioningPath = normalizeAdiPath(initOpts.provisioningPath ?? libraryPath); const dsid = options.dsid ?? DEFAULT_DSID; // Mount + load persisted IDBFS first so file state is stable before init. mountIdbfsPaths(bridge, libraryPath, provisioningPath); try { await bridge.syncIdbfsFromStorage(); } catch { // Ignore errors - might be first run with no existing data } // Load device config from explicit bytes first, then from persisted VFS. const savedDeviceJson = parseDeviceJsonBytes(initOpts.deviceJsonBytes) ?? readDeviceJsonFromVfs(bridge, joinPath(libraryPath, "device.json")); const device = Device.fromJson(savedDeviceJson, initOpts.deviceConfig); const identifier = initOpts.identifier ?? device.adiIdentifier; // Restore explicit adi.pb into VFS if provided. if (initOpts.adiPb) { bridge.writeVirtualFile(joinPath(provisioningPath, "adi.pb"), initOpts.adiPb); } // Keep VFS device.json consistent with the active in-memory device. const deviceJsonBytes = initOpts.deviceJsonBytes ?? encodeUtf8(JSON.stringify(device.toJson(), null, 2)); bridge.writeVirtualFile(joinPath(libraryPath, "device.json"), deviceJsonBytes); // Initialize WASM ADI bridge.initFromBlobs( libs.storeservicescore, libs.coreadi, libraryPath, provisioningPath, identifier ); const provisioning = new ProvisioningSession( bridge, device, options.httpClient ); return new Anisette(bridge, device, provisioning, dsid, provisioningPath, libraryPath, libs, wasmModule, identifier, options.httpClient); } // ---- public API ---- /** Whether the device is currently provisioned. */ get isProvisioned(): boolean { return this.bridge.isMachineProvisioned(this.dsid); } /** Run the provisioning flow against Apple servers. */ async provision(): Promise { await this.provisioning.provision(this.dsid); // Sync provisioning state to IndexedDB (browser only) if (this.bridge.isIdbfsAvailable()) { try { await this.bridge.syncIdbfsToStorage(); } catch (err) { console.error("[anisette] Failed to sync to IDBFS:", err); } } } /** Read adi.pb from the WASM VFS for persistence. */ getAdiPb(): Uint8Array { return this.bridge.readVirtualFile(joinPath(this.provisioningPath, "adi.pb")); } /** Generate Anisette headers. Throws if not provisioned. */ async getData(): Promise { // Reinit WASM state before each call to avoid emulator corruption on repeated use const adiPb = readOptionalFile( this.bridge, joinPath(this.provisioningPath, "adi.pb") ); const deviceJsonBytes = encodeUtf8(JSON.stringify(this.device.toJson(), null, 2)); this.bridge = new WasmBridge(this.wasmModule); // Mount + load persisted IDBFS in browser environment if (this.bridge.isIdbfsAvailable()) { mountIdbfsPaths(this.bridge, this.libraryPath, this.provisioningPath); try { await this.bridge.syncIdbfsFromStorage(); } catch { // Ignore errors - might be first run or no existing data } } if (adiPb) { this.bridge.writeVirtualFile(joinPath(this.provisioningPath, "adi.pb"), adiPb); } this.bridge.writeVirtualFile(joinPath(this.libraryPath, "device.json"), deviceJsonBytes); this.bridge.initFromBlobs(this.libs.storeservicescore, this.libs.coreadi, this.libraryPath, this.provisioningPath, this.identifier); this.provisioning = new ProvisioningSession(this.bridge, this.device, this.httpClient); const { otp, machineId } = this.bridge.requestOtp(this.dsid); const now = new Date(); const tzOffset = -now.getTimezoneOffset(); const tzSign = tzOffset >= 0 ? "+" : "-"; const tzHours = String(Math.floor(Math.abs(tzOffset) / 60)).padStart(2, "0"); const tzMins = String(Math.abs(tzOffset) % 60).padStart(2, "0"); const timezone = `${tzSign}${tzHours}${tzMins}`; return { "X-Apple-I-Client-Time": toAppleClientTime(now), "X-Apple-I-MD": toBase64(otp), "X-Apple-I-MD-LU": this.device.localUserUuid, "X-Apple-I-MD-M": toBase64(machineId), "X-Apple-I-MD-RINFO": MD_RINFO, "X-Apple-I-SRL-NO": "0", "X-Apple-I-TimeZone": timezone, "X-Apple-Locale": detectLocale(), "X-MMe-Client-Info": this.device.serverFriendlyDescription, "X-Mme-Device-Id": this.device.uniqueDeviceIdentifier, }; } /** Serialize device.json bytes for persistence. */ getDeviceJson(): Uint8Array { return encodeUtf8(JSON.stringify(this.device.toJson(), null, 2)); } /** Expose the device for inspection. */ getDevice(): Device { return this.device; } } function joinPath(base: string, file: string): string { const b = base.endsWith("/") ? base : `${base}/`; return `${b}${file}`; } function normalizeAdiPath(path: string): string { const trimmed = path.trim().replace(/\\/g, "/"); if (!trimmed || trimmed === "." || trimmed === "./" || trimmed === "/") { return "./"; } const noTrail = trimmed.replace(/\/+$/, ""); if (!noTrail || noTrail === ".") { return "./"; } if (noTrail.startsWith("./") || noTrail.startsWith("../")) { return `${noTrail}/`; } if (noTrail.startsWith("/")) { return `.${noTrail}/`; } return `./${noTrail}/`; } function mountIdbfsPaths( bridge: WasmBridge, libraryPath: string, provisioningPath: string ): void { const paths = new Set([libraryPath, provisioningPath]); for (const path of paths) { bridge.initIdbfs(path); } } function readOptionalFile(bridge: WasmBridge, path: string): Uint8Array | null { try { return bridge.readVirtualFile(path); } catch { return null; } } function parseDeviceJsonBytes( bytes: Uint8Array | undefined ): import("./types.js").DeviceJson | null { if (!bytes) { return null; } try { return JSON.parse(decodeUtf8(bytes)) as import("./types.js").DeviceJson; } catch { return null; } } function readDeviceJsonFromVfs( bridge: WasmBridge, path: string ): import("./types.js").DeviceJson | null { const bytes = readOptionalFile(bridge, path); if (!bytes) { return null; } return parseDeviceJsonBytes(bytes); }