@otakit/capacitor-updater 1.0.0 → 1.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 +53 -7
- package/android/src/main/java/com/updatekit/updater/BundleInfo.java +34 -2
- package/android/src/main/java/com/updatekit/updater/BundleStore.java +13 -2
- package/android/src/main/java/com/updatekit/updater/ManifestClient.java +56 -47
- package/android/src/main/java/com/updatekit/updater/ManifestVerifier.java +92 -19
- package/android/src/main/java/com/updatekit/updater/UpdaterPlugin.java +183 -16
- package/dist/esm/definitions.d.ts +13 -3
- package/dist/esm/definitions.d.ts.map +1 -1
- package/ios/Sources/UpdaterPlugin/BundleInfo.swift +5 -0
- package/ios/Sources/UpdaterPlugin/BundleStore.swift +3 -0
- package/ios/Sources/UpdaterPlugin/ManifestClient.swift +57 -34
- package/ios/Sources/UpdaterPlugin/ManifestVerifier.swift +73 -18
- package/ios/Sources/UpdaterPlugin/UpdaterPlugin.swift +209 -16
- package/package.json +1 -1
|
@@ -19,7 +19,7 @@ enum ManifestVerifier {
|
|
|
19
19
|
///
|
|
20
20
|
/// - Parameters:
|
|
21
21
|
/// - appId, channel, platform: Request context (known by plugin).
|
|
22
|
-
/// - version, sha256, size,
|
|
22
|
+
/// - version, sha256, size, runtimeVersion: Response fields.
|
|
23
23
|
/// - signature: The signature object from the manifest response.
|
|
24
24
|
/// - trustedKeys: Array of verification keys configured in the plugin.
|
|
25
25
|
///
|
|
@@ -31,22 +31,10 @@ enum ManifestVerifier {
|
|
|
31
31
|
version: String,
|
|
32
32
|
sha256: String,
|
|
33
33
|
size: Int,
|
|
34
|
-
|
|
34
|
+
runtimeVersion: String?,
|
|
35
35
|
signature: ManifestSignature,
|
|
36
36
|
trustedKeys: [ManifestKey]
|
|
37
37
|
) throws {
|
|
38
|
-
// Check expiry
|
|
39
|
-
let now = Int(Date().timeIntervalSince1970)
|
|
40
|
-
guard signature.exp > now else {
|
|
41
|
-
throw ManifestVerifierError.expired
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Find matching key
|
|
45
|
-
guard let keyEntry = trustedKeys.first(where: { $0.kid == signature.kid }) else {
|
|
46
|
-
throw ManifestVerifierError.unknownKid(signature.kid)
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Build canonical payload (must match server exactly)
|
|
50
38
|
let payload = buildCanonicalPayload(
|
|
51
39
|
appId: appId,
|
|
52
40
|
channel: channel,
|
|
@@ -54,11 +42,53 @@ enum ManifestVerifier {
|
|
|
54
42
|
version: version,
|
|
55
43
|
sha256: sha256,
|
|
56
44
|
size: size,
|
|
57
|
-
|
|
45
|
+
runtimeVersion: runtimeVersion,
|
|
58
46
|
kid: signature.kid,
|
|
59
47
|
iat: signature.iat,
|
|
60
48
|
exp: signature.exp
|
|
61
49
|
)
|
|
50
|
+
try verifyPayload(payload, signature: signature, trustedKeys: trustedKeys)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
static func verifyLegacy(
|
|
54
|
+
appId: String,
|
|
55
|
+
channel: String?,
|
|
56
|
+
platform: String,
|
|
57
|
+
version: String,
|
|
58
|
+
sha256: String,
|
|
59
|
+
size: Int,
|
|
60
|
+
signature: ManifestSignature,
|
|
61
|
+
trustedKeys: [ManifestKey]
|
|
62
|
+
) throws {
|
|
63
|
+
let payload = buildLegacyCanonicalPayload(
|
|
64
|
+
appId: appId,
|
|
65
|
+
channel: channel,
|
|
66
|
+
platform: platform,
|
|
67
|
+
version: version,
|
|
68
|
+
sha256: sha256,
|
|
69
|
+
size: size,
|
|
70
|
+
kid: signature.kid,
|
|
71
|
+
iat: signature.iat,
|
|
72
|
+
exp: signature.exp
|
|
73
|
+
)
|
|
74
|
+
try verifyPayload(payload, signature: signature, trustedKeys: trustedKeys)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private static func verifyPayload(
|
|
78
|
+
_ payload: String,
|
|
79
|
+
signature: ManifestSignature,
|
|
80
|
+
trustedKeys: [ManifestKey]
|
|
81
|
+
) throws {
|
|
82
|
+
// Check expiry
|
|
83
|
+
let now = Int(Date().timeIntervalSince1970)
|
|
84
|
+
guard signature.exp > now else {
|
|
85
|
+
throw ManifestVerifierError.expired
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Find matching key
|
|
89
|
+
guard let keyEntry = trustedKeys.first(where: { $0.kid == signature.kid }) else {
|
|
90
|
+
throw ManifestVerifierError.unknownKid(signature.kid)
|
|
91
|
+
}
|
|
62
92
|
|
|
63
93
|
// Decode base64url signature
|
|
64
94
|
guard let sigData = base64UrlDecode(signature.sig) else {
|
|
@@ -81,12 +111,37 @@ enum ManifestVerifier {
|
|
|
81
111
|
version: String,
|
|
82
112
|
sha256: String,
|
|
83
113
|
size: Int,
|
|
84
|
-
|
|
114
|
+
runtimeVersion: String?,
|
|
115
|
+
kid: String,
|
|
116
|
+
iat: Int,
|
|
117
|
+
exp: Int
|
|
118
|
+
) -> String {
|
|
119
|
+
return [
|
|
120
|
+
"MANIFEST_V2",
|
|
121
|
+
"appId:\(appId)",
|
|
122
|
+
"channel:\(channel ?? "null")",
|
|
123
|
+
"platform:\(platform)",
|
|
124
|
+
"version:\(version)",
|
|
125
|
+
"sha256:\(sha256)",
|
|
126
|
+
"size:\(size)",
|
|
127
|
+
"runtimeVersion:\(runtimeVersion ?? "null")",
|
|
128
|
+
"kid:\(kid)",
|
|
129
|
+
"iat:\(iat)",
|
|
130
|
+
"exp:\(exp)",
|
|
131
|
+
].joined(separator: "\n")
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private static func buildLegacyCanonicalPayload(
|
|
135
|
+
appId: String,
|
|
136
|
+
channel: String?,
|
|
137
|
+
platform: String,
|
|
138
|
+
version: String,
|
|
139
|
+
sha256: String,
|
|
140
|
+
size: Int,
|
|
85
141
|
kid: String,
|
|
86
142
|
iat: Int,
|
|
87
143
|
exp: Int
|
|
88
144
|
) -> String {
|
|
89
|
-
let minBuildStr = minNativeBuild.map { String($0) } ?? "null"
|
|
90
145
|
return [
|
|
91
146
|
"MANIFEST_V1",
|
|
92
147
|
"appId:\(appId)",
|
|
@@ -95,7 +150,7 @@ enum ManifestVerifier {
|
|
|
95
150
|
"version:\(version)",
|
|
96
151
|
"sha256:\(sha256)",
|
|
97
152
|
"size:\(size)",
|
|
98
|
-
"minNativeBuild
|
|
153
|
+
"minNativeBuild:null",
|
|
99
154
|
"kid:\(kid)",
|
|
100
155
|
"iat:\(iat)",
|
|
101
156
|
"exp:\(exp)",
|
|
@@ -7,9 +7,15 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
7
7
|
private enum UpdateMode: String {
|
|
8
8
|
case manual
|
|
9
9
|
case nextLaunch = "next-launch"
|
|
10
|
+
case nextResume = "next-resume"
|
|
10
11
|
case immediate
|
|
11
12
|
}
|
|
12
13
|
|
|
14
|
+
private enum Trigger {
|
|
15
|
+
case launch
|
|
16
|
+
case resume
|
|
17
|
+
}
|
|
18
|
+
|
|
13
19
|
public let identifier = "UpdaterPlugin"
|
|
14
20
|
public let jsName = "OtaKit"
|
|
15
21
|
public let pluginMethods: [CAPPluginMethod] = [
|
|
@@ -37,10 +43,16 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
37
43
|
private var updateUrl = UpdaterPlugin.defaultUpdateURL
|
|
38
44
|
private var appId: String?
|
|
39
45
|
private var channel: String?
|
|
46
|
+
private var runtimeVersion: String?
|
|
40
47
|
private var manifestKeys: [(kid: String, key: Data)] = []
|
|
41
48
|
private var trialTimeoutWorkItem: DispatchWorkItem?
|
|
49
|
+
private var checkIntervalMs: Int = 600_000
|
|
50
|
+
private let isCheckInProgress = NSLock()
|
|
51
|
+
private var checkInProgress = false
|
|
52
|
+
private var foregroundObserver: NSObjectProtocol?
|
|
42
53
|
private static let defaultUpdateURL = "https://www.otakit.app/api/v1"
|
|
43
54
|
private static let apiPathSuffix = "/api/v1"
|
|
55
|
+
private static let lastCheckTimestampKey = "otakit_last_check_timestamp"
|
|
44
56
|
|
|
45
57
|
public override func load() {
|
|
46
58
|
let envUpdateUrl = ProcessInfo.processInfo.environment["OTAKIT_SERVER_URL"]
|
|
@@ -50,11 +62,14 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
50
62
|
)
|
|
51
63
|
appId = getConfig().getString("appId")
|
|
52
64
|
channel = trimToNil(getConfig().getString("channel"))
|
|
65
|
+
runtimeVersion = trimToNil(getConfig().getString("runtimeVersion"))
|
|
66
|
+
store.appRuntimeVersion = runtimeVersion
|
|
53
67
|
allowInsecureUrls = getConfig().getBoolean("allowInsecureUrls", false)
|
|
54
68
|
let updateModeRaw = getConfig().getString("updateMode", UpdateMode.nextLaunch.rawValue)
|
|
55
69
|
updateMode = resolveUpdateMode(configured: updateModeRaw)
|
|
56
70
|
downloader = Downloader(allowInsecureUrls: allowInsecureUrls)
|
|
57
71
|
appReadyTimeoutMs = max(1000, getConfig().getInt("appReadyTimeout", 10_000))
|
|
72
|
+
checkIntervalMs = max(600_000, getConfig().getInt("checkInterval", 600_000))
|
|
58
73
|
|
|
59
74
|
let rawKeysValue = getConfig().getArray("manifestKeys")
|
|
60
75
|
if let rawKeys = rawKeysValue as? [[String: String]] {
|
|
@@ -77,6 +92,8 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
77
92
|
manifestKeys = HostedManifestKeys.defaults
|
|
78
93
|
}
|
|
79
94
|
|
|
95
|
+
pruneIncompatibleBundles()
|
|
96
|
+
|
|
80
97
|
var current = store.getCurrentBundle()
|
|
81
98
|
if current.status == .trial {
|
|
82
99
|
rollbackCurrentBundle(reason: "app_restarted_before_notify")
|
|
@@ -99,33 +116,126 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
99
116
|
}
|
|
100
117
|
|
|
101
118
|
if isAutomaticUpdateMode() {
|
|
102
|
-
|
|
119
|
+
runAutomaticUpdate(trigger: .launch)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if isAutomaticUpdateMode() {
|
|
123
|
+
foregroundObserver = NotificationCenter.default.addObserver(
|
|
124
|
+
forName: UIApplication.willEnterForegroundNotification,
|
|
125
|
+
object: nil,
|
|
126
|
+
queue: .main
|
|
127
|
+
) { [weak self] _ in
|
|
128
|
+
self?.handleAppWillEnterForeground()
|
|
129
|
+
}
|
|
103
130
|
}
|
|
104
131
|
}
|
|
105
132
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
133
|
+
deinit {
|
|
134
|
+
if let observer = foregroundObserver {
|
|
135
|
+
NotificationCenter.default.removeObserver(observer)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private func handleAppWillEnterForeground() {
|
|
140
|
+
runAutomaticUpdate(trigger: .resume)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private func runAutomaticUpdate(trigger: Trigger) {
|
|
144
|
+
// Resume-only guards
|
|
145
|
+
if trigger == .resume {
|
|
146
|
+
if updateMode == .manual { return }
|
|
147
|
+
|
|
148
|
+
// next-resume and immediate: activate staged bundle on resume without server check
|
|
149
|
+
if (updateMode == .nextResume || updateMode == .immediate),
|
|
150
|
+
let stagedId = store.getStagedBundleId(),
|
|
151
|
+
store.getBundle(id: stagedId) != nil {
|
|
152
|
+
activateStagedBundleForReload()
|
|
153
|
+
reloadWebView()
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if shouldThrottleCheck() { return }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Acquire in-flight guard (submit-time)
|
|
161
|
+
guard claimCheckInProgress() else { return }
|
|
162
|
+
|
|
163
|
+
// Immediate mode on cold start: block until check+download+activate completes
|
|
164
|
+
if updateMode == .immediate && trigger == .launch {
|
|
111
165
|
Task {
|
|
112
|
-
|
|
166
|
+
defer { releaseCheckInProgress() }
|
|
167
|
+
do {
|
|
168
|
+
let result = try await performCheckAndDownload(channel: nil, emitEvents: true)
|
|
169
|
+
recordCheckTimestamp()
|
|
170
|
+
if result != nil {
|
|
171
|
+
activateStagedBundleForReload()
|
|
172
|
+
reloadWebView()
|
|
173
|
+
}
|
|
174
|
+
} catch {
|
|
175
|
+
print("[OtaKit] immediate startup update failed: \(error.localizedDescription)")
|
|
176
|
+
}
|
|
113
177
|
}
|
|
114
|
-
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Immediate mode on resume: background check+download, activate when done
|
|
182
|
+
if updateMode == .immediate && trigger == .resume {
|
|
115
183
|
Task {
|
|
184
|
+
defer { releaseCheckInProgress() }
|
|
116
185
|
do {
|
|
117
|
-
let
|
|
118
|
-
|
|
186
|
+
let result = try await performCheckAndDownload(channel: nil, emitEvents: true)
|
|
187
|
+
recordCheckTimestamp()
|
|
188
|
+
if result != nil {
|
|
119
189
|
activateStagedBundleForReload()
|
|
120
190
|
reloadWebView()
|
|
121
191
|
}
|
|
122
192
|
} catch {
|
|
123
|
-
print("[
|
|
193
|
+
print("[OtaKit] immediate resume update failed: \(error.localizedDescription)")
|
|
124
194
|
}
|
|
125
195
|
}
|
|
196
|
+
return
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// next-launch / next-resume: background check+download (fire-and-forget)
|
|
200
|
+
Task {
|
|
201
|
+
defer { releaseCheckInProgress() }
|
|
202
|
+
do {
|
|
203
|
+
_ = try await performCheckAndDownload(channel: nil, emitEvents: true)
|
|
204
|
+
recordCheckTimestamp()
|
|
205
|
+
} catch {
|
|
206
|
+
// Check failed — timestamp not recorded, will retry on next trigger
|
|
207
|
+
}
|
|
126
208
|
}
|
|
127
209
|
}
|
|
128
210
|
|
|
211
|
+
private func shouldThrottleCheck() -> Bool {
|
|
212
|
+
let lastCheck = UserDefaults.standard.double(forKey: UpdaterPlugin.lastCheckTimestampKey)
|
|
213
|
+
guard lastCheck > 0 else { return false }
|
|
214
|
+
let elapsed = Date().timeIntervalSince1970 * 1000 - lastCheck
|
|
215
|
+
return elapsed < Double(checkIntervalMs)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private func recordCheckTimestamp() {
|
|
219
|
+
UserDefaults.standard.set(
|
|
220
|
+
Date().timeIntervalSince1970 * 1000,
|
|
221
|
+
forKey: UpdaterPlugin.lastCheckTimestampKey
|
|
222
|
+
)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private func claimCheckInProgress() -> Bool {
|
|
226
|
+
isCheckInProgress.lock()
|
|
227
|
+
defer { isCheckInProgress.unlock() }
|
|
228
|
+
if checkInProgress { return false }
|
|
229
|
+
checkInProgress = true
|
|
230
|
+
return true
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private func releaseCheckInProgress() {
|
|
234
|
+
isCheckInProgress.lock()
|
|
235
|
+
defer { isCheckInProgress.unlock() }
|
|
236
|
+
checkInProgress = false
|
|
237
|
+
}
|
|
238
|
+
|
|
129
239
|
private func shouldActivateStagedOnLaunch() -> Bool {
|
|
130
240
|
updateMode != .manual
|
|
131
241
|
}
|
|
@@ -141,10 +251,12 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
141
251
|
return .nextLaunch
|
|
142
252
|
case UpdateMode.manual.rawValue:
|
|
143
253
|
return .manual
|
|
254
|
+
case UpdateMode.nextResume.rawValue:
|
|
255
|
+
return .nextResume
|
|
144
256
|
case UpdateMode.immediate.rawValue:
|
|
145
257
|
return .immediate
|
|
146
258
|
default:
|
|
147
|
-
print("[
|
|
259
|
+
print("[OtaKit] Unknown updateMode '\(raw)', defaulting to 'next-launch'")
|
|
148
260
|
return .nextLaunch
|
|
149
261
|
}
|
|
150
262
|
}
|
|
@@ -189,10 +301,36 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
189
301
|
}
|
|
190
302
|
|
|
191
303
|
@objc func check(_ call: CAPPluginCall) {
|
|
304
|
+
// Respect throttle — return staged bundle as a LatestVersion if available, else null
|
|
305
|
+
if shouldThrottleCheck() || !claimCheckInProgress() {
|
|
306
|
+
if let stagedId = store.getStagedBundleId(),
|
|
307
|
+
let staged = store.getBundle(id: stagedId) {
|
|
308
|
+
var payload: [String: Any] = [
|
|
309
|
+
"version": staged.version,
|
|
310
|
+
"url": "",
|
|
311
|
+
"sha256": staged.sha256 ?? "",
|
|
312
|
+
"size": 0,
|
|
313
|
+
"downloaded": true,
|
|
314
|
+
]
|
|
315
|
+
if let runtimeVersion = staged.runtimeVersion {
|
|
316
|
+
payload["runtimeVersion"] = runtimeVersion
|
|
317
|
+
}
|
|
318
|
+
if let releaseId = staged.releaseId {
|
|
319
|
+
payload["releaseId"] = releaseId
|
|
320
|
+
}
|
|
321
|
+
call.resolve(payload)
|
|
322
|
+
} else {
|
|
323
|
+
call.resolve()
|
|
324
|
+
}
|
|
325
|
+
return
|
|
326
|
+
}
|
|
192
327
|
let targetChannel = resolveTargetChannel(nil)
|
|
193
328
|
Task {
|
|
329
|
+
defer { releaseCheckInProgress() }
|
|
194
330
|
do {
|
|
195
331
|
let latest = try await fetchLatest(channel: targetChannel)
|
|
332
|
+
// check() does NOT record timestamp — only download() and automatic paths do.
|
|
333
|
+
// This keeps check() -> download() working without the throttle blocking download().
|
|
196
334
|
if let latest {
|
|
197
335
|
let staged = findMatchingStagedBundle(latest: latest, targetChannel: targetChannel)
|
|
198
336
|
call.resolve(manifestToDictionary(latest, downloaded: staged != nil))
|
|
@@ -225,9 +363,21 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
225
363
|
}
|
|
226
364
|
|
|
227
365
|
@objc func download(_ call: CAPPluginCall) {
|
|
366
|
+
// Respect throttle — return staged info if available, else null
|
|
367
|
+
if shouldThrottleCheck() || !claimCheckInProgress() {
|
|
368
|
+
if let stagedId = store.getStagedBundleId(),
|
|
369
|
+
let staged = store.getBundle(id: stagedId) {
|
|
370
|
+
call.resolve(staged.toDictionary())
|
|
371
|
+
} else {
|
|
372
|
+
call.resolve()
|
|
373
|
+
}
|
|
374
|
+
return
|
|
375
|
+
}
|
|
228
376
|
Task {
|
|
377
|
+
defer { releaseCheckInProgress() }
|
|
229
378
|
do {
|
|
230
379
|
let bundle = try await performCheckAndDownload(channel: nil, emitEvents: true)
|
|
380
|
+
recordCheckTimestamp()
|
|
231
381
|
if let bundle {
|
|
232
382
|
call.resolve(bundle.toDictionary())
|
|
233
383
|
} else {
|
|
@@ -361,7 +511,7 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
361
511
|
channel: channel,
|
|
362
512
|
currentVersion: current.version,
|
|
363
513
|
currentReleaseId: current.releaseId,
|
|
364
|
-
|
|
514
|
+
runtimeVersion: runtimeVersion,
|
|
365
515
|
platform: "ios",
|
|
366
516
|
allowInsecureUrls: allowInsecureUrls,
|
|
367
517
|
manifestKeys: manifestKeys.map {
|
|
@@ -384,6 +534,14 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
384
534
|
return nil
|
|
385
535
|
}
|
|
386
536
|
|
|
537
|
+
guard isCompatibleRuntime(manifest.runtimeVersion) else {
|
|
538
|
+
throw NSError(
|
|
539
|
+
domain: "OtaKit",
|
|
540
|
+
code: 1,
|
|
541
|
+
userInfo: [NSLocalizedDescriptionKey: "Manifest runtimeVersion does not match the installed app runtime"]
|
|
542
|
+
)
|
|
543
|
+
}
|
|
544
|
+
|
|
387
545
|
let staged = findMatchingStagedBundle(latest: manifest, targetChannel: targetChannel)
|
|
388
546
|
if emitEvents {
|
|
389
547
|
notifyListeners("updateAvailable", data: manifestToDictionary(manifest, downloaded: staged != nil))
|
|
@@ -407,6 +565,7 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
407
565
|
version: manifest.version,
|
|
408
566
|
expectedSha256: manifest.sha256,
|
|
409
567
|
expectedSize: manifest.size,
|
|
568
|
+
runtimeVersion: manifest.runtimeVersion,
|
|
410
569
|
channel: targetChannel,
|
|
411
570
|
releaseId: manifest.releaseId
|
|
412
571
|
)
|
|
@@ -426,6 +585,7 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
426
585
|
version: manifest.version,
|
|
427
586
|
expectedSha256: manifest.sha256,
|
|
428
587
|
expectedSize: manifest.size,
|
|
588
|
+
runtimeVersion: manifest.runtimeVersion,
|
|
429
589
|
channel: targetChannel,
|
|
430
590
|
releaseId: manifest.releaseId
|
|
431
591
|
)
|
|
@@ -446,6 +606,7 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
446
606
|
version: String,
|
|
447
607
|
expectedSha256: String,
|
|
448
608
|
expectedSize: Int? = nil,
|
|
609
|
+
runtimeVersion: String? = nil,
|
|
449
610
|
channel: String? = nil,
|
|
450
611
|
releaseId: String? = nil
|
|
451
612
|
) async throws -> BundleInfo {
|
|
@@ -510,6 +671,7 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
510
671
|
let info = BundleInfo(
|
|
511
672
|
id: bundleId,
|
|
512
673
|
version: version,
|
|
674
|
+
runtimeVersion: runtimeVersion,
|
|
513
675
|
status: .pending,
|
|
514
676
|
downloadedAt: Date(),
|
|
515
677
|
sha256: expectedSha256,
|
|
@@ -581,6 +743,10 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
581
743
|
store.setStagedBundleId(nil)
|
|
582
744
|
return store.getCurrentBundle()
|
|
583
745
|
}
|
|
746
|
+
guard isCompatibleRuntime(staged) else {
|
|
747
|
+
try? store.deleteBundle(id: staged.id)
|
|
748
|
+
return store.getCurrentBundle()
|
|
749
|
+
}
|
|
584
750
|
|
|
585
751
|
store.setCurrentBundleId(staged.id)
|
|
586
752
|
store.setStagedBundleId(nil)
|
|
@@ -595,6 +761,10 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
595
761
|
store.setStagedBundleId(nil)
|
|
596
762
|
return
|
|
597
763
|
}
|
|
764
|
+
guard isCompatibleRuntime(staged) else {
|
|
765
|
+
try? store.deleteBundle(id: staged.id)
|
|
766
|
+
return
|
|
767
|
+
}
|
|
598
768
|
|
|
599
769
|
store.setCurrentBundleId(staged.id)
|
|
600
770
|
store.setStagedBundleId(nil)
|
|
@@ -653,6 +823,7 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
653
823
|
let failed = BundleInfo(
|
|
654
824
|
id: current.id,
|
|
655
825
|
version: current.version,
|
|
826
|
+
runtimeVersion: current.runtimeVersion,
|
|
656
827
|
status: .error,
|
|
657
828
|
downloadedAt: current.downloadedAt,
|
|
658
829
|
sha256: current.sha256,
|
|
@@ -759,12 +930,12 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
759
930
|
"size": latest.size,
|
|
760
931
|
"downloaded": downloaded,
|
|
761
932
|
]
|
|
933
|
+
if let runtimeVersion = latest.runtimeVersion {
|
|
934
|
+
payload["runtimeVersion"] = runtimeVersion
|
|
935
|
+
}
|
|
762
936
|
if let releaseId = latest.releaseId {
|
|
763
937
|
payload["releaseId"] = releaseId
|
|
764
938
|
}
|
|
765
|
-
if let minNativeBuild = latest.minNativeBuild {
|
|
766
|
-
payload["minNativeBuild"] = minNativeBuild
|
|
767
|
-
}
|
|
768
939
|
return payload
|
|
769
940
|
}
|
|
770
941
|
|
|
@@ -784,6 +955,10 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
784
955
|
return nil
|
|
785
956
|
}
|
|
786
957
|
|
|
958
|
+
if trimToNil(staged.runtimeVersion) != trimToNil(latest.runtimeVersion) {
|
|
959
|
+
return nil
|
|
960
|
+
}
|
|
961
|
+
|
|
787
962
|
if let latestReleaseId = latest.releaseId,
|
|
788
963
|
let stagedReleaseId = staged.releaseId,
|
|
789
964
|
latestReleaseId == stagedReleaseId {
|
|
@@ -798,6 +973,24 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
798
973
|
return nil
|
|
799
974
|
}
|
|
800
975
|
|
|
976
|
+
private func pruneIncompatibleBundles() {
|
|
977
|
+
for bundle in store.listDownloadedBundles() where !isCompatibleRuntime(bundle) {
|
|
978
|
+
try? store.deleteBundle(id: bundle.id)
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
if let failed = store.getFailedBundle(), !isCompatibleRuntime(failed) {
|
|
982
|
+
store.setFailedBundle(nil)
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
private func isCompatibleRuntime(_ runtimeVersion: String?) -> Bool {
|
|
987
|
+
trimToNil(runtimeVersion) == self.runtimeVersion
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
private func isCompatibleRuntime(_ bundle: BundleInfo) -> Bool {
|
|
991
|
+
isCompatibleRuntime(bundle.runtimeVersion)
|
|
992
|
+
}
|
|
993
|
+
|
|
801
994
|
private func buildBundleId(from version: String) -> String {
|
|
802
995
|
let trimmed = version.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
803
996
|
let allowed = CharacterSet(
|