Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8c4f89ab8a | |||
| eaa30795db | |||
| d0bb6f357e | |||
| f1054e6476 | |||
| d75671596c | |||
| d05cc41660 |
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(cat:*)",
|
||||
"Bash(mkdir:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
}
|
||||
}
|
||||
25
.github/workflows/build.yml
vendored
25
.github/workflows/build.yml
vendored
@@ -3,6 +3,7 @@ name: Build WASM
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
tags: ['v*']
|
||||
pull_request:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
@@ -94,3 +95,27 @@ jobs:
|
||||
dist/anisette_rs.node.js
|
||||
dist/anisette_rs.node.wasm
|
||||
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: Install dependencies
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
working-directory: js/
|
||||
run: npm ci
|
||||
|
||||
- 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
7
.gitignore
vendored
@@ -1,3 +1,10 @@
|
||||
target/
|
||||
test/
|
||||
dist/
|
||||
cache/
|
||||
*.apk
|
||||
anisette/*
|
||||
js/node_modules/*
|
||||
*.so
|
||||
*.wasm
|
||||
frontend/.vscode/extensions.json
|
||||
103
AGENTS.md
103
AGENTS.md
@@ -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
|
||||
|
||||
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
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Rust nightly (edition 2024)
|
||||
- Emscripten SDK (for WASM builds)
|
||||
|
||||
### Setup Unicorn Engine
|
||||
|
||||
Clone the custom Unicorn repository and checkout to the `tci-emscripten` branch:
|
||||
|
||||
Must source Emscripten before building WASM:
|
||||
```bash
|
||||
git clone https://github.com/lbr77/unicorn.git
|
||||
cd unicorn && git checkout tci-emscripten
|
||||
source "/Users/libr/Desktop/Life/emsdk/emsdk_env.sh"
|
||||
```
|
||||
|
||||
Then build Unicorn for Emscripten:
|
||||
|
||||
```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:
|
||||
- Running `emcmake cmake` with appropriate flags
|
||||
- Building only `arm` and `aarch64` architectures
|
||||
- Using static archives (`-DUNICORN_LEGACY_STATIC_ARCHIVE=ON`)
|
||||
JS bundle outputs to `dist/anisette.js`. WASM glue outputs to `dist/anisette_rs.node.{js,wasm}` and `dist/anisette_rs.{js,wasm}`.
|
||||
|
||||
### Build WASM Glue
|
||||
|
||||
```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]
|
||||
```
|
||||
The `js/package.json` build script also outputs to `../dist/anisette.js` directly.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Core Modules
|
||||
### Rust → WASM layer (`src/`)
|
||||
|
||||
- **`adi.rs`**: ADI (Apple Device Identity) wrapper — provisioning, OTP requests
|
||||
- **`emu.rs`**: Unicorn-based ARM64 emulator core — library loading, symbol resolution, function calls
|
||||
- **`exports.rs`**: C FFI exports for WASM — `anisette_*` functions
|
||||
- **`device.rs`**: Device identity management — UUIDs, identifiers, persistence
|
||||
- **`idbfs.rs`**: IndexedDB filesystem integration for Emscripten
|
||||
- **`provisioning.rs`** / **`provisioning_wasm.rs`**: Apple provisioning protocol
|
||||
- `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).
|
||||
- `adi.rs` — wraps the emulated ADI library calls
|
||||
- `emu.rs` — Unicorn ARM64 emulator core
|
||||
- `idbfs.rs` — Emscripten IndexedDB FS integration (browser only)
|
||||
|
||||
### Emulator Memory Layout
|
||||
### JS/TS layer (`js/src/`)
|
||||
|
||||
- Libraries mapped to import address space with stub hooks
|
||||
- Stack, heap, and return addresses pre-allocated
|
||||
- Import hooks dispatch to runtime stubs
|
||||
- `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.
|
||||
- `wasm-bridge.ts` — raw pointer/length marshalling to WASM exports
|
||||
- `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
|
||||
- `anisette_is_machine_provisioned` — Check provisioning state
|
||||
- `anisette_start_provisioning` / `anisette_end_provisioning` — Provisioning flow
|
||||
- `anisette_request_otp` — Generate OTP + machine ID headers
|
||||
- `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.
|
||||
- `fromSo()` accepts `init.adiPb` and `init.deviceJsonBytes` to restore a previous session into the VFS before init.
|
||||
- `loadWasm()` is environment-agnostic — no `node:` imports. Pass `locateFile` in `moduleOverrides`.
|
||||
- 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`
|
||||
2. Initialize device identity (`device.json`)
|
||||
3. Provision with Apple (if needed)
|
||||
4. Request OTP → `X-Apple-I-MD` + `X-Apple-I-MD-M` headers
|
||||
```bash
|
||||
NODE_TLS_REJECT_UNAUTHORIZED=0 bun example/anisette-api.mjs \
|
||||
<libstoreservicescore.so> <libCoreADI.so> [library_path]
|
||||
```
|
||||
|
||||
103
CLAUDE.md
103
CLAUDE.md
@@ -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
|
||||
|
||||
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
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Rust nightly (edition 2024)
|
||||
- Emscripten SDK (for WASM builds)
|
||||
|
||||
### Setup Unicorn Engine
|
||||
|
||||
Clone the custom Unicorn repository and checkout to the `tci-emscripten` branch:
|
||||
|
||||
Must source Emscripten before building WASM:
|
||||
```bash
|
||||
git clone https://github.com/lbr77/unicorn.git
|
||||
cd unicorn && git checkout tci-emscripten
|
||||
source "/Users/libr/Desktop/Life/emsdk/emsdk_env.sh"
|
||||
```
|
||||
|
||||
Then build Unicorn for Emscripten:
|
||||
|
||||
```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:
|
||||
- Running `emcmake cmake` with appropriate flags
|
||||
- Building only `arm` and `aarch64` architectures
|
||||
- Using static archives (`-DUNICORN_LEGACY_STATIC_ARCHIVE=ON`)
|
||||
JS bundle outputs to `dist/anisette.js`. WASM glue outputs to `dist/anisette_rs.node.{js,wasm}` and `dist/anisette_rs.{js,wasm}`.
|
||||
|
||||
### Build WASM Glue
|
||||
|
||||
```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]
|
||||
```
|
||||
The `js/package.json` build script also outputs to `../dist/anisette.js` directly.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Core Modules
|
||||
### Rust → WASM layer (`src/`)
|
||||
|
||||
- **`adi.rs`**: ADI (Apple Device Identity) wrapper — provisioning, OTP requests
|
||||
- **`emu.rs`**: Unicorn-based ARM64 emulator core — library loading, symbol resolution, function calls
|
||||
- **`exports.rs`**: C FFI exports for WASM — `anisette_*` functions
|
||||
- **`device.rs`**: Device identity management — UUIDs, identifiers, persistence
|
||||
- **`idbfs.rs`**: IndexedDB filesystem integration for Emscripten
|
||||
- **`provisioning.rs`** / **`provisioning_wasm.rs`**: Apple provisioning protocol
|
||||
- `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).
|
||||
- `adi.rs` — wraps the emulated ADI library calls
|
||||
- `emu.rs` — Unicorn ARM64 emulator core
|
||||
- `idbfs.rs` — Emscripten IndexedDB FS integration (browser only)
|
||||
|
||||
### Emulator Memory Layout
|
||||
### JS/TS layer (`js/src/`)
|
||||
|
||||
- Libraries mapped to import address space with stub hooks
|
||||
- Stack, heap, and return addresses pre-allocated
|
||||
- Import hooks dispatch to runtime stubs
|
||||
- `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.
|
||||
- `wasm-bridge.ts` — raw pointer/length marshalling to WASM exports
|
||||
- `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
|
||||
- `anisette_is_machine_provisioned` — Check provisioning state
|
||||
- `anisette_start_provisioning` / `anisette_end_provisioning` — Provisioning flow
|
||||
- `anisette_request_otp` — Generate OTP + machine ID headers
|
||||
- `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.
|
||||
- `fromSo()` accepts `init.adiPb` and `init.deviceJsonBytes` to restore a previous session into the VFS before init.
|
||||
- `loadWasm()` is environment-agnostic — no `node:` imports. Pass `locateFile` in `moduleOverrides`.
|
||||
- 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`
|
||||
2. Initialize device identity (`device.json`)
|
||||
3. Provision with Apple (if needed)
|
||||
4. Request OTP → `X-Apple-I-MD` + `X-Apple-I-MD-M` headers
|
||||
```bash
|
||||
NODE_TLS_REJECT_UNAUTHORIZED=0 bun example/anisette-api.mjs \
|
||||
<libstoreservicescore.so> <libCoreADI.so> [library_path]
|
||||
```
|
||||
|
||||
1916
Cargo.lock
generated
Normal file
1916
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
138
README.md
138
README.md
@@ -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)
|
||||
|
||||
[Anisette.py](https://github.com/malmeloo/Anisette.py)
|
||||
|
||||
[omnisette-server](https://github.com/SideStore/omnisette-server)
|
||||
when requiring Otp for second time there will be a "WRITE UNMAPPED" error which could be avoided by initalizing onemoretime...
|
||||
65
example/anisette-api.mjs
Normal file
65
example/anisette-api.mjs
Normal 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
1454
example/index.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@ import path from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node: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 wasmPath = path.join(distDir, 'anisette_rs.node.wasm');
|
||||
|
||||
|
||||
24
frontend/.gitignore
vendored
Normal file
24
frontend/.gitignore
vendored
Normal 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
5
frontend/README.md
Normal 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
197
frontend/bun.lock
Normal 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
12
frontend/index.html
Normal 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
20
frontend/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
1527
frontend/public/libcurl_full.js
Normal file
1527
frontend/public/libcurl_full.js
Normal file
File diff suppressed because one or more lines are too long
1527
frontend/public/libcurl_full.mjs
Normal file
1527
frontend/public/libcurl_full.mjs
Normal file
File diff suppressed because one or more lines are too long
66
frontend/src/App.vue
Normal file
66
frontend/src/App.vue
Normal 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>
|
||||
28
frontend/src/libcurl-http.ts
Normal file
28
frontend/src/libcurl-http.ts
Normal 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());
|
||||
}
|
||||
}
|
||||
20
frontend/src/libcurl-init.ts
Normal file
20
frontend/src/libcurl-init.ts
Normal 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("/libcurl.wasm");
|
||||
initialized = true;
|
||||
})();
|
||||
|
||||
return initPromise;
|
||||
}
|
||||
|
||||
export { libcurl };
|
||||
4
frontend/src/main.js
Normal file
4
frontend/src/main.js
Normal 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
40
frontend/src/types/libcurl.d.ts
vendored
Normal 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
19
frontend/vite.config.js
Normal 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
0
js/.gitignore
vendored
Normal file
22
js/bun.lock
Normal file
22
js/bun.lock
Normal 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=="],
|
||||
}
|
||||
}
|
||||
38
js/package.json
Normal file
38
js/package.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "@lbr77/anisette-js",
|
||||
"version": "0.1.0",
|
||||
"description": "High-level JavaScript/TypeScript API for Apple Anisette headers via WASM",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./src/index.ts",
|
||||
"browser": "./src/browser.ts"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"./src/anisette_rs.js",
|
||||
"./src/anisette_rs.node.js"
|
||||
],
|
||||
"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://npm.pkg.github.com",
|
||||
"access": "restricted"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/lbr77/anisette-js.git"
|
||||
}
|
||||
}
|
||||
296
js/src/anisette.ts
Normal file
296
js/src/anisette.ts
Normal file
@@ -0,0 +1,296 @@
|
||||
// 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 {
|
||||
// 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
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
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
25
js/src/browser.ts
Normal 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
64
js/src/device.ts
Normal 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
32
js/src/http.ts
Normal 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
25
js/src/index.ts
Normal 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
40
js/src/library.ts
Normal 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
159
js/src/provisioning.ts
Normal 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
48
js/src/types.ts
Normal 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
91
js/src/utils.ts
Normal 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
316
js/src/wasm-bridge.ts
Normal 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
94
js/src/wasm-loader.ts
Normal 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
18
js/tsconfig.json
Normal 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"]
|
||||
}
|
||||
@@ -3,7 +3,10 @@
|
||||
"scripts": {
|
||||
"build:unicorn": "bash script/rebuild-unicorn.sh",
|
||||
"build:glue": "bash script/build-glue.sh",
|
||||
"build:release": "bash script/build-glue.sh --release",
|
||||
"run:node": "bun test/run-node.mjs"
|
||||
"build:js": "cd js && bun install && bun run build",
|
||||
"build": "bash script/build-glue.sh && cd js && bun install && bun run build",
|
||||
"release": "bash script/build-glue.sh --release && cd js && bun install && bun run build",
|
||||
"run:node": "node example/run-node.mjs",
|
||||
"run:api": "node example/anisette-api.mjs"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ fi
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
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}"
|
||||
UNICORN_BUILD_DIR="${UNICORN_BUILD_DIR:-${ROOT_DIR}/../unicorn/build}"
|
||||
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"]'
|
||||
NODE_EXPORTED_FUNCTIONS='["_malloc","_free","_anisette_init_from_blobs","_anisette_is_machine_provisioned","_anisette_start_provisioning","_anisette_end_provisioning","_anisette_request_otp","_anisette_get_cpim_ptr","_anisette_get_cpim_len","_anisette_get_session","_anisette_get_otp_ptr","_anisette_get_otp_len","_anisette_get_mid_ptr","_anisette_get_mid_len","_anisette_last_error_ptr","_anisette_last_error_len","_anisette_fs_write_file","_anisette_set_identifier","_anisette_set_provisioning_path"]'
|
||||
WEB_EXPORTED_FUNCTIONS='["_malloc","_free","_anisette_init_from_blobs","_anisette_is_machine_provisioned","_anisette_start_provisioning","_anisette_end_provisioning","_anisette_request_otp","_anisette_get_cpim_ptr","_anisette_get_cpim_len","_anisette_get_session","_anisette_get_otp_ptr","_anisette_get_otp_len","_anisette_get_mid_ptr","_anisette_get_mid_len","_anisette_last_error_ptr","_anisette_last_error_len","_anisette_fs_write_file","_anisette_fs_read_file","_anisette_fs_read_ptr","_anisette_fs_read_len","_anisette_idbfs_sync","_anisette_set_identifier","_anisette_set_provisioning_path"]'
|
||||
NODE_EXPORTED_FUNCTIONS='["_malloc","_free","_anisette_init_from_blobs","_anisette_is_machine_provisioned","_anisette_start_provisioning","_anisette_end_provisioning","_anisette_request_otp","_anisette_get_cpim_ptr","_anisette_get_cpim_len","_anisette_get_session","_anisette_get_otp_ptr","_anisette_get_otp_len","_anisette_get_mid_ptr","_anisette_get_mid_len","_anisette_last_error_ptr","_anisette_last_error_len","_anisette_fs_write_file","_anisette_fs_read_file","_anisette_fs_read_ptr","_anisette_fs_read_len","_anisette_set_identifier","_anisette_set_provisioning_path"]'
|
||||
WEB_EXPORTED_RUNTIME_METHODS='["FS","HEAPU8","UTF8ToString","stringToUTF8","lengthBytesUTF8"]'
|
||||
NODE_EXPORTED_RUNTIME_METHODS='["HEAPU8","UTF8ToString","stringToUTF8","lengthBytesUTF8"]'
|
||||
|
||||
@@ -66,6 +66,7 @@ emcc \
|
||||
-sEXPORT_ES6=1 \
|
||||
-sENVIRONMENT=web \
|
||||
-sWASM=1 \
|
||||
-sSINGLE_FILE=1 \
|
||||
-sALLOW_MEMORY_GROWTH=1 \
|
||||
-sINITIAL_MEMORY=268435456 \
|
||||
-sWASM_BIGINT=1 \
|
||||
@@ -81,6 +82,7 @@ emcc \
|
||||
-sEXPORT_ES6=1 \
|
||||
-sENVIRONMENT=node \
|
||||
-sWASM=1 \
|
||||
-sSINGLE_FILE=1 \
|
||||
-sALLOW_MEMORY_GROWTH=1 \
|
||||
-sINITIAL_MEMORY=268435456 \
|
||||
-sWASM_BIGINT=1 \
|
||||
@@ -91,12 +93,4 @@ emcc \
|
||||
|
||||
echo "glue build done:"
|
||||
echo " ${DIST_DIR}/anisette_rs.js"
|
||||
echo " ${DIST_DIR}/anisette_rs.wasm"
|
||||
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
|
||||
echo " ${DIST_DIR}/anisette_rs.node.js"
|
||||
|
||||
12
src/debug.rs
12
src/debug.rs
@@ -33,25 +33,25 @@ pub(crate) fn trace_mem_invalid_hook(
|
||||
let pc = reg_or_zero(uc, RegisterARM64::PC);
|
||||
match access {
|
||||
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}",
|
||||
value as u64
|
||||
));
|
||||
);
|
||||
dump_registers(uc, "read 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}",
|
||||
value as u64
|
||||
));
|
||||
);
|
||||
|
||||
dump_registers(uc, "write 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}",
|
||||
value as u64
|
||||
));
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::ffi::{CStr, c_char};
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::{Adi, AdiInit, init_idbfs_for_path, sync_idbfs};
|
||||
use crate::{Adi, AdiInit, sync_idbfs};
|
||||
|
||||
#[derive(Default)]
|
||||
struct ExportState {
|
||||
@@ -13,6 +13,7 @@ struct ExportState {
|
||||
session: u32,
|
||||
otp: Vec<u8>,
|
||||
mid: Vec<u8>,
|
||||
read_buf: Vec<u8>,
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
@@ -407,15 +408,15 @@ pub extern "C" fn anisette_fs_write_file(
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn anisette_idbfs_init(path: *const c_char) -> i32 {
|
||||
let result = (|| -> Result<(), String> {
|
||||
pub extern "C" fn anisette_fs_read_file(path: *const c_char) -> i32 {
|
||||
let result = (|| -> Result<Vec<u8>, String> {
|
||||
let path = unsafe { c_string(path)? };
|
||||
init_idbfs_for_path(&path)?;
|
||||
Ok(())
|
||||
fs::read(&path).map_err(|e| format!("failed to read '{path}': {e}"))
|
||||
})();
|
||||
|
||||
match result {
|
||||
Ok(()) => {
|
||||
Ok(data) => {
|
||||
STATE.with(|state| state.borrow_mut().read_buf = data);
|
||||
clear_last_error();
|
||||
0
|
||||
}
|
||||
@@ -426,6 +427,16 @@ pub extern "C" fn anisette_idbfs_init(path: *const c_char) -> i32 {
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn anisette_fs_read_ptr() -> *const u8 {
|
||||
STATE.with(|state| state.borrow().read_buf.as_ptr())
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn anisette_fs_read_len() -> usize {
|
||||
STATE.with(|state| state.borrow().read_buf.len())
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn anisette_idbfs_sync(populate_from_storage: i32) -> i32 {
|
||||
let result = sync_idbfs(populate_from_storage != 0);
|
||||
|
||||
Reference in New Issue
Block a user