7 Commits
v0.0.0 ... main

Author SHA1 Message Date
f9f4892ef3 feat: update WASM loading in initLibcurl and enhance error logging in Anisette class 2026-03-02 10:31:45 +08:00
33db721df7 feat: update package name and improve build workflow for versioned releases 2026-03-01 16:11:58 +08:00
eaa30795db feat: add IDBFS availability check and improve error handling in Anisette and WasmBridge 2026-03-01 16:00:43 +08:00
d0bb6f357e feat: fix 2026-02-28 18:50:57 +08:00
f1054e6476 feat: frontend support. 2026-02-28 18:44:57 +08:00
d75671596c feat: Enhance Anisette API with WASM file handling and provisioning improvements 2026-02-28 14:19:32 +08:00
d05cc41660 feat: Implement Anisette JS/TS API with WASM support
- Added main Anisette class for high-level API.
- Introduced device management with Device class.
- Created HTTP client abstraction for network requests.
- Implemented provisioning session handling with ProvisioningSession class.
- Added utility functions for encoding, decoding, and random generation.
- Established library management with LibraryStore class.
- Integrated WASM loading and bridging with WasmBridge.
- Defined core types and interfaces for the API.
- Set up TypeScript configuration and build scripts.
- Updated package.json for new build and run commands.
- Added bun.lock and package.json for JS dependencies.
- Enhanced error handling and memory management in Rust code.
2026-02-28 12:32:37 +08:00
44 changed files with 19695 additions and 180 deletions

View File

@@ -1,10 +0,0 @@
{
"permissions": {
"allow": [
"Bash(cat:*)",
"Bash(mkdir:*)"
],
"deny": [],
"ask": []
}
}

View File

@@ -3,6 +3,7 @@ name: Build WASM
on: on:
push: push:
branches: [main] branches: [main]
tags: ['v*']
pull_request: pull_request:
branches: [main] branches: [main]
workflow_dispatch: workflow_dispatch:
@@ -94,3 +95,22 @@ jobs:
dist/anisette_rs.node.js dist/anisette_rs.node.js
dist/anisette_rs.node.wasm dist/anisette_rs.node.wasm
generate_release_notes: true generate_release_notes: true
- name: Setup Node.js
if: startsWith(github.ref, 'refs/tags/v')
uses: actions/setup-node@v4
with:
node-version: '20'
registry-url: 'https://npm.pkg.github.com'
- name: Build TypeScript
if: startsWith(github.ref, 'refs/tags/v')
working-directory: js/
run: npm run build
- name: Publish to GitHub Packages
if: startsWith(github.ref, 'refs/tags/v')
working-directory: js/
run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

7
.gitignore vendored
View File

@@ -1,3 +1,10 @@
target/ target/
test/ test/
dist/ dist/
cache/
*.apk
anisette/*
js/node_modules/*
*.so
*.wasm
frontend/.vscode/extensions.json

103
AGENTS.md
View File

@@ -1,91 +1,62 @@
# AGENTS.md # CLAUDE.md
This file provides guidance to Every Agents when working with code in this repository. This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Overview ## Overview
This crate provides Apple Anisette authentication in browser via WebAssembly. It uses a custom Unicorn Engine fork (https://github.com/lbr77/unicorn/tree/tci-emscripten) to emulate ARM64 Android binaries (`libstoreservicescore.so` + `libCoreADI.so`) for generating Anisette headers. Rust/WASM project that emulates ARM64 Android binaries (`libstoreservicescore.so` + `libCoreADI.so`) via a custom Unicorn Engine fork to generate Apple Anisette headers in the browser or Node.js.
**Note**: The Android library blobs (`libstoreservicescore.so`, `libCoreADI.so`) are not included in this repository. Extract them from an APK or obtain separately. The Android library blobs are not included — extract from an APK or obtain separately.
## Build Commands ## Build Commands
### Prerequisites Must source Emscripten before building WASM:
- Rust nightly (edition 2024)
- Emscripten SDK (for WASM builds)
### Setup Unicorn Engine
Clone the custom Unicorn repository and checkout to the `tci-emscripten` branch:
```bash ```bash
git clone https://github.com/lbr77/unicorn.git source "/Users/libr/Desktop/Life/emsdk/emsdk_env.sh"
cd unicorn && git checkout tci-emscripten
``` ```
Then build Unicorn for Emscripten:
```bash ```bash
bash script/rebuild-unicorn.sh bun run build # WASM (debug) + JS bundle
bun run release # WASM (release) + JS bundle
bun run build:js # JS bundle only (no WASM rebuild)
bun run build:glue # WASM only
bun run build:unicorn # Rebuild Unicorn (rarely needed)
``` ```
The rebuild script handles: JS bundle outputs to `dist/anisette.js`. WASM glue outputs to `dist/anisette_rs.node.{js,wasm}` and `dist/anisette_rs.{js,wasm}`.
- Running `emcmake cmake` with appropriate flags
- Building only `arm` and `aarch64` architectures
- Using static archives (`-DUNICORN_LEGACY_STATIC_ARCHIVE=ON`)
### Build WASM Glue The `js/package.json` build script also outputs to `../dist/anisette.js` directly.
```bash
bash script/build-glue.sh # Debug build
bash script/build-glue.sh --release # Release build
```
Outputs:
- `test/dist/anisette_rs.js` / `.wasm` (web)
- `test/dist/anisette_rs.node.js` / `.wasm` (Node.js)
- Copies to `../../frontend/public/anisette/`
### Run Native Example
```bash
cargo run --example anisette -- <libstoreservicescore.so> <libCoreADI.so> [library_path] [dsid] [apple_root_pem]
```
### Run Node.js Example
```bash
node example/run-node.mjs <libstoreservicescore.so> <libCoreADI.so> [library_path] [dsid] [identifier]
```
## Architecture ## Architecture
### Core Modules ### Rust → WASM layer (`src/`)
- **`adi.rs`**: ADI (Apple Device Identity) wrapper — provisioning, OTP requests - `exports.rs` — all `#[no_mangle]` C FFI exports. Every new public function must also be added to `EXPORTED_FUNCTIONS` in `script/build-glue.sh` (both `WEB_EXPORTED_FUNCTIONS` and `NODE_EXPORTED_FUNCTIONS` as appropriate).
- **`emu.rs`**: Unicorn-based ARM64 emulator core — library loading, symbol resolution, function calls - `adi.rs` — wraps the emulated ADI library calls
- **`exports.rs`**: C FFI exports for WASM — `anisette_*` functions - `emu.rs` — Unicorn ARM64 emulator core
- **`device.rs`**: Device identity management — UUIDs, identifiers, persistence - `idbfs.rs` — Emscripten IndexedDB FS integration (browser only)
- **`idbfs.rs`**: IndexedDB filesystem integration for Emscripten
- **`provisioning.rs`** / **`provisioning_wasm.rs`**: Apple provisioning protocol
### Emulator Memory Layout ### JS/TS layer (`js/src/`)
- Libraries mapped to import address space with stub hooks - `anisette.ts` — main `Anisette` class. **Each `getData()` call fully reinits the WASM state** (new `WasmBridge`, re-writes VFS files, re-calls `initFromBlobs`) to work around a Unicorn emulator bug that causes illegal writes on repeated use.
- Stack, heap, and return addresses pre-allocated - `wasm-bridge.ts` — raw pointer/length marshalling to WASM exports
- Import hooks dispatch to runtime stubs - `wasm-loader.ts` — thin wrapper around `ModuleFactory`; caller must pass `locateFile` via `moduleOverrides` to resolve the `.wasm` path
- `provisioning.ts` — Apple provisioning HTTP flow (fetches SPIM, sends CPIM)
- `device.ts` — loads or generates `device.json`
### Public API (exports.rs) ### Key design decisions
- `anisette_init_from_blobs` — Initialize from library blobs - `adi.pb` (provisioning state) lives in the WASM VFS. After provisioning, call `anisette.getAdiPb()` and persist it yourself — it is **not** automatically written to disk.
- `anisette_is_machine_provisioned` — Check provisioning state - `fromSo()` accepts `init.adiPb` and `init.deviceJsonBytes` to restore a previous session into the VFS before init.
- `anisette_start_provisioning` / `anisette_end_provisioning` — Provisioning flow - `loadWasm()` is environment-agnostic — no `node:` imports. Pass `locateFile` in `moduleOverrides`.
- `anisette_request_otp` — Generate OTP + machine ID headers - Browser FS/path gotcha:
- Prefer `./anisette/` (not `/anisette`) for `libraryPath` / `provisioningPath`.
- The Rust emulation stubs currently hardcode `./anisette` and `./anisette/adi.pb` checks in `src/stub.rs` (`mkdir`/`open`), so absolute paths can break provisioning (e.g. `ADIProvisioningEnd -45054`).
- In browser flow, mount IDBFS and run `syncfs(true)` before ADI init to avoid VFS state being overwritten later.
### Data Flow ### Example usage
1. Load Android `libstoreservicescore.so` + `libCoreADI.so` ```bash
2. Initialize device identity (`device.json`) NODE_TLS_REJECT_UNAUTHORIZED=0 bun example/anisette-api.mjs \
3. Provision with Apple (if needed) <libstoreservicescore.so> <libCoreADI.so> [library_path]
4. Request OTP → `X-Apple-I-MD` + `X-Apple-I-MD-M` headers ```

103
CLAUDE.md
View File

@@ -1,91 +1,62 @@
# AGENTS.md # CLAUDE.md
This file provides guidance to Every Agents when working with code in this repository. This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Overview ## Overview
This crate provides Apple Anisette authentication in browser via WebAssembly. It uses a custom Unicorn Engine fork (https://github.com/lbr77/unicorn/tree/tci-emscripten) to emulate ARM64 Android binaries (`libstoreservicescore.so` + `libCoreADI.so`) for generating Anisette headers. Rust/WASM project that emulates ARM64 Android binaries (`libstoreservicescore.so` + `libCoreADI.so`) via a custom Unicorn Engine fork to generate Apple Anisette headers in the browser or Node.js.
**Note**: The Android library blobs (`libstoreservicescore.so`, `libCoreADI.so`) are not included in this repository. Extract them from an APK or obtain separately. The Android library blobs are not included — extract from an APK or obtain separately.
## Build Commands ## Build Commands
### Prerequisites Must source Emscripten before building WASM:
- Rust nightly (edition 2024)
- Emscripten SDK (for WASM builds)
### Setup Unicorn Engine
Clone the custom Unicorn repository and checkout to the `tci-emscripten` branch:
```bash ```bash
git clone https://github.com/lbr77/unicorn.git source "/Users/libr/Desktop/Life/emsdk/emsdk_env.sh"
cd unicorn && git checkout tci-emscripten
``` ```
Then build Unicorn for Emscripten:
```bash ```bash
bash script/rebuild-unicorn.sh bun run build # WASM (debug) + JS bundle
bun run release # WASM (release) + JS bundle
bun run build:js # JS bundle only (no WASM rebuild)
bun run build:glue # WASM only
bun run build:unicorn # Rebuild Unicorn (rarely needed)
``` ```
The rebuild script handles: JS bundle outputs to `dist/anisette.js`. WASM glue outputs to `dist/anisette_rs.node.{js,wasm}` and `dist/anisette_rs.{js,wasm}`.
- Running `emcmake cmake` with appropriate flags
- Building only `arm` and `aarch64` architectures
- Using static archives (`-DUNICORN_LEGACY_STATIC_ARCHIVE=ON`)
### Build WASM Glue The `js/package.json` build script also outputs to `../dist/anisette.js` directly.
```bash
bash script/build-glue.sh # Debug build
bash script/build-glue.sh --release # Release build
```
Outputs:
- `test/dist/anisette_rs.js` / `.wasm` (web)
- `test/dist/anisette_rs.node.js` / `.wasm` (Node.js)
- Copies to `../../frontend/public/anisette/`
### Run Native Example
```bash
cargo run --example anisette -- <libstoreservicescore.so> <libCoreADI.so> [library_path] [dsid] [apple_root_pem]
```
### Run Node.js Example
```bash
node example/run-node.mjs <libstoreservicescore.so> <libCoreADI.so> [library_path] [dsid] [identifier]
```
## Architecture ## Architecture
### Core Modules ### Rust → WASM layer (`src/`)
- **`adi.rs`**: ADI (Apple Device Identity) wrapper — provisioning, OTP requests - `exports.rs` — all `#[no_mangle]` C FFI exports. Every new public function must also be added to `EXPORTED_FUNCTIONS` in `script/build-glue.sh` (both `WEB_EXPORTED_FUNCTIONS` and `NODE_EXPORTED_FUNCTIONS` as appropriate).
- **`emu.rs`**: Unicorn-based ARM64 emulator core — library loading, symbol resolution, function calls - `adi.rs` — wraps the emulated ADI library calls
- **`exports.rs`**: C FFI exports for WASM — `anisette_*` functions - `emu.rs` — Unicorn ARM64 emulator core
- **`device.rs`**: Device identity management — UUIDs, identifiers, persistence - `idbfs.rs` — Emscripten IndexedDB FS integration (browser only)
- **`idbfs.rs`**: IndexedDB filesystem integration for Emscripten
- **`provisioning.rs`** / **`provisioning_wasm.rs`**: Apple provisioning protocol
### Emulator Memory Layout ### JS/TS layer (`js/src/`)
- Libraries mapped to import address space with stub hooks - `anisette.ts` — main `Anisette` class. **Each `getData()` call fully reinits the WASM state** (new `WasmBridge`, re-writes VFS files, re-calls `initFromBlobs`) to work around a Unicorn emulator bug that causes illegal writes on repeated use.
- Stack, heap, and return addresses pre-allocated - `wasm-bridge.ts` — raw pointer/length marshalling to WASM exports
- Import hooks dispatch to runtime stubs - `wasm-loader.ts` — thin wrapper around `ModuleFactory`; caller must pass `locateFile` via `moduleOverrides` to resolve the `.wasm` path
- `provisioning.ts` — Apple provisioning HTTP flow (fetches SPIM, sends CPIM)
- `device.ts` — loads or generates `device.json`
### Public API (exports.rs) ### Key design decisions
- `anisette_init_from_blobs` — Initialize from library blobs - `adi.pb` (provisioning state) lives in the WASM VFS. After provisioning, call `anisette.getAdiPb()` and persist it yourself — it is **not** automatically written to disk.
- `anisette_is_machine_provisioned` — Check provisioning state - `fromSo()` accepts `init.adiPb` and `init.deviceJsonBytes` to restore a previous session into the VFS before init.
- `anisette_start_provisioning` / `anisette_end_provisioning` — Provisioning flow - `loadWasm()` is environment-agnostic — no `node:` imports. Pass `locateFile` in `moduleOverrides`.
- `anisette_request_otp` — Generate OTP + machine ID headers - Browser FS/path gotcha:
- Prefer `./anisette/` (not `/anisette`) for `libraryPath` / `provisioningPath`.
- The Rust emulation stubs currently hardcode `./anisette` and `./anisette/adi.pb` checks in `src/stub.rs` (`mkdir`/`open`), so absolute paths can break provisioning (e.g. `ADIProvisioningEnd -45054`).
- In browser flow, mount IDBFS and run `syncfs(true)` before ADI init to avoid VFS state being overwritten later.
### Data Flow ### Example usage
1. Load Android `libstoreservicescore.so` + `libCoreADI.so` ```bash
2. Initialize device identity (`device.json`) NODE_TLS_REJECT_UNAUTHORIZED=0 bun example/anisette-api.mjs \
3. Provision with Apple (if needed) <libstoreservicescore.so> <libCoreADI.so> [library_path]
4. Request OTP → `X-Apple-I-MD` + `X-Apple-I-MD-M` headers ```

1916
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

138
README.md
View File

@@ -1,18 +1,134 @@
# anisette.js # anisette-js
use anisette on browser locally! no more third-party server worries. Apple Anisette authentication in browser via WebAssembly. Emulates ARM64 Android binaries to generate Anisette headers locally no third-party servers required.
## usage ## Features
see examples/ - **Local execution**: All computation happens in your browser or Node.js process
- **WASM-based**: Uses Unicorn Engine compiled to WebAssembly for ARM64 emulation
- **High-level JS/TS API**: Simple async interface, handles provisioning automatically
- **Single-file bundle**: Distribute as one `.js` + one `.wasm` file
should download apple music apk from [here](https://apps.mzstatic.com/content/android-apple-music-apk/applemusic.apk) and unzip to get arm64 abi. ## Prerequisites
- Rust nightly (for building the WASM module)
- Emscripten SDK
- Bun (for bundling the TypeScript API)
Android library blobs (`libstoreservicescore.so`, `libCoreADI.so`) are not included. Extract them from an Apple Music APK or obtain separately.
## Build
```bash
# Clone and build custom Unicorn fork
git clone https://github.com/lbr77/unicorn.git
cd unicorn && git checkout tci-emscripten
# Build everything (WASM + TS API bundle)
bash script/build-glue.sh
# Or build just the JS bundle (WASM already built)
npm run build:js
```
Output files in `dist/`:
- `anisette.js` — bundled TS API + glue (single file)
- `anisette_rs.node.wasm` — WASM binary (required alongside `.js`)
## Usage
### Node.js
```javascript
import { Anisette, loadWasm } from "./dist/anisette.js";
import fs from "node:fs/promises";
const wasmModule = await loadWasm();
const storeservices = new Uint8Array(await fs.readFile("libstoreservicescore.so"));
const coreadi = new Uint8Array(await fs.readFile("libCoreADI.so"));
const anisette = await Anisette.fromSo(storeservices, coreadi, wasmModule);
if (!anisette.isProvisioned) {
await anisette.provision();
}
const headers = await anisette.getData();
console.log(headers["X-Apple-I-MD"]);
```
Run the example:
```bash
node example/anisette-api.mjs libstoreservicescore.so libCoreADI.so ./anisette/
```
### Browser
For browser usage, use the web-targeted WASM build (`anisette_rs.js` / `.wasm`) and import directly:
```javascript
import ModuleFactory from "./anisette_rs.js";
const wasmModule = await ModuleFactory({
locateFile: (f) => f.endsWith(".wasm") ? "./anisette_rs.wasm" : f
});
// Use WasmBridge for low-level access, or wrap with the TS API
```
## API Reference
### `Anisette`
Main class for generating Anisette headers.
**Static methods:**
- `Anisette.fromSo(storeservicescore, coreadi, wasmModule, options?)` — Initialize from library blobs
- `Anisette.fromSaved(ss, ca, deviceJson, adiPb, wasmModule, options?)` — Restore a saved session
**Instance properties:**
- `isProvisioned: boolean` — Whether the device is provisioned
**Instance methods:**
- `provision()` — Run Apple provisioning flow
- `getData(): Promise<AnisetteHeaders>` — Generate Anisette headers
- `getDeviceJson(): Uint8Array` — Serialize device config for persistence
### `loadWasm()`
Loads the WASM module. In Node.js, resolves `.wasm` path relative to the bundle location.
```javascript
import { loadWasm } from "./dist/anisette.js";
const wasmModule = await loadWasm();
```
## Architecture
- **Rust/WASM core** (`src/`): Emulator, ADI wrapper, provisioning protocol
- **TypeScript API** (`js/src/`): High-level wrapper around WASM exports
- **Emscripten glue**: Bridges JS and WASM memory, handles VFS
Key modules:
- `adi.rs` — ADI (Apple Device Identity) provisioning and OTP
- `emu.rs` — Unicorn-based ARM64 emulator
- `exports.rs` — C FFI exports for WASM
- `js/src/anisette.ts` — Main `Anisette` class
- `js/src/wasm-bridge.ts` — Low-level WASM memory management
## Credits
- [pyprovision-uc](https://github.com/JayFoxRox/pyprovision-uc)
- [Anisette.py](https://github.com/malmeloo/Anisette.py)
- [omnisette-server](https://github.com/SideStore/omnisette-server)
- [unicorn](https://github.com/petabyt/unicorn/tree/tci-emscripten)
## credits ## Known Issue:
[pyprovision-uc](https://github.com/JayFoxRox/pyprovision-uc) when requiring Otp for second time there will be a "WRITE UNMAPPED" error which could be avoided by initalizing onemoretime...
[Anisette.py](https://github.com/malmeloo/Anisette.py)
[omnisette-server](https://github.com/SideStore/omnisette-server)

65
example/anisette-api.mjs Normal file
View File

@@ -0,0 +1,65 @@
/**
* Example: using the high-level Anisette JS API (Node.js)
*
* Usage:
* node example/anisette-api.mjs <libstoreservicescore.so> <libCoreADI.so> [library_path]
*/
import fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const {Anisette, loadWasm} = await import("../js/dist");
// const bundlePath = path.join(__dirname, "..", "dist", "anisette.js");
// const { Anisette, loadWasm } = await import(
// pathToFileURL(bundlePath).href
// ).catch(() => {
// console.error("Bundle not found. Run: npm run build:js");
// process.exit(1);
// });
const args = process.argv.slice(2);
if (args.length < 2) {
console.error(
"usage: node example/anisette-api.mjs <libstoreservicescore.so> <libCoreADI.so> [library_path]"
);
process.exit(1);
}
const storeservicesPath = args[0];
const coreadiPath = args[1];
const libraryPath = args[2] ?? "./anisette/";
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, 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.");
}
const headers = await anisette.getData();
console.log(JSON.stringify(headers, null, 2));
console.log(JSON.stringify(await anisette.getData(), null, 2));

1454
example/index.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@ import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url'; import { fileURLToPath, pathToFileURL } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url)); const __dirname = path.dirname(fileURLToPath(import.meta.url));
const distDir = path.join(__dirname, 'dist'); const distDir = path.join(path.join(__dirname, '..'), 'dist')
const modulePath = path.join(distDir, 'anisette_rs.node.js'); const modulePath = path.join(distDir, 'anisette_rs.node.js');
const wasmPath = path.join(distDir, 'anisette_rs.node.wasm'); const wasmPath = path.join(distDir, 'anisette_rs.node.wasm');

24
frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

5
frontend/README.md Normal file
View File

@@ -0,0 +1,5 @@
# Vue 3 + Vite
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).

197
frontend/bun.lock Normal file
View File

@@ -0,0 +1,197 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "frontend",
"dependencies": {
"anisette-js": "file:../js",
"libcurl.js": "^0.7.4",
"vue": "^3.5.25",
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.2",
"vite": "^7.3.1",
},
},
},
"packages": {
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
"@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="],
"@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="],
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="],
"@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="],
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="],
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="],
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="],
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="],
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="],
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="],
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="],
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="],
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="],
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="],
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="],
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="],
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="],
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="],
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="],
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="],
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="],
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="],
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="],
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="],
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="],
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.2", "", {}, "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="],
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.59.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg=="],
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.59.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w=="],
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.59.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA=="],
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.59.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg=="],
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw=="],
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA=="],
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA=="],
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA=="],
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg=="],
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q=="],
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA=="],
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA=="],
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg=="],
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg=="],
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.59.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w=="],
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg=="],
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg=="],
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.59.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ=="],
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.59.0", "", { "os": "none", "cpu": "arm64" }, "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA=="],
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.59.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A=="],
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.59.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA=="],
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA=="],
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/node": ["@types/node@25.3.2", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-RpV6r/ij22zRRdyBPcxDeKAzH43phWVKEjL2iksqo1Vz3CuBUrgmPpPhALKiRfU7OMCmeeO9vECBMsV0hMTG8Q=="],
"@vitejs/plugin-vue": ["@vitejs/plugin-vue@6.0.4", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.2" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0", "vue": "^3.2.25" } }, "sha512-uM5iXipgYIn13UUQCZNdWkYk+sysBeA97d5mHsAoAt1u/wpN3+zxOmsVJWosuzX+IMGRzeYUNytztrYznboIkQ=="],
"@vue/compiler-core": ["@vue/compiler-core@3.5.29", "", { "dependencies": { "@babel/parser": "^7.29.0", "@vue/shared": "3.5.29", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-cuzPhD8fwRHk8IGfmYaR4eEe4cAyJEL66Ove/WZL7yWNL134nqLddSLwNRIsFlnnW1kK+p8Ck3viFnC0chXCXw=="],
"@vue/compiler-dom": ["@vue/compiler-dom@3.5.29", "", { "dependencies": { "@vue/compiler-core": "3.5.29", "@vue/shared": "3.5.29" } }, "sha512-n0G5o7R3uBVmVxjTIYcz7ovr8sy7QObFG8OQJ3xGCDNhbG60biP/P5KnyY8NLd81OuT1WJflG7N4KWYHaeeaIg=="],
"@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.29", "", { "dependencies": { "@babel/parser": "^7.29.0", "@vue/compiler-core": "3.5.29", "@vue/compiler-dom": "3.5.29", "@vue/compiler-ssr": "3.5.29", "@vue/shared": "3.5.29", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.6", "source-map-js": "^1.2.1" } }, "sha512-oJZhN5XJs35Gzr50E82jg2cYdZQ78wEwvRO6Y63TvLVTc+6xICzJHP1UIecdSPPYIbkautNBanDiWYa64QSFIA=="],
"@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.29", "", { "dependencies": { "@vue/compiler-dom": "3.5.29", "@vue/shared": "3.5.29" } }, "sha512-Y/ARJZE6fpjzL5GH/phJmsFwx3g6t2KmHKHx5q+MLl2kencADKIrhH5MLF6HHpRMmlRAYBRSvv347Mepf1zVNw=="],
"@vue/reactivity": ["@vue/reactivity@3.5.29", "", { "dependencies": { "@vue/shared": "3.5.29" } }, "sha512-zcrANcrRdcLtmGZETBxWqIkoQei8HaFpZWx/GHKxx79JZsiZ8j1du0VUJtu4eJjgFvU/iKL5lRXFXksVmI+5DA=="],
"@vue/runtime-core": ["@vue/runtime-core@3.5.29", "", { "dependencies": { "@vue/reactivity": "3.5.29", "@vue/shared": "3.5.29" } }, "sha512-8DpW2QfdwIWOLqtsNcds4s+QgwSaHSJY/SUe04LptianUQ/0xi6KVsu/pYVh+HO3NTVvVJjIPL2t6GdeKbS4Lg=="],
"@vue/runtime-dom": ["@vue/runtime-dom@3.5.29", "", { "dependencies": { "@vue/reactivity": "3.5.29", "@vue/runtime-core": "3.5.29", "@vue/shared": "3.5.29", "csstype": "^3.2.3" } }, "sha512-AHvvJEtcY9tw/uk+s/YRLSlxxQnqnAkjqvK25ZiM4CllCZWzElRAoQnCM42m9AHRLNJ6oe2kC5DCgD4AUdlvXg=="],
"@vue/server-renderer": ["@vue/server-renderer@3.5.29", "", { "dependencies": { "@vue/compiler-ssr": "3.5.29", "@vue/shared": "3.5.29" }, "peerDependencies": { "vue": "3.5.29" } }, "sha512-G/1k6WK5MusLlbxSE2YTcqAAezS+VuwHhOvLx2KnQU7G2zCH6KIb+5Wyt6UjMq7a3qPzNEjJXs1hvAxDclQH+g=="],
"@vue/shared": ["@vue/shared@3.5.29", "", {}, "sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg=="],
"anisette-js": ["anisette-js@file:../js", { "dependencies": { "@types/node": "^25.3.2" }, "devDependencies": { "typescript": "^5.4.0" } }],
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
"entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
"esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="],
"estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"libcurl.js": ["libcurl.js@0.7.4", "", {}, "sha512-UpvVirvATP7fD0t4rnsxVRuUpPVIo2QvWj4+5JrMsd1KSEvYkON36+COOPAl88hPlGJddk+DfZRvyF7KG7YcSA=="],
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
"rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
"vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="],
"vue": ["vue@3.5.29", "", { "dependencies": { "@vue/compiler-dom": "3.5.29", "@vue/compiler-sfc": "3.5.29", "@vue/runtime-dom": "3.5.29", "@vue/server-renderer": "3.5.29", "@vue/shared": "3.5.29" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-BZqN4Ze6mDQVNAni0IHeMJ5mwr8VAJ3MQC9FmprRhcBYENw+wOAAjRj8jfmN6FLl0j96OXbR+CjWhmAmM+QGnA=="],
}
}

12
frontend/index.html Normal file
View File

@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Anisette JS Demo</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

20
frontend/package.json Normal file
View File

@@ -0,0 +1,20 @@
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.5.25",
"anisette-js": "file:../js",
"libcurl.js": "^0.7.4"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.2",
"vite": "^7.3.1"
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

66
frontend/src/App.vue Normal file
View File

@@ -0,0 +1,66 @@
<script setup>
import { ref } from 'vue'
import { Anisette, loadWasmModule } from 'anisette-js'
import { initLibcurl } from './libcurl-init'
import { LibcurlHttpClient } from './libcurl-http'
const status = ref('Ready')
const headers = ref(null)
async function runDemo() {
try {
status.value = 'Loading libcurl...'
await initLibcurl()
const httpClient = new LibcurlHttpClient()
status.value = 'Loading WASM...'
const wasmModule = await loadWasmModule({ printErr: () => {}})
status.value = 'Loading library files...'
const [ssResp, caResp] = await Promise.all([
fetch('/libstoreservicescore.so'),
fetch('/libCoreADI.so')
])
if (!ssResp.ok || !caResp.ok) {
throw new Error('Failed to load .so files from public directory')
}
const ssBytes = new Uint8Array(await ssResp.arrayBuffer())
const caBytes = new Uint8Array(await caResp.arrayBuffer())
status.value = 'Initializing Anisette...'
const anisette = await Anisette.fromSo(ssBytes, caBytes, wasmModule, {
httpClient,
init: { libraryPath: './anisette/' }
})
if (!anisette.isProvisioned) {
status.value = 'Provisioning...'
await anisette.provision()
} else {
status.vaule = 'Already provisioned, skipping provisioning step'
}
status.value = 'Getting headers...'
headers.value = await anisette.getData()
status.value = 'Done'
} catch (err) {
status.value = `Error: ${err.message}`
console.error(err)
}
}
</script>
<template>
<div>
<h1>Anisette Vue Demo</h1>
<div>
<button @click="runDemo">Run</button>
</div>
<p>Status: {{ status }}</p>
<pre v-if="headers">{{ JSON.stringify(headers, null, 2) }}</pre>
</div>
</template>

View File

@@ -0,0 +1,28 @@
// Libcurl-based HTTP client for browser
import type { HttpClient } from "anisette-js";
import { libcurl } from "./libcurl-init";
export class LibcurlHttpClient implements HttpClient {
async get(url: string, headers: Record<string, string>): Promise<Uint8Array> {
// @ts-ignore
const response = (await libcurl.fetch(url, { method: "GET", headers, insecure: true })) as Response;
if (!response.ok) {
throw new Error(`HTTP GET ${url} failed: ${response.status} ${response.statusText}`);
}
return new Uint8Array(await response.arrayBuffer());
}
async post(
url: string,
body: string,
headers: Record<string, string>
): Promise<Uint8Array> {
// @ts-ignore
const response = (await libcurl.fetch(url, { method: "POST", body, headers, insecure: true})) as Response;
if (!response.ok) {
throw new Error(`HTTP POST ${url} failed: ${response.status} ${response.statusText}`);
}
return new Uint8Array(await response.arrayBuffer());
}
}

View File

@@ -0,0 +1,20 @@
import { libcurl } from "../public/libcurl_full.mjs";
let initialized = false;
let initPromise: Promise<void> | null = null;
export async function initLibcurl(): Promise<void> {
if (initialized) return;
if (initPromise) return initPromise;
initPromise = (async () => {
const wsProto = location.protocol === "https:" ? "wss:" : "ws:";
let wsUrl = `${wsProto}//${location.host}/wisp/`;
libcurl.set_websocket(wsUrl);
await libcurl.load_wasm();
initialized = true;
})();
return initPromise;
}
export { libcurl };

4
frontend/src/main.js Normal file
View File

@@ -0,0 +1,4 @@
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')

40
frontend/src/types/libcurl.d.ts vendored Normal file
View File

@@ -0,0 +1,40 @@
declare module "libcurl.js" {
interface LibcurlResponse {
ok: boolean;
status: number;
statusText: string;
headers: Headers;
raw_headers: [string, string][];
text(): Promise<string>;
json(): Promise<any>;
arrayBuffer(): Promise<ArrayBuffer>;
}
interface LibcurlFetchOptions {
method?: string;
headers?: Record<string, string> | Headers;
body?: string | ArrayBuffer | Uint8Array;
redirect?: "follow" | "manual" | "error";
proxy?: string;
_libcurl_verbose?: number;
_libcurl_http_version?: number;
}
interface Libcurl {
ready: boolean;
onload: (() => void) | null;
set_websocket(url: string): void;
load_wasm(url?: string): Promise<void>;
fetch(url: string, options?: LibcurlFetchOptions): Promise<LibcurlResponse>;
get_error_string(code: number): string;
get_cacert(): string;
version: string;
}
export const libcurl: Libcurl;
}
declare module "libcurl.js/bundled" {
import type { Libcurl } from "libcurl.js";
export const libcurl: Libcurl;
}

19
frontend/vite.config.js Normal file
View File

@@ -0,0 +1,19 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vite.dev/config/
export default defineConfig({
plugins: [vue()],
optimizeDeps: {
exclude: ['anisette-js']
},
build: {
target: 'esnext'
},
server: {
proxy: {
"/api": "http://localhost:8080",
"/wisp": { target: "ws://localhost:8080", ws: true },
},
},
})

0
js/.gitignore vendored Normal file
View File

22
js/bun.lock Normal file
View File

@@ -0,0 +1,22 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "anisette-js",
"dependencies": {
"@types/node": "^25.3.2",
},
"devDependencies": {
"typescript": "^5.4.0",
},
},
},
"packages": {
"@types/node": ["@types/node@25.3.2", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-RpV6r/ij22zRRdyBPcxDeKAzH43phWVKEjL2iksqo1Vz3CuBUrgmPpPhALKiRfU7OMCmeeO9vECBMsV0hMTG8Q=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
}
}

43
js/package.json Normal file
View File

@@ -0,0 +1,43 @@
{
"name": "@lbr77/anisette-js",
"version": "0.1.2",
"description": "High-level JavaScript/TypeScript API for Apple Anisette headers via WASM",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"browser": "./dist/browser.js",
"import": "./dist/index.js",
"default": "./dist/index.js"
},
"./browser": {
"types": "./dist/browser.d.ts",
"default": "./dist/browser.js"
},
"./package.json": "./package.json"
},
"files": [
"dist"
],
"scripts": {
"build": "tsc && cp src/anisette_rs.js dist/ && cp src/anisette_rs.node.js dist/",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"typescript": "^5.4.0"
},
"dependencies": {
"@types/node": "^25.3.2"
},
"publishConfig": {
"registry": "https://registry.npmjs.org/",
"access": "public"
},
"repository": {
"type": "git",
"url": "git+https://github.com/lbr77/anisette-js.git"
}
}

297
js/src/anisette.ts Normal file
View File

@@ -0,0 +1,297 @@
// 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<Anisette> {
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<Anisette> {
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 (err) {
console.log("[anisette] Failed to sync IDBFS from storage:", err);
// 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<void> {
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<AnisetteHeaders> {
// 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);
}

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

25
js/src/browser.ts Normal file
View File

@@ -0,0 +1,25 @@
//@ts-expect-error no types for the Emscripten module factory
import ModuleFactory from "./anisette_rs.js";
export * from './index';
export type EmscriptenModule = any;
export interface ModuleOverrides {
[key: string]: any;
}
const wasmUrl = new URL("./anisette_rs.wasm", import.meta.url).href;
export async function loadWasmModule(
moduleOverrides: ModuleOverrides = {}
): Promise<EmscriptenModule> {
return ModuleFactory({
...moduleOverrides,
locateFile: (filename: string) => {
if (filename.endsWith(".wasm")) return wasmUrl;
return filename;
},
});
}
export const loadWasm = loadWasmModule;
export const isNodeEnvironment = () => false;
export const getWasmBinaryPath = () => wasmUrl;

64
js/src/device.ts Normal file
View File

@@ -0,0 +1,64 @@
// Device identity management — loads or generates device.json
import type { AnisetteDeviceConfig, DeviceJson } from "./types.js";
import { randomHex, randomUUID } from "./utils.js";
const DEFAULT_CLIENT_INFO =
"<MacBookPro13,2> <macOS;13.1;22C65> <com.apple.AuthKit/1 (com.apple.dt.Xcode/3594.4.19)>";
export class Device {
readonly uniqueDeviceIdentifier: string;
readonly serverFriendlyDescription: string;
readonly adiIdentifier: string;
readonly localUserUuid: string;
private constructor(data: DeviceJson) {
this.uniqueDeviceIdentifier = data.UUID;
this.serverFriendlyDescription = data.clientInfo;
this.adiIdentifier = data.identifier;
this.localUserUuid = data.localUUID;
}
/** Load from a parsed device.json object, or generate defaults if null. */
static fromJson(
json: DeviceJson | null,
overrides?: Partial<AnisetteDeviceConfig>
): Device {
const defaults = Device.generateDefaults();
const base: DeviceJson = json ?? {
UUID: defaults.uniqueDeviceId,
clientInfo: defaults.serverFriendlyDescription,
identifier: defaults.adiId,
localUUID: defaults.localUserUuid,
};
if (overrides) {
if (overrides.uniqueDeviceId) base.UUID = overrides.uniqueDeviceId;
if (overrides.serverFriendlyDescription)
base.clientInfo = overrides.serverFriendlyDescription;
if (overrides.adiId) base.identifier = overrides.adiId;
if (overrides.localUserUuid) base.localUUID = overrides.localUserUuid;
}
return new Device(base);
}
/** Serialize back to the device.json wire format. */
toJson(): DeviceJson {
return {
UUID: this.uniqueDeviceIdentifier,
clientInfo: this.serverFriendlyDescription,
identifier: this.adiIdentifier,
localUUID: this.localUserUuid,
};
}
static generateDefaults(): AnisetteDeviceConfig {
return {
serverFriendlyDescription: DEFAULT_CLIENT_INFO,
uniqueDeviceId: randomUUID(),
adiId: randomHex(8, false),
localUserUuid: randomHex(32, true),
};
}
}

32
js/src/http.ts Normal file
View File

@@ -0,0 +1,32 @@
// HTTP client abstraction — allows swapping fetch vs Node.js http in tests
export interface HttpClient {
get(url: string, headers: Record<string, string>): Promise<Uint8Array>;
post(
url: string,
body: string,
headers: Record<string, string>
): Promise<Uint8Array>;
}
export class FetchHttpClient implements HttpClient {
async get(url: string, headers: Record<string, string>): Promise<Uint8Array> {
const response = await fetch(url, { method: "GET", headers });
if (!response.ok) {
throw new Error(`HTTP GET ${url} failed: ${response.status} ${response.statusText}`);
}
return new Uint8Array(await response.arrayBuffer());
}
async post(
url: string,
body: string,
headers: Record<string, string>
): Promise<Uint8Array> {
const response = await fetch(url, { method: "POST", body, headers });
if (!response.ok) {
throw new Error(`HTTP POST ${url} failed: ${response.status} ${response.statusText}`);
}
return new Uint8Array(await response.arrayBuffer());
}
}

25
js/src/index.ts Normal file
View File

@@ -0,0 +1,25 @@
// Public entry point — re-exports everything users need
export { Anisette } from "./anisette.js";
export { WasmBridge } from "./wasm-bridge.js";
export { Device } from "./device.js";
export { LibraryStore } from "./library.js";
export { ProvisioningSession } from "./provisioning.js";
export { FetchHttpClient } from "./http.js";
export type { HttpClient } from "./http.js";
export type {
AnisetteHeaders,
AnisetteDeviceConfig,
InitOptions,
DeviceJson,
} from "./types.js";
export type { AnisetteOptions } from "./anisette.js";
export {
loadWasmModule,
loadWasmModule as loadWasm,
isNodeEnvironment,
getWasmBinaryPath,
type EmscriptenModule,
type ModuleOverrides,
} from "./wasm-loader.js";

40
js/src/library.ts Normal file
View File

@@ -0,0 +1,40 @@
// LibraryStore — holds the two required Android .so blobs
const REQUIRED_LIBS = [
"libstoreservicescore.so",
"libCoreADI.so",
] as const;
export type LibraryName = (typeof REQUIRED_LIBS)[number];
export class LibraryStore {
private libs: Map<LibraryName, Uint8Array>;
private constructor(libs: Map<LibraryName, Uint8Array>) {
this.libs = libs;
}
static fromBlobs(
storeservicescore: Uint8Array,
coreadi: Uint8Array
): LibraryStore {
const map = new Map<LibraryName, Uint8Array>();
map.set("libstoreservicescore.so", storeservicescore);
map.set("libCoreADI.so", coreadi);
return new LibraryStore(map);
}
get(name: LibraryName): Uint8Array {
const data = this.libs.get(name);
if (!data) throw new Error(`Library not loaded: ${name}`);
return data;
}
get storeservicescore(): Uint8Array {
return this.get("libstoreservicescore.so");
}
get coreadi(): Uint8Array {
return this.get("libCoreADI.so");
}
}

159
js/src/provisioning.ts Normal file
View File

@@ -0,0 +1,159 @@
// ProvisioningSession — communicates with Apple servers to provision the device
import { fromBase64, toBase64, toAppleClientTime } from "./utils.js";
import type { WasmBridge } from "./wasm-bridge.js";
import type { Device } from "./device.js";
import type { HttpClient } from "./http.js";
import { FetchHttpClient } from "./http.js";
const LOOKUP_URL = "https://gsa.apple.com/grandslam/GsService2/lookup";
const START_PROVISIONING_BODY = `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Header</key>
<dict/>
<key>Request</key>
<dict/>
</dict>
</plist>`;
export class ProvisioningSession {
private bridge: WasmBridge;
private device: Device;
private http: HttpClient;
private urlBag: Record<string, string> = {};
constructor(bridge: WasmBridge, device: Device, http?: HttpClient) {
this.bridge = bridge;
this.device = device;
this.http = http ?? new FetchHttpClient();
}
async provision(dsid: bigint): Promise<void> {
if (Object.keys(this.urlBag).length === 0) {
await this.loadUrlBag();
}
const startUrl = this.urlBag["midStartProvisioning"];
const finishUrl = this.urlBag["midFinishProvisioning"];
if (!startUrl) throw new Error("url bag missing midStartProvisioning");
if (!finishUrl) throw new Error("url bag missing midFinishProvisioning");
// Step 1: get SPIM from Apple
const startBytes = await this.http.post(
startUrl,
START_PROVISIONING_BODY,
this.commonHeaders(true)
);
const startPlist = parsePlist(startBytes);
const spimB64 = plistGetStringInResponse(startPlist, "spim");
const spim = fromBase64(spimB64);
// Step 2: call WASM start_provisioning
const { cpim, session } = this.bridge.startProvisioning(dsid, spim);
const cpimB64 = toBase64(cpim);
// Step 3: send CPIM to Apple, get PTM + TK
const finishBody = buildFinishBody(cpimB64);
const finishBytes = await this.http.post(
finishUrl,
finishBody,
this.commonHeaders(true)
);
const finishPlist = parsePlist(finishBytes);
const ptm = fromBase64(plistGetStringInResponse(finishPlist, "ptm"));
const tk = fromBase64(plistGetStringInResponse(finishPlist, "tk"));
// Step 4: finalize provisioning in WASM
this.bridge.endProvisioning(session, ptm, tk);
}
private async loadUrlBag(): Promise<void> {
const bytes = await this.http.get(LOOKUP_URL, this.commonHeaders(false));
const plist = parsePlist(bytes);
const urls = plistGetDict(plist, "urls");
this.urlBag = {};
for (const [k, v] of Object.entries(urls)) {
if (typeof v === "string") this.urlBag[k] = v;
}
}
private commonHeaders(includeTime: boolean): Record<string, string> {
const headers: Record<string, string> = {
"User-Agent": "akd/1.0 CFNetwork/1404.0.5 Darwin/22.3.0",
"Content-Type": "application/x-www-form-urlencoded",
Connection: "keep-alive",
"X-Mme-Device-Id": this.device.uniqueDeviceIdentifier,
"X-MMe-Client-Info": this.device.serverFriendlyDescription,
"X-Apple-I-MD-LU": this.device.localUserUuid,
"X-Apple-Client-App-Name": "Setup",
};
if (includeTime) {
headers["X-Apple-I-Client-Time"] = toAppleClientTime();
}
return headers;
}
}
// ---- minimal plist XML parser ----
// We only need to extract string values from Apple's response plists.
interface PlistDict {
[key: string]: string | PlistDict;
}
function parsePlist(bytes: Uint8Array): PlistDict {
const xml = new TextDecoder("utf-8").decode(bytes);
return parsePlistDict(xml);
}
function parsePlistDict(xml: string): PlistDict {
const result: PlistDict = {};
// Match <key>...</key> followed by <string>...</string> or <dict>...</dict>
const keyRe = /<key>([^<]*)<\/key>\s*(<string>([^<]*)<\/string>|<dict>([\s\S]*?)<\/dict>)/g;
let m: RegExpExecArray | null;
while ((m = keyRe.exec(xml)) !== null) {
const key = m[1];
if (m[3] !== undefined) {
result[key] = m[3];
} else if (m[4] !== undefined) {
result[key] = parsePlistDict(m[4]);
}
}
return result;
}
function plistGetStringInResponse(plist: PlistDict, key: string): string {
const response = plist;
const value = (response as PlistDict)[key];
if (typeof value !== "string") {
throw new Error(`plist Response missing string field: ${key}`);
}
return value;
}
function plistGetDict(plist: PlistDict, key: string): PlistDict {
const value = plist[key];
if (!value || typeof value === "string") {
throw new Error(`plist missing dict field: ${key}`);
}
return value as PlistDict;
}
function buildFinishBody(cpimB64: string): string {
return `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Header</key>
<dict/>
<key>Request</key>
<dict>
<key>cpim</key>
<string>${cpimB64}</string>
</dict>
</dict>
</plist>`;
}

48
js/src/types.ts Normal file
View File

@@ -0,0 +1,48 @@
// Core type definitions for the Anisette JS/TS API
export interface AnisetteDeviceConfig {
/** Human-readable device description sent to Apple servers */
serverFriendlyDescription: string;
/** Unique device UUID (uppercase) */
uniqueDeviceId: string;
/** ADI identifier (hex string) */
adiId: string;
/** Local user UUID (uppercase hex) */
localUserUuid: string;
}
export interface AnisetteHeaders {
"X-Apple-I-Client-Time": string;
"X-Apple-I-MD": string;
"X-Apple-I-MD-LU": string;
"X-Apple-I-MD-M": string;
"X-Apple-I-MD-RINFO": string;
"X-Apple-I-SRL-NO": string;
"X-Apple-I-TimeZone": string;
"X-Apple-Locale": string;
"X-MMe-Client-Info": string;
"X-Mme-Device-Id": string;
}
export interface InitOptions {
/** Path prefix used inside the WASM virtual filesystem for library files */
libraryPath?: string;
/** Path prefix used inside the WASM virtual filesystem for provisioning data */
provisioningPath?: string;
/** ADI identifier override */
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 */
export interface DeviceJson {
UUID: string;
clientInfo: string;
identifier: string;
localUUID: string;
}

91
js/src/utils.ts Normal file
View File

@@ -0,0 +1,91 @@
// Utility functions shared across modules
const TEXT_ENCODER = new TextEncoder();
const TEXT_DECODER = new TextDecoder("utf-8");
/** Encode string to UTF-8 bytes */
export function encodeUtf8(str: string): Uint8Array {
return TEXT_ENCODER.encode(str);
}
/** Decode UTF-8 bytes to string */
export function decodeUtf8(bytes: Uint8Array): string {
return TEXT_DECODER.decode(bytes);
}
/** Encode bytes to base64 string */
export function toBase64(bytes: Uint8Array): string {
if (bytes.length === 0) return "";
// Works in both browser and Node.js (Node 16+)
if (typeof Buffer !== "undefined") {
return Buffer.from(bytes).toString("base64");
}
let binary = "";
for (let i = 0; i < bytes.length; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
}
/** Decode base64 string to bytes */
export function fromBase64(b64: string): Uint8Array {
if (typeof Buffer !== "undefined") {
return new Uint8Array(Buffer.from(b64, "base64"));
}
const binary = atob(b64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes;
}
/** Format a Date as Apple client time string (ISO 8601 without milliseconds) */
export function toAppleClientTime(date: Date = new Date()): string {
return date.toISOString().replace(/\.\d{3}Z$/, "Z");
}
/** Detect locale string in Apple format (e.g. "en_US") */
export function detectLocale(): string {
const locale =
(typeof Intl !== "undefined" &&
Intl.DateTimeFormat().resolvedOptions().locale) ||
"en-US";
return locale.replace("-", "_");
}
/** Generate a random hex string of the given byte length */
export function randomHex(byteLen: number, uppercase = false): string {
const bytes = new Uint8Array(byteLen);
if (typeof crypto !== "undefined" && crypto.getRandomValues) {
crypto.getRandomValues(bytes);
} else {
// Node.js fallback
// eslint-disable-next-line @typescript-eslint/no-require-imports
const nodeCrypto = require("crypto") as typeof import("crypto");
const buf = nodeCrypto.randomBytes(byteLen);
bytes.set(buf);
}
let hex = Array.from(bytes)
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
return uppercase ? hex.toUpperCase() : hex;
}
/** Generate a random UUID v4 (uppercase) */
export function randomUUID(): string {
if (typeof crypto !== "undefined" && crypto.randomUUID) {
return crypto.randomUUID().toUpperCase();
}
// Manual fallback
const hex = randomHex(16);
return [
hex.slice(0, 8),
hex.slice(8, 12),
"4" + hex.slice(13, 16),
((parseInt(hex[16], 16) & 0x3) | 0x8).toString(16) + hex.slice(17, 20),
hex.slice(20, 32),
]
.join("-")
.toUpperCase();
}

316
js/src/wasm-bridge.ts Normal file
View File

@@ -0,0 +1,316 @@
// 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<void> {
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<void> {
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;
}
}
}

94
js/src/wasm-loader.ts Normal file
View File

@@ -0,0 +1,94 @@
// Unified WASM loader with automatic environment detection
// Uses import.meta.url + protocol detection to support both Node.js and Browser
//
// Browser (Vite/Webpack/etc): https://... or http://... -> anisette_rs.js + .wasm
// Node.js: file://... -> anisette_rs.node.js + .wasm
// Get the base URL from import.meta.url
const MODULE_URL = new URL(import.meta.url);
const IS_NODE = MODULE_URL.protocol === "file:";
// Determine which WASM build to use based on environment
const WASM_JS_PATH = IS_NODE
? new URL("../dist/anisette_rs.node.js", MODULE_URL)
: new URL("../dist/anisette_rs.js", MODULE_URL);
const WASM_BINARY_PATH = IS_NODE
? new URL("../dist/anisette_rs.node.wasm", MODULE_URL)
: new URL("../dist/anisette_rs.wasm", MODULE_URL);
// Module overrides type (Emscripten module configuration)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type EmscriptenModule = any;
export interface ModuleOverrides {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any;
}
/**
* Load the Emscripten WASM module with automatic environment detection.
*
* This function automatically:
* - Detects Node.js vs Browser environment via import.meta.url protocol
* - Loads the appropriate WASM build (node or web)
* - Configures locateFile to find the .wasm binary
* - Initializes the module with optional overrides
*
* @param moduleOverrides - Optional Emscripten module configuration overrides
* @returns Initialized Emscripten module with all exports (_malloc, _free, _anisette_* etc.)
*
* @example
* ```ts
* // Browser (Vue/React/Next.js) and Node.js - same code!
* const module = await loadWasmModule();
*
* // With custom overrides
* const module = await loadWasmModule({
* print: (text: string) => console.log("WASM:", text),
* printErr: (text: string) => console.error("WASM Error:", text),
* });
* ```
*/
export async function loadWasmModule(
moduleOverrides: ModuleOverrides = {}
): Promise<EmscriptenModule> {
// Dynamic import of the appropriate Emscripten glue file
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { default: ModuleFactory } = await import(/* @vite-ignore */ WASM_JS_PATH.href) as { default: (config: ModuleOverrides) => Promise<EmscriptenModule> };
// In browser: let Emscripten use default fetch behavior
// In Node.js: provide locateFile to resolve the .wasm path
const config: ModuleOverrides = IS_NODE
? {
...moduleOverrides,
locateFile: (filename: string) => {
if (filename.endsWith(".wasm")) {
// In Node.js, return the absolute file path
return WASM_BINARY_PATH.pathname;
}
return filename;
},
}
: moduleOverrides;
return ModuleFactory(config);
}
/**
* Convenience function to check if running in Node.js environment.
* Uses the same protocol detection as the loader.
*/
export function isNodeEnvironment(): boolean {
return IS_NODE;
}
/**
* Get the resolved WASM binary path (useful for debugging).
*/
export function getWasmBinaryPath(): string {
return IS_NODE ? WASM_BINARY_PATH.pathname : WASM_BINARY_PATH.href;
}
// Re-export for backward compatibility
export { loadWasmModule as loadWasm };

18
js/tsconfig.json Normal file
View File

@@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2022", "DOM"],
"outDir": "./dist",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -3,7 +3,10 @@
"scripts": { "scripts": {
"build:unicorn": "bash script/rebuild-unicorn.sh", "build:unicorn": "bash script/rebuild-unicorn.sh",
"build:glue": "bash script/build-glue.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",
"run:node": "bun test/run-node.mjs" "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

@@ -8,7 +8,7 @@ fi
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TARGET_DIR="${ROOT_DIR}/target/wasm32-unknown-emscripten/${BUILD_MODE}" TARGET_DIR="${ROOT_DIR}/target/wasm32-unknown-emscripten/${BUILD_MODE}"
DIST_DIR="${ROOT_DIR}/dist" DIST_DIR="${ROOT_DIR}/js/src"
# EMSDK_DIR="${EMSDK:-/Users/libr/Desktop/Life/emsdk}" # EMSDK_DIR="${EMSDK:-/Users/libr/Desktop/Life/emsdk}"
UNICORN_BUILD_DIR="${UNICORN_BUILD_DIR:-${ROOT_DIR}/../unicorn/build}" UNICORN_BUILD_DIR="${UNICORN_BUILD_DIR:-${ROOT_DIR}/../unicorn/build}"
NODE_DIST_JS="${DIST_DIR}/anisette_rs.node.js" NODE_DIST_JS="${DIST_DIR}/anisette_rs.node.js"
@@ -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"]' 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_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"]' WEB_EXPORTED_RUNTIME_METHODS='["FS","HEAPU8","UTF8ToString","stringToUTF8","lengthBytesUTF8"]'
NODE_EXPORTED_RUNTIME_METHODS='["HEAPU8","UTF8ToString","stringToUTF8","lengthBytesUTF8"]' NODE_EXPORTED_RUNTIME_METHODS='["HEAPU8","UTF8ToString","stringToUTF8","lengthBytesUTF8"]'
@@ -66,6 +66,7 @@ emcc \
-sEXPORT_ES6=1 \ -sEXPORT_ES6=1 \
-sENVIRONMENT=web \ -sENVIRONMENT=web \
-sWASM=1 \ -sWASM=1 \
-sSINGLE_FILE=1 \
-sALLOW_MEMORY_GROWTH=1 \ -sALLOW_MEMORY_GROWTH=1 \
-sINITIAL_MEMORY=268435456 \ -sINITIAL_MEMORY=268435456 \
-sWASM_BIGINT=1 \ -sWASM_BIGINT=1 \
@@ -81,6 +82,7 @@ emcc \
-sEXPORT_ES6=1 \ -sEXPORT_ES6=1 \
-sENVIRONMENT=node \ -sENVIRONMENT=node \
-sWASM=1 \ -sWASM=1 \
-sSINGLE_FILE=1 \
-sALLOW_MEMORY_GROWTH=1 \ -sALLOW_MEMORY_GROWTH=1 \
-sINITIAL_MEMORY=268435456 \ -sINITIAL_MEMORY=268435456 \
-sWASM_BIGINT=1 \ -sWASM_BIGINT=1 \
@@ -91,12 +93,4 @@ emcc \
echo "glue build done:" echo "glue build done:"
echo " ${DIST_DIR}/anisette_rs.js" echo " ${DIST_DIR}/anisette_rs.js"
echo " ${DIST_DIR}/anisette_rs.wasm" echo " ${DIST_DIR}/anisette_rs.node.js"
echo " ${NODE_DIST_JS}"
echo " ${NODE_DIST_WASM}"
# Copy to frontend if directory exists (skip in CI if not present)
if [[ -d "${ROOT_DIR}/../../frontend/public/anisette" ]]; then
cp "${DIST_DIR}/anisette_rs.js" "${ROOT_DIR}/../../frontend/public/anisette/anisette_rs.js"
cp "${DIST_DIR}/anisette_rs.wasm" "${ROOT_DIR}/../../frontend/public/anisette/anisette_rs.wasm"
fi

View File

@@ -33,25 +33,25 @@ pub(crate) fn trace_mem_invalid_hook(
let pc = reg_or_zero(uc, RegisterARM64::PC); let pc = reg_or_zero(uc, RegisterARM64::PC);
match access { match access {
MemType::READ_UNMAPPED => { MemType::READ_UNMAPPED => {
debug_print(format!( println!(
">>> Missing memory is being READ at 0x{address:x}, data size = {size}, data value = 0x{:x}, PC=0x{pc:x}", ">>> Missing memory is being READ at 0x{address:x}, data size = {size}, data value = 0x{:x}, PC=0x{pc:x}",
value as u64 value as u64
)); );
dump_registers(uc, "read unmapped"); dump_registers(uc, "read unmapped");
} }
MemType::WRITE_UNMAPPED => { MemType::WRITE_UNMAPPED => {
debug_print(format!( println!(
">>> Missing memory is being WRITE at 0x{address:x}, data size = {size}, data value = 0x{:x}, PC=0x{pc:x}", ">>> Missing memory is being WRITE at 0x{address:x}, data size = {size}, data value = 0x{:x}, PC=0x{pc:x}",
value as u64 value as u64
)); );
dump_registers(uc, "write unmapped"); dump_registers(uc, "write unmapped");
} }
MemType::FETCH_UNMAPPED => { MemType::FETCH_UNMAPPED => {
debug_print(format!( println!(
">>> Missing memory is being FETCH at 0x{address:x}, data size = {size}, data value = 0x{:x}, PC=0x{pc:x}", ">>> Missing memory is being FETCH at 0x{address:x}, data size = {size}, data value = 0x{:x}, PC=0x{pc:x}",
value as u64 value as u64
)); );
} }
_ => {} _ => {}
} }

View File

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