react-native-bspatch 1.1.2 → 1.1.4

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 (59) hide show
  1. package/README.md +145 -42
  2. package/android/src/main/java/vn/reactnativebspatch/BsPatchModule.kt +117 -17
  3. package/cpp/CMakeLists.txt +9 -1
  4. package/cpp/bsdiff/bspatch.c +14 -3
  5. package/cpp/bsdiff/bspatch_wrapper.c +124 -40
  6. package/ios/RNBsPatch.mm +142 -1
  7. package/lib/commonjs/NativeBsPatch.js.map +1 -1
  8. package/lib/commonjs/ota/OtaProvider.js +61 -12
  9. package/lib/commonjs/ota/OtaProvider.js.map +1 -1
  10. package/lib/commonjs/ota/otaCore.js +43 -20
  11. package/lib/commonjs/ota/otaCore.js.map +1 -1
  12. package/lib/commonjs/ota/otaMetadata.js +41 -5
  13. package/lib/commonjs/ota/otaMetadata.js.map +1 -1
  14. package/lib/commonjs/ota/storage/CustomStorage.js +23 -3
  15. package/lib/commonjs/ota/storage/CustomStorage.js.map +1 -1
  16. package/lib/commonjs/ota/storage/GitHubRawStorage.js +3 -0
  17. package/lib/commonjs/ota/storage/GitHubRawStorage.js.map +1 -1
  18. package/lib/commonjs/ota/storage/GitHubReleaseStorage.js +3 -0
  19. package/lib/commonjs/ota/storage/GitHubReleaseStorage.js.map +1 -1
  20. package/lib/module/NativeBsPatch.js.map +1 -1
  21. package/lib/module/ota/OtaProvider.js +61 -12
  22. package/lib/module/ota/OtaProvider.js.map +1 -1
  23. package/lib/module/ota/otaCore.js +43 -20
  24. package/lib/module/ota/otaCore.js.map +1 -1
  25. package/lib/module/ota/otaMetadata.js +38 -3
  26. package/lib/module/ota/otaMetadata.js.map +1 -1
  27. package/lib/module/ota/storage/CustomStorage.js +23 -3
  28. package/lib/module/ota/storage/CustomStorage.js.map +1 -1
  29. package/lib/module/ota/storage/GitHubRawStorage.js +3 -0
  30. package/lib/module/ota/storage/GitHubRawStorage.js.map +1 -1
  31. package/lib/module/ota/storage/GitHubReleaseStorage.js +3 -0
  32. package/lib/module/ota/storage/GitHubReleaseStorage.js.map +1 -1
  33. package/lib/typescript/NativeBsPatch.d.ts +11 -0
  34. package/lib/typescript/NativeBsPatch.d.ts.map +1 -1
  35. package/lib/typescript/ota/OtaProvider.d.ts.map +1 -1
  36. package/lib/typescript/ota/otaCore.d.ts +3 -1
  37. package/lib/typescript/ota/otaCore.d.ts.map +1 -1
  38. package/lib/typescript/ota/otaMetadata.d.ts +6 -0
  39. package/lib/typescript/ota/otaMetadata.d.ts.map +1 -1
  40. package/lib/typescript/ota/storage/CustomStorage.d.ts.map +1 -1
  41. package/lib/typescript/ota/storage/GitHubRawStorage.d.ts.map +1 -1
  42. package/lib/typescript/ota/storage/GitHubReleaseStorage.d.ts.map +1 -1
  43. package/lib/typescript/ota/types.d.ts +6 -2
  44. package/lib/typescript/ota/types.d.ts.map +1 -1
  45. package/package.json +3 -1
  46. package/react-native-bspatch.podspec +6 -1
  47. package/src/NativeBsPatch.ts +17 -0
  48. package/src/ota/OtaProvider.tsx +80 -23
  49. package/src/ota/otaCore.ts +54 -24
  50. package/src/ota/otaMetadata.ts +46 -4
  51. package/src/ota/storage/CustomStorage.ts +26 -3
  52. package/src/ota/storage/GitHubRawStorage.ts +5 -0
  53. package/src/ota/storage/GitHubReleaseStorage.ts +5 -0
  54. package/src/ota/types.ts +6 -2
  55. package/tools/build_ota.sh +366 -18
  56. package/tools/bump_version.js +133 -0
  57. package/tools/generate_keys.js +75 -0
  58. package/tools/setup_project.js +69 -4
  59. package/tools/update_registry.js +51 -1
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env node
2
+ // ─────────────────────────────────────────────────────────────────────────────
3
+ // bump_version.js — Synchronize App Version Across Platforms
4
+ //
5
+ // Updates:
6
+ // - package.json ("version")
7
+ // - android/app/build.gradle (versionName & increments versionCode)
8
+ // - ios/*.xcodeproj/project.pbxproj (MARKETING_VERSION & increments CURRENT_PROJECT_VERSION)
9
+ // - src/ota/otaConfig.ts (APP_VERSION, if it exists)
10
+ //
11
+ // Usage:
12
+ // node bump_version.js <new_version> [--root <path>]
13
+ // e.g. node bump_version.js 1.0.2
14
+ // ─────────────────────────────────────────────────────────────────────────────
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+
19
+ const args = process.argv.slice(2);
20
+ let newVersion = '';
21
+ let rootDir = process.cwd();
22
+
23
+ for (let i = 0; i < args.length; i++) {
24
+ if (args[i] === '--root' && args[i + 1]) {
25
+ rootDir = path.resolve(args[i + 1]);
26
+ i++;
27
+ } else if (!args[i].startsWith('--')) {
28
+ newVersion = args[i].trim();
29
+ }
30
+ }
31
+
32
+ if (!newVersion) {
33
+ console.error('Usage: node bump_version.js <new_version> [--root <path>]');
34
+ console.error('Example: node bump_version.js 1.0.2');
35
+ process.exit(1);
36
+ }
37
+
38
+ // Validate semver format loosely (e.g. 1.0.2 or 1.0.2-beta)
39
+ if (!/^\d+\.\d+\.\d+.*$/.test(newVersion)) {
40
+ console.error(`Invalid version format: "${newVersion}". Expected semver (e.g. 1.0.2)`);
41
+ process.exit(1);
42
+ }
43
+
44
+ console.log(`\n=== Bumping App Version to ${newVersion} ===\n`);
45
+ let changedCount = 0;
46
+
47
+ // 1. package.json
48
+ const pkgPath = path.join(rootDir, 'package.json');
49
+ if (fs.existsSync(pkgPath)) {
50
+ try {
51
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
52
+ const oldVer = pkg.version;
53
+ pkg.version = newVersion;
54
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
55
+ console.log(` ✓ package.json: ${oldVer} → ${newVersion}`);
56
+ changedCount++;
57
+ } catch (e) {
58
+ console.warn(` ⚠ Failed to update package.json: ${e.message}`);
59
+ }
60
+ }
61
+
62
+ // 2. android/app/build.gradle
63
+ const gradlePath = path.join(rootDir, 'android/app/build.gradle');
64
+ if (fs.existsSync(gradlePath)) {
65
+ try {
66
+ let content = fs.readFileSync(gradlePath, 'utf8');
67
+ let oldName = '';
68
+ let newCode = 1;
69
+
70
+ // Bump versionName
71
+ content = content.replace(/versionName\s+["']([^"']+)["']/, (_, old) => {
72
+ oldName = old;
73
+ return `versionName "${newVersion}"`;
74
+ });
75
+
76
+ // Increment versionCode
77
+ content = content.replace(/versionCode\s+(\d+)/, (_, code) => {
78
+ newCode = parseInt(code, 10) + 1;
79
+ return `versionCode ${newCode}`;
80
+ });
81
+
82
+ fs.writeFileSync(gradlePath, content, 'utf8');
83
+ console.log(` ✓ android/app/build.gradle: versionName "${oldName}" → "${newVersion}", versionCode → ${newCode}`);
84
+ changedCount++;
85
+ } catch (e) {
86
+ console.warn(` ⚠ Failed to update build.gradle: ${e.message}`);
87
+ }
88
+ }
89
+
90
+ // 3. iOS Xcode project.pbxproj
91
+ const iosDir = path.join(rootDir, 'ios');
92
+ if (fs.existsSync(iosDir)) {
93
+ try {
94
+ const files = fs.readdirSync(iosDir);
95
+ for (const f of files) {
96
+ if (f.endsWith('.xcodeproj')) {
97
+ const pbxPath = path.join(iosDir, f, 'project.pbxproj');
98
+ if (fs.existsSync(pbxPath)) {
99
+ let content = fs.readFileSync(pbxPath, 'utf8');
100
+
101
+ // Replace MARKETING_VERSION
102
+ content = content.replace(/MARKETING_VERSION\s*=\s*[^;]+;/g, `MARKETING_VERSION = ${newVersion};`);
103
+
104
+ // Increment CURRENT_PROJECT_VERSION if numeric
105
+ content = content.replace(/CURRENT_PROJECT_VERSION\s*=\s*(\d+);/g, (_, v) => {
106
+ const nextV = parseInt(v, 10) + 1;
107
+ return `CURRENT_PROJECT_VERSION = ${nextV};`;
108
+ });
109
+
110
+ fs.writeFileSync(pbxPath, content, 'utf8');
111
+ console.log(` ✓ ios/${f}/project.pbxproj: MARKETING_VERSION → ${newVersion}`);
112
+ changedCount++;
113
+ }
114
+ }
115
+ }
116
+ } catch (e) {
117
+ console.warn(` ⚠ Failed to update iOS project: ${e.message}`);
118
+ }
119
+ }
120
+
121
+ // 4. src/ota/otaConfig.ts (if exists)
122
+ const otaConfigPath = path.join(rootDir, 'src/ota/otaConfig.ts');
123
+ if (fs.existsSync(otaConfigPath)) {
124
+ try {
125
+ let content = fs.readFileSync(otaConfigPath, 'utf8');
126
+ content = content.replace(/export const APP_VERSION = ['"][^'"]+['"];/, `export const APP_VERSION = '${newVersion}';`);
127
+ fs.writeFileSync(otaConfigPath, content, 'utf8');
128
+ console.log(` ✓ src/ota/otaConfig.ts: APP_VERSION → '${newVersion}'`);
129
+ changedCount++;
130
+ } catch (e) {}
131
+ }
132
+
133
+ console.log(`\n✓ Done! Updated ${changedCount} file(s) to version ${newVersion}.\n`);
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env node
2
+ // ─────────────────────────────────────────────────────────────────────────────
3
+ // generate_keys.js — Generate Asymmetric Keypair for OTA Patch Signing
4
+ //
5
+ // Usage:
6
+ // node tools/generate_keys.js [--type rsa|ec] [--out <dir>]
7
+ // e.g. node tools/generate_keys.js
8
+ // ─────────────────────────────────────────────────────────────────────────────
9
+
10
+ const crypto = require('crypto');
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+
14
+ const args = process.argv.slice(2);
15
+ let keyType = 'rsa';
16
+ let outDir = process.cwd();
17
+
18
+ for (let i = 0; i < args.length; i++) {
19
+ if (args[i] === '--type' && args[i + 1]) {
20
+ keyType = args[i + 1].toLowerCase();
21
+ i++;
22
+ } else if (args[i] === '--out' && args[i + 1]) {
23
+ outDir = path.resolve(args[i + 1]);
24
+ i++;
25
+ }
26
+ }
27
+
28
+ console.log(`\n=== Generating ${keyType.toUpperCase()} Keypair for OTA Patch Security ===\n`);
29
+
30
+ let privateKey, publicKey;
31
+
32
+ if (keyType === 'ec') {
33
+ const keypair = crypto.generateKeyPairSync('ec', {
34
+ namedCurve: 'prime256v1',
35
+ publicKeyEncoding: { type: 'spki', format: 'pem' },
36
+ privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
37
+ });
38
+ privateKey = keypair.privateKey;
39
+ publicKey = keypair.publicKey;
40
+ } else {
41
+ // Default: RSA 2048
42
+ const keypair = crypto.generateKeyPairSync('rsa', {
43
+ modulusLength: 2048,
44
+ publicKeyEncoding: { type: 'spki', format: 'pem' },
45
+ privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
46
+ });
47
+ privateKey = keypair.privateKey;
48
+ publicKey = keypair.publicKey;
49
+ }
50
+
51
+ const privPath = path.join(outDir, 'ota_private_key.pem');
52
+ const pubPath = path.join(outDir, 'ota_public_key.pem');
53
+
54
+ fs.writeFileSync(privPath, privateKey, { mode: 0o600 });
55
+ fs.writeFileSync(pubPath, publicKey, { mode: 0o644 });
56
+
57
+ // Clean public key string for inline JS config
58
+ const pubKeyClean = publicKey
59
+ .replace(/-----BEGIN PUBLIC KEY-----/, '')
60
+ .replace(/-----END PUBLIC KEY-----/, '')
61
+ .replace(/\s+/g, '');
62
+
63
+ console.log(` ✓ Private Key saved to: ${privPath}`);
64
+ console.log(` ✓ Public Key saved to: ${pubPath}`);
65
+ console.log('\n────────────────────────────────────────────────────────');
66
+ console.log(' HOW TO USE:');
67
+ console.log(' 1. KEEP PRIVATE KEY SAFE on your build machine / CI server:');
68
+ console.log(' Add "ota_private_key.pem" to .gitignore!');
69
+ console.log('');
70
+ console.log(' 2. ADD PUBLIC KEY TO YOUR CLIENT APP CONFIG:');
71
+ console.log(' const otaConfig: OtaConfig = {');
72
+ console.log(` publicKey: '${pubKeyClean}',`);
73
+ console.log(' ...');
74
+ console.log(' };');
75
+ console.log('────────────────────────────────────────────────────────\n');
@@ -60,6 +60,13 @@ const XCODE_BUNDLE_SYNC_SNIPPET = [
60
60
  ' mkdir -p "$(dirname \\"$DEST_FILE\\")"',
61
61
  ' cp "$BUNDLE_RESULT" "$DEST_FILE"',
62
62
  ' echo "Bundle synced to $DEST_FILE"',
63
+ ' if [ "$CONFIGURATION" = "Release" ]; then',
64
+ ' APP_VER="${MARKETING_VERSION:-1.0.0}"',
65
+ ' mkdir -p "$PROJECT_DIR/output"',
66
+ ' cp "$BUNDLE_RESULT" "$PROJECT_DIR/output/bundle_${APP_VER}_v1_ios_production.hbc"',
67
+ ' cp "$BUNDLE_RESULT" "$PROJECT_DIR/output/bundle_${APP_VER}_v1_ios_development.hbc" 2>/dev/null || true',
68
+ ' echo "[react-native-bspatch] ✓ Auto-captured iOS base bundle v1 to output/"',
69
+ ' fi',
63
70
  'else',
64
71
  ' echo "Bundle not found at $BUNDLE_RESULT (skipping sync)"',
65
72
  'fi',
@@ -68,6 +75,48 @@ const XCODE_BUNDLE_SYNC_SNIPPET = [
68
75
  // The escaped version as it appears in pbxproj shellScript value
69
76
  const XCODE_BUNDLE_SYNC_MARKER = 'react-native-bspatch] Sync compiled bundle to output/';
70
77
 
78
+ const ANDROID_GRADLE_MARKER = '[react-native-bspatch] Auto-capture Base Bundle';
79
+ const ANDROID_GRADLE_SNIPPET = `
80
+ // [react-native-bspatch] Auto-capture Base Bundle on Release builds
81
+ afterEvaluate {
82
+ android.applicationVariants.all { variant ->
83
+ if (variant.buildType.name == 'release') {
84
+ def flavorName = (variant.flavorName ?: 'development').toLowerCase()
85
+ if (flavorName == 'dev') flavorName = 'development'
86
+ if (flavorName == 'prod') flavorName = 'production'
87
+ def versionName = variant.versionName ?: '1.0.0'
88
+ def capVariant = variant.name.capitalize()
89
+ def bundleTask = tasks.findByName("createBundle\${capVariant}JsAndAssets")
90
+ if (bundleTask != null) {
91
+ def saveTask = tasks.register("saveOtaBaseBundle\${capVariant}") {
92
+ doLast {
93
+ def bundleDir = file("\$buildDir/generated/assets/react/\${variant.name}")
94
+ def bundleFile = file("\$bundleDir/index.android.bundle")
95
+ def outputDir = file("\$rootDir/output")
96
+ if (bundleFile.exists()) {
97
+ outputDir.mkdirs()
98
+ def destName = "bundle_\${versionName}_v1_android_\${flavorName}.hbc"
99
+ copy {
100
+ from bundleFile
101
+ into outputDir
102
+ rename { destName }
103
+ }
104
+ println "[react-native-bspatch] ✓ Auto-captured base bundle: android/output/\${destName}"
105
+ }
106
+ }
107
+ }
108
+ bundleTask.finalizedBy(saveTask)
109
+ }
110
+ }
111
+ }
112
+ }
113
+ `;
114
+
115
+ function patchAppBuildGradle(content) {
116
+ if (content.includes(ANDROID_GRADLE_MARKER)) return content;
117
+ return content.trimEnd() + '\n' + ANDROID_GRADLE_SNIPPET;
118
+ }
119
+
71
120
  const IOS_IMPORT = 'import react_native_bspatch';
72
121
  const IOS_BUNDLE_URL_OVERRIDE = `
73
122
  override func bundleURL() -> URL? {
@@ -275,12 +324,28 @@ function main() {
275
324
  patchFile(mainAppFiles[0], 'MainApplication.kt', patchMainApplication);
276
325
  }
277
326
 
327
+ const appGradleFiles = findFile(path.join(ANDROID_DIR, 'app'), (name) => name === 'build.gradle');
328
+ if (appGradleFiles.length === 0) {
329
+ console.log(' ⚠ No android/app/build.gradle found.');
330
+ } else {
331
+ patchFile(appGradleFiles[0], 'android/app/build.gradle (auto-capture base bundle)', patchAppBuildGradle);
332
+ }
333
+
334
+ // Ensure android/output/ exists
335
+ const androidOutputDir = path.join(ANDROID_DIR, 'output');
336
+ if (!fs.existsSync(androidOutputDir)) {
337
+ fs.mkdirSync(androidOutputDir, { recursive: true });
338
+ console.log(' ✓ Created android/output/ directory');
339
+ }
340
+
278
341
  console.log('\n=== Setup Complete ===');
279
342
  console.log('Next steps:');
280
- console.log(' 1. iOS: Run Xcode build once to populate ios/output/main.jsbundle');
281
- console.log(' (or run: yarn ota:init --platform ios)');
282
- console.log(' 2. Android: Run: yarn ota:init --platform android');
283
- console.log(' 3. Then: yarn ota:build to start publishing OTA updates\n');
343
+ console.log(' 1. Base bundles will now automatically be captured on Release builds (Gradle & Xcode).');
344
+ console.log(' 2. Or seed directly from an existing Store binary:');
345
+ console.log(' yarn react-native-bspatch seed --apk path/to/release.apk');
346
+ console.log(' yarn react-native-bspatch seed --ipa path/to/release.ipa');
347
+ console.log(' 3. Or create base release manually: yarn ota:init');
348
+ console.log(' 4. Then publish OTA updates safely with: yarn ota:build\n');
284
349
  }
285
350
 
286
351
  main();
@@ -12,6 +12,7 @@
12
12
 
13
13
  const fs = require('fs');
14
14
  const path = require('path');
15
+ const crypto = require('crypto');
15
16
 
16
17
  const [
17
18
  projectRoot,
@@ -61,6 +62,24 @@ if (!registry.apps[appVersion].flavors[flavor]) {
61
62
  const flavorGroup = registry.apps[appVersion].flavors[flavor];
62
63
  flavorGroup.latestOtaVersion = toOtaVersion;
63
64
 
65
+ // ── Check for private key for asymmetric signing ───────────────────────────
66
+ let signingKey = process.env.OTA_PRIVATE_KEY || null;
67
+ if (!signingKey) {
68
+ const candidateKeyPaths = [
69
+ path.join(process.cwd(), 'ota_private_key.pem'),
70
+ path.join(path.dirname(registryPath), 'ota_private_key.pem'),
71
+ path.join(path.dirname(registryPath), '../ota_private_key.pem'),
72
+ ];
73
+ for (const kp of candidateKeyPaths) {
74
+ if (fs.existsSync(kp)) {
75
+ try {
76
+ signingKey = fs.readFileSync(kp, 'utf8');
77
+ break;
78
+ } catch (e) {}
79
+ }
80
+ }
81
+ }
82
+
64
83
  // ── Update patches ──────────────────────────────────────────────────────────
65
84
  if (newPatches.length > 0) {
66
85
  // We merge patches by platform to avoid overwriting between Android and iOS
@@ -71,7 +90,32 @@ if (newPatches.length > 0) {
71
90
  const fullBundleName = `bundle_${appVersion}_v${toOtaVersion}_${platform}_${flavor}.hbc`;
72
91
  const manifestName = path.basename(_assetManifestFile);
73
92
 
74
- return {
93
+ let signature = undefined;
94
+ if (signingKey) {
95
+ try {
96
+ const patchFileCandidates = [
97
+ path.join(path.dirname(registryPath), p.file),
98
+ path.join(process.cwd(), p.file),
99
+ ];
100
+ let patchData = null;
101
+ for (const pfc of patchFileCandidates) {
102
+ if (fs.existsSync(pfc)) {
103
+ patchData = fs.readFileSync(pfc);
104
+ break;
105
+ }
106
+ }
107
+ if (patchData) {
108
+ const signer = crypto.createSign('SHA256');
109
+ signer.update(patchData);
110
+ signature = signer.sign(signingKey, 'base64');
111
+ console.log(` ✓ Signed patch ${p.file} with asymmetric private key`);
112
+ }
113
+ } catch (signErr) {
114
+ console.warn(` ⚠ Could not sign patch ${p.file}: ${signErr.message}`);
115
+ }
116
+ }
117
+
118
+ const patchObj = {
75
119
  id: `v${p.from}-to-v${p.to}-${flavor}-${platform}`,
76
120
  label: `V${p.from} → V${p.to} (${flavor} ${platform})`,
77
121
  appVersion,
@@ -85,6 +129,12 @@ if (newPatches.length > 0) {
85
129
  assetManifestUrl: manifestName,
86
130
  flavor: flavor,
87
131
  };
132
+
133
+ if (signature) {
134
+ patchObj.signature = signature;
135
+ }
136
+
137
+ return patchObj;
88
138
  });
89
139
 
90
140
  flavorGroup.patches = [...otherPlatformPatches, ...mappedNewPatches];