npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ruhiverse/ota

v0.0.10

Published

Capacitor plugin for OTA updates - dynamically replace web assets at runtime

Readme


Table of Contents


Install

npm install @ruhiverse/ota
npx cap sync

Supported 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.html is 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

  1. DownloaddownloadUpdate() 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.

  2. ExtractapplyUpdate() unzips the archive into <app_data>/ota/www/. Old extracted files are removed first to prevent stale content.

  3. Validate — The plugin checks that index.html exists in the extracted directory. If not, the extracted files are deleted and the update is rejected.

  4. Apply — The native bridge's server base path is set to the extracted directory. The WebView reloads automatically to serve the new assets.

  5. Persist — The OTA path is saved to SharedPreferences (Android) / UserDefaults (iOS). On app launch, the plugin's load() method restores this path so the OTA version survives restarts.

  6. ResetresetToBundle() 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.zip

Zip the contents of www/, not the www/ folder itself.


License

MIT