feat: fix

This commit is contained in:
2026-02-28 18:50:57 +08:00
parent f1054e6476
commit d0bb6f357e
8 changed files with 11474 additions and 85 deletions

View File

@@ -11,6 +11,7 @@ import {
toAppleClientTime,
detectLocale,
encodeUtf8,
decodeUtf8,
} from "./utils.js";
const DEFAULT_DSID = BigInt(-2);
@@ -91,31 +92,41 @@ export class Anisette {
): Promise<Anisette> {
const bridge = new WasmBridge(wasmModule);
const initOpts = options.init ?? {};
const libraryPath = initOpts.libraryPath ?? DEFAULT_LIBRARY_PATH;
const provisioningPath = initOpts.provisioningPath ?? libraryPath;
const libraryPath = normalizeAdiPath(initOpts.libraryPath ?? DEFAULT_LIBRARY_PATH);
const provisioningPath = normalizeAdiPath(initOpts.provisioningPath ?? libraryPath);
const dsid = options.dsid ?? DEFAULT_DSID;
// Load or generate device config
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);
// 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
}
// Restore adi.pb into VFS if provided
// 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);
}
// Write device.json into WASM VFS
// 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,
initOpts.identifier ?? device.adiIdentifier
identifier
);
const provisioning = new ProvisioningSession(
@@ -124,8 +135,6 @@ export class Anisette {
options.httpClient
);
const identifier = initOpts.identifier ?? device.adiIdentifier;
return new Anisette(bridge, device, provisioning, dsid, provisioningPath, libraryPath, libs, wasmModule, identifier, options.httpClient);
}
@@ -139,6 +148,12 @@ export class Anisette {
/** Run the provisioning flow against Apple servers. */
async provision(): Promise<void> {
await this.provisioning.provision(this.dsid);
// Sync provisioning state to IndexedDB (browser only)
try {
await this.bridge.syncIdbfsToStorage();
} catch {
// Ignore errors in Node.js or if IDBFS unavailable
}
}
/** Read adi.pb from the WASM VFS for persistence. */
@@ -149,12 +164,27 @@ export class Anisette {
/** 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 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);
this.bridge.writeVirtualFile(joinPath(this.provisioningPath, "adi.pb"), adiPb);
mountIdbfsPaths(this.bridge, this.libraryPath, this.provisioningPath);
try {
await this.bridge.syncIdbfsFromStorage();
} catch {
// Ignore errors - might be first run or Node.js
}
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);
@@ -195,3 +225,66 @@ 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);
}

5782
js/src/anisette_rs.js Normal file

File diff suppressed because one or more lines are too long

5452
js/src/anisette_rs.node.js Normal file

File diff suppressed because one or more lines are too long

View File

@@ -215,4 +215,89 @@ export class WasmBridge {
machineId: this.readBytes(midPtr, midLen),
};
}
/**
* 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.m.FS || !this.m.FS.filesystems?.IDBFS) {
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.
*/
async syncIdbfsFromStorage(): Promise<void> {
if (!this.m.FS) {
return; // FS not available
}
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.
*/
async syncIdbfsToStorage(): Promise<void> {
if (!this.m.FS) {
return; // FS not available
}
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;
}
}
}