@otakit/capacitor-updater 2.1.2 → 2.2.0
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 +142 -4
- package/android/src/main/java/com/otakit/updater/BundleCrypto.java +81 -0
- package/android/src/main/java/com/otakit/updater/BundleStore.java +15 -0
- package/android/src/main/java/com/otakit/updater/ManifestClient.java +69 -2
- package/android/src/main/java/com/otakit/updater/ManifestVerifier.java +43 -0
- package/android/src/main/java/com/otakit/updater/UpdaterPlugin.java +277 -19
- package/dist/esm/definitions.d.ts +98 -0
- package/dist/esm/definitions.d.ts.map +1 -1
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +6 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/web.d.ts +7 -1
- package/dist/esm/web.d.ts.map +1 -1
- package/dist/esm/web.js +25 -0
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +31 -0
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +31 -0
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/UpdaterPlugin/BundleCrypto.swift +91 -0
- package/ios/Sources/UpdaterPlugin/BundleStore.swift +13 -0
- package/ios/Sources/UpdaterPlugin/ManifestClient.swift +45 -1
- package/ios/Sources/UpdaterPlugin/ManifestVerifier.swift +29 -0
- package/ios/Sources/UpdaterPlugin/UpdaterPlugin.swift +256 -19
- package/package.json +1 -1
|
@@ -20,7 +20,7 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
20
20
|
|
|
21
21
|
private enum DownloadResolution {
|
|
22
22
|
case noUpdate
|
|
23
|
-
case staged(BundleInfo)
|
|
23
|
+
case staged(BundleInfo, forceImmediate: Bool)
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
public let identifier = "UpdaterPlugin"
|
|
@@ -33,6 +33,8 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
33
33
|
CAPPluginMethod(name: "update", returnType: CAPPluginReturnPromise),
|
|
34
34
|
CAPPluginMethod(name: "notifyAppReady", returnType: CAPPluginReturnPromise),
|
|
35
35
|
CAPPluginMethod(name: "getLastFailure", returnType: CAPPluginReturnPromise),
|
|
36
|
+
CAPPluginMethod(name: "setChannel", returnType: CAPPluginReturnPromise),
|
|
37
|
+
CAPPluginMethod(name: "getChannel", returnType: CAPPluginReturnPromise),
|
|
36
38
|
]
|
|
37
39
|
|
|
38
40
|
private let store = BundleStore()
|
|
@@ -52,6 +54,7 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
52
54
|
private var channel: String?
|
|
53
55
|
private var runtimeVersion: String?
|
|
54
56
|
private var manifestKeys: [(kid: String, key: Data)] = []
|
|
57
|
+
private var bundleKeys: [(kid: String, key: Data)] = []
|
|
55
58
|
private var trialTimeoutWorkItem: DispatchWorkItem?
|
|
56
59
|
private var checkIntervalMs: Int = 600_000
|
|
57
60
|
private var foregroundObserver: NSObjectProtocol?
|
|
@@ -105,6 +108,22 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
105
108
|
manifestKeys = HostedManifestKeys.defaults
|
|
106
109
|
}
|
|
107
110
|
|
|
111
|
+
let rawBundleKeysValue = getConfig().getArray("bundleKeys")
|
|
112
|
+
if let rawBundleKeys = rawBundleKeysValue as? [[String: String]] {
|
|
113
|
+
bundleKeys = rawBundleKeys.compactMap { entry in
|
|
114
|
+
guard let kid = entry["kid"],
|
|
115
|
+
let keyBase64 = entry["key"],
|
|
116
|
+
let keyData = Data(base64Encoded: keyBase64),
|
|
117
|
+
keyData.count == 32 else { return nil }
|
|
118
|
+
return (kid: kid, key: keyData)
|
|
119
|
+
}
|
|
120
|
+
if bundleKeys.isEmpty && !rawBundleKeys.isEmpty {
|
|
121
|
+
print("[OtaKit] ERROR: bundleKeys configured but all entries are invalid. Encrypted bundles cannot be decrypted.")
|
|
122
|
+
}
|
|
123
|
+
} else if rawBundleKeysValue != nil {
|
|
124
|
+
print("[OtaKit] ERROR: bundleKeys has wrong format (expected array of {kid, key}). Encrypted bundles cannot be decrypted.")
|
|
125
|
+
}
|
|
126
|
+
|
|
108
127
|
pruneIncompatibleBundles()
|
|
109
128
|
|
|
110
129
|
let startup = coordinator.normalizeStartupState(
|
|
@@ -196,13 +215,19 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
196
215
|
return
|
|
197
216
|
}
|
|
198
217
|
executeAutomaticUpdate(label: "runtime apply-staged fallback") { [self] in
|
|
199
|
-
|
|
218
|
+
let result = try await downloadLatest(respectInterval: false, channel: nil)
|
|
200
219
|
resolveCurrentRuntimeKey()
|
|
220
|
+
if isForcedStaged(result) {
|
|
221
|
+
try requireApplyStaged(reloadAfterApply: true)
|
|
222
|
+
}
|
|
201
223
|
}
|
|
202
224
|
case .shadow:
|
|
203
225
|
executeAutomaticUpdate(label: "runtime shadow") { [self] in
|
|
204
|
-
|
|
226
|
+
let result = try await downloadLatest(respectInterval: false, channel: nil)
|
|
205
227
|
resolveCurrentRuntimeKey()
|
|
228
|
+
if isForcedStaged(result) {
|
|
229
|
+
try requireApplyStaged(reloadAfterApply: true)
|
|
230
|
+
}
|
|
206
231
|
}
|
|
207
232
|
case .immediate:
|
|
208
233
|
executeAutomaticUpdate(label: "runtime immediate") { [self] in
|
|
@@ -232,11 +257,17 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
232
257
|
return
|
|
233
258
|
}
|
|
234
259
|
executeAutomaticUpdate(label: "launch apply-staged fallback") { [self] in
|
|
235
|
-
|
|
260
|
+
let result = try await downloadLatest(respectInterval: false, channel: nil)
|
|
261
|
+
if isForcedStaged(result) {
|
|
262
|
+
try requireApplyStaged(reloadAfterApply: true)
|
|
263
|
+
}
|
|
236
264
|
}
|
|
237
265
|
case .shadow:
|
|
238
266
|
executeAutomaticUpdate(label: "launch shadow") { [self] in
|
|
239
|
-
|
|
267
|
+
let result = try await downloadLatest(respectInterval: false, channel: nil)
|
|
268
|
+
if isForcedStaged(result) {
|
|
269
|
+
try requireApplyStaged(reloadAfterApply: true)
|
|
270
|
+
}
|
|
240
271
|
}
|
|
241
272
|
case .immediate:
|
|
242
273
|
executeAutomaticUpdate(label: "launch immediate") { [self] in
|
|
@@ -257,11 +288,17 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
257
288
|
if try applyStaged(reloadAfterApply: true) {
|
|
258
289
|
return
|
|
259
290
|
}
|
|
260
|
-
|
|
291
|
+
let result = try await downloadLatest(respectInterval: true, channel: nil)
|
|
292
|
+
if isForcedStaged(result) {
|
|
293
|
+
try requireApplyStaged(reloadAfterApply: true)
|
|
294
|
+
}
|
|
261
295
|
}
|
|
262
296
|
case .shadow:
|
|
263
297
|
executeAutomaticUpdate(label: "resume shadow") { [self] in
|
|
264
|
-
|
|
298
|
+
let result = try await downloadLatest(respectInterval: true, channel: nil)
|
|
299
|
+
if isForcedStaged(result) {
|
|
300
|
+
try requireApplyStaged(reloadAfterApply: true)
|
|
301
|
+
}
|
|
265
302
|
}
|
|
266
303
|
case .immediate:
|
|
267
304
|
executeAutomaticUpdate(label: "resume immediate") { [self] in
|
|
@@ -413,6 +450,9 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
413
450
|
coordinator.cleanupBundles(preparation.cleanupBundleIds)
|
|
414
451
|
if let eventPayload = preparation.eventPayload {
|
|
415
452
|
sendDeviceEvent(eventPayload)
|
|
453
|
+
// eventPayload is non-nil only on a genuine trial -> success transition,
|
|
454
|
+
// so repeat notifyAppReady() calls never double-emit.
|
|
455
|
+
emitEvent("updateApplied", ["bundle": store.getCurrentBundle().toDictionary()])
|
|
416
456
|
}
|
|
417
457
|
|
|
418
458
|
call.resolve()
|
|
@@ -426,6 +466,54 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
426
466
|
call.resolve(failed.toDictionary())
|
|
427
467
|
}
|
|
428
468
|
|
|
469
|
+
@objc func setChannel(_ call: CAPPluginCall) {
|
|
470
|
+
let raw = call.options["channel"]
|
|
471
|
+
if raw == nil || raw is NSNull {
|
|
472
|
+
store.setOverrideChannel(nil)
|
|
473
|
+
call.resolve()
|
|
474
|
+
return
|
|
475
|
+
}
|
|
476
|
+
guard let name = raw as? String else {
|
|
477
|
+
call.reject("channel must be a string or null")
|
|
478
|
+
return
|
|
479
|
+
}
|
|
480
|
+
guard isValidChannelName(name) else {
|
|
481
|
+
call.reject(
|
|
482
|
+
"Invalid channel name '\(name)': use 1-64 letters, numbers, '.', '_' or '-' (reserved names: base, default)"
|
|
483
|
+
)
|
|
484
|
+
return
|
|
485
|
+
}
|
|
486
|
+
store.setOverrideChannel(name)
|
|
487
|
+
call.resolve()
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
@objc func getChannel(_ call: CAPPluginCall) {
|
|
491
|
+
if let override = store.getOverrideChannel() {
|
|
492
|
+
call.resolve(["channel": override, "source": "override"])
|
|
493
|
+
return
|
|
494
|
+
}
|
|
495
|
+
call.resolve([
|
|
496
|
+
"channel": channel ?? NSNull(),
|
|
497
|
+
"source": "config",
|
|
498
|
+
])
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/// Mirrors the server's isValidChannelName (console/lib/validation.ts):
|
|
502
|
+
/// charset regex plus reserved names. The channel is interpolated into the
|
|
503
|
+
/// manifest CDN path, so anything outside this charset (or a ".." sequence)
|
|
504
|
+
/// is rejected before it is persisted or used.
|
|
505
|
+
private func isValidChannelName(_ name: String) -> Bool {
|
|
506
|
+
// \A/\z anchor the whole input: ICU's ^/$ would accept a trailing
|
|
507
|
+
// line terminator (e.g. "beta\n"), diverging from Android/server.
|
|
508
|
+
guard name.range(of: "\\A[A-Za-z0-9._-]{1,64}\\z", options: .regularExpression) != nil else {
|
|
509
|
+
return false
|
|
510
|
+
}
|
|
511
|
+
if name.contains("..") || name == "." {
|
|
512
|
+
return false
|
|
513
|
+
}
|
|
514
|
+
return !["base", "default"].contains(name.lowercased())
|
|
515
|
+
}
|
|
516
|
+
|
|
429
517
|
private func fetchLatest(channel: String?) async throws -> LatestManifest? {
|
|
430
518
|
guard let appId else {
|
|
431
519
|
throw NSError(domain: "OtaKit", code: 1, userInfo: [NSLocalizedDescriptionKey: "Missing appId in plugin config"])
|
|
@@ -463,6 +551,10 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
463
551
|
|
|
464
552
|
let resolution = try classifyLatestManifest(manifest, targetChannel: targetChannel)
|
|
465
553
|
|
|
554
|
+
if case let .updateAvailable(available) = resolution {
|
|
555
|
+
emitEvent("updateAvailable", manifestToDictionary(available))
|
|
556
|
+
}
|
|
557
|
+
|
|
466
558
|
if respectInterval {
|
|
467
559
|
recordCheckTimestamp()
|
|
468
560
|
}
|
|
@@ -478,15 +570,15 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
478
570
|
switch result {
|
|
479
571
|
case .noUpdate:
|
|
480
572
|
return .noUpdate
|
|
481
|
-
case let .alreadyStaged(
|
|
482
|
-
return .staged(bundle)
|
|
573
|
+
case let .alreadyStaged(latest, bundle):
|
|
574
|
+
return .staged(bundle, forceImmediate: latest.forceImmediate)
|
|
483
575
|
case let .updateAvailable(manifest):
|
|
484
576
|
do {
|
|
485
577
|
let bundle = try await downloadLatestManifest(
|
|
486
578
|
manifest,
|
|
487
579
|
targetChannel: targetChannel
|
|
488
580
|
)
|
|
489
|
-
return .staged(bundle)
|
|
581
|
+
return .staged(bundle, forceImmediate: manifest.forceImmediate)
|
|
490
582
|
} catch let error as NSError where isExpiredURLError(error) {
|
|
491
583
|
guard let refreshed = try await fetchLatest(channel: targetChannel) else {
|
|
492
584
|
return .noUpdate
|
|
@@ -495,14 +587,14 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
495
587
|
switch try classifyLatestManifest(refreshed, targetChannel: targetChannel) {
|
|
496
588
|
case .noUpdate:
|
|
497
589
|
return .noUpdate
|
|
498
|
-
case let .alreadyStaged(
|
|
499
|
-
return .staged(bundle)
|
|
590
|
+
case let .alreadyStaged(latest, bundle):
|
|
591
|
+
return .staged(bundle, forceImmediate: latest.forceImmediate)
|
|
500
592
|
case let .updateAvailable(retryManifest):
|
|
501
593
|
let bundle = try await downloadLatestManifest(
|
|
502
594
|
retryManifest,
|
|
503
595
|
targetChannel: targetChannel
|
|
504
596
|
)
|
|
505
|
-
return .staged(bundle)
|
|
597
|
+
return .staged(bundle, forceImmediate: retryManifest.forceImmediate)
|
|
506
598
|
}
|
|
507
599
|
}
|
|
508
600
|
}
|
|
@@ -555,7 +647,7 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
555
647
|
switch result {
|
|
556
648
|
case .noUpdate:
|
|
557
649
|
return ["kind": "no_update"]
|
|
558
|
-
case let .staged(bundle):
|
|
650
|
+
case let .staged(bundle, _):
|
|
559
651
|
return [
|
|
560
652
|
"kind": "staged",
|
|
561
653
|
"bundle": bundle.toDictionary(),
|
|
@@ -563,6 +655,15 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
563
655
|
}
|
|
564
656
|
}
|
|
565
657
|
|
|
658
|
+
/// True when an automatic flow staged a bundle whose release is marked
|
|
659
|
+
/// force-immediate — the flow escalates to apply + reload.
|
|
660
|
+
private func isForcedStaged(_ result: DownloadResolution) -> Bool {
|
|
661
|
+
if case let .staged(_, forceImmediate) = result {
|
|
662
|
+
return forceImmediate
|
|
663
|
+
}
|
|
664
|
+
return false
|
|
665
|
+
}
|
|
666
|
+
|
|
566
667
|
private func isExpiredURLError(_ error: NSError) -> Bool {
|
|
567
668
|
// HTTP 403 or 410 typically indicates an expired presigned URL
|
|
568
669
|
if error.domain == "Downloader" && (error.code == 403 || error.code == 410) {
|
|
@@ -579,11 +680,15 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
579
680
|
expectedSize: Int? = nil,
|
|
580
681
|
runtimeVersion: String? = nil,
|
|
581
682
|
channel: String? = nil,
|
|
582
|
-
releaseId: String? = nil
|
|
683
|
+
releaseId: String? = nil,
|
|
684
|
+
encryption: ManifestEncryption? = nil
|
|
583
685
|
) async throws -> BundleInfo {
|
|
584
686
|
// Check disk space before downloading
|
|
585
687
|
if let size = expectedSize {
|
|
586
|
-
|
|
688
|
+
// zip + extracted + buffer; encrypted bundles keep an extra decrypted
|
|
689
|
+
// zip copy on disk between decrypt and extract.
|
|
690
|
+
let multiplier = encryption != nil ? 3.5 : 2.5
|
|
691
|
+
let requiredSpace = Int64(Double(size) * multiplier)
|
|
587
692
|
let availableSpace = getFreeDiskSpace()
|
|
588
693
|
if availableSpace < requiredSpace {
|
|
589
694
|
let error = NSError(
|
|
@@ -599,20 +704,60 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
599
704
|
releaseId: releaseId,
|
|
600
705
|
detail: "insufficient_disk_space"
|
|
601
706
|
)
|
|
707
|
+
emitEvent(
|
|
708
|
+
"downloadFailed",
|
|
709
|
+
failureEventData(
|
|
710
|
+
version: version,
|
|
711
|
+
runtimeVersion: runtimeVersion,
|
|
712
|
+
channel: channel,
|
|
713
|
+
releaseId: releaseId,
|
|
714
|
+
reason: "insufficient_disk_space"
|
|
715
|
+
)
|
|
716
|
+
)
|
|
602
717
|
throw error
|
|
603
718
|
}
|
|
604
719
|
}
|
|
605
720
|
|
|
606
|
-
let zipURL
|
|
721
|
+
let zipURL: URL
|
|
722
|
+
do {
|
|
723
|
+
zipURL = try await downloader.download(from: url)
|
|
724
|
+
} catch {
|
|
725
|
+
// Network failures must report like every other download-path failure
|
|
726
|
+
// (Android's downloadZip already sits inside its try block).
|
|
727
|
+
sendDeviceEvent(
|
|
728
|
+
action: .downloadError,
|
|
729
|
+
bundleVersion: version,
|
|
730
|
+
runtimeVersion: runtimeVersion,
|
|
731
|
+
channel: channel,
|
|
732
|
+
releaseId: releaseId,
|
|
733
|
+
detail: error.localizedDescription
|
|
734
|
+
)
|
|
735
|
+
emitEvent(
|
|
736
|
+
"downloadFailed",
|
|
737
|
+
failureEventData(
|
|
738
|
+
version: version,
|
|
739
|
+
runtimeVersion: runtimeVersion,
|
|
740
|
+
channel: channel,
|
|
741
|
+
releaseId: releaseId,
|
|
742
|
+
reason: failureReason(from: error)
|
|
743
|
+
)
|
|
744
|
+
)
|
|
745
|
+
throw error
|
|
746
|
+
}
|
|
607
747
|
|
|
608
748
|
let extractDirectory = fileManager.temporaryDirectory
|
|
609
749
|
.appendingPathComponent("otakit-extract-\(UUID().uuidString)", isDirectory: true)
|
|
750
|
+
let decryptedZipURL = fileManager.temporaryDirectory
|
|
751
|
+
.appendingPathComponent("otakit-decrypted-\(UUID().uuidString).zip")
|
|
610
752
|
defer {
|
|
611
753
|
try? fileManager.removeItem(at: zipURL)
|
|
754
|
+
try? fileManager.removeItem(at: decryptedZipURL)
|
|
612
755
|
try? fileManager.removeItem(at: extractDirectory)
|
|
613
756
|
}
|
|
614
757
|
|
|
615
758
|
do {
|
|
759
|
+
// The manifest sha256 covers the downloaded object as-is — the
|
|
760
|
+
// ciphertext when the bundle is encrypted.
|
|
616
761
|
let valid = try HashUtils.verify(
|
|
617
762
|
fileURL: zipURL,
|
|
618
763
|
expectedSha256: expectedSha256
|
|
@@ -625,7 +770,26 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
625
770
|
)
|
|
626
771
|
}
|
|
627
772
|
|
|
628
|
-
|
|
773
|
+
var zipToExtract = zipURL
|
|
774
|
+
if let encryption {
|
|
775
|
+
guard let bundleKey = bundleKeys.first(where: { $0.kid == encryption.kid }) else {
|
|
776
|
+
throw BundleCryptoError.noMatchingKey(encryption.kid)
|
|
777
|
+
}
|
|
778
|
+
let dek = try BundleCrypto.unwrapDek(
|
|
779
|
+
kek: bundleKey.key,
|
|
780
|
+
wrapNonceB64: encryption.wrapNonce,
|
|
781
|
+
wrappedDekB64: encryption.wrappedDek
|
|
782
|
+
)
|
|
783
|
+
try BundleCrypto.decryptFile(
|
|
784
|
+
dek: dek,
|
|
785
|
+
nonceB64: encryption.nonce,
|
|
786
|
+
input: zipURL,
|
|
787
|
+
output: decryptedZipURL
|
|
788
|
+
)
|
|
789
|
+
zipToExtract = decryptedZipURL
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
try zipUtils.extractSecurely(zipURL: zipToExtract, to: extractDirectory)
|
|
629
793
|
let bundleRoot = try resolveBundleRoot(extractedDirectory: extractDirectory)
|
|
630
794
|
|
|
631
795
|
let bundleId = buildBundleId(
|
|
@@ -665,6 +829,7 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
665
829
|
channel: channel,
|
|
666
830
|
releaseId: releaseId
|
|
667
831
|
)
|
|
832
|
+
emitEvent("updateStaged", ["bundle": info.toDictionary()])
|
|
668
833
|
return info
|
|
669
834
|
} catch {
|
|
670
835
|
sendDeviceEvent(
|
|
@@ -675,6 +840,16 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
675
840
|
releaseId: releaseId,
|
|
676
841
|
detail: error.localizedDescription
|
|
677
842
|
)
|
|
843
|
+
emitEvent(
|
|
844
|
+
"downloadFailed",
|
|
845
|
+
failureEventData(
|
|
846
|
+
version: version,
|
|
847
|
+
runtimeVersion: runtimeVersion,
|
|
848
|
+
channel: channel,
|
|
849
|
+
releaseId: releaseId,
|
|
850
|
+
reason: failureReason(from: error)
|
|
851
|
+
)
|
|
852
|
+
)
|
|
678
853
|
throw error
|
|
679
854
|
}
|
|
680
855
|
}
|
|
@@ -778,6 +953,16 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
778
953
|
coordinator.cleanupBundles(preparation.cleanupBundleIds)
|
|
779
954
|
if let eventPayload = preparation.eventPayload {
|
|
780
955
|
sendDeviceEvent(eventPayload)
|
|
956
|
+
emitEvent(
|
|
957
|
+
"rollback",
|
|
958
|
+
failureEventData(
|
|
959
|
+
version: eventPayload.bundleVersion ?? "",
|
|
960
|
+
runtimeVersion: eventPayload.runtimeVersion,
|
|
961
|
+
channel: eventPayload.channel,
|
|
962
|
+
releaseId: eventPayload.releaseId,
|
|
963
|
+
reason: reason
|
|
964
|
+
)
|
|
965
|
+
)
|
|
781
966
|
}
|
|
782
967
|
|
|
783
968
|
do {
|
|
@@ -863,6 +1048,7 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
863
1048
|
payload["runtimeVersion"] = runtimeVersion
|
|
864
1049
|
}
|
|
865
1050
|
payload["releaseId"] = latest.releaseId
|
|
1051
|
+
payload["forceImmediate"] = latest.forceImmediate
|
|
866
1052
|
return payload
|
|
867
1053
|
}
|
|
868
1054
|
|
|
@@ -885,7 +1071,8 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
885
1071
|
expectedSize: manifest.size,
|
|
886
1072
|
runtimeVersion: manifest.runtimeVersion,
|
|
887
1073
|
channel: targetChannel,
|
|
888
|
-
releaseId: manifest.releaseId
|
|
1074
|
+
releaseId: manifest.releaseId,
|
|
1075
|
+
encryption: manifest.encryption
|
|
889
1076
|
)
|
|
890
1077
|
}
|
|
891
1078
|
|
|
@@ -943,6 +1130,53 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
943
1130
|
return "\(normalized)-\(suffix)"
|
|
944
1131
|
}
|
|
945
1132
|
|
|
1133
|
+
/// Emit a JS lifecycle event. `notifyListeners` marshals to the bridge
|
|
1134
|
+
/// safely from any thread, so no manual dispatch is needed.
|
|
1135
|
+
private func emitEvent(_ name: String, _ data: [String: Any]) {
|
|
1136
|
+
notifyListeners(name, data: data)
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
private func failureEventData(
|
|
1140
|
+
version: String,
|
|
1141
|
+
runtimeVersion: String?,
|
|
1142
|
+
channel: String?,
|
|
1143
|
+
releaseId: String?,
|
|
1144
|
+
reason: String
|
|
1145
|
+
) -> [String: Any] {
|
|
1146
|
+
var data: [String: Any] = [
|
|
1147
|
+
"version": version,
|
|
1148
|
+
"reason": reason,
|
|
1149
|
+
]
|
|
1150
|
+
if let runtimeVersion = trimToNil(runtimeVersion) {
|
|
1151
|
+
data["runtimeVersion"] = runtimeVersion
|
|
1152
|
+
}
|
|
1153
|
+
if let channel = trimToNil(channel) {
|
|
1154
|
+
data["channel"] = channel
|
|
1155
|
+
}
|
|
1156
|
+
if let releaseId = trimToNil(releaseId) {
|
|
1157
|
+
data["releaseId"] = releaseId
|
|
1158
|
+
}
|
|
1159
|
+
return data
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
/// Map a native error to a stable event reason string.
|
|
1163
|
+
private func failureReason(from error: Error) -> String {
|
|
1164
|
+
if error is ZipUtilsError {
|
|
1165
|
+
return "extract_failed"
|
|
1166
|
+
}
|
|
1167
|
+
let description = error.localizedDescription.lowercased()
|
|
1168
|
+
if description.contains("hash mismatch") {
|
|
1169
|
+
return "hash_mismatch"
|
|
1170
|
+
}
|
|
1171
|
+
if description.contains("disk space") {
|
|
1172
|
+
return "insufficient_disk_space"
|
|
1173
|
+
}
|
|
1174
|
+
if description.contains("index.html") {
|
|
1175
|
+
return "invalid_bundle"
|
|
1176
|
+
}
|
|
1177
|
+
return "download_failed"
|
|
1178
|
+
}
|
|
1179
|
+
|
|
946
1180
|
private func sendDeviceEvent(
|
|
947
1181
|
action: DeviceEventAction,
|
|
948
1182
|
bundleVersion: String? = nil,
|
|
@@ -1021,6 +1255,9 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1021
1255
|
if let channel = trimToNil(channel) {
|
|
1022
1256
|
return channel
|
|
1023
1257
|
}
|
|
1258
|
+
if let override = store.getOverrideChannel() {
|
|
1259
|
+
return override
|
|
1260
|
+
}
|
|
1024
1261
|
return self.channel
|
|
1025
1262
|
}
|
|
1026
1263
|
|