react-native-bspatch 2.0.1 → 3.0.1
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 +127 -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 +2 -2
- 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 +206 -516
- package/tools/cli.js +9 -0
- package/tools/setup_project.js +17 -115
- package/tools/update_registry.js +31 -32
- package/tools/upload_ota.sh +61 -14
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.verifyHash = exports.semverGte = exports.getLocalPatchPath = exports.getLocalAssetPath = exports.fetchOtaManifest = exports.applyOtaPatchInternal = void 0;
|
|
6
|
+
exports.verifyHash = exports.validateOtaConfig = exports.semverGte = exports.selectUpdate = exports.getLocalPatchPath = exports.getLocalAssetPath = exports.fetchOtaManifest = exports.applyOtaPatchInternal = void 0;
|
|
7
7
|
var _reactNative = require("react-native");
|
|
8
8
|
var _reactNativeBlobUtil = _interopRequireDefault(require("react-native-blob-util"));
|
|
9
9
|
var _NativeBsPatch = _interopRequireDefault(require("../NativeBsPatch"));
|
|
@@ -19,9 +19,9 @@ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e
|
|
|
19
19
|
* - Download patches with retry
|
|
20
20
|
* - Verify integrity (SHA-256 hash)
|
|
21
21
|
* - Verify security (RSA/ECDSA signature)
|
|
22
|
-
* - Apply binary patch (bsdiff)
|
|
22
|
+
* - Apply binary patch (bsdiff) from the installed OTA bundle
|
|
23
|
+
* - Full bundle download (first OTA after a store install, or patch failure)
|
|
23
24
|
* - Atomic bundle swap
|
|
24
|
-
* - Full bundle fallback on failure
|
|
25
25
|
* - Structured telemetry logging
|
|
26
26
|
*/
|
|
27
27
|
|
|
@@ -31,9 +31,28 @@ const OTA_DIR = `${_reactNativeBlobUtil.default.fs.dirs.DocumentDir}/ota`;
|
|
|
31
31
|
|
|
32
32
|
const DEFAULT_STORAGE = new _GitHubRawStorage.GitHubRawStorageProvider();
|
|
33
33
|
|
|
34
|
-
/** Explicit `config.storage`
|
|
34
|
+
/** Explicit `config.storage` or the default static-host provider (GitHub raw, R2, CDN...). */
|
|
35
35
|
const getStorage = config => config.storage ?? DEFAULT_STORAGE;
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Returns a message for config the default provider can't work with, else null.
|
|
39
|
+
* No fallbacks: a missing env value must surface, not silently point somewhere else.
|
|
40
|
+
*/
|
|
41
|
+
const validateOtaConfig = config => {
|
|
42
|
+
if (config.storage) return null;
|
|
43
|
+
const {
|
|
44
|
+
baseUrl,
|
|
45
|
+
channel
|
|
46
|
+
} = config.customServer ?? {};
|
|
47
|
+
if (!baseUrl || !/^https?:\/\//.test(baseUrl)) {
|
|
48
|
+
return `customServer.baseUrl must be an http(s) URL, got ${JSON.stringify(baseUrl)}`;
|
|
49
|
+
}
|
|
50
|
+
if (!channel) {
|
|
51
|
+
return `customServer.channel is required, got ${JSON.stringify(channel)}`;
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
};
|
|
55
|
+
|
|
37
56
|
// ─── Helpers ──────────────────────────────────────────────────────────
|
|
38
57
|
|
|
39
58
|
/**
|
|
@@ -48,6 +67,7 @@ const getStorage = config => config.storage ?? DEFAULT_STORAGE;
|
|
|
48
67
|
* semverGte("1.2.4", "1.2.3") // true
|
|
49
68
|
* semverGte("1.2.2", "1.2.3") // false
|
|
50
69
|
*/
|
|
70
|
+
exports.validateOtaConfig = validateOtaConfig;
|
|
51
71
|
const semverGte = (a, b) => {
|
|
52
72
|
const pa = a.split('.').map(Number);
|
|
53
73
|
const pb = b.split('.').map(Number);
|
|
@@ -152,17 +172,9 @@ const downloadWithRetry = async (url, destPath, config, onProgress, headers = {}
|
|
|
152
172
|
* the app can still apply updates that were bundled at build time.
|
|
153
173
|
*
|
|
154
174
|
* @param config - OTA configuration containing storage provider and fallback patches
|
|
155
|
-
* @returns
|
|
175
|
+
* @returns Patches for this app version + channel; rejects if the registry can't be read
|
|
156
176
|
*/
|
|
157
|
-
const fetchOtaManifest =
|
|
158
|
-
try {
|
|
159
|
-
const storage = getStorage(config);
|
|
160
|
-
return await storage.fetchManifest(config);
|
|
161
|
-
} catch (error) {
|
|
162
|
-
console.warn('[otaCore] Manifest fetch error:', error.message);
|
|
163
|
-
return config.bundledPatches || [];
|
|
164
|
-
}
|
|
165
|
-
};
|
|
177
|
+
const fetchOtaManifest = config => getStorage(config).fetchManifest(config);
|
|
166
178
|
|
|
167
179
|
// ─── Hashed Asset Store ───────────────────────────────────────────────
|
|
168
180
|
exports.fetchOtaManifest = fetchOtaManifest;
|
|
@@ -213,9 +225,8 @@ const downloadAndSyncAssets = async (assetManifestUrl, version, config, log, onS
|
|
|
213
225
|
console.log(`[otaCore] Saved asset manifest to: ${manifestPath}`);
|
|
214
226
|
}
|
|
215
227
|
const manifest = JSON.parse(manifestData);
|
|
216
|
-
// Filter out base assets, we don't need to download them
|
|
217
228
|
// Hash/ext become file names: reject anything that isn't a plain sha256 + extension
|
|
218
|
-
assets = (manifest.assets || []).filter(a =>
|
|
229
|
+
assets = (manifest.assets || []).filter(a => /^[a-f0-9]{64}$/i.test(a.hash) && /^[a-z0-9]+$/i.test(a.ext));
|
|
219
230
|
} catch (e) {
|
|
220
231
|
// Fatal: activating the bundle without its manifest leaves images unresolved
|
|
221
232
|
throw new Error(`Failed to fetch asset manifest: ${e.message}`);
|
|
@@ -324,26 +335,31 @@ const getLocalAssetPath = (hash, ext) => `${ASSET_STORE_DIR}/${hash}.${ext}`;
|
|
|
324
335
|
exports.getLocalAssetPath = getLocalAssetPath;
|
|
325
336
|
const getLocalPatchPath = (patch, docDir) => `${docDir}/ota/patch-${patch.appVersion}-${patch.id}.patch`;
|
|
326
337
|
|
|
338
|
+
/**
|
|
339
|
+
* Picks the registry entry for `localV → targetV`: a patch from the OTA bundle
|
|
340
|
+
* already on disk, else the full-bundle entry (store build, or outside the patch window).
|
|
341
|
+
*/
|
|
342
|
+
exports.getLocalPatchPath = getLocalPatchPath;
|
|
343
|
+
const selectUpdate = (patchList, localV, targetV) => {
|
|
344
|
+
const forTarget = patchList.filter(p => p.toVersion === targetV && (!p.platform || p.platform === _reactNative.Platform.OS));
|
|
345
|
+
return forTarget.find(p => p.filename && p.fromVersion === localV) ?? forTarget.find(p => !p.filename);
|
|
346
|
+
};
|
|
347
|
+
|
|
327
348
|
// ─── Main Update Pipeline ─────────────────────────────────────────────
|
|
328
349
|
|
|
329
350
|
/**
|
|
330
|
-
*
|
|
351
|
+
* Installs one OTA version.
|
|
331
352
|
*
|
|
332
|
-
*
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
* 6. Verify source hash
|
|
338
|
-
* 7. Apply binary patch
|
|
339
|
-
* 8. Verify result hash
|
|
340
|
-
* 9. Atomic commit (mv)
|
|
341
|
-
* 10. Update metadata
|
|
342
|
-
* 11. Reload bundle
|
|
353
|
+
* Patch path (an OTA bundle for `patch.fromVersion` is on disk):
|
|
354
|
+
* download patch → signature → source hash → bspatch → result hash/signature
|
|
355
|
+
* Full path (store build, no patch, or patch path failed):
|
|
356
|
+
* download full bundle → hash → signature
|
|
357
|
+
* Both then: sync assets → atomic commit → metadata → mark healthy → reload.
|
|
343
358
|
*
|
|
344
|
-
*
|
|
359
|
+
* The store build's bundle is never used as a patch source: it can't be
|
|
360
|
+
* reproduced byte-for-byte at build time, so the first OTA is always a full download.
|
|
345
361
|
*/
|
|
346
|
-
exports.
|
|
362
|
+
exports.selectUpdate = selectUpdate;
|
|
347
363
|
let updateInFlight = false;
|
|
348
364
|
|
|
349
365
|
/** Runs the pipeline below; rejects if another update is already running. */
|
|
@@ -369,195 +385,135 @@ const runUpdatePipeline = async (patch, config, onStatus, onProgress, options =
|
|
|
369
385
|
const log = (0, _otaLogger.createOtaLogger)(config.onEvent);
|
|
370
386
|
await (0, _otaMetadata.ensureOtaDir)();
|
|
371
387
|
const totalStart = Date.now();
|
|
388
|
+
const fs = _reactNativeBlobUtil.default.fs;
|
|
372
389
|
const finalBundlePath = (0, _otaMetadata.getBundlePath)(patch.toVersion, config.baseAppVersion) || '';
|
|
373
390
|
const tempBundle = `${finalBundlePath}.tmp`;
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
// ── Step 1: Version Compatibility ──
|
|
384
|
-
if (patch.minAppVersion) {
|
|
385
|
-
let currentAppVer = config.appVersion;
|
|
386
|
-
if (!currentAppVer && typeof _NativeBsPatch.default.getAppVersion === 'function') {
|
|
387
|
-
try {
|
|
388
|
-
currentAppVer = await _NativeBsPatch.default.getAppVersion();
|
|
389
|
-
} catch {
|
|
390
|
-
currentAppVer = undefined;
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
if (!currentAppVer) {
|
|
394
|
-
throw new Error(`[otaCore] Cannot verify minAppVersion: Native App Version could not be determined.`);
|
|
395
|
-
}
|
|
396
|
-
if (!semverGte(currentAppVer, patch.minAppVersion)) {
|
|
397
|
-
log.emit(_otaLogger.OtaEvent.VERSION_INCOMPATIBLE, {
|
|
398
|
-
message: `App ${currentAppVer} < required ${patch.minAppVersion}`,
|
|
399
|
-
version: patch.toVersion
|
|
400
|
-
});
|
|
401
|
-
throw new Error(`Native app ${currentAppVer} is too old. Update to ${patch.minAppVersion}+ first.`);
|
|
402
|
-
}
|
|
391
|
+
const versionDir = `${OTA_DIR}/v${patch.toVersion}`;
|
|
392
|
+
const localPatchPath = getLocalPatchPath(patch, fs.dirs.DocumentDir);
|
|
393
|
+
|
|
394
|
+
// ── Version Compatibility ──
|
|
395
|
+
// Checked before anything else so an incompatible update can't reach the full-bundle path
|
|
396
|
+
if (patch.minAppVersion) {
|
|
397
|
+
let currentAppVer = config.appVersion;
|
|
398
|
+
if (!currentAppVer) {
|
|
399
|
+
currentAppVer = await _NativeBsPatch.default.getAppVersion().catch(() => undefined);
|
|
403
400
|
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
const stat = await _reactNativeBlobUtil.default.fs.stat(localPatchPath);
|
|
411
|
-
patchSizeBytes = Number(stat.size);
|
|
412
|
-
fromCache = true;
|
|
413
|
-
onProgress(100);
|
|
414
|
-
} else {
|
|
415
|
-
log.emit(_otaLogger.OtaEvent.DOWNLOAD_START, {
|
|
401
|
+
if (!currentAppVer) {
|
|
402
|
+
throw new Error('[otaCore] Cannot verify minAppVersion: Native App Version could not be determined.');
|
|
403
|
+
}
|
|
404
|
+
if (!semverGte(currentAppVer, patch.minAppVersion)) {
|
|
405
|
+
log.emit(_otaLogger.OtaEvent.VERSION_INCOMPATIBLE, {
|
|
406
|
+
message: `App ${currentAppVer} < required ${patch.minAppVersion}`,
|
|
416
407
|
version: patch.toVersion
|
|
417
408
|
});
|
|
418
|
-
|
|
419
|
-
const dlStart = Date.now();
|
|
420
|
-
try {
|
|
421
|
-
const storage = getStorage(config);
|
|
422
|
-
const url = await storage.getUpdateFileUrl(patch.filename, config);
|
|
423
|
-
await downloadWithRetry(url, localPatchPath, config, onProgress, {});
|
|
424
|
-
downloadMs = Date.now() - dlStart;
|
|
425
|
-
const stat = await _reactNativeBlobUtil.default.fs.stat(localPatchPath);
|
|
426
|
-
patchSizeBytes = Number(stat.size);
|
|
427
|
-
log.emit(_otaLogger.OtaEvent.DOWNLOAD_SUCCESS, {
|
|
428
|
-
version: patch.toVersion,
|
|
429
|
-
durationMs: downloadMs,
|
|
430
|
-
sizeKB: patchSizeBytes / 1024
|
|
431
|
-
});
|
|
432
|
-
} catch (dlError) {
|
|
433
|
-
log.emit(_otaLogger.OtaEvent.DOWNLOAD_FAIL, {
|
|
434
|
-
error: dlError.message,
|
|
435
|
-
version: patch.toVersion
|
|
436
|
-
});
|
|
437
|
-
throw dlError;
|
|
438
|
-
}
|
|
409
|
+
throw new Error(`Native app ${currentAppVer} is too old. Update to ${patch.minAppVersion}+ first.`);
|
|
439
410
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
411
|
+
}
|
|
412
|
+
if (!(await fs.exists(versionDir))) {
|
|
413
|
+
await fs.mkdir(versionDir);
|
|
414
|
+
}
|
|
415
|
+
const download = async (remoteName, dest) => {
|
|
416
|
+
log.emit(_otaLogger.OtaEvent.DOWNLOAD_START, {
|
|
417
|
+
version: patch.toVersion
|
|
418
|
+
});
|
|
419
|
+
const dlStart = Date.now();
|
|
420
|
+
try {
|
|
421
|
+
const url = await getStorage(config).getUpdateFileUrl(remoteName, config);
|
|
422
|
+
await downloadWithRetry(url, dest, config, onProgress);
|
|
423
|
+
} catch (dlError) {
|
|
424
|
+
log.emit(_otaLogger.OtaEvent.DOWNLOAD_FAIL, {
|
|
425
|
+
error: dlError.message,
|
|
426
|
+
version: patch.toVersion
|
|
427
|
+
});
|
|
428
|
+
throw dlError;
|
|
453
429
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
let actualOldPath = '';
|
|
477
|
-
if (!oldBundlePath) {
|
|
478
|
-
// currentVersion == baseAppVersion: prefer OTA-stored bundle for this
|
|
479
|
-
// version over APK extraction (handles BASE_APP_VERSION bump scenarios).
|
|
480
|
-
const otaStoredPath = `${OTA_DIR}/v${metadata.currentVersion}/${bundleFilename}`;
|
|
481
|
-
if (await _reactNativeBlobUtil.default.fs.exists(otaStoredPath)) {
|
|
482
|
-
if (__DEV__) {
|
|
483
|
-
console.log(`[otaCore] Source: OTA-stored bundle for V${metadata.currentVersion} (BASE_APP_VERSION match)`);
|
|
484
|
-
}
|
|
485
|
-
actualOldPath = otaStoredPath;
|
|
486
|
-
} else {
|
|
487
|
-
// True base: extract byte-for-byte from APK assets.
|
|
488
|
-
// RNFS.copyFileAssets is known to produce inconsistent bytes in some
|
|
489
|
-
// APK configurations (e.g., release signing + resource shrinking).
|
|
490
|
-
// Re-extract every time: a cached copy can be partial or left over
|
|
491
|
-
// from the previous store build. It's one local copy per update.
|
|
492
|
-
actualOldPath = `${OTA_DIR}/base.hbc`;
|
|
493
|
-
await _NativeBsPatch.default.extractBundleFromAssets(bundleFilename, actualOldPath);
|
|
430
|
+
const downloadMs = Date.now() - dlStart;
|
|
431
|
+
const sizeBytes = Number((await fs.stat(dest)).size);
|
|
432
|
+
log.emit(_otaLogger.OtaEvent.DOWNLOAD_SUCCESS, {
|
|
433
|
+
version: patch.toVersion,
|
|
434
|
+
durationMs: downloadMs,
|
|
435
|
+
sizeKB: sizeBytes / 1024
|
|
436
|
+
});
|
|
437
|
+
return {
|
|
438
|
+
downloadMs,
|
|
439
|
+
sizeBytes
|
|
440
|
+
};
|
|
441
|
+
};
|
|
442
|
+
const assertHash = async (path, expected, what) => {
|
|
443
|
+
if (!expected || (await verifyHash(path, expected))) return;
|
|
444
|
+
const actual = await fs.hash(path, 'sha256').catch(() => '');
|
|
445
|
+
log.emit(_otaLogger.OtaEvent.HASH_MISMATCH, {
|
|
446
|
+
message: `${what} hash mismatch`,
|
|
447
|
+
version: patch.toVersion,
|
|
448
|
+
metadata: {
|
|
449
|
+
expected,
|
|
450
|
+
actual,
|
|
451
|
+
path
|
|
494
452
|
}
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
|
|
453
|
+
});
|
|
454
|
+
throw new Error(`${what} hash mismatch`);
|
|
455
|
+
};
|
|
498
456
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
const
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
}
|
|
508
|
-
} catch {
|
|
509
|
-
// Non-fatal: stat is for diagnostic only
|
|
457
|
+
/** tempBundle is verified: sync assets, swap it in, record the version, reload. */
|
|
458
|
+
const commit = async metrics => {
|
|
459
|
+
if (patch.assetManifestUrl) {
|
|
460
|
+
const manifestUrl = await getStorage(config).getUpdateFileUrl(patch.assetManifestUrl, config);
|
|
461
|
+
await downloadAndSyncAssets(manifestUrl, patch.toVersion, config, log, onStatus, onProgress);
|
|
462
|
+
}
|
|
463
|
+
if (await fs.exists(finalBundlePath)) {
|
|
464
|
+
await fs.unlink(finalBundlePath);
|
|
510
465
|
}
|
|
466
|
+
await fs.mv(tempBundle, finalBundlePath);
|
|
511
467
|
|
|
512
|
-
//
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
log.emit(_otaLogger.OtaEvent.HASH_MISMATCH, {
|
|
534
|
-
version: metadata.currentVersion,
|
|
535
|
-
message: 'Source bundle hash mismatch',
|
|
536
|
-
metadata: {
|
|
537
|
-
expected: patch.baseHash,
|
|
538
|
-
actual: actualSourceHash,
|
|
539
|
-
path: actualOldPath
|
|
540
|
-
}
|
|
541
|
-
});
|
|
542
|
-
throw new Error('Source bundle hash mismatch. Fallback required.');
|
|
543
|
-
}
|
|
544
|
-
log.emit(_otaLogger.OtaEvent.HASH_VERIFY_SUCCESS, {
|
|
545
|
-
message: 'Source bundle verified'
|
|
546
|
-
});
|
|
468
|
+
// Native version, not config.appVersion: the native resolver compares against it on boot
|
|
469
|
+
const nativeAppVersion = await _NativeBsPatch.default.getAppVersion().catch(() => config.appVersion);
|
|
470
|
+
await (0, _otaMetadata.updateMetadata)(config.baseAppVersion, metadata => (0, _otaMetadata.registerNewVersion)(metadata, patch.toVersion, nativeAppVersion));
|
|
471
|
+
const totalMs = Date.now() - totalStart;
|
|
472
|
+
log.emit(_otaLogger.OtaEvent.PATCH_SUCCESS, {
|
|
473
|
+
version: patch.toVersion,
|
|
474
|
+
durationMs: totalMs,
|
|
475
|
+
sizeKB: metrics.patchSizeKB
|
|
476
|
+
});
|
|
477
|
+
onStatus(_types.OtaStatus.SUCCESS, `Update V${patch.toVersion} Successful!`, {
|
|
478
|
+
...metrics,
|
|
479
|
+
totalMs,
|
|
480
|
+
fromCache: false
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
// registerNewVersion left lastLaunchSuccessful=true: the native resolver marks the
|
|
484
|
+
// first launch of the new version pending, and JS marks it healthy once it renders.
|
|
485
|
+
if (options.autoReload !== false) {
|
|
486
|
+
_NativeBsPatch.default.reloadBundle();
|
|
487
|
+
} else if (__DEV__) {
|
|
488
|
+
console.log(`[otaCore] V${patch.toVersion} prepared in background — applies on next launch.`);
|
|
547
489
|
}
|
|
490
|
+
};
|
|
491
|
+
const applyPatchFile = async sourcePath => {
|
|
492
|
+
onStatus(_types.OtaStatus.DOWNLOADING, `Downloading V${patch.toVersion}...`);
|
|
493
|
+
const {
|
|
494
|
+
downloadMs,
|
|
495
|
+
sizeBytes
|
|
496
|
+
} = await download(patch.filename, localPatchPath);
|
|
548
497
|
|
|
549
|
-
//
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
await
|
|
498
|
+
// Verify before feeding the patch to native bspatch
|
|
499
|
+
onStatus(_types.OtaStatus.APPLYING, 'Verifying...');
|
|
500
|
+
try {
|
|
501
|
+
await assertSigned(localPatchPath, patch.signature, config, 'Patch');
|
|
502
|
+
} catch (sigError) {
|
|
503
|
+
log.emit(_otaLogger.OtaEvent.SIGNATURE_INVALID, {
|
|
504
|
+
version: patch.toVersion
|
|
505
|
+
});
|
|
506
|
+
throw sigError;
|
|
553
507
|
}
|
|
508
|
+
await assertHash(sourcePath, patch.baseHash, 'Source bundle');
|
|
554
509
|
log.emit(_otaLogger.OtaEvent.PATCH_START, {
|
|
555
510
|
version: patch.toVersion
|
|
556
511
|
});
|
|
557
512
|
onStatus(_types.OtaStatus.APPLYING, 'Applying binary patch...');
|
|
558
513
|
const applyStart = Date.now();
|
|
559
|
-
const success = await _NativeBsPatch.default.applyPatch(
|
|
514
|
+
const success = await _NativeBsPatch.default.applyPatch(sourcePath, tempBundle, localPatchPath);
|
|
560
515
|
const applyMs = Date.now() - applyStart;
|
|
516
|
+
await fs.unlink(localPatchPath).catch(() => {});
|
|
561
517
|
if (!success) {
|
|
562
518
|
log.emit(_otaLogger.OtaEvent.PATCH_FAIL, {
|
|
563
519
|
message: 'BsPatch returned false',
|
|
@@ -565,148 +521,62 @@ const runUpdatePipeline = async (patch, config, onStatus, onProgress, options =
|
|
|
565
521
|
});
|
|
566
522
|
throw new Error('BsPatch application failed');
|
|
567
523
|
}
|
|
568
|
-
|
|
569
|
-
// ── Step 8: Verify Result Integrity ──
|
|
570
|
-
if (patch.bundleHash) {
|
|
571
|
-
log.emit(_otaLogger.OtaEvent.HASH_VERIFY_START, {
|
|
572
|
-
message: 'Verifying result bundle'
|
|
573
|
-
});
|
|
574
|
-
onStatus(_types.OtaStatus.APPLYING, 'Verifying integrity...');
|
|
575
|
-
const isValid = await verifyHash(tempBundle, patch.bundleHash);
|
|
576
|
-
if (!isValid) {
|
|
577
|
-
console.error('[otaCore] Result bundle hash mismatch | expected:', patch.bundleHash, 'path:', tempBundle);
|
|
578
|
-
log.emit(_otaLogger.OtaEvent.HASH_MISMATCH, {
|
|
579
|
-
message: 'Result bundle hash mismatch',
|
|
580
|
-
version: patch.toVersion,
|
|
581
|
-
metadata: {
|
|
582
|
-
expected: patch.bundleHash,
|
|
583
|
-
path: tempBundle
|
|
584
|
-
}
|
|
585
|
-
});
|
|
586
|
-
throw new Error('Result bundle hash mismatch');
|
|
587
|
-
}
|
|
588
|
-
log.emit(_otaLogger.OtaEvent.HASH_VERIFY_SUCCESS, {
|
|
589
|
-
message: 'Result bundle verified'
|
|
590
|
-
});
|
|
591
|
-
}
|
|
524
|
+
await assertHash(tempBundle, patch.bundleHash, 'Result bundle');
|
|
592
525
|
await assertSigned(tempBundle, patch.bundleSignature, config, 'Bundle');
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
526
|
+
await commit({
|
|
527
|
+
patchSizeKB: sizeBytes / 1024,
|
|
528
|
+
downloadMs,
|
|
529
|
+
applyMs
|
|
530
|
+
});
|
|
531
|
+
};
|
|
532
|
+
const installFullBundle = async () => {
|
|
533
|
+
if (!patch.fullBundleUrl) {
|
|
534
|
+
throw new Error(`No full bundle for V${patch.toVersion}`);
|
|
597
535
|
}
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
// ── Step 10: Update Metadata ──
|
|
602
|
-
const newMetadata = (0, _otaMetadata.registerNewVersion)(metadata, patch.toVersion, config.appVersion);
|
|
603
|
-
await (0, _otaMetadata.saveMetadata)(newMetadata);
|
|
604
|
-
const totalMs = Date.now() - totalStart;
|
|
605
|
-
log.emit(_otaLogger.OtaEvent.PATCH_SUCCESS, {
|
|
606
|
-
version: patch.toVersion,
|
|
607
|
-
durationMs: totalMs,
|
|
608
|
-
sizeKB: patchSizeBytes / 1024
|
|
536
|
+
log.emit(_otaLogger.OtaEvent.FALLBACK_FULL_BUNDLE, {
|
|
537
|
+
version: patch.toVersion
|
|
609
538
|
});
|
|
610
|
-
onStatus(_types.OtaStatus.
|
|
611
|
-
|
|
539
|
+
onStatus(_types.OtaStatus.DOWNLOADING, `Downloading V${patch.toVersion} (full bundle)...`);
|
|
540
|
+
const {
|
|
612
541
|
downloadMs,
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
542
|
+
sizeBytes
|
|
543
|
+
} = await download(patch.fullBundleUrl, tempBundle);
|
|
544
|
+
await assertHash(tempBundle, patch.bundleHash, 'Full bundle');
|
|
545
|
+
await assertSigned(tempBundle, patch.bundleSignature, config, 'Full bundle');
|
|
546
|
+
await commit({
|
|
547
|
+
patchSizeKB: sizeBytes / 1024,
|
|
548
|
+
downloadMs,
|
|
549
|
+
applyMs: 0
|
|
616
550
|
});
|
|
551
|
+
};
|
|
552
|
+
const cleanup = async () => {
|
|
553
|
+
await fs.unlink(localPatchPath).catch(() => {});
|
|
554
|
+
await fs.unlink(tempBundle).catch(() => {});
|
|
555
|
+
};
|
|
617
556
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
557
|
+
// Patch only from an OTA bundle we installed ourselves (byte-identical to the build output)
|
|
558
|
+
const metadata = await (0, _otaMetadata.loadMetadata)(config.baseAppVersion);
|
|
559
|
+
const sourcePath = (0, _otaMetadata.getBundlePath)(metadata.currentVersion, config.baseAppVersion);
|
|
560
|
+
const canPatch = !!patch.filename && patch.fromVersion === metadata.currentVersion && !!sourcePath && (await fs.exists(sourcePath));
|
|
561
|
+
if (canPatch) {
|
|
562
|
+
try {
|
|
563
|
+
await applyPatchFile(sourcePath);
|
|
564
|
+
return;
|
|
565
|
+
} catch (error) {
|
|
566
|
+
await cleanup();
|
|
622
567
|
if (__DEV__) {
|
|
623
|
-
console.
|
|
568
|
+
console.warn('[otaCore] Patch failed, falling back to full bundle:', error);
|
|
624
569
|
}
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
console.log(`[otaCore] V${patch.toVersion} prepared silently in background — will apply on next launch.`);
|
|
570
|
+
if (!patch.fullBundleUrl) {
|
|
571
|
+
onStatus(_types.OtaStatus.ERROR, `Error: ${error.message}`);
|
|
572
|
+
throw error;
|
|
629
573
|
}
|
|
630
574
|
}
|
|
575
|
+
}
|
|
576
|
+
try {
|
|
577
|
+
await installFullBundle();
|
|
631
578
|
} catch (error) {
|
|
632
|
-
|
|
633
|
-
console.error('[otaCore] Pipeline failed:', error);
|
|
634
|
-
}
|
|
635
|
-
await _reactNativeBlobUtil.default.fs.unlink(localPatchPath).catch(() => {});
|
|
636
|
-
await _reactNativeBlobUtil.default.fs.unlink(tempBundle).catch(() => {});
|
|
637
|
-
|
|
638
|
-
// ── Fallback: Full Bundle Download ──
|
|
639
|
-
if (patch.fullBundleUrl) {
|
|
640
|
-
try {
|
|
641
|
-
onStatus(_types.OtaStatus.DOWNLOADING, 'Fallback: Downloading full bundle...');
|
|
642
|
-
const storage = getStorage(config);
|
|
643
|
-
const url = await storage.getUpdateFileUrl(patch.fullBundleUrl || patch.filename, config);
|
|
644
|
-
const versionDir = `${OTA_DIR}/v${patch.toVersion}`;
|
|
645
|
-
if (!(await _reactNativeBlobUtil.default.fs.exists(versionDir))) {
|
|
646
|
-
await _reactNativeBlobUtil.default.fs.mkdir(versionDir);
|
|
647
|
-
}
|
|
648
|
-
await downloadWithRetry(url, tempBundle, config, onProgress, {});
|
|
649
|
-
|
|
650
|
-
// Verify full bundle hash
|
|
651
|
-
if (patch.bundleHash) {
|
|
652
|
-
const isValid = await verifyHash(tempBundle, patch.bundleHash);
|
|
653
|
-
if (!isValid) {
|
|
654
|
-
let actualHash = '';
|
|
655
|
-
let size;
|
|
656
|
-
try {
|
|
657
|
-
actualHash = await _reactNativeBlobUtil.default.fs.hash(tempBundle, 'sha256');
|
|
658
|
-
const stat = await _reactNativeBlobUtil.default.fs.stat(tempBundle);
|
|
659
|
-
size = stat.size;
|
|
660
|
-
} catch {
|
|
661
|
-
// stat failure is non-fatal
|
|
662
|
-
}
|
|
663
|
-
if (__DEV__) {
|
|
664
|
-
console.error('[otaCore] Full bundle hash mismatch | expected:', patch.bundleHash, 'actual:', actualHash);
|
|
665
|
-
}
|
|
666
|
-
log.emit(_otaLogger.OtaEvent.HASH_MISMATCH, {
|
|
667
|
-
message: 'Full bundle hash mismatch',
|
|
668
|
-
metadata: {
|
|
669
|
-
expected: patch.bundleHash,
|
|
670
|
-
actual: actualHash,
|
|
671
|
-
size
|
|
672
|
-
}
|
|
673
|
-
});
|
|
674
|
-
throw new Error('Full bundle hash mismatch after fallback');
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
await assertSigned(tempBundle, patch.bundleSignature, config, 'Full bundle');
|
|
678
|
-
await syncAssets();
|
|
679
|
-
|
|
680
|
-
// Atomic swap
|
|
681
|
-
if (await _reactNativeBlobUtil.default.fs.exists(finalBundlePath)) {
|
|
682
|
-
await _reactNativeBlobUtil.default.fs.unlink(finalBundlePath);
|
|
683
|
-
}
|
|
684
|
-
await _reactNativeBlobUtil.default.fs.mv(tempBundle, finalBundlePath);
|
|
685
|
-
const metadata = await (0, _otaMetadata.loadMetadata)(config.baseAppVersion);
|
|
686
|
-
const newMetadata = (0, _otaMetadata.registerNewVersion)(metadata, patch.toVersion, config.appVersion);
|
|
687
|
-
await (0, _otaMetadata.saveMetadata)(newMetadata);
|
|
688
|
-
|
|
689
|
-
// ── CRITICAL: Mark healthy BEFORE reload so native OtaBundleResolver
|
|
690
|
-
// does NOT see lastLaunchSuccessful=false on the very next boot and
|
|
691
|
-
// immediately rollback the version we just successfully downloaded.
|
|
692
|
-
// (reloadBundle() calls Runtime.exit(0) — JS won't run again before restart)
|
|
693
|
-
_NativeBsPatch.default.markAsHealthy();
|
|
694
|
-
if (__DEV__) {
|
|
695
|
-
console.log(`[otaCore] Fallback V${patch.toVersion} marked healthy — reloading.`);
|
|
696
|
-
}
|
|
697
|
-
if (options.autoReload !== false) {
|
|
698
|
-
onStatus(_types.OtaStatus.SUCCESS, `Fallback successful! Reloading...`);
|
|
699
|
-
_NativeBsPatch.default.reloadBundle();
|
|
700
|
-
} else {
|
|
701
|
-
onStatus(_types.OtaStatus.SUCCESS, `Fallback bundle prepared silently in background.`);
|
|
702
|
-
}
|
|
703
|
-
return;
|
|
704
|
-
} catch (fallbackError) {
|
|
705
|
-
if (__DEV__) {
|
|
706
|
-
console.error('[otaCore] Full bundle fallback also failed:', fallbackError);
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
}
|
|
579
|
+
await cleanup();
|
|
710
580
|
onStatus(_types.OtaStatus.ERROR, `Error: ${error.message}`);
|
|
711
581
|
throw error;
|
|
712
582
|
}
|