@otakit/capacitor-updater 2.3.2 → 3.0.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.
Files changed (57) hide show
  1. package/README.md +46 -5
  2. package/android/build.gradle +16 -0
  3. package/android/src/main/java/com/otakit/updater/BundleCrypto.java +38 -17
  4. package/android/src/main/java/com/otakit/updater/BundleInfo.java +10 -0
  5. package/android/src/main/java/com/otakit/updater/BundleStore.java +134 -86
  6. package/android/src/main/java/com/otakit/updater/CheckFailure.java +53 -0
  7. package/android/src/main/java/com/otakit/updater/DeltaAssembler.java +46 -41
  8. package/android/src/main/java/com/otakit/updater/DeviceEventClient.java +125 -26
  9. package/android/src/main/java/com/otakit/updater/DocumentReadyBridge.java +71 -0
  10. package/android/src/main/java/com/otakit/updater/DownloadRetry.java +144 -0
  11. package/android/src/main/java/com/otakit/updater/EventOutbox.java +174 -0
  12. package/android/src/main/java/com/otakit/updater/FileDownloader.java +80 -0
  13. package/android/src/main/java/com/otakit/updater/ForegroundDeadline.java +84 -0
  14. package/android/src/main/java/com/otakit/updater/HashUtils.java +26 -0
  15. package/android/src/main/java/com/otakit/updater/ManifestClient.java +12 -9
  16. package/android/src/main/java/com/otakit/updater/ManifestKeyConfig.java +47 -0
  17. package/android/src/main/java/com/otakit/updater/ManifestVerifier.java +22 -4
  18. package/android/src/main/java/com/otakit/updater/SDKVersion.java +9 -0
  19. package/android/src/main/java/com/otakit/updater/UpdateOwner.java +27 -0
  20. package/android/src/main/java/com/otakit/updater/UpdaterCoordinator.java +154 -59
  21. package/android/src/main/java/com/otakit/updater/UpdaterPlugin.java +284 -209
  22. package/android/src/main/java/com/otakit/updater/WebViewActivation.java +26 -0
  23. package/dist/esm/definitions.d.ts +3 -3
  24. package/dist/esm/definitions.d.ts.map +1 -1
  25. package/ios/Sources/UpdaterPlugin/BundleCrypto.swift +35 -3
  26. package/ios/Sources/UpdaterPlugin/BundleInfo.swift +13 -0
  27. package/ios/Sources/UpdaterPlugin/BundleStore.swift +103 -77
  28. package/ios/Sources/UpdaterPlugin/CheckFailure.swift +41 -0
  29. package/ios/Sources/UpdaterPlugin/DeltaAssembler.swift +29 -17
  30. package/ios/Sources/UpdaterPlugin/DeviceEventClient.swift +12 -10
  31. package/ios/Sources/UpdaterPlugin/DocumentReadyBridge.swift +43 -0
  32. package/ios/Sources/UpdaterPlugin/DownloadRetry.swift +51 -0
  33. package/ios/Sources/UpdaterPlugin/Downloader.swift +97 -39
  34. package/ios/Sources/UpdaterPlugin/EventOutbox.swift +183 -0
  35. package/ios/Sources/UpdaterPlugin/ForegroundDeadline.swift +94 -0
  36. package/ios/Sources/UpdaterPlugin/HashUtils.swift +16 -0
  37. package/ios/Sources/UpdaterPlugin/ManifestClient.swift +2 -4
  38. package/ios/Sources/UpdaterPlugin/ManifestKeyConfig.swift +20 -0
  39. package/ios/Sources/UpdaterPlugin/ManifestVerifier.swift +7 -1
  40. package/ios/Sources/UpdaterPlugin/SDKVersion.swift +4 -0
  41. package/ios/Sources/UpdaterPlugin/UpdateOwner.swift +12 -0
  42. package/ios/Sources/UpdaterPlugin/UpdaterCoordinator.swift +146 -110
  43. package/ios/Sources/UpdaterPlugin/UpdaterPlugin.swift +206 -202
  44. package/ios/Tests/UpdaterPluginTests/BundleCryptoTests.swift +80 -0
  45. package/ios/Tests/UpdaterPluginTests/BundlePersistenceTests.swift +102 -0
  46. package/ios/Tests/UpdaterPluginTests/CheckFailureTests.swift +55 -0
  47. package/ios/Tests/UpdaterPluginTests/DeltaCacheIntegrityTests.swift +69 -0
  48. package/ios/Tests/UpdaterPluginTests/DocumentReadyBridgeTests.swift +102 -0
  49. package/ios/Tests/UpdaterPluginTests/DownloadIntegrityTests.swift +40 -0
  50. package/ios/Tests/UpdaterPluginTests/DownloadRetryTests.swift +189 -0
  51. package/ios/Tests/UpdaterPluginTests/EventDeliveryTests.swift +56 -0
  52. package/ios/Tests/UpdaterPluginTests/EventOutboxTests.swift +86 -0
  53. package/ios/Tests/UpdaterPluginTests/ForegroundDeadlineTests.swift +144 -0
  54. package/ios/Tests/UpdaterPluginTests/ManifestKeyConfigTests.swift +48 -0
  55. package/ios/Tests/UpdaterPluginTests/UpdateOwnerTests.swift +34 -0
  56. package/ios/Tests/UpdaterPluginTests/UpdaterCoordinatorTests.swift +394 -0
  57. package/package.json +7 -3
@@ -0,0 +1,26 @@
1
+ package com.otakit.updater;
2
+
3
+ final class WebViewActivation {
4
+
5
+ @FunctionalInterface
6
+ interface PathActivator {
7
+ void activate(String path);
8
+ }
9
+
10
+ private WebViewActivation() {}
11
+
12
+ static void activate(
13
+ String path,
14
+ String builtinAssetPath,
15
+ PathActivator setServerBasePath,
16
+ PathActivator setServerAssetPath
17
+ ) {
18
+ // Capacitor's Android setters both switch the hosted files and enqueue the
19
+ // WebView navigation. Calling WebView.reload() as well causes two navigations.
20
+ if (path == null || path.isEmpty()) {
21
+ setServerAssetPath.activate(builtinAssetPath);
22
+ } else {
23
+ setServerBasePath.activate(path);
24
+ }
25
+ }
26
+ }
@@ -15,7 +15,7 @@ export declare enum BundleStatus {
15
15
  ERROR = "error"
16
16
  }
17
17
  export interface BundleInfo {
18
- /** Unique bundle identifier */
18
+ /** Opaque local installation identifier. Separate downloads can have different IDs for the same release. */
19
19
  id: string;
20
20
  /** Semantic version string */
21
21
  version: string;
@@ -154,7 +154,7 @@ export interface OtaKitConfig {
154
154
  * Set to 0 or a negative value to disable resume throttling.
155
155
  */
156
156
  checkInterval?: number;
157
- /** Milliseconds to wait for notifyAppReady(). Defaults to 10000. */
157
+ /** Foreground milliseconds to wait for notifyAppReady(). Pauses while inactive/backgrounded. Defaults to 10000. */
158
158
  appReadyTimeout?: number;
159
159
  /** Custom event ingest base URL. Hosted default: https://ingest.otakit.app/v1 */
160
160
  ingestUrl?: string;
@@ -205,7 +205,7 @@ export interface OtaKitPlugin {
205
205
  update(): Promise<void>;
206
206
  /**
207
207
  * **CRITICAL**: Call this when your app has successfully started.
208
- * Must be called within appReadyTimeout (default 10s) or rollback occurs.
208
+ * Must be called within appReadyTimeout of foreground time (default 10s) or rollback occurs.
209
209
  */
210
210
  notifyAppReady(): Promise<void>;
211
211
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"definitions.d.ts","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"definitions.d.ts","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"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"}
@@ -1,10 +1,12 @@
1
1
  import CryptoKit
2
2
  import Foundation
3
+ import os
3
4
 
4
5
  enum BundleCryptoError: Error, LocalizedError {
5
6
  case noMatchingKey(String)
6
7
  case invalidParameter(String)
7
8
  case decryptionFailed(String)
9
+ case insufficientMemory(encryptedBytes: Int64, budgetBytes: Int64)
8
10
 
9
11
  var errorDescription: String? {
10
12
  switch self {
@@ -14,6 +16,8 @@ enum BundleCryptoError: Error, LocalizedError {
14
16
  return "invalid bundle encryption parameter: \(name)"
15
17
  case let .decryptionFailed(detail):
16
18
  return "bundle decryption failed: \(detail)"
19
+ case let .insufficientMemory(encryptedBytes, budgetBytes):
20
+ return "insufficient_memory_for_encrypted_bundle; encryptedBytes=\(encryptedBytes); budgetBytes=\(budgetBytes)"
17
21
  }
18
22
  }
19
23
  }
@@ -25,6 +29,25 @@ enum BundleCryptoError: Error, LocalizedError {
25
29
  enum BundleCrypto {
26
30
  private static let tagLength = 16
27
31
  private static let keyLength = 32
32
+ private static let reserveBytes: Int64 = 32 * 1024 * 1024
33
+ private static let maximumEncryptedBytes: Int64 = 128 * 1024 * 1024
34
+
35
+ static func decryptionMemoryEstimate() -> Int64 {
36
+ #if targetEnvironment(simulator)
37
+ // Simulator apps can report zero from os_proc_available_memory(). Host RAM
38
+ // is only an estimate; keep the same reserve, multiplier, and hard cap.
39
+ return Int64(clamping: ProcessInfo.processInfo.physicalMemory)
40
+ #else
41
+ return Int64(os_proc_available_memory())
42
+ #endif
43
+ }
44
+
45
+ static func requireMemoryBudget(encryptedBytes: Int64, availableBytes: Int64) throws {
46
+ let budget = availableBytes <= reserveBytes ? 0 : min(maximumEncryptedBytes, (availableBytes - reserveBytes) / 4)
47
+ guard encryptedBytes <= budget else {
48
+ throw BundleCryptoError.insufficientMemory(encryptedBytes: encryptedBytes, budgetBytes: budget)
49
+ }
50
+ }
28
51
 
29
52
  static func unwrapDek(
30
53
  kek: Data,
@@ -63,16 +86,25 @@ enum BundleCrypto {
63
86
  dek: Data,
64
87
  nonceB64: String,
65
88
  input: URL,
66
- output: URL
89
+ output: URL,
90
+ availableBytes: Int64 = BundleCrypto.decryptionMemoryEstimate()
67
91
  ) throws {
68
92
  guard let nonce = Data(base64Encoded: nonceB64) else {
69
93
  throw BundleCryptoError.invalidParameter("nonce")
70
94
  }
71
95
 
72
- let ciphertextWithTag = try Data(contentsOf: input)
73
- guard ciphertextWithTag.count > tagLength else {
96
+ guard dek.count == keyLength, nonce.count == 12 else {
97
+ throw BundleCryptoError.invalidParameter("key or nonce length")
98
+ }
99
+ let attributes = try FileManager.default.attributesOfItem(atPath: input.path)
100
+ guard let size = attributes[.size] as? NSNumber, size.int64Value > tagLength else {
74
101
  throw BundleCryptoError.invalidParameter("ciphertext too short")
75
102
  }
103
+ try requireMemoryBudget(encryptedBytes: size.int64Value, availableBytes: availableBytes)
104
+ let ciphertextWithTag = try Data(contentsOf: input, options: .mappedIfSafe)
105
+ guard ciphertextWithTag.count == size.intValue else {
106
+ throw BundleCryptoError.invalidParameter("ciphertext changed during read")
107
+ }
76
108
 
77
109
  do {
78
110
  let sealedBox = try AES.GCM.SealedBox(
@@ -15,6 +15,19 @@ struct BundleInfo: Codable {
15
15
  id == "builtin"
16
16
  }
17
17
 
18
+ var attemptId: String? {
19
+ guard id.hasPrefix("bundle-"), UUID(uuidString: String(id.dropFirst(7))) != nil else { return nil }
20
+ return id
21
+ }
22
+
23
+ func withStatus(_ status: BundleStatus) -> BundleInfo {
24
+ BundleInfo(
25
+ id: id, version: version, runtimeVersion: runtimeVersion, status: status,
26
+ downloadedAt: downloadedAt, sha256: sha256, path: path, channel: channel,
27
+ releaseId: releaseId
28
+ )
29
+ }
30
+
18
31
  func toDictionary() -> [String: Any] {
19
32
  var result: [String: Any] = [
20
33
  "id": id,
@@ -1,6 +1,9 @@
1
1
  import Foundation
2
2
 
3
3
  final class BundleStore {
4
+ /// Release identity is metadata; an installation must never reuse a live directory.
5
+ static func newInstallationId() -> String { "bundle-\(UUID().uuidString.lowercased())" }
6
+
4
7
  private enum Keys {
5
8
  static let currentBundleId = "otakit_current_bundle_id"
6
9
  static let fallbackBundleId = "otakit_fallback_bundle_id"
@@ -10,28 +13,33 @@ final class BundleStore {
10
13
  static let overrideChannel = "otakit_override_channel"
11
14
  }
12
15
 
13
- private let defaults = UserDefaults.standard
16
+ private let defaults: UserDefaults
17
+ private let rootDirectory: URL
14
18
  private let fileManager = FileManager.default
15
19
  var appRuntimeVersion: String?
16
20
 
17
- private lazy var decoder: JSONDecoder = {
21
+ init(defaults: UserDefaults = .standard, rootDirectory: URL? = nil) {
22
+ self.defaults = defaults
23
+ self.rootDirectory = rootDirectory ?? FileManager.default.urls(
24
+ for: .applicationSupportDirectory,
25
+ in: .userDomainMask
26
+ )[0]
27
+ }
28
+
29
+ private var decoder: JSONDecoder {
18
30
  let decoder = JSONDecoder()
19
31
  decoder.dateDecodingStrategy = .iso8601
20
32
  return decoder
21
- }()
33
+ }
22
34
 
23
- private lazy var encoder: JSONEncoder = {
35
+ private var encoder: JSONEncoder {
24
36
  let encoder = JSONEncoder()
25
37
  encoder.dateEncodingStrategy = .iso8601
26
38
  return encoder
27
- }()
39
+ }
28
40
 
29
41
  private(set) lazy var bundlesDirectory: URL = {
30
- let appSupport = fileManager.urls(
31
- for: .applicationSupportDirectory,
32
- in: .userDomainMask
33
- ).first!
34
- let directory = appSupport.appendingPathComponent(
42
+ let directory = rootDirectory.appendingPathComponent(
35
43
  "otakit_bundles",
36
44
  isDirectory: true
37
45
  )
@@ -44,11 +52,7 @@ final class BundleStore {
44
52
 
45
53
  /// Content-addressed file cache for the deltas strategy (`otakit_files/<sha256>`).
46
54
  private(set) lazy var filesCacheDirectory: URL = {
47
- let appSupport = fileManager.urls(
48
- for: .applicationSupportDirectory,
49
- in: .userDomainMask
50
- ).first!
51
- let directory = appSupport.appendingPathComponent(
55
+ let directory = rootDirectory.appendingPathComponent(
52
56
  "otakit_files",
53
57
  isDirectory: true
54
58
  )
@@ -151,79 +155,106 @@ final class BundleStore {
151
155
  return bundles
152
156
  }
153
157
 
154
- func getCurrentBundle() -> BundleInfo {
155
- guard
156
- let bundleId = defaults.string(forKey: Keys.currentBundleId),
157
- let bundle = getBundle(id: bundleId)
158
- else {
159
- return builtinBundle()
158
+ private struct CoreState: Codable {
159
+ var version = 1
160
+ var currentId: String?
161
+ var fallbackId: String?
162
+ var stagedId: String?
163
+ var lastFailed: BundleInfo?
164
+ }
165
+
166
+ private var stateURL: URL { rootDirectory.appendingPathComponent("otakit-state.json") }
167
+ private let persistenceLock = NSRecursiveLock()
168
+
169
+ private func readCoreState() throws -> CoreState {
170
+ persistenceLock.lock()
171
+ defer { persistenceLock.unlock() }
172
+ if fileManager.fileExists(atPath: stateURL.path) {
173
+ let state = try decoder.decode(CoreState.self, from: Data(contentsOf: stateURL))
174
+ guard state.version == 1 else {
175
+ throw CocoaError(.coderReadCorrupt)
176
+ }
177
+ return state
160
178
  }
161
- return bundle
179
+ // Existing installations migrate on their first successful state write.
180
+ let failed = defaults.data(forKey: Keys.lastFailedBundleInfo)
181
+ .flatMap { try? decoder.decode(BundleInfo.self, from: $0) }
182
+ return CoreState(
183
+ currentId: defaults.string(forKey: Keys.currentBundleId),
184
+ fallbackId: defaults.string(forKey: Keys.fallbackBundleId),
185
+ stagedId: defaults.string(forKey: Keys.stagedBundleId),
186
+ lastFailed: failed
187
+ )
162
188
  }
163
189
 
164
- func getCurrentBundleId() -> String? {
165
- defaults.string(forKey: Keys.currentBundleId)
190
+ private func writeCoreState(_ state: CoreState) throws {
191
+ try fileManager.createDirectory(at: rootDirectory, withIntermediateDirectories: true)
192
+ try encoder.encode(state).write(to: stateURL, options: .atomic)
166
193
  }
167
194
 
168
- func setCurrentBundleId(_ id: String?) {
169
- if let id {
170
- defaults.set(id, forKey: Keys.currentBundleId)
171
- } else {
172
- defaults.removeObject(forKey: Keys.currentBundleId)
173
- }
195
+ private func mutateCoreState(_ mutation: (inout CoreState) -> Void) throws {
196
+ persistenceLock.lock()
197
+ defer { persistenceLock.unlock() }
198
+ var state = try readCoreState()
199
+ mutation(&state)
200
+ try writeCoreState(state)
174
201
  }
175
202
 
176
- func getFallbackBundle() -> BundleInfo {
177
- guard
178
- let bundleId = defaults.string(forKey: Keys.fallbackBundleId),
179
- let bundle = getBundle(id: bundleId)
180
- else {
203
+ func setCoreState(
204
+ currentId: String?, fallbackId: String?, stagedId: String?, lastFailed: BundleInfo?
205
+ ) throws {
206
+ persistenceLock.lock()
207
+ defer { persistenceLock.unlock() }
208
+ // Validate an existing state before replacing it; corruption must fail closed.
209
+ _ = try readCoreState()
210
+ try writeCoreState(CoreState(
211
+ currentId: currentId, fallbackId: fallbackId, stagedId: stagedId, lastFailed: lastFailed
212
+ ))
213
+ }
214
+
215
+ func protectedBundleIds() throws -> Set<String> {
216
+ let state = try readCoreState()
217
+ return Set([state.currentId, state.fallbackId, state.stagedId].compactMap { $0 })
218
+ }
219
+
220
+ func getCurrentBundle() -> BundleInfo {
221
+ guard let id = getCurrentBundleId(), let bundle = getBundle(id: id) else {
181
222
  return builtinBundle()
182
223
  }
183
224
  return bundle
184
225
  }
185
226
 
186
- func getFallbackBundleId() -> String? {
187
- defaults.string(forKey: Keys.fallbackBundleId)
227
+ func getCurrentBundleId() -> String? { (try? readCoreState())?.currentId }
228
+
229
+ func setCurrentBundleId(_ id: String?) throws {
230
+ try mutateCoreState { $0.currentId = id }
188
231
  }
189
232
 
190
- func setFallbackBundleId(_ id: String?) {
191
- if let id {
192
- defaults.set(id, forKey: Keys.fallbackBundleId)
193
- } else {
194
- defaults.removeObject(forKey: Keys.fallbackBundleId)
233
+ func getFallbackBundle() -> BundleInfo {
234
+ guard let id = getFallbackBundleId(), let bundle = getBundle(id: id) else {
235
+ return builtinBundle()
195
236
  }
237
+ return bundle
196
238
  }
197
239
 
198
- func getStagedBundleId() -> String? {
199
- defaults.string(forKey: Keys.stagedBundleId)
200
- }
240
+ func getFallbackBundleId() -> String? { (try? readCoreState())?.fallbackId }
201
241
 
202
- func setStagedBundleId(_ id: String?) {
203
- if let id {
204
- defaults.set(id, forKey: Keys.stagedBundleId)
205
- } else {
206
- defaults.removeObject(forKey: Keys.stagedBundleId)
207
- }
242
+ func setFallbackBundleId(_ id: String?) throws {
243
+ try mutateCoreState { $0.fallbackId = id }
208
244
  }
209
245
 
210
- func setLastFailedBundle(_ bundle: BundleInfo?) {
211
- if let bundle {
212
- if let data = try? encoder.encode(bundle) {
213
- defaults.set(data, forKey: Keys.lastFailedBundleInfo)
214
- }
215
- } else {
216
- defaults.removeObject(forKey: Keys.lastFailedBundleInfo)
217
- }
246
+ func getStagedBundleId() -> String? { (try? readCoreState())?.stagedId }
247
+
248
+ func setStagedBundleId(_ id: String?) throws {
249
+ try mutateCoreState { $0.stagedId = id }
218
250
  }
219
251
 
220
- func getLastFailedBundle() -> BundleInfo? {
221
- guard let data = defaults.data(forKey: Keys.lastFailedBundleInfo) else {
222
- return nil
223
- }
224
- return try? decoder.decode(BundleInfo.self, from: data)
252
+ func setLastFailedBundle(_ bundle: BundleInfo?) throws {
253
+ try mutateCoreState { $0.lastFailed = bundle }
225
254
  }
226
255
 
256
+ func getLastFailedBundle() -> BundleInfo? { (try? readCoreState())?.lastFailed }
257
+
227
258
  func getOverrideChannel() -> String? {
228
259
  defaults.string(forKey: Keys.overrideChannel)
229
260
  }
@@ -249,7 +280,7 @@ final class BundleStore {
249
280
  }
250
281
  }
251
282
 
252
- func markStatus(bundleId: String, status: BundleStatus) {
283
+ func markStatus(bundleId: String, status: BundleStatus) throws {
253
284
  guard var bundle = getBundle(id: bundleId) else {
254
285
  return
255
286
  }
@@ -264,7 +295,7 @@ final class BundleStore {
264
295
  channel: bundle.channel,
265
296
  releaseId: bundle.releaseId
266
297
  )
267
- try? saveBundle(bundle)
298
+ try saveBundle(bundle)
268
299
  }
269
300
 
270
301
  func deleteBundle(id: String) throws {
@@ -272,17 +303,12 @@ final class BundleStore {
272
303
  return
273
304
  }
274
305
 
306
+ try setCoreState(
307
+ currentId: getCurrentBundleId() == id ? nil : getCurrentBundleId(),
308
+ fallbackId: getFallbackBundleId() == id ? nil : getFallbackBundleId(),
309
+ stagedId: getStagedBundleId() == id ? nil : getStagedBundleId(),
310
+ lastFailed: getLastFailedBundle()
311
+ )
275
312
  try fileManager.removeItem(at: bundleDirectory(for: id))
276
-
277
- if getStagedBundleId() == id {
278
- setStagedBundleId(nil)
279
- }
280
- if defaults.string(forKey: Keys.currentBundleId) == id {
281
- setCurrentBundleId(nil)
282
- }
283
- if defaults.string(forKey: Keys.fallbackBundleId) == id {
284
- setFallbackBundleId(nil)
285
- }
286
313
  }
287
-
288
314
  }
@@ -0,0 +1,41 @@
1
+ import Foundation
2
+
3
+ /// Bounded diagnostics: never include manifest bodies, URLs, signatures, or key IDs.
4
+ struct CheckFailure {
5
+ let phase: String
6
+ let detail: String
7
+
8
+ static func from(_ error: Error) -> CheckFailure? {
9
+ if error is CancellationError { return nil }
10
+ if let verifier = error as? ManifestVerifierError {
11
+ let detail: String
12
+ switch verifier {
13
+ case .expired: detail = "signature_expired"
14
+ case .unknownKid: detail = "signature_unknown_key"
15
+ case .invalidSignature: detail = "signature_invalid"
16
+ case .missingSignature: detail = "signature_missing"
17
+ }
18
+ return CheckFailure(phase: "signature", detail: detail)
19
+ }
20
+ if let manifest = error as? ManifestClientError {
21
+ if case let .httpStatus(status) = manifest {
22
+ return CheckFailure(phase: "check", detail: "manifest_http_\(status)")
23
+ }
24
+ return CheckFailure(phase: "check", detail: "manifest_invalid")
25
+ }
26
+ let failure = error as NSError
27
+ if failure.domain == NSURLErrorDomain {
28
+ if failure.code == NSURLErrorCancelled { return nil }
29
+ return CheckFailure(phase: "check", detail: "manifest_network_\(failure.code)")
30
+ }
31
+ return CheckFailure(phase: "check", detail: "manifest_check_failed")
32
+ }
33
+
34
+ static func observe<T>(_ operation: () async throws -> T, report: (CheckFailure) throws -> Void) async throws -> T {
35
+ do { return try await operation() }
36
+ catch {
37
+ if let failure = from(error) { try report(failure) }
38
+ throw error
39
+ }
40
+ }
41
+ }
@@ -47,14 +47,14 @@ final class DeltaAssembler {
47
47
  private let maxTotalSize: UInt64 = 500_000_000 // 500 MB
48
48
 
49
49
  private let cacheDirectory: URL
50
- private let downloader: Downloader
50
+ private let download: (URL) async throws -> URL
51
51
  private let fileManager = FileManager.default
52
52
 
53
53
  private static let builtinSeedMarkerName = "builtin_seed.json"
54
54
 
55
- init(cacheDirectory: URL, downloader: Downloader) {
55
+ init(cacheDirectory: URL, downloader: Downloader, fetch: ((URL) async throws -> URL)? = nil) {
56
56
  self.cacheDirectory = cacheDirectory
57
- self.downloader = downloader
57
+ self.download = fetch ?? { try await downloader.download(from: $0) }
58
58
  }
59
59
 
60
60
  // MARK: - Canonical file list
@@ -153,36 +153,43 @@ final class DeltaAssembler {
153
153
  cacheDirectory.appendingPathComponent(sha256.lowercased(), isDirectory: false)
154
154
  }
155
155
 
156
- private func isCached(_ sha256: String) -> Bool {
157
- fileManager.fileExists(atPath: cachePath(for: sha256).path)
156
+ private func validCached(_ entry: ManifestFileEntry) -> Bool {
157
+ let file = cachePath(for: entry.sha256)
158
+ guard let attributes = try? fileManager.attributesOfItem(atPath: file.path),
159
+ attributes[.type] as? FileAttributeType == .typeRegular,
160
+ let size = attributes[.size] as? NSNumber,
161
+ entry.size == nil || size.intValue == entry.size else { return false }
162
+ return (try? HashUtils.verify(fileURL: file, expectedSha256: entry.sha256)) == true
163
+ }
164
+
165
+ private func discardDamagedCache(_ file: URL) throws {
166
+ if fileManager.fileExists(atPath: file.path) { try fileManager.removeItem(at: file) }
158
167
  }
159
168
 
160
169
  private func ensureCached(_ entry: ManifestFileEntry) async throws {
161
- if isCached(entry.sha256) {
162
- return
163
- }
170
+ if validCached(entry) { return }
171
+ let destination = cachePath(for: entry.sha256)
172
+ try discardDamagedCache(destination)
164
173
 
165
174
  guard let url = URL(string: entry.url) else {
166
175
  throw DeltaAssemblerError.invalidFileURL(entry.url)
167
176
  }
168
177
 
169
- let temporary = try await downloader.download(from: url)
178
+ let temporary = try await download(url)
170
179
  defer { try? fileManager.removeItem(at: temporary) }
171
180
 
172
- guard try HashUtils.verify(fileURL: temporary, expectedSha256: entry.sha256) else {
173
- throw DeltaAssemblerError.fileHashMismatch(entry.path)
174
- }
181
+ try HashUtils.verifyDownload(fileURL: temporary, expectedSha256: entry.sha256,
182
+ expectedBytes: entry.size, kind: "file")
175
183
 
176
- let destination = cachePath(for: entry.sha256)
177
- if fileManager.fileExists(atPath: destination.path) {
178
- return
179
- }
184
+ if validCached(entry) { return }
185
+ try discardDamagedCache(destination)
180
186
  // Write via temp + rename so a crash mid-copy can never leave a
181
- // truncated file at a content-addressed path (exists() implies valid).
187
+ // truncated file at a content-addressed path. Existing entries are still verified.
182
188
  let staging = cacheDirectory.appendingPathComponent(
183
189
  ".tmp-\(UUID().uuidString)",
184
190
  isDirectory: false
185
191
  )
192
+ defer { try? fileManager.removeItem(at: staging) }
186
193
  try fileManager.copyItem(at: temporary, to: staging)
187
194
  do {
188
195
  try fileManager.moveItem(at: staging, to: destination)
@@ -193,6 +200,8 @@ final class DeltaAssembler {
193
200
  throw error
194
201
  }
195
202
  }
203
+ try HashUtils.verifyDownload(fileURL: destination, expectedSha256: entry.sha256,
204
+ expectedBytes: entry.size, kind: "cached file")
196
205
  }
197
206
 
198
207
  // MARK: - Assembly
@@ -228,6 +237,9 @@ final class DeltaAssembler {
228
237
  let parent = target.deletingLastPathComponent()
229
238
  try fileManager.createDirectory(at: parent, withIntermediateDirectories: true)
230
239
  try fileManager.copyItem(at: cachePath(for: entry.sha256), to: target)
240
+ // Recheck the installed copy if the cache changed during assembly.
241
+ try HashUtils.verifyDownload(fileURL: target, expectedSha256: entry.sha256,
242
+ expectedBytes: entry.size, kind: "assembled file")
231
243
  }
232
244
  }
233
245
 
@@ -4,10 +4,13 @@ enum DeviceEventAction: String {
4
4
  case downloaded
5
5
  case applied
6
6
  case downloadError = "download_error"
7
+ case checkError = "check_error"
7
8
  case rollback
8
9
  }
9
10
 
10
11
  enum DeviceEventClient {
12
+ private static let delivery = EventDelivery()
13
+ static func resume() { delivery.resume() }
11
14
  static func send(
12
15
  ingestUrl: String,
13
16
  appId: String,
@@ -18,7 +21,10 @@ enum DeviceEventClient {
18
21
  runtimeVersion: String?,
19
22
  releaseId: String,
20
23
  nativeBuild: String,
21
- detail: String?
24
+ detail: String?,
25
+ attemptId: String? = nil,
26
+ phase: String? = nil,
27
+ lifecycle: String = "unknown"
22
28
  ) {
23
29
  let sanitizedBase = ingestUrl.replacingOccurrences(
24
30
  of: "/+$",
@@ -38,7 +44,11 @@ enum DeviceEventClient {
38
44
  "bundleVersion": bundleVersion,
39
45
  "releaseId": releaseId,
40
46
  "nativeBuild": nativeBuild,
47
+ "nativeSdkVersion": SDKVersion.value,
48
+ "lifecycle": lifecycle,
41
49
  ]
50
+ if let attemptId { payload["attemptId"] = attemptId }
51
+ if let phase { payload["phase"] = phase }
42
52
  if let channel, !channel.isEmpty {
43
53
  payload["channel"] = channel
44
54
  }
@@ -53,14 +63,6 @@ enum DeviceEventClient {
53
63
  return
54
64
  }
55
65
 
56
- var request = URLRequest(url: url)
57
- request.httpMethod = "POST"
58
- request.setValue(appId, forHTTPHeaderField: "X-App-Id")
59
- request.setValue("application/json", forHTTPHeaderField: "Content-Type")
60
- request.httpBody = body
61
- request.timeoutInterval = 10
62
-
63
- // Device events are best-effort and should never block the update flow.
64
- URLSession.shared.dataTask(with: request).resume()
66
+ delivery.enqueue(url: url, appId: appId, body: body)
65
67
  }
66
68
  }
@@ -0,0 +1,43 @@
1
+ import Foundation
2
+ import WebKit
3
+
4
+ /// Captures the activation in the document's bridge before application JavaScript runs.
5
+ final class DocumentReadyBridge {
6
+ private var script: WKUserScript?
7
+
8
+ static func source(activationId: String) throws -> String {
9
+ let quotedId = String(data: try JSONEncoder().encode(activationId), encoding: .utf8)!
10
+ return """
11
+ (() => {
12
+ if (window !== window.top) return;
13
+ const cap = window.Capacitor;
14
+ if (!cap || typeof cap.toNative !== 'function') return;
15
+ const original = cap.toNative;
16
+ const activationId = \(quotedId);
17
+ cap.toNative = function(plugin, method, options, callback) {
18
+ if (plugin === 'OtaKit' && method === 'notifyAppReady') {
19
+ options = Object.assign({}, options, { _otakitActivationId: activationId });
20
+ }
21
+ return original.call(this, plugin, method, options, callback);
22
+ };
23
+ })();
24
+ """
25
+ }
26
+
27
+ func install(on webView: WKWebView, activationId: String) throws {
28
+ precondition(Thread.isMainThread)
29
+ let controller = webView.configuration.userContentController
30
+ let replacement = WKUserScript(
31
+ source: try Self.source(activationId: activationId),
32
+ injectionTime: .atDocumentStart, forMainFrameOnly: true
33
+ )
34
+ // WebKit has no individual removal API. Preserve every other plugin's scripts and order.
35
+ if let script {
36
+ let retained = controller.userScripts.filter { $0 !== script }
37
+ controller.removeAllUserScripts()
38
+ for retainedScript in retained { controller.addUserScript(retainedScript) }
39
+ }
40
+ controller.addUserScript(replacement)
41
+ script = replacement
42
+ }
43
+ }