feat: Enhance Anisette API with WASM file handling and provisioning improvements

This commit is contained in:
2026-02-28 14:19:32 +08:00
parent d05cc41660
commit d75671596c
10 changed files with 110 additions and 99 deletions

View File

@@ -2,7 +2,11 @@
"permissions": {
"allow": [
"Bash(cat:*)",
"Bash(mkdir:*)"
"Bash(mkdir:*)",
"mcp__ide__getDiagnostics",
"Bash(bun run:*)",
"Bash(bun:*)",
"Bash(NODE_TLS_REJECT_UNAUTHORIZED=0 bun:*)"
],
"deny": [],
"ask": []

View File

@@ -31,18 +31,27 @@ const storeservicesPath = args[0];
const coreadiPath = args[1];
const libraryPath = args[2] ?? "./anisette/";
const wasmModule = await loadWasm();
const wasmModule = await loadWasm({ printErr: () => {}});
const storeservices = new Uint8Array(await fs.readFile(storeservicesPath));
const coreadi = new Uint8Array(await fs.readFile(coreadiPath));
const readOptional = (p) => fs.readFile(p).then((b) => new Uint8Array(b)).catch(() => {console.warn(`Optional file not found: ${p}`); return null; });
const [adiPb, deviceJsonBytes] = await Promise.all([
readOptional(path.join(libraryPath, "adi.pb")),
readOptional(path.join(libraryPath, "device.json")),
]);
const anisette = await Anisette.fromSo(storeservices, coreadi, wasmModule, {
init: { libraryPath },
init: { libraryPath, adiPb, deviceJsonBytes },
});
if (!anisette.isProvisioned) {
console.log("Device not provisioned — running provisioning...");
await anisette.provision();
await fs.mkdir(libraryPath, { recursive: true });
await fs.writeFile(path.join(libraryPath, "adi.pb"), anisette.getAdiPb());
await fs.writeFile(path.join(libraryPath, "device.json"), anisette.getDeviceJson());
console.log("Provisioning complete.");
} else {
console.log("Device already provisioned.");
@@ -50,3 +59,6 @@ if (!anisette.isProvisioned) {
const headers = await anisette.getData();
console.log(JSON.stringify(headers, null, 2));
console.log(JSON.stringify(await anisette.getData(), null, 2));

View File

@@ -12,7 +12,7 @@
}
},
"scripts": {
"build": "bun build src/index.ts --outfile dist/anisette.js --target node --format esm --minify",
"build": "bun build src/index.ts --outfile ../dist/anisette.js --target node --format esm --minify-syntax --minify-whitespace",
"build:cjs": "bun build src/index.ts --outfile dist/anisette.cjs --target node --format cjs --minify",
"typecheck": "tsc --noEmit"
},

View File

@@ -1,6 +1,6 @@
// Main Anisette class — the public-facing API
import type { AnisetteDeviceConfig, AnisetteHeaders, InitOptions } from "./types.js";
import type { AnisetteHeaders, InitOptions } from "./types.js";
import type { HttpClient } from "./http.js";
import { WasmBridge } from "./wasm-bridge.js";
import { Device } from "./device.js";
@@ -12,7 +12,6 @@ import {
detectLocale,
encodeUtf8,
} from "./utils.js";
import type { DeviceJson } from "./types.js";
const DEFAULT_DSID = BigInt(-2);
const DEFAULT_LIBRARY_PATH = "./anisette/";
@@ -30,25 +29,39 @@ export interface AnisetteOptions {
export class Anisette {
private bridge: WasmBridge;
private device: Device;
private libs: LibraryStore;
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,
libs: LibraryStore,
provisioning: ProvisioningSession,
dsid: bigint,
libraryPath: string
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.libs = libs;
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 ----
@@ -83,13 +96,19 @@ export class Anisette {
const dsid = options.dsid ?? DEFAULT_DSID;
// Load or generate device config
const device = Device.fromJson(null, initOpts.deviceConfig);
const savedDeviceJson = initOpts.deviceJsonBytes
? (() => { try { return JSON.parse(new TextDecoder().decode(initOpts.deviceJsonBytes)) as import("./types.js").DeviceJson; } catch { return null; } })()
: null;
const device = Device.fromJson(savedDeviceJson, initOpts.deviceConfig);
// Write device.json into WASM VFS so the emulator can read it
const deviceJson = device.toJson();
const deviceJsonBytes = encodeUtf8(JSON.stringify(deviceJson, null, 2));
// Restore adi.pb into VFS if provided
if (initOpts.adiPb) {
bridge.writeVirtualFile(joinPath(provisioningPath, "adi.pb"), initOpts.adiPb);
}
// Write device.json into WASM VFS
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,
@@ -105,58 +124,9 @@ export class Anisette {
options.httpClient
);
return new Anisette(bridge, device, libs, provisioning, dsid, libraryPath);
}
const identifier = initOpts.identifier ?? device.adiIdentifier;
/**
* Load a previously saved session (device.json + adi.pb written back into VFS).
* Pass the saved device.json and adi.pb bytes alongside the library blobs.
*/
static async fromSaved(
storeservicescore: Uint8Array,
coreadi: Uint8Array,
deviceJsonBytes: Uint8Array,
adiPbBytes: Uint8Array,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
wasmModule: any,
options: AnisetteOptions = {}
): Promise<Anisette> {
const bridge = new WasmBridge(wasmModule);
const initOpts = options.init ?? {};
const libraryPath = initOpts.libraryPath ?? DEFAULT_LIBRARY_PATH;
const provisioningPath = initOpts.provisioningPath ?? libraryPath;
const dsid = options.dsid ?? DEFAULT_DSID;
// Parse saved device config
let deviceJson: DeviceJson | null = null;
try {
deviceJson = JSON.parse(new TextDecoder().decode(deviceJsonBytes)) as DeviceJson;
} catch {
// ignore parse errors — will generate fresh device
}
const device = Device.fromJson(deviceJson, initOpts.deviceConfig);
// Restore VFS files
bridge.writeVirtualFile(joinPath(libraryPath, "device.json"), deviceJsonBytes);
bridge.writeVirtualFile(joinPath(libraryPath, "adi.pb"), adiPbBytes);
const libs = LibraryStore.fromBlobs(storeservicescore, coreadi);
bridge.initFromBlobs(
libs.storeservicescore,
libs.coreadi,
libraryPath,
provisioningPath,
initOpts.identifier ?? device.adiIdentifier
);
const provisioning = new ProvisioningSession(
bridge,
device,
options.httpClient
);
return new Anisette(bridge, device, libs, provisioning, dsid, libraryPath);
return new Anisette(bridge, device, provisioning, dsid, provisioningPath, libraryPath, libs, wasmModule, identifier, options.httpClient);
}
// ---- public API ----
@@ -171,8 +141,22 @@ export class Anisette {
await this.provisioning.provision(this.dsid);
}
/** 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<AnisetteHeaders> {
// Reinit WASM state before each call to avoid emulator corruption on repeated use
const adiPb = this.bridge.readVirtualFile(joinPath(this.provisioningPath, "adi.pb"));
const deviceJsonBytes = encodeUtf8(JSON.stringify(this.device.toJson(), null, 2));
this.bridge = new WasmBridge(this.wasmModule);
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();

View File

@@ -33,6 +33,10 @@ export interface InitOptions {
identifier?: string;
/** Override parts of the generated device config */
deviceConfig?: Partial<AnisetteDeviceConfig>;
/** Existing adi.pb bytes to restore into the WASM VFS (for resuming a provisioned session) */
adiPb?: Uint8Array;
/** Existing device.json bytes to restore into the WASM VFS */
deviceJsonBytes?: Uint8Array;
}
/** Raw device.json structure as stored on disk / in WASM VFS */

View File

@@ -100,6 +100,22 @@ export class WasmBridge {
}
}
/**
* 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.
*/

View File

@@ -1,27 +1,7 @@
// Loads the Emscripten WASM glue bundled alongside this file.
// The .wasm binary is resolved relative to this JS file at runtime.
// @ts-ignore — glue file is generated, no types available
// @ts-expect-error — glue file is generated, no types available
import ModuleFactory from "../../dist/anisette_rs.node.js";
import { createRequire } from "node:module";
import { fileURLToPath } from "node:url";
import path from "node:path";
// Resolve the .wasm file next to the bundled output JS
function resolveWasmPath(outputFile: string): string {
// __filename of the *bundled* output — bun sets import.meta.url correctly
const dir = path.dirname(fileURLToPath(import.meta.url));
return path.join(dir, outputFile);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export async function loadWasm(): Promise<any> {
return ModuleFactory({
locateFile(file: string) {
if (file.endsWith(".wasm")) {
return resolveWasmPath("anisette_rs.node.wasm");
}
return file;
},
});
export async function loadWasm(moduleOverrides?: Record<string, any>): Promise<any> {
return ModuleFactory({ ...moduleOverrides });
}

View File

@@ -3,9 +3,9 @@
"scripts": {
"build:unicorn": "bash script/rebuild-unicorn.sh",
"build:glue": "bash script/build-glue.sh",
"build:release": "bash script/build-glue.sh --release",
"build:js": "cd js && bun install && bun run build",
"build": "bash script/build-glue.sh && cd js && bun install && bun run build",
"release": "bash script/build-glue.sh --release && cd js && bun install && bun run build",
"run:node": "node example/run-node.mjs",
"run:api": "node example/anisette-api.mjs"
}

View File

@@ -16,8 +16,8 @@ NODE_DIST_WASM="${DIST_DIR}/anisette_rs.node.wasm"
WEB_EXPORTED_FUNCTIONS='["_malloc","_free","_anisette_init_from_blobs","_anisette_is_machine_provisioned","_anisette_start_provisioning","_anisette_end_provisioning","_anisette_request_otp","_anisette_get_cpim_ptr","_anisette_get_cpim_len","_anisette_get_session","_anisette_get_otp_ptr","_anisette_get_otp_len","_anisette_get_mid_ptr","_anisette_get_mid_len","_anisette_last_error_ptr","_anisette_last_error_len","_anisette_fs_write_file","_anisette_idbfs_init","_anisette_idbfs_sync","_anisette_set_identifier","_anisette_set_provisioning_path"]'
NODE_EXPORTED_FUNCTIONS='["_malloc","_free","_anisette_init_from_blobs","_anisette_is_machine_provisioned","_anisette_start_provisioning","_anisette_end_provisioning","_anisette_request_otp","_anisette_get_cpim_ptr","_anisette_get_cpim_len","_anisette_get_session","_anisette_get_otp_ptr","_anisette_get_otp_len","_anisette_get_mid_ptr","_anisette_get_mid_len","_anisette_last_error_ptr","_anisette_last_error_len","_anisette_fs_write_file","_anisette_set_identifier","_anisette_set_provisioning_path"]'
WEB_EXPORTED_FUNCTIONS='["_malloc","_free","_anisette_init_from_blobs","_anisette_is_machine_provisioned","_anisette_start_provisioning","_anisette_end_provisioning","_anisette_request_otp","_anisette_get_cpim_ptr","_anisette_get_cpim_len","_anisette_get_session","_anisette_get_otp_ptr","_anisette_get_otp_len","_anisette_get_mid_ptr","_anisette_get_mid_len","_anisette_last_error_ptr","_anisette_last_error_len","_anisette_fs_write_file","_anisette_fs_read_file","_anisette_fs_read_ptr","_anisette_fs_read_len","_anisette_idbfs_sync","_anisette_set_identifier","_anisette_set_provisioning_path"]'
NODE_EXPORTED_FUNCTIONS='["_malloc","_free","_anisette_init_from_blobs","_anisette_is_machine_provisioned","_anisette_start_provisioning","_anisette_end_provisioning","_anisette_request_otp","_anisette_get_cpim_ptr","_anisette_get_cpim_len","_anisette_get_session","_anisette_get_otp_ptr","_anisette_get_otp_len","_anisette_get_mid_ptr","_anisette_get_mid_len","_anisette_last_error_ptr","_anisette_last_error_len","_anisette_fs_write_file","_anisette_fs_read_file","_anisette_fs_read_ptr","_anisette_fs_read_len","_anisette_set_identifier","_anisette_set_provisioning_path"]'
WEB_EXPORTED_RUNTIME_METHODS='["FS","HEAPU8","UTF8ToString","stringToUTF8","lengthBytesUTF8"]'
NODE_EXPORTED_RUNTIME_METHODS='["HEAPU8","UTF8ToString","stringToUTF8","lengthBytesUTF8"]'
@@ -103,7 +103,7 @@ if command -v bun >/dev/null 2>&1 && [[ -f "${JS_DIR}/src/index.ts" ]]; then
--outfile "${DIST_DIR}/anisette.js" \
--target node \
--format esm \
--minify
--minify-syntax --minify-whitespace
echo " ${DIST_DIR}/anisette.js"
fi

View File

@@ -3,7 +3,7 @@ use std::ffi::{CStr, c_char};
use std::fs;
use std::path::Path;
use crate::{Adi, AdiInit, init_idbfs_for_path, sync_idbfs};
use crate::{Adi, AdiInit, sync_idbfs};
#[derive(Default)]
struct ExportState {
@@ -13,6 +13,7 @@ struct ExportState {
session: u32,
otp: Vec<u8>,
mid: Vec<u8>,
read_buf: Vec<u8>,
}
thread_local! {
@@ -407,15 +408,15 @@ pub extern "C" fn anisette_fs_write_file(
}
#[unsafe(no_mangle)]
pub extern "C" fn anisette_idbfs_init(path: *const c_char) -> i32 {
let result = (|| -> Result<(), String> {
pub extern "C" fn anisette_fs_read_file(path: *const c_char) -> i32 {
let result = (|| -> Result<Vec<u8>, String> {
let path = unsafe { c_string(path)? };
init_idbfs_for_path(&path)?;
Ok(())
fs::read(&path).map_err(|e| format!("failed to read '{path}': {e}"))
})();
match result {
Ok(()) => {
Ok(data) => {
STATE.with(|state| state.borrow_mut().read_buf = data);
clear_last_error();
0
}
@@ -426,6 +427,16 @@ pub extern "C" fn anisette_idbfs_init(path: *const c_char) -> i32 {
}
}
#[unsafe(no_mangle)]
pub extern "C" fn anisette_fs_read_ptr() -> *const u8 {
STATE.with(|state| state.borrow().read_buf.as_ptr())
}
#[unsafe(no_mangle)]
pub extern "C" fn anisette_fs_read_len() -> usize {
STATE.with(|state| state.borrow().read_buf.len())
}
#[unsafe(no_mangle)]
pub extern "C" fn anisette_idbfs_sync(populate_from_storage: i32) -> i32 {
let result = sync_idbfs(populate_from_storage != 0);