@ruhiverse/ota
v0.0.10
Published
Capacitor plugin for OTA updates - dynamically replace web assets at runtime
Maintainers
Readme
Table of Contents
- Install
- Supported Platforms
- Quick Start
- API
- Interfaces
- How It Works
- ZIP Requirements
- Security
- Rollback & Error Handling
- Configuration
- Full Example
- License
Install
npm install @ruhiverse/ota
npx cap syncSupported Platforms
| Platform | Supported | Native Implementation |
| -------- | ------------------ | ---------------------------------- |
| Android | Capacitor 5+ / 6+ | Java (ZipInputStream, HttpURLConnection) |
| iOS | Capacitor 5+ / 6+ | Swift (SSZipArchive, CryptoKit) |
| Web | Not supported | — |
OTA updates replace web assets only (HTML, CSS, JS). Native code is never modified.
Quick Start
import { Ota } from '@ruhiverse/ota';
// Download update from your server
await Ota.downloadUpdate({
url: 'https://your-server.com/releases/v2.0.0.zip',
});
// Extract & apply — WebView reloads automatically
await Ota.applyUpdate();API
downloadUpdate(options)
Downloads a ZIP file from the given URL and stores it locally at <app_data>/ota/update.zip.
const result = await Ota.downloadUpdate({
url: 'https://example.com/update-v2.zip',
checksum: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
});
console.log('Downloaded to:', result.path);Parameters:
| Param | Type | Required | Description |
| ------------ | -------- | -------- | -------------------------------------------------------- |
| url | string | Yes | URL of the ZIP file containing updated web assets |
| checksum | string | No | SHA-256 checksum to validate the download before storing |
Returns: Promise<DownloadUpdateResult>
| Property | Type | Description |
| -------- | -------- | ----------------------------------- |
| path | string | Local file path of the saved ZIP |
applyUpdate()
Extracts the previously downloaded ZIP to <app_data>/ota/www/, validates the contents, sets the WebView server base path, and reloads the app.
const result = await Ota.applyUpdate();
console.log('Now serving from:', result.path);Returns: Promise<ApplyUpdateResult>
| Property | Type | Description |
| -------- | -------- | ---------------------------------------- |
| path | string | The new server base path being served |
If extraction fails or
index.htmlis missing, the operation is rolled back automatically.
getCurrentPath()
Returns the current server base path. If the app is using the original bundled assets, returns "default".
const result = await Ota.getCurrentPath();
if (result.path !== 'default') {
console.log('Running OTA version from:', result.path);
}Returns: Promise<GetCurrentPathResult>
| Property | Type | Description |
| -------- | -------- | ---------------------------------------------------- |
| path | string | Current OTA path, or "default" for bundled assets |
resetToBundle()
Resets the WebView to serve the original bundled assets. Removes all downloaded OTA files and reloads the app.
const result = await Ota.resetToBundle();
console.log('Reset successful:', result.success);Returns: Promise<ResetToDefaultResult>
| Property | Type | Description |
| -------- | --------- | ------------------------------- |
| success | boolean | Whether the reset succeeded |
Interfaces
interface DownloadUpdateOptions {
url: string;
checksum?: string;
}
interface DownloadUpdateResult {
path: string;
}
interface ApplyUpdateResult {
path: string;
}
interface GetCurrentPathResult {
path: string;
}
interface ResetToDefaultResult {
success: boolean;
}
interface OtaPlugin {
downloadUpdate(options: DownloadUpdateOptions): Promise<DownloadUpdateResult>;
applyUpdate(): Promise<ApplyUpdateResult>;
getCurrentPath(): Promise<GetCurrentPathResult>;
resetToBundle(): Promise<ResetToDefaultResult>;
}How It Works
┌─────────────────────────────────────────────────────────┐
│ OTA Update Flow │
├─────────────────────────────────────────────────────────┤
│ │
│ 1. downloadUpdate() │
│ ┌──────────┐ ┌──────────────────────────┐ │
│ │ Server │ ───> │ <app_data>/ota/update.zip │ │
│ └──────────┘ └──────────────────────────┘ │
│ │ │
│ 2. applyUpdate() ▼ │
│ ┌──────────────────────────┐ │
│ │ <app_data>/ota/www/ │ │
│ │ ├── index.html │ │
│ │ ├── main.js │ │
│ │ └── assets/... │ │
│ └──────────────────────────┘ │
│ │ │
│ 3. setServerBasePath() ▼ │
│ ┌──────────────────────────┐ │
│ │ WebView reloads with │ │
│ │ new assets │ │
│ └──────────────────────────┘ │
│ │
│ 4. Path persisted — survives app restarts │
│ │
│ 5. resetToBundle() — clears OTA, restores original │
│ │
└─────────────────────────────────────────────────────────┘Lifecycle Detail
Download —
downloadUpdate()fetches the ZIP via HTTP and saves it to<app_data>/ota/update.zip. If a SHA-256 checksum is provided, the file is validated immediately after download. On mismatch, the file is deleted and an error is thrown.Extract —
applyUpdate()unzips the archive into<app_data>/ota/www/. Old extracted files are removed first to prevent stale content.Validate — The plugin checks that
index.htmlexists in the extracted directory. If not, the extracted files are deleted and the update is rejected.Apply — The native bridge's server base path is set to the extracted directory. The WebView reloads automatically to serve the new assets.
Persist — The OTA path is saved to
SharedPreferences(Android) /UserDefaults(iOS). On app launch, the plugin'sload()method restores this path so the OTA version survives restarts.Reset —
resetToBundle()clears the saved path, deletes all OTA files, resets the server base path, and reloads the WebView with the original bundled assets.
On-Device File Structure
Android: /data/user/0/{app.package}/files/ota/
iOS: <AppSupport>/ota/
├── update.zip ← downloaded archive
└── www/ ← extracted web assets
├── index.html
├── main.js
├── styles.css
└── assets/
└── ...ZIP Requirements
Your ZIP file must have index.html at the root level. The plugin validates this before applying the update.
Correct structure:
update.zip
├── index.html ✅ Required at root
├── main.js
├── styles.css
└── assets/
├── icon.png
└── ...Incorrect structure (nested folder):
update.zip
└── www/ ❌ index.html is not at root
├── index.html
└── ...When building your Ionic app, use the contents of the
www/build output folder — not the folder itself.
Security
The plugin includes multiple layers of protection:
| Protection | Description |
| ----------------------- | ------------------------------------------------------------------ |
| SHA-256 Checksum | Optional integrity check — if provided, the downloaded ZIP is validated against the expected hash before it's stored. Mismatches cause immediate deletion. |
| Zip-Slip Prevention | Both Android and iOS implementations verify that extracted file paths don't escape the target directory (path traversal attack protection). |
| Content Validation | The extracted directory must contain index.html. Without it, the update is rejected and cleaned up. |
| Atomic Writes (iOS) | ZIP data is written with .atomic option to prevent partial/corrupt files on disk. |
| HTTPS | Always use HTTPS URLs for your update server to prevent man-in-the-middle attacks. |
Rollback & Error Handling
The plugin handles failures gracefully at every stage:
| Failure Scenario | What Happens |
| ------------------------ | --------------------------------------------------------------- |
| Download fails | Partial ZIP is deleted. Error thrown to caller. |
| Checksum mismatch | Downloaded ZIP is deleted. Error thrown with expected vs actual. |
| Corrupt ZIP | Extraction fails, extracted files are cleaned up automatically. |
| Missing index.html | Extracted files are deleted. Error thrown to caller. |
| Zip-slip detected | Extraction aborted with SecurityException. |
| App won't load after update | Call resetToBundle() to revert to original assets. |
Manual Rollback
try {
await Ota.applyUpdate();
} catch (error) {
console.error('Update failed, rolling back:', error);
await Ota.resetToBundle();
}Check Before Applying
const { path } = await Ota.getCurrentPath();
if (path !== 'default') {
console.log('Already running OTA version from:', path);
}Configuration
Android
No extra permissions required. The plugin uses the app's internal files directory (context.getFilesDir()), which doesn't need WRITE_EXTERNAL_STORAGE.
The plugin requires INTERNET permission for downloading, which is already included by default in Capacitor apps.
iOS
The SSZipArchive dependency is pulled in automatically via CocoaPods when you run npx cap sync. No manual setup needed.
The plugin uses ApplicationSupport directory for storage, which doesn't require any special entitlements.
Full Example
import { Ota } from '@ruhiverse/ota';
interface UpdateInfo {
url: string;
version: string;
checksum: string;
}
async function checkForUpdate(): Promise<void> {
// 1. Fetch update metadata from your API
const response = await fetch('https://api.your-server.com/updates/latest');
const update: UpdateInfo = await response.json();
// 2. Check if we're already on this version
const { path } = await Ota.getCurrentPath();
if (path.includes(update.version)) {
console.log('Already up to date');
return;
}
try {
// 3. Download with checksum validation
console.log(`Downloading v${update.version}...`);
await Ota.downloadUpdate({
url: update.url,
checksum: update.checksum,
});
// 4. Extract & apply — WebView reloads automatically
console.log('Applying update...');
const result = await Ota.applyUpdate();
console.log('Update applied from:', result.path);
} catch (error) {
console.error('OTA update failed:', error);
await Ota.resetToBundle();
}
}
// Run on app startup
checkForUpdate();Server-Side ZIP Generation
Example script to generate an update ZIP from an Ionic build:
# Build your Ionic app
ionic build --prod
# Create the update ZIP from the build output
cd www
zip -r ../update-v2.0.0.zip ./*
cd ..
# Generate SHA-256 checksum
shasum -a 256 update-v2.0.0.zip
# e3b0c44298fc1c149afbf4c8996fb924... update-v2.0.0.zipZip the contents of
www/, not thewww/folder itself.
License
MIT
