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.
- package/README.md +126 -151
- package/android/src/main/java/vn/reactnativebspatch/BsPatchModule.kt +0 -25
- package/android/src/main/java/vn/reactnativebspatch/OtaBundleResolver.kt +14 -6
- package/ios/OtaBundleResolver.swift +14 -6
- package/ios/RNBsPatch.mm +1 -28
- package/lib/commonjs/NativeBsPatch.js.map +1 -1
- package/lib/commonjs/ota/OtaProvider.js +29 -16
- package/lib/commonjs/ota/OtaProvider.js.map +1 -1
- package/lib/commonjs/ota/otaAssetResolver.js +0 -20
- package/lib/commonjs/ota/otaAssetResolver.js.map +1 -1
- package/lib/commonjs/ota/otaCore.js +200 -330
- package/lib/commonjs/ota/otaCore.js.map +1 -1
- package/lib/commonjs/ota/otaMetadata.js +21 -21
- package/lib/commonjs/ota/otaMetadata.js.map +1 -1
- package/lib/commonjs/ota/storage/GitHubRawStorage.js +13 -15
- package/lib/commonjs/ota/storage/GitHubRawStorage.js.map +1 -1
- package/lib/commonjs/ota/types.js.map +1 -1
- package/lib/module/NativeBsPatch.js.map +1 -1
- package/lib/module/ota/OtaProvider.js +30 -17
- package/lib/module/ota/OtaProvider.js.map +1 -1
- package/lib/module/ota/otaAssetResolver.js +1 -21
- package/lib/module/ota/otaAssetResolver.js.map +1 -1
- package/lib/module/ota/otaCore.js +197 -329
- package/lib/module/ota/otaCore.js.map +1 -1
- package/lib/module/ota/otaMetadata.js +19 -19
- package/lib/module/ota/otaMetadata.js.map +1 -1
- package/lib/module/ota/storage/GitHubRawStorage.js +13 -15
- package/lib/module/ota/storage/GitHubRawStorage.js.map +1 -1
- package/lib/module/ota/types.js.map +1 -1
- package/lib/typescript/NativeBsPatch.d.ts +0 -1
- package/lib/typescript/NativeBsPatch.d.ts.map +1 -1
- package/lib/typescript/ota/OtaProvider.d.ts.map +1 -1
- package/lib/typescript/ota/otaAssetResolver.d.ts.map +1 -1
- package/lib/typescript/ota/otaCore.d.ts +13 -3
- package/lib/typescript/ota/otaCore.d.ts.map +1 -1
- package/lib/typescript/ota/otaMetadata.d.ts +9 -7
- package/lib/typescript/ota/otaMetadata.d.ts.map +1 -1
- package/lib/typescript/ota/storage/GitHubRawStorage.d.ts.map +1 -1
- package/lib/typescript/ota/types.d.ts +5 -19
- package/lib/typescript/ota/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/NativeBsPatch.ts +0 -5
- package/src/ota/OtaProvider.tsx +30 -23
- package/src/ota/otaAssetResolver.ts +1 -17
- package/src/ota/otaCore.ts +203 -457
- package/src/ota/otaMetadata.ts +30 -20
- package/src/ota/storage/GitHubRawStorage.ts +13 -21
- package/src/ota/types.ts +5 -20
- package/tools/build_ota.sh +222 -525
- package/tools/setup_project.js +17 -115
- package/tools/update_registry.js +31 -32
- package/tools/upload_ota.sh +61 -14
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["Image","
|
|
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,
|
|
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`
|
|
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
|
|
167
|
+
* @returns Patches for this app version + channel; rejects if the registry can't be read
|
|
149
168
|
*/
|
|
150
|
-
export const fetchOtaManifest =
|
|
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 =>
|
|
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
|
-
*
|
|
341
|
+
* Installs one OTA version.
|
|
323
342
|
*
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
*
|
|
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
|
-
*
|
|
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
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
-
|
|
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
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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
|
-
}
|
|
486
|
-
|
|
487
|
-
|
|
441
|
+
});
|
|
442
|
+
throw new Error(`${what} hash mismatch`);
|
|
443
|
+
};
|
|
488
444
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
const
|
|
493
|
-
|
|
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
|
-
//
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
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
|
-
//
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
await
|
|
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(
|
|
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
|
-
|
|
585
|
-
|
|
586
|
-
|
|
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
|
-
|
|
589
|
-
|
|
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.
|
|
601
|
-
|
|
527
|
+
onStatus(OtaStatus.DOWNLOADING, `Downloading V${patch.toVersion} (full bundle)...`);
|
|
528
|
+
const {
|
|
602
529
|
downloadMs,
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
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
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
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.
|
|
556
|
+
console.warn('[otaCore] Patch failed, falling back to full bundle:', error);
|
|
614
557
|
}
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
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
|
-
|
|
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
|
}
|