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

ra-cordova-ota-update

v1.0.5

Published

Production-ready OTA (over-the-air) update system for Ionic + Angular + Cordova applications. Downloads, verifies, activates and rolls back updated web assets without requiring a new App Store / Google Play release.

Readme

ra-cordova-ota-update

Production-ready OTA (over-the-air) update system for Ionic + Angular + Cordova apps — a CodePush-style mechanism that downloads updated Angular web assets and activates them without a new App Store / Google Play release.

  • Real Cordova plugin (Android/Java, iOS/Objective-C). Not Capacitor.
  • Updates web assets only. Never touches native code.
  • Clean, Angular-injectable public API: RaOTA.
  • force, background, and optional update types. No permanent skip.
import { RaOTA } from 'ra-cordova-ota-update';

constructor(private otaUpdate: RaOTA) {}

const info = await this.otaUpdate.checkForUpdate({
  url: 'https://example.com/ota/latest.json',
});

Table of contents

  1. Installation
  2. Angular import & DI
  3. Architecture
  4. RaOTA API
  5. TypeScript types
  6. Manifest format
  7. ZIP format
  8. SHA-256 verification
  9. Update types: force / background / optional
  10. Pending updates & activation
  11. Rollback & startup confirmation
  12. OTA version vs. app version
  13. Android notes
  14. iOS notes
  15. Security
  16. Storage layout
  17. Download progress
  18. Failure recovery
  19. Limitations

Installation

cordova plugin add ra-cordova-ota-update
npm install ra-cordova-ota-update

Both steps are required: cordova plugin add installs the native Android/iOS code and the JS bridge into your Cordova project; npm install makes the RaOTA Angular service and its TypeScript types resolvable from your app's node_modules at build time. They ship from the same package.

Angular import & DI

import { RaOTA } from 'ra-cordova-ota-update';

@Injectable({ providedIn: 'root' })
// or in any component/service:
export class UpdateService {
  constructor(private otaUpdate: RaOTA) {}
}

RaOTA is itself @Injectable({ providedIn: 'root' }), so:

  • No providers: [RaOTA] needed anywhere.
  • No new RaOTA() needed anywhere.
  • No factory / InjectionToken needed anywhere.
  • No window.cordova, window.cordova.plugins, or cordova.plugins.raOtaUpdate in your application code, ever. The bridge is a private implementation detail of RaOTA.

The package's package.json exports/main/module/types all resolve to the compiled RaOTA class at the package root, so import { RaOTA } from 'ra-cordova-ota-update' works immediately after npm install — never ra-cordova-ota-update/dist/... or ra-cordova-ota-update/www/....

Architecture

Angular application
        |
        v
      RaOTA                     <-- the only thing your app talks to
        |
        v
  Cordova JS bridge              (www/ra-ota-update.js, private)
        |
        v
Native Android (Java) / iOS (Objective-C)
        |
        v
   OTA file system                (private app storage)
        |
        v
 Android WebView / WKWebView

RaOTA internally calls cordova.plugins.raOtaUpdate.*, which calls cordova.exec(...). That is purely an implementation detail — it can change in a future version without breaking your code.

RaOTA API

All methods return Promises.

| Method | Description | |---|---| | checkForUpdate(options) | Fetches the JSON manifest and compares it to the current OTA version. Returns OtaUpdateInfo. | | downloadUpdate(manifest, onProgress?) | Downloads, verifies (SHA-256), extracts, and validates the ZIP; stores it as pending. Used for background updates and optional → "Later". | | installUpdate(manifest, onProgress?) | downloadUpdate + applyPendingUpdate + restart in one call. Used for force updates and optional → "Update". | | applyPendingUpdate() | Atomically activates the pending OTA (current → backup, pending → current). Does not restart. | | markUpdateSuccessful() | Call after your app finishes initializing post-activation, to confirm the new bundle is healthy. | | rollback() | Restores the previous known-good OTA (or the bundled app if there's no backup). | | clearUpdate() | Removes the pending OTA and temp files. Never touches the active OTA. | | getCurrentVersion() | The active OTA version, or null if running the bundled app. | | getPendingVersion() | The pending OTA version, or null. | | hasPendingUpdate() | true/false. | | getStatus() | { status, currentVersion, pendingVersion }. | | restart() | Reloads the WebView into the active OTA (or bundled app). |

RaOTA also exposes downloadProgress$: Observable<OtaDownloadProgress> for apps that prefer RxJS over the onProgress callback.

RaOTA deliberately does not implement getAppVersion(). Get the native application version from @awesome-cordova-plugins/app-version/ngx in your own Angular code — see OTA version vs. app version.

TypeScript types

import {
  RaOTA,
  OtaUpdateType,      // 'force' | 'background' | 'optional'
  OtaManifest,
  OtaUpdateInfo,
  OtaDownloadProgress,
  OtaStatus,
  OtaError,
  OtaStatusSnapshot,
} from 'ra-cordova-ota-update';

See dist/index.d.ts / ts-src/types.ts for full definitions. No any is used in the public API.

Manifest format

Your OTA server serves a JSON document such as:

{
  "version": 42,
  "type": "background",
  "url": "https://example.com/ota/releases/42/update.zip",
  "sha256": "ABCDEF...",
  "size": 18439221,
  "minAppVersion": {
    "ios": "8.5.0",
    "android": "8.2.0"
  },
  "message": {
    "en": "A new update is available.",
    "ar": "يتوفر تحديث جديد."
  }
}

Only version, type, and url are required. sha256, size, minAppVersion, and message are all optional.

ZIP format

The ZIP must contain the contents of your Angular www/ directory, with index.html at the ZIP root:

✅ correct                 ❌ incorrect
update.zip                 update.zip
├── index.html              └── www/
├── main.js                     ├── index.html
├── polyfills.js                └── ...
├── styles.css
└── assets/

If index.html is not found at the ZIP root after extraction, the update is rejected with INVALID_OTA and no files are activated or marked pending.

SHA-256 verification

If the manifest includes sha256, the native layer computes the SHA-256 of the downloaded ZIP and compares it (case-insensitively) to the manifest value before extraction. On mismatch:

  • the ZIP is deleted,
  • nothing is extracted,
  • nothing is marked pending or activated,
  • the Promise rejects with { code: 'CHECKSUM_MISMATCH', ... }.

If sha256 is omitted, verification is skipped — but path-traversal protection and index.html validation still run.

Update types

There are exactly three types. There is no "skip forever."

                    UPDATE FOUND
                         |
          +--------------+--------------+
          |              |              |
        FORCE       BACKGROUND      OPTIONAL
          |              |              |
      DOWNLOAD        DOWNLOAD       ASK USER
          |              |           /       \
       VERIFY         VERIFY      UPDATE    LATER
          |              |           |         |
       INSTALL        PENDING     INSTALL   PENDING
          |              |           |         |
       RESTART         KEEP       RESTART   KEEP
                         |
                    NEXT APP START
                         |
                  ACTIVATE PENDING
                         |
                    NEW OTA ACTIVE
const info = await this.otaUpdate.checkForUpdate({ url: MANIFEST_URL });
if (!info.available) return;

switch (info.type) {
  case 'force':
    await this.otaUpdate.installUpdate(info as OtaManifest);
    break;

  case 'background':
    await this.otaUpdate.downloadUpdate(info as OtaManifest);
    break; // activates automatically on next app start

  case 'optional': {
    const wantsUpdate = await askUser(info.message?.en);
    if (wantsUpdate) {
      await this.otaUpdate.installUpdate(info as OtaManifest);
    } else {
      await this.otaUpdate.downloadUpdate(info as OtaManifest); // "Later"
    }
    break;
  }
}

Call this once, early, on every app start (e.g. in your root component or an app initializer) to activate anything left pending from a previous session:

if (await this.otaUpdate.hasPendingUpdate()) {
  await this.otaUpdate.applyPendingUpdate();
  await this.otaUpdate.restart();
}

Pending updates & activation

Pending state is persisted natively (a metadata.json file in private app storage, plus the extracted pending/ directory) — never only in JavaScript memory. It survives app close, app restart, and device reboot.

Activation is atomic:

current  ->  backup
pending  ->  current

The plugin never partially replaces the active OTA: it only removes the old current after the new one has fully moved into place, and only clears pending after a successful move.

Rollback & startup confirmation

After applyPendingUpdate(), the plugin marks its internal state as "activating" and does not clear that flag until your app calls markUpdateSuccessful():

// e.g. in your app's root component, after your app has confirmed
// it initialized correctly (data loaded, no fatal boot errors, etc.)
await this.otaUpdate.markUpdateSuccessful();

If the app is killed or crashes before markUpdateSuccessful() is ever called, the next app launch detects the still-"activating" flag and automatically rolls back to the previous backup (or to the bundled app, if there is no backup) before your JavaScript even runs.

This is best-effort protection — it can catch "app never got far enough to confirm" scenarios, but it cannot detect every possible JavaScript error (e.g. a bug that leaves the UI in a broken-but-not-crashed state). Design your own health checks accordingly, and call rollback() manually if you detect a broken update at runtime.

OTA version vs. app version

These are two completely independent version systems. Never compare them to each other.

Native app version (App Store / Google Play):  8.6.0
Current OTA version:                            41
New OTA version:                                42

RaOTA compares 42 > 41 (OTA vs. OTA). It never compares 42 against 8.6.0.

If a manifest has no OTA installed yet, the current OTA version is treated as "the bundled application" (getCurrentVersion() returns null), and any manifest version is considered newer.

minAppVersion in the manifest is preserved and returned by checkForUpdate(), but RaOTA does not evaluate it. Get the running native app version yourself with @awesome-cordova-plugins/app-version/ngx and compare it against info.minAppVersion?.[platform] using semantic versioning in your own Angular code before deciding whether to apply an update.

Android notes

  • Java, no external storage permissions — everything lives under context.getFilesDir()/ota/.
  • ZIP extraction includes explicit path-traversal ("zip slip") protection: absolute paths, .. segments, and Windows drive-letter paths are all rejected before anything is written to disk.
  • OTA assets are served through Cordova's normal https://<hostname>/ origin — never through a file:// redirect. The WebView is never navigated away from Cordova's default page. Instead, the plugin contributes a CordovaPluginPathHandler (RaOtaUpdate.getPathHandler()), which Cordova's WebViewAssetLoader consults on every resource request before falling back to the bundled www/ assets: if ota/current/<requested path> exists, it's streamed back directly with the correct MIME type; otherwise the request falls through untouched to the normal bundled asset (or the next plugin's handler). This means:
    • cordova.js, cordova_plugins.js, and everything under plugins/ are always served from the bundled build (the OTA never needs to include them), so the bridge always matches whichever native plugins are actually compiled into the running APK.
    • Every other Cordova plugin keeps working normally on an active OTA, since the page origin, scheme, and bridge injection are completely unaffected — this plugin only overrides individual file contents, never the navigation/origin model.
    • No setAllowFileAccess* WebView settings are needed or touched.
    • restart() simply reloads https://<hostname>/index.html (the same URL Cordova always uses); the path handler resolves fresh on every request, so it automatically reflects whichever OTA is current at that moment — no separate "load the OTA" vs. "load bundled" code path.

iOS notes

  • Objective-C, storage under the app's Application Support sandbox (excluded from iCloud/iTunes backup).
  • ZIP extraction uses the SSZipArchive CocoaPod (declared in plugin.xml via <framework type="podspec">); cordova build ios runs pod install automatically as part of a standard CocoaPods-integrated cordova-ios build. SHA-256 uses CommonCrypto, which ships with the iOS SDK.
  • Extraction includes a defense-in-depth containment check on top of the ZIP library's own protections: every extracted path is verified to resolve inside the destination directory, and symlinked entries are rejected outright.
  • The active OTA is loaded with [webView loadFileURL:allowingReadAccessToURL:], granting the WKWebView read access to the OTA directory so relative asset URLs resolve correctly.

Security

  • OTA can update only web assets (HTML, JS, CSS, images, fonts).
  • OTA cannot update: Java, Kotlin, Swift, Objective-C, Cordova native plugins, AndroidManifest.xml, Info.plist, native frameworks, the .apk/.ipa itself, or native permissions. Any of those require a normal App Store / Google Play release.
  • The bundled www/ (or file:///android_asset/www) is never modified — it's always available as the ultimate fallback.
  • SHA-256 verification (when the manifest provides it) happens before extraction.
  • ZIP path-traversal protection on both platforms.
  • No skippedVersion / wasSkipped / permanent-skip state exists anywhere in the plugin.

Storage layout

ota/
  metadata.json     currentVersion, pendingVersion, activating flag, ...
  current/          the currently active OTA (served by the WebView)
  pending/          downloaded + verified, waiting for applyPendingUpdate()
  backup/           previous known-good OTA, for rollback
  tmp/              scratch space for downloads/extraction (always cleaned up)

Download progress

await this.otaUpdate.downloadUpdate(manifest, (progress) => {
  console.log(`${progress.percentage.toFixed(1)}% (${progress.downloadedBytes}/${progress.totalBytes})`);
});

// or, RxJS-style:
this.otaUpdate.downloadProgress$.subscribe((p) => this.progressBar = p.percentage);

totalBytes/percentage will be 0 if the server response didn't include a usable content length.

Failure recovery

Every native operation is written to fail closed:

  • Network/HTTP failure during download → nothing is written to pending.
  • SHA-256 mismatch → ZIP deleted, nothing extracted or activated.
  • ZIP traversal detected, or extraction fails → extracted files deleted, nothing staged.
  • Missing index.html at ZIP root → extracted files deleted, INVALID_OTA.
  • Interrupted activation (app killed mid-applyPendingUpdate() or crashed before markUpdateSuccessful()) → automatic rollback on next launch.
  • rollback() with no backup → falls back to the bundled application.

All errors reject the returned Promise with a structured OtaError ({ code, message }) — see OtaErrorCode in the type definitions.

Limitations

  • Android vs. iOS asset serving differ. Android serves OTA assets through Cordova's https://<hostname>/ origin via a CordovaPluginPathHandler (see Android notes) — the WebView's origin/scheme never changes. iOS still loads the OTA with [webView loadFileURL:allowingReadAccessToURL:] (a file:// URL scoped to the OTA directory), which does not carry the same "other plugins keep working transparently" guarantee if a specific iOS plugin assumes the bundled file:// origin — most plugins built against WKWebView are unaffected in practice, but this is a real platform difference to be aware of. Bringing iOS to the same asset-loader-based model as Android would require WKURLSchemeHandler — ask if you'd like that added. On iOS, the file:// redirect happens from the plugin's pluginInitialize() hook, which runs very early but not necessarily before the very first paint of the bundled index.html; a very brief flash of the bundled page is theoretically possible on slow devices.
  • markUpdateSuccessful() is best-effort and cannot catch every class of JavaScript failure — see Rollback & startup confirmation.
  • The OTA system only updates what ships inside your Angular www/ output. Anything requiring a native rebuild (new Cordova plugins, native permission changes, config.xml changes, etc.) still requires a normal store release.
  • checkForUpdate() fetches your manifest URL directly from the WebView context (via fetch), so your OTA server must respond with the appropriate CORS headers if the manifest is hosted on a different origin than your app expects to call.