@capgo/capacitor-updater 5.40.5 → 5.42.3
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/Package.swift +3 -3
- package/README.md +27 -0
- package/android/build.gradle +4 -2
- package/android/src/main/java/ee/forgr/capacitor_updater/AppLifecycleObserver.java +88 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +82 -16
- package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +181 -11
- package/android/src/main/java/ee/forgr/capacitor_updater/CryptoCipher.java +19 -5
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +21 -13
- package/android/src/main/java/ee/forgr/capacitor_updater/Logger.java +36 -14
- package/dist/docs.json +16 -0
- package/dist/esm/definitions.d.ts +14 -1
- package/dist/esm/definitions.js +0 -6
- package/dist/esm/definitions.js.map +1 -1
- package/dist/plugin.cjs.js +0 -6
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +0 -5
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +42 -2
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +446 -163
- package/ios/Sources/CapacitorUpdaterPlugin/CryptoCipher.swift +15 -5
- package/ios/Sources/CapacitorUpdaterPlugin/InternalUtils.swift +70 -0
- package/package.json +1 -1
|
@@ -210,7 +210,7 @@ public class CryptoCipher {
|
|
|
210
210
|
detectedFormat = "base64";
|
|
211
211
|
}
|
|
212
212
|
logger.debug(
|
|
213
|
-
"Received
|
|
213
|
+
"Received checksum format: " +
|
|
214
214
|
detectedFormat +
|
|
215
215
|
" (length: " +
|
|
216
216
|
checksum.length() +
|
|
@@ -218,6 +218,18 @@ public class CryptoCipher {
|
|
|
218
218
|
checksumBytes.length +
|
|
219
219
|
" bytes)"
|
|
220
220
|
);
|
|
221
|
+
|
|
222
|
+
// RSA-2048 encrypted data must be exactly 256 bytes
|
|
223
|
+
// If the checksum is not 256 bytes, the bundle was not encrypted properly
|
|
224
|
+
if (checksumBytes.length != 256) {
|
|
225
|
+
logger.error(
|
|
226
|
+
"Checksum is not RSA encrypted (size: " +
|
|
227
|
+
checksumBytes.length +
|
|
228
|
+
" bytes, expected 256 for RSA-2048). Bundle must be uploaded with encryption when public key is configured."
|
|
229
|
+
);
|
|
230
|
+
throw new IOException("Bundle checksum is not encrypted. Upload bundle with --key flag when encryption is configured.");
|
|
231
|
+
}
|
|
232
|
+
|
|
221
233
|
PublicKey pKey = CryptoCipher.stringToPublicKey(publicKey);
|
|
222
234
|
byte[] decryptedChecksum = CryptoCipher.decryptRSA(checksumBytes, pKey);
|
|
223
235
|
// Return as hex string to match calcChecksum output format
|
|
@@ -357,8 +369,10 @@ public class CryptoCipher {
|
|
|
357
369
|
}
|
|
358
370
|
|
|
359
371
|
/**
|
|
360
|
-
* Get first
|
|
361
|
-
* Returns
|
|
372
|
+
* Get first 20 characters of the public key for identification.
|
|
373
|
+
* Returns 20-character string or empty string if key is invalid/empty.
|
|
374
|
+
* The first 12 chars are always "MIIBCgKCAQEA" for RSA 2048-bit keys,
|
|
375
|
+
* so the unique part starts at character 13.
|
|
362
376
|
*/
|
|
363
377
|
public static String calcKeyId(String publicKey) {
|
|
364
378
|
if (publicKey == null || publicKey.isEmpty()) {
|
|
@@ -371,7 +385,7 @@ public class CryptoCipher {
|
|
|
371
385
|
.replace("-----BEGINRSAPUBLICKEY-----", "")
|
|
372
386
|
.replace("-----ENDRSAPUBLICKEY-----", "");
|
|
373
387
|
|
|
374
|
-
// Return first
|
|
375
|
-
return cleanedKey.length() >=
|
|
388
|
+
// Return first 20 characters of the base64-encoded key
|
|
389
|
+
return cleanedKey.length() >= 20 ? cleanedKey.substring(0, 20) : cleanedKey;
|
|
376
390
|
}
|
|
377
391
|
}
|
|
@@ -291,7 +291,7 @@ public class DownloadService extends Worker {
|
|
|
291
291
|
String fileHash = entry.getString("file_hash");
|
|
292
292
|
String downloadUrl = entry.getString("download_url");
|
|
293
293
|
|
|
294
|
-
if (!publicKey.isEmpty() && sessionKey != null && !sessionKey.isEmpty()) {
|
|
294
|
+
if (publicKey != null && !publicKey.isEmpty() && sessionKey != null && !sessionKey.isEmpty()) {
|
|
295
295
|
try {
|
|
296
296
|
fileHash = CryptoCipher.decryptChecksum(fileHash, publicKey);
|
|
297
297
|
} catch (Exception e) {
|
|
@@ -308,7 +308,9 @@ public class DownloadService extends Worker {
|
|
|
308
308
|
String targetFileName = isBrotli ? fileName.substring(0, fileName.length() - 3) : fileName;
|
|
309
309
|
|
|
310
310
|
File targetFile = new File(destFolder, targetFileName);
|
|
311
|
-
|
|
311
|
+
String cacheBaseName = new File(isBrotli ? targetFileName : fileName).getName();
|
|
312
|
+
File cacheFile = new File(cacheFolder, finalFileHash + "_" + cacheBaseName);
|
|
313
|
+
final File legacyCacheFile = isBrotli ? new File(cacheFolder, finalFileHash + "_" + new File(fileName).getName()) : null;
|
|
312
314
|
File builtinFile = new File(builtinFolder, fileName);
|
|
313
315
|
|
|
314
316
|
// Ensure parent directories of the target file exist
|
|
@@ -324,7 +326,10 @@ public class DownloadService extends Worker {
|
|
|
324
326
|
if (builtinFile.exists() && verifyChecksum(builtinFile, finalFileHash)) {
|
|
325
327
|
copyFile(builtinFile, targetFile);
|
|
326
328
|
logger.debug("using builtin file " + fileName);
|
|
327
|
-
} else if (
|
|
329
|
+
} else if (
|
|
330
|
+
tryCopyFromCache(cacheFile, targetFile, finalFileHash) ||
|
|
331
|
+
(legacyCacheFile != null && tryCopyFromCache(legacyCacheFile, targetFile, finalFileHash))
|
|
332
|
+
) {
|
|
328
333
|
logger.debug("already cached " + fileName);
|
|
329
334
|
} else {
|
|
330
335
|
downloadAndVerify(downloadUrl, targetFile, cacheFile, finalFileHash, sessionKey, publicKey, finalIsBrotli);
|
|
@@ -388,8 +393,9 @@ public class DownloadService extends Worker {
|
|
|
388
393
|
sendStatsAsync("download_zip_start", version);
|
|
389
394
|
|
|
390
395
|
File target = new File(documentsDir, dest);
|
|
391
|
-
|
|
392
|
-
File
|
|
396
|
+
// Use bundle ID in temp file names to prevent collisions when multiple downloads run concurrently
|
|
397
|
+
File infoFile = new File(documentsDir, "update_" + id + ".dat");
|
|
398
|
+
File tempFile = new File(documentsDir, "temp_" + id + ".tmp");
|
|
393
399
|
|
|
394
400
|
// Check available disk space before starting
|
|
395
401
|
long availableSpace = target.getParentFile().getUsableSpace();
|
|
@@ -420,7 +426,7 @@ public class DownloadService extends Worker {
|
|
|
420
426
|
reader = new BufferedReader(new FileReader(infoFile));
|
|
421
427
|
String updateVersion = reader.readLine();
|
|
422
428
|
if (updateVersion != null && !updateVersion.equals(version)) {
|
|
423
|
-
clearDownloadData(documentsDir);
|
|
429
|
+
clearDownloadData(documentsDir, id);
|
|
424
430
|
} else {
|
|
425
431
|
downloadedBytes = tempFile.length();
|
|
426
432
|
}
|
|
@@ -432,7 +438,7 @@ public class DownloadService extends Worker {
|
|
|
432
438
|
}
|
|
433
439
|
}
|
|
434
440
|
} else {
|
|
435
|
-
clearDownloadData(documentsDir);
|
|
441
|
+
clearDownloadData(documentsDir, id);
|
|
436
442
|
}
|
|
437
443
|
|
|
438
444
|
if (downloadedBytes > 0) {
|
|
@@ -544,9 +550,9 @@ public class DownloadService extends Worker {
|
|
|
544
550
|
}
|
|
545
551
|
}
|
|
546
552
|
|
|
547
|
-
private void clearDownloadData(String docDir) {
|
|
548
|
-
File tempFile = new File(docDir, "
|
|
549
|
-
File infoFile = new File(docDir,
|
|
553
|
+
private void clearDownloadData(String docDir, String id) {
|
|
554
|
+
File tempFile = new File(docDir, "temp_" + id + ".tmp");
|
|
555
|
+
File infoFile = new File(docDir, "update_" + id + ".dat");
|
|
550
556
|
try {
|
|
551
557
|
tempFile.delete();
|
|
552
558
|
infoFile.delete();
|
|
@@ -632,7 +638,7 @@ public class DownloadService extends Worker {
|
|
|
632
638
|
// Use OkIO for atomic write
|
|
633
639
|
writeFileAtomic(compressedFile, responseBody.byteStream(), null);
|
|
634
640
|
|
|
635
|
-
if (!publicKey.isEmpty() && sessionKey != null && !sessionKey.isEmpty()) {
|
|
641
|
+
if (publicKey != null && !publicKey.isEmpty() && sessionKey != null && !sessionKey.isEmpty()) {
|
|
636
642
|
logger.debug("Decrypting file " + targetFile.getName());
|
|
637
643
|
CryptoCipher.decryptFile(compressedFile, publicKey, sessionKey);
|
|
638
644
|
}
|
|
@@ -824,12 +830,14 @@ public class DownloadService extends Worker {
|
|
|
824
830
|
}
|
|
825
831
|
|
|
826
832
|
/**
|
|
827
|
-
* Clean up old temporary files
|
|
833
|
+
* Clean up old temporary files (both .tmp and update_*.dat files)
|
|
828
834
|
*/
|
|
829
835
|
private void cleanupOldTempFiles(File directory) {
|
|
830
836
|
if (directory == null || !directory.exists()) return;
|
|
831
837
|
|
|
832
|
-
File[] tempFiles = directory.listFiles(
|
|
838
|
+
File[] tempFiles = directory.listFiles(
|
|
839
|
+
(dir, name) -> name.endsWith(".tmp") || (name.startsWith("update_") && name.endsWith(".dat"))
|
|
840
|
+
);
|
|
833
841
|
if (tempFiles != null) {
|
|
834
842
|
long oneHourAgo = System.currentTimeMillis() - 3600000;
|
|
835
843
|
for (File tempFile : tempFiles) {
|
|
@@ -28,30 +28,48 @@ public class Logger {
|
|
|
28
28
|
|
|
29
29
|
LogLevel level;
|
|
30
30
|
Map<String, String> labels;
|
|
31
|
+
boolean useSystemLog;
|
|
31
32
|
|
|
32
33
|
Options() {
|
|
33
34
|
level = LogLevel.info;
|
|
34
35
|
labels = null;
|
|
36
|
+
useSystemLog = true; // Default to true for backward compatibility
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
Options(@NonNull Options other) {
|
|
38
40
|
level = other.level;
|
|
39
41
|
labels = other.labels;
|
|
42
|
+
useSystemLog = other.useSystemLog;
|
|
40
43
|
}
|
|
41
44
|
|
|
42
45
|
Options(LogLevel level) {
|
|
43
46
|
this.level = level;
|
|
44
47
|
this.labels = null;
|
|
48
|
+
useSystemLog = true;
|
|
45
49
|
}
|
|
46
50
|
|
|
47
51
|
Options(Map<String, String> labels) {
|
|
48
52
|
this.level = LogLevel.info;
|
|
49
53
|
this.labels = labels;
|
|
54
|
+
useSystemLog = true;
|
|
50
55
|
}
|
|
51
56
|
|
|
52
57
|
Options(LogLevel level, Map<String, String> labels) {
|
|
53
58
|
this.level = level;
|
|
54
59
|
this.labels = labels;
|
|
60
|
+
useSystemLog = true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
Options(LogLevel level, Map<String, String> labels, boolean useSystemLog) {
|
|
64
|
+
this.level = level;
|
|
65
|
+
this.labels = labels;
|
|
66
|
+
this.useSystemLog = useSystemLog;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
Options(boolean useSystemLog) {
|
|
70
|
+
this.level = LogLevel.info;
|
|
71
|
+
this.labels = null;
|
|
72
|
+
this.useSystemLog = useSystemLog;
|
|
55
73
|
}
|
|
56
74
|
}
|
|
57
75
|
|
|
@@ -60,6 +78,7 @@ public class Logger {
|
|
|
60
78
|
private String tag;
|
|
61
79
|
private final ArrayMap<String, Long> timers = new ArrayMap<>();
|
|
62
80
|
private final String kDefaultTimerLabel = "default";
|
|
81
|
+
private boolean useSystemLog;
|
|
63
82
|
|
|
64
83
|
public void setBridge(Bridge bridge) {
|
|
65
84
|
this.bridge = bridge;
|
|
@@ -77,6 +96,7 @@ public class Logger {
|
|
|
77
96
|
|
|
78
97
|
private void init(String tag, @NonNull Options options) {
|
|
79
98
|
this.level = options.level;
|
|
99
|
+
this.useSystemLog = options.useSystemLog;
|
|
80
100
|
this.labels.putAll(
|
|
81
101
|
Map.of(LogLevel.silent, "", LogLevel.error, "🔴", LogLevel.warn, "🟠", LogLevel.info, "🟢", LogLevel.debug, "\uD83D\uDD0E")
|
|
82
102
|
);
|
|
@@ -210,20 +230,22 @@ public class Logger {
|
|
|
210
230
|
tag = this.tag;
|
|
211
231
|
}
|
|
212
232
|
|
|
213
|
-
//
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
233
|
+
// Log to Android system log if enabled
|
|
234
|
+
if (useSystemLog) {
|
|
235
|
+
switch (level) {
|
|
236
|
+
case error:
|
|
237
|
+
Log.e(tag, formattedMessage);
|
|
238
|
+
break;
|
|
239
|
+
case warn:
|
|
240
|
+
Log.w(tag, formattedMessage);
|
|
241
|
+
break;
|
|
242
|
+
case info:
|
|
243
|
+
Log.i(tag, formattedMessage);
|
|
244
|
+
break;
|
|
245
|
+
case debug:
|
|
246
|
+
Log.d(tag, formattedMessage);
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
227
249
|
}
|
|
228
250
|
|
|
229
251
|
// Send to JavaScript if webView is available
|
package/dist/docs.json
CHANGED
|
@@ -3627,6 +3627,22 @@
|
|
|
3627
3627
|
"complexTypes": [],
|
|
3628
3628
|
"type": "boolean | undefined"
|
|
3629
3629
|
},
|
|
3630
|
+
{
|
|
3631
|
+
"name": "osLogging",
|
|
3632
|
+
"tags": [
|
|
3633
|
+
{
|
|
3634
|
+
"text": "true",
|
|
3635
|
+
"name": "default"
|
|
3636
|
+
},
|
|
3637
|
+
{
|
|
3638
|
+
"text": "8.42.0",
|
|
3639
|
+
"name": "since"
|
|
3640
|
+
}
|
|
3641
|
+
],
|
|
3642
|
+
"docs": "Enable OS-level logging. When enabled, logs are written to the system log which can be inspected in production builds.\n\n- **iOS**: Uses os_log instead of Swift.print, logs accessible via Console.app or Instruments\n- **Android**: Logs to Logcat (android.util.Log)\n\nWhen set to false, system logging is disabled on both platforms (only JavaScript console logging will occur if enabled).\n\nThis is useful for debugging production apps (App Store/TestFlight builds on iOS, or production APKs on Android).",
|
|
3643
|
+
"complexTypes": [],
|
|
3644
|
+
"type": "boolean | undefined"
|
|
3645
|
+
},
|
|
3630
3646
|
{
|
|
3631
3647
|
"name": "shakeMenu",
|
|
3632
3648
|
"tags": [
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import '@capacitor/cli';
|
|
2
1
|
import type { PluginListenerHandle } from '@capacitor/core';
|
|
3
2
|
declare module '@capacitor/cli' {
|
|
4
3
|
interface PluginsConfig {
|
|
@@ -309,6 +308,20 @@ declare module '@capacitor/cli' {
|
|
|
309
308
|
* @since 7.3.0
|
|
310
309
|
*/
|
|
311
310
|
disableJSLogging?: boolean;
|
|
311
|
+
/**
|
|
312
|
+
* Enable OS-level logging. When enabled, logs are written to the system log which can be inspected in production builds.
|
|
313
|
+
*
|
|
314
|
+
* - **iOS**: Uses os_log instead of Swift.print, logs accessible via Console.app or Instruments
|
|
315
|
+
* - **Android**: Logs to Logcat (android.util.Log)
|
|
316
|
+
*
|
|
317
|
+
* When set to false, system logging is disabled on both platforms (only JavaScript console logging will occur if enabled).
|
|
318
|
+
*
|
|
319
|
+
* This is useful for debugging production apps (App Store/TestFlight builds on iOS, or production APKs on Android).
|
|
320
|
+
*
|
|
321
|
+
* @default true
|
|
322
|
+
* @since 8.42.0
|
|
323
|
+
*/
|
|
324
|
+
osLogging?: boolean;
|
|
312
325
|
/**
|
|
313
326
|
* Enable shake gesture to show update menu for debugging/testing purposes
|
|
314
327
|
*
|
package/dist/esm/definitions.js
CHANGED
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
3
|
-
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
-
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
5
|
-
*/
|
|
6
|
-
import '@capacitor/cli';
|
|
7
1
|
/**
|
|
8
2
|
* Update availability status.
|
|
9
3
|
*
|