react-native-bspatch 2.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +126 -151
  2. package/android/src/main/java/vn/reactnativebspatch/BsPatchModule.kt +0 -25
  3. package/android/src/main/java/vn/reactnativebspatch/OtaBundleResolver.kt +14 -6
  4. package/ios/OtaBundleResolver.swift +14 -6
  5. package/ios/RNBsPatch.mm +1 -28
  6. package/lib/commonjs/NativeBsPatch.js.map +1 -1
  7. package/lib/commonjs/ota/OtaProvider.js +29 -16
  8. package/lib/commonjs/ota/OtaProvider.js.map +1 -1
  9. package/lib/commonjs/ota/otaAssetResolver.js +0 -20
  10. package/lib/commonjs/ota/otaAssetResolver.js.map +1 -1
  11. package/lib/commonjs/ota/otaCore.js +200 -330
  12. package/lib/commonjs/ota/otaCore.js.map +1 -1
  13. package/lib/commonjs/ota/otaMetadata.js +21 -21
  14. package/lib/commonjs/ota/otaMetadata.js.map +1 -1
  15. package/lib/commonjs/ota/storage/GitHubRawStorage.js +13 -15
  16. package/lib/commonjs/ota/storage/GitHubRawStorage.js.map +1 -1
  17. package/lib/commonjs/ota/types.js.map +1 -1
  18. package/lib/module/NativeBsPatch.js.map +1 -1
  19. package/lib/module/ota/OtaProvider.js +30 -17
  20. package/lib/module/ota/OtaProvider.js.map +1 -1
  21. package/lib/module/ota/otaAssetResolver.js +1 -21
  22. package/lib/module/ota/otaAssetResolver.js.map +1 -1
  23. package/lib/module/ota/otaCore.js +197 -329
  24. package/lib/module/ota/otaCore.js.map +1 -1
  25. package/lib/module/ota/otaMetadata.js +19 -19
  26. package/lib/module/ota/otaMetadata.js.map +1 -1
  27. package/lib/module/ota/storage/GitHubRawStorage.js +13 -15
  28. package/lib/module/ota/storage/GitHubRawStorage.js.map +1 -1
  29. package/lib/module/ota/types.js.map +1 -1
  30. package/lib/typescript/NativeBsPatch.d.ts +0 -1
  31. package/lib/typescript/NativeBsPatch.d.ts.map +1 -1
  32. package/lib/typescript/ota/OtaProvider.d.ts.map +1 -1
  33. package/lib/typescript/ota/otaAssetResolver.d.ts.map +1 -1
  34. package/lib/typescript/ota/otaCore.d.ts +13 -3
  35. package/lib/typescript/ota/otaCore.d.ts.map +1 -1
  36. package/lib/typescript/ota/otaMetadata.d.ts +9 -7
  37. package/lib/typescript/ota/otaMetadata.d.ts.map +1 -1
  38. package/lib/typescript/ota/storage/GitHubRawStorage.d.ts.map +1 -1
  39. package/lib/typescript/ota/types.d.ts +5 -19
  40. package/lib/typescript/ota/types.d.ts.map +1 -1
  41. package/package.json +1 -1
  42. package/src/NativeBsPatch.ts +0 -5
  43. package/src/ota/OtaProvider.tsx +30 -23
  44. package/src/ota/otaAssetResolver.ts +1 -17
  45. package/src/ota/otaCore.ts +203 -457
  46. package/src/ota/otaMetadata.ts +30 -20
  47. package/src/ota/storage/GitHubRawStorage.ts +13 -21
  48. package/src/ota/types.ts +5 -20
  49. package/tools/build_ota.sh +222 -525
  50. package/tools/setup_project.js +17 -115
  51. package/tools/update_registry.js +31 -32
  52. package/tools/upload_ota.sh +61 -14
@@ -1 +1 @@
1
- {"version":3,"names":["Image","Platform","ReactNativeBlobUtil","NativeBsPatch","ASSET_STORE_DIR","fs","dirs","DocumentDir","assetManifest","assetMap","Map","assetList","assetLookupCache","isPatched","log","msg","formatted","console","logNative","initOtaAssetResolver","version","otaDir","versionDir","manifestPath","exists","content","readFile","JSON","parse","map","list","assets","forEach","a","normPath","originalPath","toLowerCase","replace","_normPath","set","length","patchAssetResolver","e","message","resolveAssetSource","setCustomSourceTransformer","resolver","res","defaultAsset","uri","startsWith","normUri","cached","get","found","undefined","find","endsWith","isBase","OS","finalUri","MainBundleDir","resName","split","pop","localPath","hash","ext","includes","resetOtaAssetResolver"],"sourceRoot":"../../../src","sources":["ota/otaAssetResolver.ts"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"names":["Image","ReactNativeBlobUtil","NativeBsPatch","ASSET_STORE_DIR","fs","dirs","DocumentDir","assetManifest","assetMap","Map","assetList","assetLookupCache","isPatched","log","msg","formatted","console","logNative","initOtaAssetResolver","version","otaDir","versionDir","manifestPath","exists","content","readFile","JSON","parse","map","list","assets","forEach","a","normPath","originalPath","toLowerCase","replace","_normPath","set","length","patchAssetResolver","e","message","resolveAssetSource","setCustomSourceTransformer","resolver","res","defaultAsset","uri","startsWith","normUri","cached","get","found","undefined","find","endsWith","localPath","hash","ext","finalUri","includes","resetOtaAssetResolver"],"sourceRoot":"../../../src","sources":["ota/otaAssetResolver.ts"],"mappings":"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","ignoreList":[]}
@@ -7,9 +7,9 @@
7
7
  * - Download patches with retry
8
8
  * - Verify integrity (SHA-256 hash)
9
9
  * - Verify security (RSA/ECDSA signature)
10
- * - Apply binary patch (bsdiff)
10
+ * - Apply binary patch (bsdiff) from the installed OTA bundle
11
+ * - Full bundle download (first OTA after a store install, or patch failure)
11
12
  * - Atomic bundle swap
12
- * - Full bundle fallback on failure
13
13
  * - Structured telemetry logging
14
14
  */
15
15
 
@@ -17,7 +17,7 @@ import { Platform } from 'react-native';
17
17
  import ReactNativeBlobUtil from 'react-native-blob-util';
18
18
  import NativeBsPatch from '../NativeBsPatch';
19
19
  import { createOtaLogger, OtaEvent } from './otaLogger';
20
- import { ensureOtaDir, getBundlePath, loadMetadata, registerNewVersion, saveMetadata } from './otaMetadata';
20
+ import { ensureOtaDir, getBundlePath, loadMetadata, registerNewVersion, updateMetadata } from './otaMetadata';
21
21
  import { GitHubRawStorageProvider } from './storage/GitHubRawStorage';
22
22
  import { OtaStatus } from './types';
23
23
  const OTA_DIR = `${ReactNativeBlobUtil.fs.dirs.DocumentDir}/ota`;
@@ -26,9 +26,28 @@ const OTA_DIR = `${ReactNativeBlobUtil.fs.dirs.DocumentDir}/ota`;
26
26
 
27
27
  const DEFAULT_STORAGE = new GitHubRawStorageProvider();
28
28
 
29
- /** Explicit `config.storage` (e.g. a future Cloudflare provider) or GitHub raw. */
29
+ /** Explicit `config.storage` or the default static-host provider (GitHub raw, R2, CDN...). */
30
30
  const getStorage = config => config.storage ?? DEFAULT_STORAGE;
31
31
 
32
+ /**
33
+ * Returns a message for config the default provider can't work with, else null.
34
+ * No fallbacks: a missing env value must surface, not silently point somewhere else.
35
+ */
36
+ export const validateOtaConfig = config => {
37
+ if (config.storage) return null;
38
+ const {
39
+ baseUrl,
40
+ channel
41
+ } = config.customServer ?? {};
42
+ if (!baseUrl || !/^https?:\/\//.test(baseUrl)) {
43
+ return `customServer.baseUrl must be an http(s) URL, got ${JSON.stringify(baseUrl)}`;
44
+ }
45
+ if (!channel) {
46
+ return `customServer.channel is required, got ${JSON.stringify(channel)}`;
47
+ }
48
+ return null;
49
+ };
50
+
32
51
  // ─── Helpers ──────────────────────────────────────────────────────────
33
52
 
34
53
  /**
@@ -145,17 +164,9 @@ const downloadWithRetry = async (url, destPath, config, onProgress, headers = {}
145
164
  * the app can still apply updates that were bundled at build time.
146
165
  *
147
166
  * @param config - OTA configuration containing storage provider and fallback patches
148
- * @returns Array of available patches, or bundled patches if remote fetch fails
167
+ * @returns Patches for this app version + channel; rejects if the registry can't be read
149
168
  */
150
- export const fetchOtaManifest = async config => {
151
- try {
152
- const storage = getStorage(config);
153
- return await storage.fetchManifest(config);
154
- } catch (error) {
155
- console.warn('[otaCore] Manifest fetch error:', error.message);
156
- return config.bundledPatches || [];
157
- }
158
- };
169
+ export const fetchOtaManifest = config => getStorage(config).fetchManifest(config);
159
170
 
160
171
  // ─── Hashed Asset Store ───────────────────────────────────────────────
161
172
 
@@ -206,9 +217,8 @@ const downloadAndSyncAssets = async (assetManifestUrl, version, config, log, onS
206
217
  console.log(`[otaCore] Saved asset manifest to: ${manifestPath}`);
207
218
  }
208
219
  const manifest = JSON.parse(manifestData);
209
- // Filter out base assets, we don't need to download them
210
220
  // Hash/ext become file names: reject anything that isn't a plain sha256 + extension
211
- assets = (manifest.assets || []).filter(a => !a.isBase && /^[a-f0-9]{64}$/i.test(a.hash) && /^[a-z0-9]+$/i.test(a.ext));
221
+ assets = (manifest.assets || []).filter(a => /^[a-f0-9]{64}$/i.test(a.hash) && /^[a-z0-9]+$/i.test(a.ext));
212
222
  } catch (e) {
213
223
  // Fatal: activating the bundle without its manifest leaves images unresolved
214
224
  throw new Error(`Failed to fetch asset manifest: ${e.message}`);
@@ -316,24 +326,28 @@ export const getLocalAssetPath = (hash, ext) => `${ASSET_STORE_DIR}/${hash}.${ex
316
326
  */
317
327
  export const getLocalPatchPath = (patch, docDir) => `${docDir}/ota/patch-${patch.appVersion}-${patch.id}.patch`;
318
328
 
329
+ /**
330
+ * Picks the registry entry for `localV → targetV`: a patch from the OTA bundle
331
+ * already on disk, else the full-bundle entry (store build, or outside the patch window).
332
+ */
333
+ export const selectUpdate = (patchList, localV, targetV) => {
334
+ const forTarget = patchList.filter(p => p.toVersion === targetV && (!p.platform || p.platform === Platform.OS));
335
+ return forTarget.find(p => p.filename && p.fromVersion === localV) ?? forTarget.find(p => !p.filename);
336
+ };
337
+
319
338
  // ─── Main Update Pipeline ─────────────────────────────────────────────
320
339
 
321
340
  /**
322
- * Applies a single patch and reloads the bundle.
341
+ * Installs one OTA version.
323
342
  *
324
- * Pipeline:
325
- * 1. Version compatibility check
326
- * 2. Download patch (with retry)
327
- * 4. Security signature check
328
- * 5. Resolve source bundle
329
- * 6. Verify source hash
330
- * 7. Apply binary patch
331
- * 8. Verify result hash
332
- * 9. Atomic commit (mv)
333
- * 10. Update metadata
334
- * 11. Reload bundle
343
+ * Patch path (an OTA bundle for `patch.fromVersion` is on disk):
344
+ * download patch → signature → source hash → bspatch → result hash/signature
345
+ * Full path (store build, no patch, or patch path failed):
346
+ * download full bundle → hash → signature
347
+ * Both then: sync assets → atomic commit → metadata → mark healthy → reload.
335
348
  *
336
- * On failure → fallback to full bundle download if URL available.
349
+ * The store build's bundle is never used as a patch source: it can't be
350
+ * reproduced byte-for-byte at build time, so the first OTA is always a full download.
337
351
  */
338
352
  let updateInFlight = false;
339
353
 
@@ -359,195 +373,135 @@ const runUpdatePipeline = async (patch, config, onStatus, onProgress, options =
359
373
  const log = createOtaLogger(config.onEvent);
360
374
  await ensureOtaDir();
361
375
  const totalStart = Date.now();
376
+ const fs = ReactNativeBlobUtil.fs;
362
377
  const finalBundlePath = getBundlePath(patch.toVersion, config.baseAppVersion) || '';
363
378
  const tempBundle = `${finalBundlePath}.tmp`;
364
- const localPatchPath = getLocalPatchPath(patch, ReactNativeBlobUtil.fs.dirs.DocumentDir);
365
-
366
- // Cached per asset hash, so calling it again in the fallback is cheap
367
- const syncAssets = async () => {
368
- if (!patch.assetManifestUrl) return;
369
- const manifestUrl = await getStorage(config).getUpdateFileUrl(patch.assetManifestUrl, config);
370
- await downloadAndSyncAssets(manifestUrl, patch.toVersion, config, log, onStatus, onProgress);
371
- };
372
- try {
373
- // ── Step 1: Version Compatibility ──
374
- if (patch.minAppVersion) {
375
- let currentAppVer = config.appVersion;
376
- if (!currentAppVer && typeof NativeBsPatch.getAppVersion === 'function') {
377
- try {
378
- currentAppVer = await NativeBsPatch.getAppVersion();
379
- } catch {
380
- currentAppVer = undefined;
381
- }
382
- }
383
- if (!currentAppVer) {
384
- throw new Error(`[otaCore] Cannot verify minAppVersion: Native App Version could not be determined.`);
385
- }
386
- if (!semverGte(currentAppVer, patch.minAppVersion)) {
387
- log.emit(OtaEvent.VERSION_INCOMPATIBLE, {
388
- message: `App ${currentAppVer} < required ${patch.minAppVersion}`,
389
- version: patch.toVersion
390
- });
391
- throw new Error(`Native app ${currentAppVer} is too old. Update to ${patch.minAppVersion}+ first.`);
392
- }
379
+ const versionDir = `${OTA_DIR}/v${patch.toVersion}`;
380
+ const localPatchPath = getLocalPatchPath(patch, fs.dirs.DocumentDir);
381
+
382
+ // ── Version Compatibility ──
383
+ // Checked before anything else so an incompatible update can't reach the full-bundle path
384
+ if (patch.minAppVersion) {
385
+ let currentAppVer = config.appVersion;
386
+ if (!currentAppVer) {
387
+ currentAppVer = await NativeBsPatch.getAppVersion().catch(() => undefined);
393
388
  }
394
-
395
- // ── Step 2: Download ──
396
- let downloadMs = 0;
397
- let patchSizeBytes = 0;
398
- let fromCache = false;
399
- if (await ReactNativeBlobUtil.fs.exists(localPatchPath)) {
400
- const stat = await ReactNativeBlobUtil.fs.stat(localPatchPath);
401
- patchSizeBytes = Number(stat.size);
402
- fromCache = true;
403
- onProgress(100);
404
- } else {
405
- log.emit(OtaEvent.DOWNLOAD_START, {
389
+ if (!currentAppVer) {
390
+ throw new Error('[otaCore] Cannot verify minAppVersion: Native App Version could not be determined.');
391
+ }
392
+ if (!semverGte(currentAppVer, patch.minAppVersion)) {
393
+ log.emit(OtaEvent.VERSION_INCOMPATIBLE, {
394
+ message: `App ${currentAppVer} < required ${patch.minAppVersion}`,
406
395
  version: patch.toVersion
407
396
  });
408
- onStatus(OtaStatus.DOWNLOADING, `Downloading V${patch.toVersion}...`);
409
- const dlStart = Date.now();
410
- try {
411
- const storage = getStorage(config);
412
- const url = await storage.getUpdateFileUrl(patch.filename, config);
413
- await downloadWithRetry(url, localPatchPath, config, onProgress, {});
414
- downloadMs = Date.now() - dlStart;
415
- const stat = await ReactNativeBlobUtil.fs.stat(localPatchPath);
416
- patchSizeBytes = Number(stat.size);
417
- log.emit(OtaEvent.DOWNLOAD_SUCCESS, {
418
- version: patch.toVersion,
419
- durationMs: downloadMs,
420
- sizeKB: patchSizeBytes / 1024
421
- });
422
- } catch (dlError) {
423
- log.emit(OtaEvent.DOWNLOAD_FAIL, {
424
- error: dlError.message,
425
- version: patch.toVersion
426
- });
427
- throw dlError;
428
- }
397
+ throw new Error(`Native app ${currentAppVer} is too old. Update to ${patch.minAppVersion}+ first.`);
429
398
  }
430
-
431
- // ── Step 4: Signature Verification ──
432
- // Verify before feeding the patch to native bspatch
433
- if (config.publicKey) {
434
- onStatus(OtaStatus.APPLYING, 'Verifying signature...');
435
- try {
436
- await assertSigned(localPatchPath, patch.signature, config, 'Patch');
437
- } catch (sigError) {
438
- log.emit(OtaEvent.SIGNATURE_INVALID, {
439
- version: patch.toVersion
440
- });
441
- throw sigError;
442
- }
399
+ }
400
+ if (!(await fs.exists(versionDir))) {
401
+ await fs.mkdir(versionDir);
402
+ }
403
+ const download = async (remoteName, dest) => {
404
+ log.emit(OtaEvent.DOWNLOAD_START, {
405
+ version: patch.toVersion
406
+ });
407
+ const dlStart = Date.now();
408
+ try {
409
+ const url = await getStorage(config).getUpdateFileUrl(remoteName, config);
410
+ await downloadWithRetry(url, dest, config, onProgress);
411
+ } catch (dlError) {
412
+ log.emit(OtaEvent.DOWNLOAD_FAIL, {
413
+ error: dlError.message,
414
+ version: patch.toVersion
415
+ });
416
+ throw dlError;
443
417
  }
444
-
445
- // ── Step 4.2: Download & Sync Assets ──
446
- await syncAssets();
447
-
448
- // ── Step 5: Resolve Source Bundle ──
449
- onStatus(OtaStatus.APPLYING, 'Preparing source...');
450
- const metadata = await loadMetadata(config.baseAppVersion);
451
- const oldBundlePath = getBundlePath(metadata.currentVersion, config.baseAppVersion);
452
-
453
- // ── Resolve the actual old bundle path ──────────────────────────────
454
- // getBundlePath returns null when currentVersion == baseAppVersion, meaning
455
- // "use the APK's baked-in bundle". However, if BASE_APP_VERSION was bumped
456
- // to match a version that was applied via OTA (not actually baked into the
457
- // installed APK), direct APK extraction would produce a DIFFERENT file than
458
- // what the build script recorded as the baseHash for the next patch.
459
- //
460
- // Strategy (in order of precedence):
461
- // 1. Use the explicit OTA bundle path from getBundlePath (version > base) ✓
462
- // 2. If null (version == base), check for a previously OTA-stored bundle
463
- // at ota/v{currentVersion}/ — its hash matches the server registry.
464
- // 3. Fall back to APK extraction only if no OTA bundle exists on disk.
465
- const bundleFilename = Platform.OS === 'android' ? 'index.android.bundle' : 'main.jsbundle';
466
- let actualOldPath = '';
467
- if (!oldBundlePath) {
468
- // currentVersion == baseAppVersion: prefer OTA-stored bundle for this
469
- // version over APK extraction (handles BASE_APP_VERSION bump scenarios).
470
- const otaStoredPath = `${OTA_DIR}/v${metadata.currentVersion}/${bundleFilename}`;
471
- if (await ReactNativeBlobUtil.fs.exists(otaStoredPath)) {
472
- if (__DEV__) {
473
- console.log(`[otaCore] Source: OTA-stored bundle for V${metadata.currentVersion} (BASE_APP_VERSION match)`);
474
- }
475
- actualOldPath = otaStoredPath;
476
- } else {
477
- // True base: extract byte-for-byte from APK assets.
478
- // RNFS.copyFileAssets is known to produce inconsistent bytes in some
479
- // APK configurations (e.g., release signing + resource shrinking).
480
- // Re-extract every time: a cached copy can be partial or left over
481
- // from the previous store build. It's one local copy per update.
482
- actualOldPath = `${OTA_DIR}/base.hbc`;
483
- await NativeBsPatch.extractBundleFromAssets(bundleFilename, actualOldPath);
418
+ const downloadMs = Date.now() - dlStart;
419
+ const sizeBytes = Number((await fs.stat(dest)).size);
420
+ log.emit(OtaEvent.DOWNLOAD_SUCCESS, {
421
+ version: patch.toVersion,
422
+ durationMs: downloadMs,
423
+ sizeKB: sizeBytes / 1024
424
+ });
425
+ return {
426
+ downloadMs,
427
+ sizeBytes
428
+ };
429
+ };
430
+ const assertHash = async (path, expected, what) => {
431
+ if (!expected || (await verifyHash(path, expected))) return;
432
+ const actual = await fs.hash(path, 'sha256').catch(() => '');
433
+ log.emit(OtaEvent.HASH_MISMATCH, {
434
+ message: `${what} hash mismatch`,
435
+ version: patch.toVersion,
436
+ metadata: {
437
+ expected,
438
+ actual,
439
+ path
484
440
  }
485
- } else {
486
- actualOldPath = oldBundlePath;
487
- }
441
+ });
442
+ throw new Error(`${what} hash mismatch`);
443
+ };
488
444
 
489
- // ── Debug: log file sizes for diagnostic ──
490
- try {
491
- const oldStat = await ReactNativeBlobUtil.fs.stat(actualOldPath);
492
- const patchStat = await ReactNativeBlobUtil.fs.stat(localPatchPath);
493
- if (__DEV__) {
494
- console.log(`[otaCore] DEBUG old bundle size=${oldStat.size} path=${actualOldPath}`);
495
- console.log(`[otaCore] DEBUG patch size=${patchStat.size} path=${localPatchPath}`);
496
- console.log(`[otaCore] DEBUG baseHash expected=${patch.baseHash}`);
497
- }
498
- } catch {
499
- // Non-fatal: stat is for diagnostic only
445
+ /** tempBundle is verified: sync assets, swap it in, record the version, reload. */
446
+ const commit = async metrics => {
447
+ if (patch.assetManifestUrl) {
448
+ const manifestUrl = await getStorage(config).getUpdateFileUrl(patch.assetManifestUrl, config);
449
+ await downloadAndSyncAssets(manifestUrl, patch.toVersion, config, log, onStatus, onProgress);
500
450
  }
451
+ if (await fs.exists(finalBundlePath)) {
452
+ await fs.unlink(finalBundlePath);
453
+ }
454
+ await fs.mv(tempBundle, finalBundlePath);
501
455
 
502
- // ── Step 6: Verify Source Integrity ──
503
- if (patch.baseHash) {
504
- log.emit(OtaEvent.HASH_VERIFY_START, {
505
- message: 'Verifying source bundle'
506
- });
507
- onStatus(OtaStatus.APPLYING, 'Verifying source integrity...');
508
- const isValid = await verifyHash(actualOldPath, patch.baseHash);
509
- if (!isValid) {
510
- // Re-hash only for the diagnostic log (verifyHash doesn't return the actual hash)
511
- let actualSourceHash = '';
512
- let srcSize = 'unknown';
513
- try {
514
- actualSourceHash = await ReactNativeBlobUtil.fs.hash(actualOldPath, 'sha256');
515
- const srcStat = await ReactNativeBlobUtil.fs.stat(actualOldPath);
516
- srcSize = `${srcStat.size} bytes`;
517
- } catch {
518
- // stat failure is non-fatal for the error message
519
- }
520
- if (__DEV__) {
521
- console.error('[otaCore] SOURCE HASH MISMATCH\n' + ` path: ${actualOldPath}\n` + ` size: ${srcSize}\n` + ` expected: ${patch.baseHash}\n` + ` actual: ${actualSourceHash}\n` + ` patch.id: ${patch.id} (v${patch.fromVersion}→v${patch.toVersion})`);
522
- }
523
- log.emit(OtaEvent.HASH_MISMATCH, {
524
- version: metadata.currentVersion,
525
- message: 'Source bundle hash mismatch',
526
- metadata: {
527
- expected: patch.baseHash,
528
- actual: actualSourceHash,
529
- path: actualOldPath
530
- }
531
- });
532
- throw new Error('Source bundle hash mismatch. Fallback required.');
533
- }
534
- log.emit(OtaEvent.HASH_VERIFY_SUCCESS, {
535
- message: 'Source bundle verified'
536
- });
456
+ // Native version, not config.appVersion: the native resolver compares against it on boot
457
+ const nativeAppVersion = await NativeBsPatch.getAppVersion().catch(() => config.appVersion);
458
+ await updateMetadata(config.baseAppVersion, metadata => registerNewVersion(metadata, patch.toVersion, nativeAppVersion));
459
+ const totalMs = Date.now() - totalStart;
460
+ log.emit(OtaEvent.PATCH_SUCCESS, {
461
+ version: patch.toVersion,
462
+ durationMs: totalMs,
463
+ sizeKB: metrics.patchSizeKB
464
+ });
465
+ onStatus(OtaStatus.SUCCESS, `Update V${patch.toVersion} Successful!`, {
466
+ ...metrics,
467
+ totalMs,
468
+ fromCache: false
469
+ });
470
+
471
+ // registerNewVersion left lastLaunchSuccessful=true: the native resolver marks the
472
+ // first launch of the new version pending, and JS marks it healthy once it renders.
473
+ if (options.autoReload !== false) {
474
+ NativeBsPatch.reloadBundle();
475
+ } else if (__DEV__) {
476
+ console.log(`[otaCore] V${patch.toVersion} prepared in background — applies on next launch.`);
537
477
  }
478
+ };
479
+ const applyPatchFile = async sourcePath => {
480
+ onStatus(OtaStatus.DOWNLOADING, `Downloading V${patch.toVersion}...`);
481
+ const {
482
+ downloadMs,
483
+ sizeBytes
484
+ } = await download(patch.filename, localPatchPath);
538
485
 
539
- // ── Step 7: Apply Patch ──
540
- const versionDir = `${OTA_DIR}/v${patch.toVersion}`;
541
- if (!(await ReactNativeBlobUtil.fs.exists(versionDir))) {
542
- await ReactNativeBlobUtil.fs.mkdir(versionDir);
486
+ // Verify before feeding the patch to native bspatch
487
+ onStatus(OtaStatus.APPLYING, 'Verifying...');
488
+ try {
489
+ await assertSigned(localPatchPath, patch.signature, config, 'Patch');
490
+ } catch (sigError) {
491
+ log.emit(OtaEvent.SIGNATURE_INVALID, {
492
+ version: patch.toVersion
493
+ });
494
+ throw sigError;
543
495
  }
496
+ await assertHash(sourcePath, patch.baseHash, 'Source bundle');
544
497
  log.emit(OtaEvent.PATCH_START, {
545
498
  version: patch.toVersion
546
499
  });
547
500
  onStatus(OtaStatus.APPLYING, 'Applying binary patch...');
548
501
  const applyStart = Date.now();
549
- const success = await NativeBsPatch.applyPatch(actualOldPath, tempBundle, localPatchPath);
502
+ const success = await NativeBsPatch.applyPatch(sourcePath, tempBundle, localPatchPath);
550
503
  const applyMs = Date.now() - applyStart;
504
+ await fs.unlink(localPatchPath).catch(() => {});
551
505
  if (!success) {
552
506
  log.emit(OtaEvent.PATCH_FAIL, {
553
507
  message: 'BsPatch returned false',
@@ -555,148 +509,62 @@ const runUpdatePipeline = async (patch, config, onStatus, onProgress, options =
555
509
  });
556
510
  throw new Error('BsPatch application failed');
557
511
  }
558
-
559
- // ── Step 8: Verify Result Integrity ──
560
- if (patch.bundleHash) {
561
- log.emit(OtaEvent.HASH_VERIFY_START, {
562
- message: 'Verifying result bundle'
563
- });
564
- onStatus(OtaStatus.APPLYING, 'Verifying integrity...');
565
- const isValid = await verifyHash(tempBundle, patch.bundleHash);
566
- if (!isValid) {
567
- console.error('[otaCore] Result bundle hash mismatch | expected:', patch.bundleHash, 'path:', tempBundle);
568
- log.emit(OtaEvent.HASH_MISMATCH, {
569
- message: 'Result bundle hash mismatch',
570
- version: patch.toVersion,
571
- metadata: {
572
- expected: patch.bundleHash,
573
- path: tempBundle
574
- }
575
- });
576
- throw new Error('Result bundle hash mismatch');
577
- }
578
- log.emit(OtaEvent.HASH_VERIFY_SUCCESS, {
579
- message: 'Result bundle verified'
580
- });
581
- }
512
+ await assertHash(tempBundle, patch.bundleHash, 'Result bundle');
582
513
  await assertSigned(tempBundle, patch.bundleSignature, config, 'Bundle');
583
-
584
- // ── Step 9: Atomic Commit ──
585
- if (await ReactNativeBlobUtil.fs.exists(finalBundlePath)) {
586
- await ReactNativeBlobUtil.fs.unlink(finalBundlePath);
514
+ await commit({
515
+ patchSizeKB: sizeBytes / 1024,
516
+ downloadMs,
517
+ applyMs
518
+ });
519
+ };
520
+ const installFullBundle = async () => {
521
+ if (!patch.fullBundleUrl) {
522
+ throw new Error(`No full bundle for V${patch.toVersion}`);
587
523
  }
588
- await ReactNativeBlobUtil.fs.mv(tempBundle, finalBundlePath);
589
- await ReactNativeBlobUtil.fs.unlink(localPatchPath).catch(() => {});
590
-
591
- // ── Step 10: Update Metadata ──
592
- const newMetadata = registerNewVersion(metadata, patch.toVersion, config.appVersion);
593
- await saveMetadata(newMetadata);
594
- const totalMs = Date.now() - totalStart;
595
- log.emit(OtaEvent.PATCH_SUCCESS, {
596
- version: patch.toVersion,
597
- durationMs: totalMs,
598
- sizeKB: patchSizeBytes / 1024
524
+ log.emit(OtaEvent.FALLBACK_FULL_BUNDLE, {
525
+ version: patch.toVersion
599
526
  });
600
- onStatus(OtaStatus.SUCCESS, `Update V${patch.toVersion} Successful!`, {
601
- patchSizeKB: patchSizeBytes / 1024,
527
+ onStatus(OtaStatus.DOWNLOADING, `Downloading V${patch.toVersion} (full bundle)...`);
528
+ const {
602
529
  downloadMs,
603
- applyMs,
604
- totalMs,
605
- fromCache
530
+ sizeBytes
531
+ } = await download(patch.fullBundleUrl, tempBundle);
532
+ await assertHash(tempBundle, patch.bundleHash, 'Full bundle');
533
+ await assertSigned(tempBundle, patch.bundleSignature, config, 'Full bundle');
534
+ await commit({
535
+ patchSizeKB: sizeBytes / 1024,
536
+ downloadMs,
537
+ applyMs: 0
606
538
  });
539
+ };
540
+ const cleanup = async () => {
541
+ await fs.unlink(localPatchPath).catch(() => {});
542
+ await fs.unlink(tempBundle).catch(() => {});
543
+ };
607
544
 
608
- // ── Step 11: Reload ──
609
- // Mark healthy BEFORE reload — reloadBundle() calls Runtime.exit(0)
610
- NativeBsPatch.markAsHealthy();
611
- if (options.autoReload !== false) {
545
+ // Patch only from an OTA bundle we installed ourselves (byte-identical to the build output)
546
+ const metadata = await loadMetadata(config.baseAppVersion);
547
+ const sourcePath = getBundlePath(metadata.currentVersion, config.baseAppVersion);
548
+ const canPatch = !!patch.filename && patch.fromVersion === metadata.currentVersion && !!sourcePath && (await fs.exists(sourcePath));
549
+ if (canPatch) {
550
+ try {
551
+ await applyPatchFile(sourcePath);
552
+ return;
553
+ } catch (error) {
554
+ await cleanup();
612
555
  if (__DEV__) {
613
- console.log(`[otaCore] V${patch.toVersion} marked healthy — reloading.`);
556
+ console.warn('[otaCore] Patch failed, falling back to full bundle:', error);
614
557
  }
615
- NativeBsPatch.reloadBundle();
616
- } else {
617
- if (__DEV__) {
618
- console.log(`[otaCore] V${patch.toVersion} prepared silently in background — will apply on next launch.`);
558
+ if (!patch.fullBundleUrl) {
559
+ onStatus(OtaStatus.ERROR, `Error: ${error.message}`);
560
+ throw error;
619
561
  }
620
562
  }
563
+ }
564
+ try {
565
+ await installFullBundle();
621
566
  } catch (error) {
622
- if (__DEV__) {
623
- console.error('[otaCore] Pipeline failed:', error);
624
- }
625
- await ReactNativeBlobUtil.fs.unlink(localPatchPath).catch(() => {});
626
- await ReactNativeBlobUtil.fs.unlink(tempBundle).catch(() => {});
627
-
628
- // ── Fallback: Full Bundle Download ──
629
- if (patch.fullBundleUrl) {
630
- try {
631
- onStatus(OtaStatus.DOWNLOADING, 'Fallback: Downloading full bundle...');
632
- const storage = getStorage(config);
633
- const url = await storage.getUpdateFileUrl(patch.fullBundleUrl || patch.filename, config);
634
- const versionDir = `${OTA_DIR}/v${patch.toVersion}`;
635
- if (!(await ReactNativeBlobUtil.fs.exists(versionDir))) {
636
- await ReactNativeBlobUtil.fs.mkdir(versionDir);
637
- }
638
- await downloadWithRetry(url, tempBundle, config, onProgress, {});
639
-
640
- // Verify full bundle hash
641
- if (patch.bundleHash) {
642
- const isValid = await verifyHash(tempBundle, patch.bundleHash);
643
- if (!isValid) {
644
- let actualHash = '';
645
- let size;
646
- try {
647
- actualHash = await ReactNativeBlobUtil.fs.hash(tempBundle, 'sha256');
648
- const stat = await ReactNativeBlobUtil.fs.stat(tempBundle);
649
- size = stat.size;
650
- } catch {
651
- // stat failure is non-fatal
652
- }
653
- if (__DEV__) {
654
- console.error('[otaCore] Full bundle hash mismatch | expected:', patch.bundleHash, 'actual:', actualHash);
655
- }
656
- log.emit(OtaEvent.HASH_MISMATCH, {
657
- message: 'Full bundle hash mismatch',
658
- metadata: {
659
- expected: patch.bundleHash,
660
- actual: actualHash,
661
- size
662
- }
663
- });
664
- throw new Error('Full bundle hash mismatch after fallback');
665
- }
666
- }
667
- await assertSigned(tempBundle, patch.bundleSignature, config, 'Full bundle');
668
- await syncAssets();
669
-
670
- // Atomic swap
671
- if (await ReactNativeBlobUtil.fs.exists(finalBundlePath)) {
672
- await ReactNativeBlobUtil.fs.unlink(finalBundlePath);
673
- }
674
- await ReactNativeBlobUtil.fs.mv(tempBundle, finalBundlePath);
675
- const metadata = await loadMetadata(config.baseAppVersion);
676
- const newMetadata = registerNewVersion(metadata, patch.toVersion, config.appVersion);
677
- await saveMetadata(newMetadata);
678
-
679
- // ── CRITICAL: Mark healthy BEFORE reload so native OtaBundleResolver
680
- // does NOT see lastLaunchSuccessful=false on the very next boot and
681
- // immediately rollback the version we just successfully downloaded.
682
- // (reloadBundle() calls Runtime.exit(0) — JS won't run again before restart)
683
- NativeBsPatch.markAsHealthy();
684
- if (__DEV__) {
685
- console.log(`[otaCore] Fallback V${patch.toVersion} marked healthy — reloading.`);
686
- }
687
- if (options.autoReload !== false) {
688
- onStatus(OtaStatus.SUCCESS, `Fallback successful! Reloading...`);
689
- NativeBsPatch.reloadBundle();
690
- } else {
691
- onStatus(OtaStatus.SUCCESS, `Fallback bundle prepared silently in background.`);
692
- }
693
- return;
694
- } catch (fallbackError) {
695
- if (__DEV__) {
696
- console.error('[otaCore] Full bundle fallback also failed:', fallbackError);
697
- }
698
- }
699
- }
567
+ await cleanup();
700
568
  onStatus(OtaStatus.ERROR, `Error: ${error.message}`);
701
569
  throw error;
702
570
  }