@capgo/capacitor-updater 5.41.1 → 5.42.9
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 +66 -39
- package/android/build.gradle +5 -3
- 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 +188 -11
- package/android/src/main/java/ee/forgr/capacitor_updater/CryptoCipher.java +13 -1
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +32 -10
- package/android/src/main/java/ee/forgr/capacitor_updater/Logger.java +36 -14
- package/dist/docs.json +20 -4
- package/dist/esm/definitions.d.ts +19 -4
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +42 -2
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +168 -31
- package/ios/Sources/CapacitorUpdaterPlugin/CryptoCipher.swift +9 -1
- package/ios/Sources/CapacitorUpdaterPlugin/InternalUtils.swift +23 -0
- package/package.json +1 -1
|
@@ -16,7 +16,18 @@ import Version
|
|
|
16
16
|
*/
|
|
17
17
|
@objc(CapacitorUpdaterPlugin)
|
|
18
18
|
public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
19
|
-
|
|
19
|
+
lazy var logger: Logger = {
|
|
20
|
+
// Default to true for OS logging. In test environments without a bridge,
|
|
21
|
+
// this will default to true. In production, it reads from config.
|
|
22
|
+
let osLogging: Bool
|
|
23
|
+
if self.bridge != nil {
|
|
24
|
+
osLogging = getConfig().getBoolean("osLogging", true)
|
|
25
|
+
} else {
|
|
26
|
+
osLogging = true
|
|
27
|
+
}
|
|
28
|
+
let options = Logger.Options(useSyslog: osLogging)
|
|
29
|
+
return Logger(withTag: "✨ CapgoUpdater", options: options)
|
|
30
|
+
}()
|
|
20
31
|
|
|
21
32
|
public let identifier = "CapacitorUpdaterPlugin"
|
|
22
33
|
public let jsName = "CapacitorUpdater"
|
|
@@ -60,7 +71,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
60
71
|
CAPPluginMethod(name: "completeFlexibleUpdate", returnType: CAPPluginReturnPromise)
|
|
61
72
|
]
|
|
62
73
|
public var implementation = CapgoUpdater()
|
|
63
|
-
private let pluginVersion: String = "5.
|
|
74
|
+
private let pluginVersion: String = "5.42.9"
|
|
64
75
|
static let updateUrlDefault = "https://plugin.capgo.app/updates"
|
|
65
76
|
static let statsUrlDefault = "https://plugin.capgo.app/stats"
|
|
66
77
|
static let channelUrlDefault = "https://plugin.capgo.app/channel_self"
|
|
@@ -382,6 +393,32 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
382
393
|
self.logger.info("Cleanup complete")
|
|
383
394
|
}
|
|
384
395
|
|
|
396
|
+
// Michael (WcaleNieWolny) at 04.01.2026
|
|
397
|
+
// The following line of code contains a bug. After having evaluated it, I have decided not to fix it.
|
|
398
|
+
// The initial report: https://discord.com/channels/912707985829163099/1456985639345061969
|
|
399
|
+
// The bug happens in a very specific scenario. Here is the reproduction steps, followed by the lackof busniess impact
|
|
400
|
+
// Reproduction steps:
|
|
401
|
+
// 1. Install iOS app via app store. Version: 10.13.0. Version v10 of the app uses Capacitor 6 (6.3.13) - a version where the key was still "LatestVersionNative"
|
|
402
|
+
// 2. The plugin writes "10.13.0" to the key "LatestVersionNative"
|
|
403
|
+
// 3. Update the app to version 10.17.0 via Capgo.
|
|
404
|
+
// 4. Update the app via testflight to version 11.0.0. This version uses Capacitor 8 (8.41.3) - a version where the key was changed to "LatestNativeBuildVersion"
|
|
405
|
+
// 5. During the initial load of then new native version, the plugin will read "LatestNativeBuildVersion", not find it, read "LatestVersionNative", find it and revert to builtin version sucessfully.
|
|
406
|
+
// 6. The plugin writes "11.0.0" to the key "LatestNativeBuildVersion"
|
|
407
|
+
// 7. The app is now in a state where it is using the builtin version, but the key "LatestNativeBuildVersion" is still set to "11.0.0" and "LatestVersionNative" is still set to "10.13.0".
|
|
408
|
+
// 8. The user downgrades using app store back to version 10.13.0.
|
|
409
|
+
// 9. The old plugin reads "LatestVersionNative", finds "10.13.0," so it doesn't revert to builtin version. // <--- THIS IS THE FIRST PART OF THE BUG
|
|
410
|
+
// 10. "LatestVersionNative" is written to "10.13.0" but "LatestNativeBuildVersion" is not touched, and stays at "11.0.0"
|
|
411
|
+
// 11. A capgo update happesn to version 10.17.0.
|
|
412
|
+
// 12. The user updates again to version 11.0.0 via Testflight.
|
|
413
|
+
// 13. The plugin reads "LatestNativeBuildVersion", finds "11.0.0", so it doesn't revert to builtin version. It is unaware of the native update that happended.
|
|
414
|
+
// 14. Capgo loads the 10.13.0 version, while it should have loaded the builtin 11.0.0 version. // <--- THIS IS THE SECOND PART OF THE BUG
|
|
415
|
+
// The business impact:
|
|
416
|
+
// None - no one will ever be affected by this bug as reverting via app store should in practice never happen. You are not SUPPOSE to go from Capacitor v8 to v6.
|
|
417
|
+
// Downgrading isn't supported.
|
|
418
|
+
// Possible fixes:
|
|
419
|
+
// 1. Write "LatestVersionNative" - this fixes the part 1 of this bug
|
|
420
|
+
// 2. Compare both keys. If any is not equal to "currentBuildVersion", then revert to builtin version. This fixes the part 2 of this bug
|
|
421
|
+
|
|
385
422
|
let previous = UserDefaults.standard.string(forKey: "LatestNativeBuildVersion") ?? UserDefaults.standard.string(forKey: "LatestVersionNative") ?? "0"
|
|
386
423
|
if previous != "0" && self.currentBuildVersion != previous {
|
|
387
424
|
_ = self._reset(toLastSuccessful: false)
|
|
@@ -1571,6 +1608,9 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1571
1608
|
}
|
|
1572
1609
|
|
|
1573
1610
|
@objc func appMovedToBackground() {
|
|
1611
|
+
// Reset timeout flag at start of each background cycle
|
|
1612
|
+
self.autoSplashscreenTimedOut = false
|
|
1613
|
+
|
|
1574
1614
|
let current: BundleInfo = self.implementation.getCurrentBundle()
|
|
1575
1615
|
self.implementation.sendStats(action: "app_moved_to_background", versionName: current.getVersionName())
|
|
1576
1616
|
logger.info("Check for pending update")
|
|
@@ -48,6 +48,12 @@ import UIKit
|
|
|
48
48
|
// Flag to track if we've already sent the rate limit statistic - prevents infinite loop
|
|
49
49
|
private static var rateLimitStatisticSent = false
|
|
50
50
|
|
|
51
|
+
// Stats batching - queue events and send max once per second
|
|
52
|
+
private var statsQueue: [StatsEvent] = []
|
|
53
|
+
private let statsQueueLock = NSLock()
|
|
54
|
+
private var statsFlushTimer: Timer?
|
|
55
|
+
private static let statsFlushInterval: TimeInterval = 1.0
|
|
56
|
+
|
|
51
57
|
private var userAgent: String {
|
|
52
58
|
let safePluginVersion = pluginVersion.isEmpty ? "unknown" : pluginVersion
|
|
53
59
|
let safeAppId = appId.isEmpty ? "unknown" : appId
|
|
@@ -70,6 +76,15 @@ import UIKit
|
|
|
70
76
|
self.logger = logger
|
|
71
77
|
}
|
|
72
78
|
|
|
79
|
+
deinit {
|
|
80
|
+
// Invalidate the stats timer to prevent memory leaks
|
|
81
|
+
statsFlushTimer?.invalidate()
|
|
82
|
+
statsFlushTimer = nil
|
|
83
|
+
|
|
84
|
+
// Flush any remaining stats before deallocation
|
|
85
|
+
flushStatsQueue()
|
|
86
|
+
}
|
|
87
|
+
|
|
73
88
|
private func calcTotalPercent(percent: Int, min: Int, max: Int) -> Int {
|
|
74
89
|
return (percent * (max - min)) / 100 + min
|
|
75
90
|
}
|
|
@@ -80,12 +95,20 @@ import UIKit
|
|
|
80
95
|
}
|
|
81
96
|
|
|
82
97
|
public func setPublicKey(_ publicKey: String) {
|
|
83
|
-
|
|
84
|
-
if
|
|
85
|
-
self.
|
|
86
|
-
} else {
|
|
98
|
+
// Empty string means no encryption - proceed normally
|
|
99
|
+
if publicKey.isEmpty {
|
|
100
|
+
self.publicKey = ""
|
|
87
101
|
self.cachedKeyId = nil
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Non-empty: must be a valid RSA key or crash
|
|
106
|
+
guard RSAPublicKey.load(rsaPublicKey: publicKey) != nil else {
|
|
107
|
+
fatalError("Invalid public key in capacitor.config.json: failed to parse RSA key. Remove the key or provide a valid PEM-formatted RSA public key.")
|
|
88
108
|
}
|
|
109
|
+
|
|
110
|
+
self.publicKey = publicKey
|
|
111
|
+
self.cachedKeyId = CryptoCipher.calcKeyId(publicKey: publicKey)
|
|
89
112
|
}
|
|
90
113
|
|
|
91
114
|
public func getKeyId() -> String? {
|
|
@@ -360,6 +383,56 @@ import UIKit
|
|
|
360
383
|
}
|
|
361
384
|
}
|
|
362
385
|
|
|
386
|
+
private func populateDeltaCacheAsync(for id: String) {
|
|
387
|
+
DispatchQueue.global(qos: .utility).async { [weak self] in
|
|
388
|
+
self?.populateDeltaCache(for: id)
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
private func populateDeltaCache(for id: String) {
|
|
393
|
+
let bundleDir = self.getBundleDirectory(id: id)
|
|
394
|
+
let fileManager = FileManager.default
|
|
395
|
+
|
|
396
|
+
guard fileManager.fileExists(atPath: bundleDir.path) else {
|
|
397
|
+
logger.debug("Skip delta cache population: bundle dir missing")
|
|
398
|
+
return
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
do {
|
|
402
|
+
try fileManager.createDirectory(at: cacheFolder, withIntermediateDirectories: true, attributes: nil)
|
|
403
|
+
} catch {
|
|
404
|
+
logger.debug("Skip delta cache population: failed to create cache dir")
|
|
405
|
+
return
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
guard let enumerator = fileManager.enumerator(at: bundleDir, includingPropertiesForKeys: [.isDirectoryKey], options: [.skipsHiddenFiles]) else {
|
|
409
|
+
return
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
for case let fileURL as URL in enumerator {
|
|
413
|
+
let resourceValues = try? fileURL.resourceValues(forKeys: [.isDirectoryKey])
|
|
414
|
+
if resourceValues?.isDirectory == true {
|
|
415
|
+
continue
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
let checksum = CryptoCipher.calcChecksum(filePath: fileURL)
|
|
419
|
+
if checksum.isEmpty {
|
|
420
|
+
continue
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
let cacheFile = cacheFolder.appendingPathComponent("\(checksum)_\(fileURL.lastPathComponent)")
|
|
424
|
+
if fileManager.fileExists(atPath: cacheFile.path) {
|
|
425
|
+
continue
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
do {
|
|
429
|
+
try fileManager.copyItem(at: fileURL, to: cacheFile)
|
|
430
|
+
} catch {
|
|
431
|
+
logger.debug("Delta cache copy failed: \(fileURL.path)")
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
363
436
|
private func createInfoObject() -> InfoObject {
|
|
364
437
|
return InfoObject(
|
|
365
438
|
platform: "ios",
|
|
@@ -504,10 +577,15 @@ import UIKit
|
|
|
504
577
|
|
|
505
578
|
for entry in manifest {
|
|
506
579
|
guard let fileName = entry.file_name,
|
|
507
|
-
var fileHash = entry.file_hash,
|
|
508
580
|
let downloadUrl = entry.download_url else {
|
|
509
581
|
continue
|
|
510
582
|
}
|
|
583
|
+
guard let entryFileHash = entry.file_hash, !entryFileHash.isEmpty else {
|
|
584
|
+
logger.error("Missing file_hash for manifest entry: \(entry.file_name ?? "unknown")")
|
|
585
|
+
hasError.value = true
|
|
586
|
+
continue
|
|
587
|
+
}
|
|
588
|
+
var fileHash = entryFileHash
|
|
511
589
|
|
|
512
590
|
// Decrypt checksum if needed (done before creating operation)
|
|
513
591
|
if !self.publicKey.isEmpty && !sessionKey.isEmpty {
|
|
@@ -526,10 +604,11 @@ import UIKit
|
|
|
526
604
|
|
|
527
605
|
let finalFileHash = fileHash
|
|
528
606
|
let fileNameWithoutPath = (fileName as NSString).lastPathComponent
|
|
529
|
-
let cacheFileName = "\(finalFileHash)_\(fileNameWithoutPath)"
|
|
530
|
-
let cacheFilePath = cacheFolder.appendingPathComponent(cacheFileName)
|
|
531
|
-
|
|
532
607
|
let isBrotli = fileName.hasSuffix(".br")
|
|
608
|
+
let cacheBaseName = isBrotli ? String(fileNameWithoutPath.dropLast(3)) : fileNameWithoutPath
|
|
609
|
+
let cacheFilePath = cacheFolder.appendingPathComponent("\(finalFileHash)_\(cacheBaseName)")
|
|
610
|
+
let legacyCacheFilePath: URL? = isBrotli ? cacheFolder.appendingPathComponent("\(finalFileHash)_\(fileNameWithoutPath)") : nil
|
|
611
|
+
|
|
533
612
|
let destFileName = isBrotli ? String(fileName.dropLast(3)) : fileName
|
|
534
613
|
let destFilePath = destFolder.appendingPathComponent(destFileName)
|
|
535
614
|
let builtinFilePath = builtinFolder.appendingPathComponent(fileName)
|
|
@@ -548,7 +627,9 @@ import UIKit
|
|
|
548
627
|
self.logger.info("downloadManifest \(fileName) using builtin file \(id)")
|
|
549
628
|
}
|
|
550
629
|
// Try cache
|
|
551
|
-
else if
|
|
630
|
+
else if
|
|
631
|
+
self.tryCopyFromCache(from: cacheFilePath, to: destFilePath, expectedHash: finalFileHash) ||
|
|
632
|
+
(legacyCacheFilePath != nil && self.tryCopyFromCache(from: legacyCacheFilePath!, to: destFilePath, expectedHash: finalFileHash)) {
|
|
552
633
|
self.logger.info("downloadManifest \(fileName) copy from cache \(id)")
|
|
553
634
|
}
|
|
554
635
|
// Download
|
|
@@ -668,16 +749,14 @@ import UIKit
|
|
|
668
749
|
// Write to destination
|
|
669
750
|
try finalData.write(to: destFilePath)
|
|
670
751
|
|
|
671
|
-
//
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
throw NSError(domain: "ChecksumError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Computed checksum is not equal to required checksum (\(calculatedChecksum) != \(fileHash)) for file \(fileName) at url \(downloadUrl)"])
|
|
680
|
-
}
|
|
752
|
+
// Always verify checksum when file_hash is present
|
|
753
|
+
let calculatedChecksum = CryptoCipher.calcChecksum(filePath: destFilePath)
|
|
754
|
+
CryptoCipher.logChecksumInfo(label: "Calculated checksum", hexChecksum: calculatedChecksum)
|
|
755
|
+
CryptoCipher.logChecksumInfo(label: "Expected checksum", hexChecksum: fileHash)
|
|
756
|
+
if calculatedChecksum != fileHash {
|
|
757
|
+
try? FileManager.default.removeItem(at: destFilePath)
|
|
758
|
+
self.sendStats(action: "download_manifest_checksum_fail", versionName: "\(version):\(destFileName)")
|
|
759
|
+
throw NSError(domain: "ChecksumError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Computed checksum is not equal to required checksum (\(calculatedChecksum) != \(fileHash)) for file \(fileName) at url \(downloadUrl)"])
|
|
681
760
|
}
|
|
682
761
|
|
|
683
762
|
// Save to cache
|
|
@@ -962,6 +1041,7 @@ import UIKit
|
|
|
962
1041
|
CryptoCipher.logChecksumInfo(label: "Calculated bundle checksum", hexChecksum: checksum)
|
|
963
1042
|
logger.info("Downloading: 80% (unzipping)")
|
|
964
1043
|
try self.saveDownloaded(sourceZip: finalPath, id: id, base: self.libraryDir.appendingPathComponent(self.bundleDirectory), notify: true)
|
|
1044
|
+
self.populateDeltaCacheAsync(for: id)
|
|
965
1045
|
|
|
966
1046
|
} catch {
|
|
967
1047
|
logger.error("Failed to unzip file")
|
|
@@ -1306,7 +1386,7 @@ import UIKit
|
|
|
1306
1386
|
let fileName = url.lastPathComponent
|
|
1307
1387
|
// Only cleanup package_*.tmp and update_*.dat files
|
|
1308
1388
|
let isDownloadTemp = (fileName.hasPrefix("package_") && fileName.hasSuffix(".tmp")) ||
|
|
1309
|
-
|
|
1389
|
+
(fileName.hasPrefix("update_") && fileName.hasSuffix(".dat"))
|
|
1310
1390
|
if !isDownloadTemp {
|
|
1311
1391
|
continue
|
|
1312
1392
|
}
|
|
@@ -1472,6 +1552,15 @@ import UIKit
|
|
|
1472
1552
|
if let responseValue = response.value {
|
|
1473
1553
|
if let error = responseValue.error {
|
|
1474
1554
|
setChannel.error = error
|
|
1555
|
+
} else if responseValue.unset == true {
|
|
1556
|
+
// Server requested to unset channel (public channel was requested)
|
|
1557
|
+
// Clear persisted defaultChannel and revert to config value
|
|
1558
|
+
UserDefaults.standard.removeObject(forKey: defaultChannelKey)
|
|
1559
|
+
UserDefaults.standard.synchronize()
|
|
1560
|
+
self.logger.info("Public channel requested, channel override removed")
|
|
1561
|
+
|
|
1562
|
+
setChannel.status = responseValue.status ?? "ok"
|
|
1563
|
+
setChannel.message = responseValue.message ?? "Public channel requested, channel override removed. Device will use public channel automatically."
|
|
1475
1564
|
} else {
|
|
1476
1565
|
// Success - persist defaultChannel
|
|
1477
1566
|
self.defaultChannel = channel
|
|
@@ -1667,21 +1756,70 @@ import UIKit
|
|
|
1667
1756
|
guard !statsUrl.isEmpty else {
|
|
1668
1757
|
return
|
|
1669
1758
|
}
|
|
1670
|
-
operationQueue.maxConcurrentOperationCount = 1
|
|
1671
1759
|
|
|
1672
|
-
let
|
|
1760
|
+
let resolvedVersionName = versionName ?? getCurrentBundle().getVersionName()
|
|
1761
|
+
let info = createInfoObject()
|
|
1762
|
+
|
|
1763
|
+
let event = StatsEvent(
|
|
1764
|
+
platform: info.platform,
|
|
1765
|
+
device_id: info.device_id,
|
|
1766
|
+
app_id: info.app_id,
|
|
1767
|
+
custom_id: info.custom_id,
|
|
1768
|
+
version_build: info.version_build,
|
|
1769
|
+
version_code: info.version_code,
|
|
1770
|
+
version_os: info.version_os,
|
|
1771
|
+
version_name: resolvedVersionName,
|
|
1772
|
+
old_version_name: oldVersionName ?? "",
|
|
1773
|
+
plugin_version: info.plugin_version,
|
|
1774
|
+
is_emulator: info.is_emulator,
|
|
1775
|
+
is_prod: info.is_prod,
|
|
1776
|
+
action: action,
|
|
1777
|
+
channel: info.channel,
|
|
1778
|
+
defaultChannel: info.defaultChannel,
|
|
1779
|
+
key_id: info.key_id,
|
|
1780
|
+
timestamp: Int64(Date().timeIntervalSince1970 * 1000)
|
|
1781
|
+
)
|
|
1673
1782
|
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1783
|
+
statsQueueLock.lock()
|
|
1784
|
+
statsQueue.append(event)
|
|
1785
|
+
statsQueueLock.unlock()
|
|
1786
|
+
|
|
1787
|
+
ensureStatsTimerStarted()
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
private func ensureStatsTimerStarted() {
|
|
1791
|
+
DispatchQueue.main.async { [weak self] in
|
|
1792
|
+
guard let self = self else { return }
|
|
1793
|
+
if self.statsFlushTimer == nil || !self.statsFlushTimer!.isValid {
|
|
1794
|
+
// Use closure-based timer to avoid strong reference cycle
|
|
1795
|
+
self.statsFlushTimer = Timer.scheduledTimer(
|
|
1796
|
+
withTimeInterval: CapgoUpdater.statsFlushInterval,
|
|
1797
|
+
repeats: true
|
|
1798
|
+
) { [weak self] _ in
|
|
1799
|
+
self?.flushStatsQueue()
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
private func flushStatsQueue() {
|
|
1806
|
+
statsQueueLock.lock()
|
|
1807
|
+
guard !statsQueue.isEmpty else {
|
|
1808
|
+
statsQueueLock.unlock()
|
|
1809
|
+
return
|
|
1810
|
+
}
|
|
1811
|
+
let eventsToSend = statsQueue
|
|
1812
|
+
statsQueue.removeAll()
|
|
1813
|
+
statsQueueLock.unlock()
|
|
1814
|
+
|
|
1815
|
+
operationQueue.maxConcurrentOperationCount = 1
|
|
1678
1816
|
|
|
1679
1817
|
let operation = BlockOperation {
|
|
1680
1818
|
let semaphore = DispatchSemaphore(value: 0)
|
|
1681
1819
|
self.alamofireSession.request(
|
|
1682
1820
|
self.statsUrl,
|
|
1683
1821
|
method: .post,
|
|
1684
|
-
parameters:
|
|
1822
|
+
parameters: eventsToSend,
|
|
1685
1823
|
encoder: JSONParameterEncoder.default,
|
|
1686
1824
|
requestModifier: { $0.timeoutInterval = self.timeout }
|
|
1687
1825
|
).responseData { response in
|
|
@@ -1693,10 +1831,10 @@ import UIKit
|
|
|
1693
1831
|
|
|
1694
1832
|
switch response.result {
|
|
1695
1833
|
case .success:
|
|
1696
|
-
self.logger.info("Stats sent successfully")
|
|
1697
|
-
self.logger.debug("
|
|
1834
|
+
self.logger.info("Stats batch sent successfully")
|
|
1835
|
+
self.logger.debug("Sent \(eventsToSend.count) events")
|
|
1698
1836
|
case let .failure(error):
|
|
1699
|
-
self.logger.error("Error sending stats")
|
|
1837
|
+
self.logger.error("Error sending stats batch")
|
|
1700
1838
|
self.logger.debug("Response: \(response.value?.debugDescription ?? "nil"), Error: \(error.localizedDescription)")
|
|
1701
1839
|
}
|
|
1702
1840
|
semaphore.signal()
|
|
@@ -1704,7 +1842,6 @@ import UIKit
|
|
|
1704
1842
|
semaphore.wait()
|
|
1705
1843
|
}
|
|
1706
1844
|
operationQueue.addOperation(operation)
|
|
1707
|
-
|
|
1708
1845
|
}
|
|
1709
1846
|
|
|
1710
1847
|
public func getBundleInfo(id: String?) -> BundleInfo {
|
|
@@ -63,13 +63,21 @@ public struct CryptoCipher {
|
|
|
63
63
|
detectedFormat = "base64"
|
|
64
64
|
}
|
|
65
65
|
// swiftlint:disable:next line_length
|
|
66
|
-
logger.debug("Received
|
|
66
|
+
logger.debug("Received checksum format: \(detectedFormat) (length: \(checksum.count) chars, \(checksumBytes.count) bytes)")
|
|
67
67
|
|
|
68
68
|
if checksumBytes.isEmpty {
|
|
69
69
|
logger.error("Decoded checksum is empty")
|
|
70
70
|
throw CustomError.cannotDecode
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
// RSA-2048 encrypted data must be exactly 256 bytes
|
|
74
|
+
// If the checksum is not 256 bytes, the bundle was not encrypted properly
|
|
75
|
+
if checksumBytes.count != 256 {
|
|
76
|
+
// swiftlint:disable:next line_length
|
|
77
|
+
logger.error("Checksum is not RSA encrypted (size: \(checksumBytes.count) bytes, expected 256 for RSA-2048). Bundle must be uploaded with encryption when public key is configured.")
|
|
78
|
+
throw CustomError.cannotDecode
|
|
79
|
+
}
|
|
80
|
+
|
|
73
81
|
guard let rsaPublicKey = RSAPublicKey.load(rsaPublicKey: publicKey) else {
|
|
74
82
|
logger.error("The public key is not a valid RSA Public key")
|
|
75
83
|
throw CustomError.cannotDecode
|
|
@@ -23,6 +23,7 @@ struct SetChannelDec: Decodable {
|
|
|
23
23
|
let status: String?
|
|
24
24
|
let error: String?
|
|
25
25
|
let message: String?
|
|
26
|
+
let unset: Bool?
|
|
26
27
|
}
|
|
27
28
|
public class SetChannel: NSObject {
|
|
28
29
|
var status: String = ""
|
|
@@ -135,6 +136,28 @@ struct InfoObject: Codable {
|
|
|
135
136
|
}
|
|
136
137
|
// swiftlint:enable identifier_name
|
|
137
138
|
|
|
139
|
+
// swiftlint:disable identifier_name
|
|
140
|
+
struct StatsEvent: Codable {
|
|
141
|
+
let platform: String?
|
|
142
|
+
let device_id: String?
|
|
143
|
+
let app_id: String?
|
|
144
|
+
let custom_id: String?
|
|
145
|
+
let version_build: String?
|
|
146
|
+
let version_code: String?
|
|
147
|
+
let version_os: String?
|
|
148
|
+
let version_name: String?
|
|
149
|
+
let old_version_name: String?
|
|
150
|
+
let plugin_version: String?
|
|
151
|
+
let is_emulator: Bool?
|
|
152
|
+
let is_prod: Bool?
|
|
153
|
+
let action: String?
|
|
154
|
+
let channel: String?
|
|
155
|
+
let defaultChannel: String?
|
|
156
|
+
let key_id: String?
|
|
157
|
+
let timestamp: Int64
|
|
158
|
+
}
|
|
159
|
+
// swiftlint:enable identifier_name
|
|
160
|
+
|
|
138
161
|
// swiftlint:disable identifier_name
|
|
139
162
|
public struct ManifestEntry: Codable {
|
|
140
163
|
let file_name: String?
|