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.
- package/README.md +145 -42
- package/android/src/main/java/vn/reactnativebspatch/BsPatchModule.kt +117 -17
- package/cpp/CMakeLists.txt +9 -1
- package/cpp/bsdiff/bspatch.c +14 -3
- package/cpp/bsdiff/bspatch_wrapper.c +124 -40
- package/ios/RNBsPatch.mm +142 -1
- package/lib/commonjs/NativeBsPatch.js.map +1 -1
- package/lib/commonjs/ota/OtaProvider.js +61 -12
- package/lib/commonjs/ota/OtaProvider.js.map +1 -1
- package/lib/commonjs/ota/otaCore.js +43 -20
- package/lib/commonjs/ota/otaCore.js.map +1 -1
- package/lib/commonjs/ota/otaMetadata.js +41 -5
- package/lib/commonjs/ota/otaMetadata.js.map +1 -1
- package/lib/commonjs/ota/storage/CustomStorage.js +23 -3
- package/lib/commonjs/ota/storage/CustomStorage.js.map +1 -1
- package/lib/commonjs/ota/storage/GitHubRawStorage.js +3 -0
- package/lib/commonjs/ota/storage/GitHubRawStorage.js.map +1 -1
- package/lib/commonjs/ota/storage/GitHubReleaseStorage.js +3 -0
- package/lib/commonjs/ota/storage/GitHubReleaseStorage.js.map +1 -1
- package/lib/module/NativeBsPatch.js.map +1 -1
- package/lib/module/ota/OtaProvider.js +61 -12
- package/lib/module/ota/OtaProvider.js.map +1 -1
- package/lib/module/ota/otaCore.js +43 -20
- package/lib/module/ota/otaCore.js.map +1 -1
- package/lib/module/ota/otaMetadata.js +38 -3
- package/lib/module/ota/otaMetadata.js.map +1 -1
- package/lib/module/ota/storage/CustomStorage.js +23 -3
- package/lib/module/ota/storage/CustomStorage.js.map +1 -1
- package/lib/module/ota/storage/GitHubRawStorage.js +3 -0
- package/lib/module/ota/storage/GitHubRawStorage.js.map +1 -1
- package/lib/module/ota/storage/GitHubReleaseStorage.js +3 -0
- package/lib/module/ota/storage/GitHubReleaseStorage.js.map +1 -1
- package/lib/typescript/NativeBsPatch.d.ts +11 -0
- package/lib/typescript/NativeBsPatch.d.ts.map +1 -1
- package/lib/typescript/ota/OtaProvider.d.ts.map +1 -1
- package/lib/typescript/ota/otaCore.d.ts +3 -1
- package/lib/typescript/ota/otaCore.d.ts.map +1 -1
- package/lib/typescript/ota/otaMetadata.d.ts +6 -0
- package/lib/typescript/ota/otaMetadata.d.ts.map +1 -1
- package/lib/typescript/ota/storage/CustomStorage.d.ts.map +1 -1
- package/lib/typescript/ota/storage/GitHubRawStorage.d.ts.map +1 -1
- package/lib/typescript/ota/storage/GitHubReleaseStorage.d.ts.map +1 -1
- package/lib/typescript/ota/types.d.ts +6 -2
- package/lib/typescript/ota/types.d.ts.map +1 -1
- package/package.json +3 -1
- package/react-native-bspatch.podspec +6 -1
- package/src/NativeBsPatch.ts +17 -0
- package/src/ota/OtaProvider.tsx +80 -23
- package/src/ota/otaCore.ts +54 -24
- package/src/ota/otaMetadata.ts +46 -4
- package/src/ota/storage/CustomStorage.ts +26 -3
- package/src/ota/storage/GitHubRawStorage.ts +5 -0
- package/src/ota/storage/GitHubReleaseStorage.ts +5 -0
- package/src/ota/types.ts +6 -2
- package/tools/build_ota.sh +366 -18
- package/tools/bump_version.js +133 -0
- package/tools/generate_keys.js +75 -0
- package/tools/setup_project.js +69 -4
- 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');
|
package/tools/setup_project.js
CHANGED
|
@@ -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.
|
|
281
|
-
console.log('
|
|
282
|
-
console.log('
|
|
283
|
-
console.log('
|
|
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();
|
package/tools/update_registry.js
CHANGED
|
@@ -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
|
-
|
|
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];
|