feat: Enhance Anisette API with WASM file handling and provisioning improvements
This commit is contained in:
@@ -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": []
|
||||
|
||||
@@ -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));
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user