@hot-updater/react-native 0.23.0 → 0.24.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/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +393 -49
- package/android/src/main/java/com/hotupdater/BundleMetadata.kt +204 -0
- package/android/src/main/java/com/hotupdater/HotUpdater.kt +48 -36
- package/android/src/main/java/com/hotupdater/HotUpdaterException.kt +134 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +168 -95
- package/android/src/main/java/com/hotupdater/OkHttpDownloadService.kt +15 -3
- package/android/src/main/java/com/hotupdater/SignatureVerifier.kt +17 -12
- package/android/src/newarch/HotUpdaterModule.kt +88 -23
- package/android/src/oldarch/HotUpdaterModule.kt +89 -22
- package/android/src/oldarch/HotUpdaterSpec.kt +6 -0
- package/ios/HotUpdater/Internal/BundleFileStorageService.swift +401 -77
- package/ios/HotUpdater/Internal/BundleMetadata.swift +177 -0
- package/ios/HotUpdater/Internal/HotUpdater.mm +213 -47
- package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +96 -25
- package/ios/HotUpdater/Internal/SignatureVerifier.swift +35 -29
- package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +2 -2
- package/ios/HotUpdater/Public/HotUpdater.h +8 -2
- package/lib/commonjs/checkForUpdate.js +31 -28
- package/lib/commonjs/checkForUpdate.js.map +1 -1
- package/lib/commonjs/error.js +45 -1
- package/lib/commonjs/error.js.map +1 -1
- package/lib/commonjs/fetchUpdateInfo.js +7 -45
- package/lib/commonjs/fetchUpdateInfo.js.map +1 -1
- package/lib/commonjs/index.js +237 -208
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/native.js +103 -3
- package/lib/commonjs/native.js.map +1 -1
- package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
- package/lib/commonjs/wrap.js +39 -1
- package/lib/commonjs/wrap.js.map +1 -1
- package/lib/module/checkForUpdate.js +32 -26
- package/lib/module/checkForUpdate.js.map +1 -1
- package/lib/module/error.js +45 -0
- package/lib/module/error.js.map +1 -1
- package/lib/module/fetchUpdateInfo.js +7 -45
- package/lib/module/fetchUpdateInfo.js.map +1 -1
- package/lib/module/index.js +238 -203
- package/lib/module/index.js.map +1 -1
- package/lib/module/native.js +87 -2
- package/lib/module/native.js.map +1 -1
- package/lib/module/specs/NativeHotUpdater.js.map +1 -1
- package/lib/module/wrap.js +40 -2
- package/lib/module/wrap.js.map +1 -1
- package/lib/typescript/commonjs/checkForUpdate.d.ts +11 -13
- package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/commonjs/error.d.ts +120 -0
- package/lib/typescript/commonjs/error.d.ts.map +1 -1
- package/lib/typescript/commonjs/fetchUpdateInfo.d.ts +3 -5
- package/lib/typescript/commonjs/fetchUpdateInfo.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +35 -41
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/native.d.ts +58 -2
- package/lib/typescript/commonjs/native.d.ts.map +1 -1
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +62 -0
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/commonjs/wrap.d.ts +76 -5
- package/lib/typescript/commonjs/wrap.d.ts.map +1 -1
- package/lib/typescript/module/checkForUpdate.d.ts +11 -13
- package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/module/error.d.ts +120 -0
- package/lib/typescript/module/error.d.ts.map +1 -1
- package/lib/typescript/module/fetchUpdateInfo.d.ts +3 -5
- package/lib/typescript/module/fetchUpdateInfo.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +35 -41
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/native.d.ts +58 -2
- package/lib/typescript/module/native.d.ts.map +1 -1
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts +62 -0
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/module/wrap.d.ts +76 -5
- package/lib/typescript/module/wrap.d.ts.map +1 -1
- package/package.json +8 -7
- package/plugin/build/withHotUpdater.js +55 -4
- package/src/checkForUpdate.ts +51 -40
- package/src/error.ts +153 -0
- package/src/fetchUpdateInfo.ts +10 -58
- package/src/index.ts +283 -206
- package/src/native.ts +88 -2
- package/src/specs/NativeHotUpdater.ts +63 -0
- package/src/wrap.tsx +131 -9
- package/android/src/main/java/com/hotupdater/HotUpdaterFactory.kt +0 -52
- package/ios/HotUpdater/Internal/HotUpdaterFactory.swift +0 -24
- package/lib/commonjs/runUpdateProcess.js +0 -69
- package/lib/commonjs/runUpdateProcess.js.map +0 -1
- package/lib/module/runUpdateProcess.js +0 -64
- package/lib/module/runUpdateProcess.js.map +0 -1
- package/lib/typescript/commonjs/runUpdateProcess.d.ts +0 -49
- package/lib/typescript/commonjs/runUpdateProcess.d.ts.map +0 -1
- package/lib/typescript/module/runUpdateProcess.d.ts +0 -49
- package/lib/typescript/module/runUpdateProcess.d.ts.map +0 -1
- package/src/runUpdateProcess.ts +0 -80
|
@@ -8,7 +8,7 @@ import React
|
|
|
8
8
|
private static let DEFAULT_CHANNEL = "production"
|
|
9
9
|
|
|
10
10
|
// MARK: - Initialization
|
|
11
|
-
|
|
11
|
+
|
|
12
12
|
/**
|
|
13
13
|
* Convenience initializer that creates and configures all dependencies.
|
|
14
14
|
*/
|
|
@@ -27,7 +27,7 @@ import React
|
|
|
27
27
|
|
|
28
28
|
self.init(bundleStorage: bundleStorage, preferences: preferences)
|
|
29
29
|
}
|
|
30
|
-
|
|
30
|
+
|
|
31
31
|
/**
|
|
32
32
|
* Primary initializer with dependency injection.
|
|
33
33
|
* @param bundleStorage Service for bundle storage operations
|
|
@@ -66,15 +66,14 @@ import React
|
|
|
66
66
|
public static func getIsolationKey() -> String {
|
|
67
67
|
// Get fingerprint hash from Info.plist
|
|
68
68
|
let fingerprintHash = Bundle.main.object(forInfoDictionaryKey: "HOT_UPDATER_FINGERPRINT_HASH") as? String
|
|
69
|
-
|
|
69
|
+
|
|
70
70
|
// Get app version and channel
|
|
71
71
|
let appVersion = self.appVersion ?? "unknown"
|
|
72
72
|
let appChannel = self.appChannel
|
|
73
|
-
|
|
73
|
+
|
|
74
74
|
// Use fingerprint if available, otherwise use app version
|
|
75
75
|
let baseKey = (fingerprintHash != nil && !fingerprintHash!.isEmpty) ? fingerprintHash! : appVersion
|
|
76
|
-
|
|
77
|
-
// Build complete isolation key
|
|
76
|
+
|
|
78
77
|
return "hotupdater_\(baseKey)_\(appChannel)_"
|
|
79
78
|
}
|
|
80
79
|
|
|
@@ -122,22 +121,28 @@ import React
|
|
|
122
121
|
do {
|
|
123
122
|
// Validate parameters (this runs on calling thread - typically JS thread)
|
|
124
123
|
guard let data = params else {
|
|
125
|
-
|
|
126
|
-
|
|
124
|
+
let error = NSError(domain: "HotUpdater", code: 0,
|
|
125
|
+
userInfo: [NSLocalizedDescriptionKey: "Missing or invalid parameters for updateBundle"])
|
|
126
|
+
reject("UNKNOWN_ERROR", error.localizedDescription, error)
|
|
127
|
+
return
|
|
127
128
|
}
|
|
128
|
-
|
|
129
|
+
|
|
129
130
|
guard let bundleId = data["bundleId"] as? String, !bundleId.isEmpty else {
|
|
130
|
-
|
|
131
|
-
|
|
131
|
+
let error = NSError(domain: "HotUpdater", code: 0,
|
|
132
|
+
userInfo: [NSLocalizedDescriptionKey: "Missing or empty 'bundleId'"])
|
|
133
|
+
reject("MISSING_BUNDLE_ID", error.localizedDescription, error)
|
|
134
|
+
return
|
|
132
135
|
}
|
|
133
|
-
|
|
136
|
+
|
|
134
137
|
let fileUrlString = data["fileUrl"] as? String ?? ""
|
|
135
138
|
|
|
136
139
|
var fileUrl: URL? = nil
|
|
137
140
|
if !fileUrlString.isEmpty {
|
|
138
141
|
guard let url = URL(string: fileUrlString) else {
|
|
139
|
-
|
|
140
|
-
|
|
142
|
+
let error = NSError(domain: "HotUpdater", code: 0,
|
|
143
|
+
userInfo: [NSLocalizedDescriptionKey: "Invalid 'fileUrl' provided: \(fileUrlString)"])
|
|
144
|
+
reject("INVALID_FILE_URL", error.localizedDescription, error)
|
|
145
|
+
return
|
|
141
146
|
}
|
|
142
147
|
fileUrl = url
|
|
143
148
|
}
|
|
@@ -160,10 +165,10 @@ import React
|
|
|
160
165
|
}
|
|
161
166
|
}) { [weak self] result in
|
|
162
167
|
guard self != nil else {
|
|
163
|
-
let error = NSError(domain: "
|
|
164
|
-
userInfo: [NSLocalizedDescriptionKey: "
|
|
168
|
+
let error = NSError(domain: "HotUpdater", code: 0,
|
|
169
|
+
userInfo: [NSLocalizedDescriptionKey: "Internal error: self deallocated during update"])
|
|
165
170
|
DispatchQueue.main.async {
|
|
166
|
-
reject("
|
|
171
|
+
reject("SELF_DEALLOCATED", error.localizedDescription, error)
|
|
167
172
|
}
|
|
168
173
|
return
|
|
169
174
|
}
|
|
@@ -174,12 +179,11 @@ import React
|
|
|
174
179
|
NSLog("[HotUpdaterImpl] Update successful for \(bundleId). Resolving promise.")
|
|
175
180
|
resolve(true)
|
|
176
181
|
case .failure(let error):
|
|
177
|
-
|
|
178
|
-
NSLog("[HotUpdaterImpl] Update failed for \(bundleId) - Domain: \(nsError.domain), Code: \(nsError.code), Description: \(nsError.localizedDescription)")
|
|
182
|
+
NSLog("[HotUpdaterImpl] Update failed for \(bundleId) - Error: \(error)")
|
|
179
183
|
|
|
180
|
-
|
|
181
|
-
let
|
|
182
|
-
reject(
|
|
184
|
+
let normalizedCode = HotUpdaterImpl.normalizeErrorCode(from: error)
|
|
185
|
+
let nsError = error as NSError
|
|
186
|
+
reject(normalizedCode, nsError.localizedDescription, nsError)
|
|
183
187
|
}
|
|
184
188
|
}
|
|
185
189
|
}
|
|
@@ -188,8 +192,75 @@ import React
|
|
|
188
192
|
let nsError = error as NSError
|
|
189
193
|
NSLog("[HotUpdaterImpl] Error in updateBundleFromJS - Domain: \(nsError.domain), Code: \(nsError.code), Description: \(nsError.localizedDescription)")
|
|
190
194
|
|
|
191
|
-
|
|
192
|
-
reject(errorCode, nsError.localizedDescription, nsError)
|
|
195
|
+
reject("UNKNOWN_ERROR", nsError.localizedDescription, nsError)
|
|
193
196
|
}
|
|
194
197
|
}
|
|
195
|
-
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Normalizes native errors to a small, predictable set of JS-facing error codes.
|
|
201
|
+
* Rare or platform-specific codes are collapsed to UNKNOWN_ERROR to reduce surface area.
|
|
202
|
+
*/
|
|
203
|
+
private static func normalizeErrorCode(from error: Error) -> String {
|
|
204
|
+
let baseCode: String
|
|
205
|
+
|
|
206
|
+
if let storageError = error as? BundleStorageError {
|
|
207
|
+
// Collapse signature sub-errors into a single public code
|
|
208
|
+
if case .signatureVerificationFailed = storageError {
|
|
209
|
+
baseCode = "SIGNATURE_VERIFICATION_FAILED"
|
|
210
|
+
} else {
|
|
211
|
+
baseCode = storageError.errorCodeString
|
|
212
|
+
}
|
|
213
|
+
} else if error is SignatureVerificationError {
|
|
214
|
+
baseCode = "SIGNATURE_VERIFICATION_FAILED"
|
|
215
|
+
} else {
|
|
216
|
+
baseCode = "UNKNOWN_ERROR"
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return userFacingErrorCodes.contains(baseCode) ? baseCode : "UNKNOWN_ERROR"
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Error codes we intentionally expose to JS callers.
|
|
223
|
+
private static let userFacingErrorCodes: Set<String> = [
|
|
224
|
+
"MISSING_BUNDLE_ID",
|
|
225
|
+
"INVALID_FILE_URL",
|
|
226
|
+
"DIRECTORY_CREATION_FAILED",
|
|
227
|
+
"DOWNLOAD_FAILED",
|
|
228
|
+
"INCOMPLETE_DOWNLOAD",
|
|
229
|
+
"EXTRACTION_FORMAT_ERROR",
|
|
230
|
+
"INVALID_BUNDLE",
|
|
231
|
+
"INSUFFICIENT_DISK_SPACE",
|
|
232
|
+
"SIGNATURE_VERIFICATION_FAILED",
|
|
233
|
+
"MOVE_OPERATION_FAILED",
|
|
234
|
+
"BUNDLE_IN_CRASHED_HISTORY",
|
|
235
|
+
"SELF_DEALLOCATED",
|
|
236
|
+
"UNKNOWN_ERROR",
|
|
237
|
+
]
|
|
238
|
+
|
|
239
|
+
// MARK: - Rollback Support
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Notifies the system that the app has successfully started with the given bundle.
|
|
243
|
+
* If the bundle matches the staging bundle, it promotes to stable.
|
|
244
|
+
* @param bundleId The ID of the currently running bundle
|
|
245
|
+
* @return true if promotion was successful or no action was needed
|
|
246
|
+
*/
|
|
247
|
+
public func notifyAppReady(bundleId: String) -> [String: Any] {
|
|
248
|
+
return bundleStorage.notifyAppReady(bundleId: bundleId)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Gets the crashed bundle history.
|
|
253
|
+
* @return Array of crashed bundle IDs
|
|
254
|
+
*/
|
|
255
|
+
public func getCrashHistory() -> [String] {
|
|
256
|
+
return bundleStorage.getCrashHistory().bundles.map { $0.bundleId }
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Clears the crashed bundle history.
|
|
261
|
+
* @return true if clearing was successful
|
|
262
|
+
*/
|
|
263
|
+
public func clearCrashHistory() -> Bool {
|
|
264
|
+
return bundleStorage.clearCrashHistory()
|
|
265
|
+
}
|
|
266
|
+
}
|
|
@@ -5,35 +5,37 @@ import Security
|
|
|
5
5
|
private let SIGNED_HASH_PREFIX = "sig:"
|
|
6
6
|
|
|
7
7
|
/// Error types for signature verification failures.
|
|
8
|
-
///
|
|
9
|
-
/// **IMPORTANT**: The error messages in `errorUserInfo` are used by the JavaScript layer
|
|
10
|
-
/// (`packages/react-native/src/types.ts`) to detect signature verification failures.
|
|
11
|
-
/// If you change these messages, update `isSignatureVerificationError()` in types.ts accordingly.
|
|
12
8
|
public enum SignatureVerificationError: Error, CustomNSError {
|
|
13
9
|
case publicKeyNotConfigured
|
|
14
10
|
case invalidPublicKeyFormat
|
|
11
|
+
case missingFileHash
|
|
15
12
|
case invalidSignatureFormat
|
|
16
|
-
case
|
|
17
|
-
case
|
|
18
|
-
case
|
|
13
|
+
case signatureVerificationFailed
|
|
14
|
+
case fileHashMismatch
|
|
15
|
+
case fileReadFailed
|
|
19
16
|
case unsignedNotAllowed
|
|
20
17
|
case securityFrameworkError(OSStatus)
|
|
21
18
|
|
|
22
19
|
// CustomNSError protocol implementation
|
|
23
20
|
public static var errorDomain: String {
|
|
24
|
-
return "
|
|
21
|
+
return "HotUpdater"
|
|
25
22
|
}
|
|
26
23
|
|
|
27
24
|
public var errorCode: Int {
|
|
25
|
+
return 0
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
public var errorCodeString: String {
|
|
28
29
|
switch self {
|
|
29
|
-
case .publicKeyNotConfigured: return
|
|
30
|
-
case .invalidPublicKeyFormat: return
|
|
31
|
-
case .
|
|
32
|
-
case .
|
|
33
|
-
case .
|
|
34
|
-
case .
|
|
35
|
-
case .
|
|
36
|
-
case .
|
|
30
|
+
case .publicKeyNotConfigured: return "PUBLIC_KEY_NOT_CONFIGURED"
|
|
31
|
+
case .invalidPublicKeyFormat: return "INVALID_PUBLIC_KEY_FORMAT"
|
|
32
|
+
case .missingFileHash: return "MISSING_FILE_HASH"
|
|
33
|
+
case .invalidSignatureFormat: return "INVALID_SIGNATURE_FORMAT"
|
|
34
|
+
case .signatureVerificationFailed: return "SIGNATURE_VERIFICATION_FAILED"
|
|
35
|
+
case .fileHashMismatch: return "FILE_HASH_MISMATCH"
|
|
36
|
+
case .fileReadFailed: return "FILE_READ_FAILED"
|
|
37
|
+
case .unsignedNotAllowed: return "UNSIGNED_NOT_ALLOWED"
|
|
38
|
+
case .securityFrameworkError: return "SECURITY_FRAMEWORK_ERROR"
|
|
37
39
|
}
|
|
38
40
|
}
|
|
39
41
|
|
|
@@ -49,20 +51,24 @@ public enum SignatureVerificationError: Error, CustomNSError {
|
|
|
49
51
|
userInfo[NSLocalizedDescriptionKey] = "Public key format is invalid"
|
|
50
52
|
userInfo[NSLocalizedRecoverySuggestionErrorKey] = "Ensure the public key is in PEM format (BEGIN PUBLIC KEY)"
|
|
51
53
|
|
|
54
|
+
case .missingFileHash:
|
|
55
|
+
userInfo[NSLocalizedDescriptionKey] = "File hash is missing or empty"
|
|
56
|
+
userInfo[NSLocalizedRecoverySuggestionErrorKey] = "Ensure the bundle update includes a valid file hash"
|
|
57
|
+
|
|
52
58
|
case .invalidSignatureFormat:
|
|
53
|
-
userInfo[NSLocalizedDescriptionKey] = "Signature format is invalid"
|
|
54
|
-
userInfo[NSLocalizedRecoverySuggestionErrorKey] = "The signature
|
|
59
|
+
userInfo[NSLocalizedDescriptionKey] = "Signature format is invalid or corrupted"
|
|
60
|
+
userInfo[NSLocalizedRecoverySuggestionErrorKey] = "The signature data is malformed or cannot be decoded. Bundle may be corrupted"
|
|
55
61
|
|
|
56
|
-
case .
|
|
62
|
+
case .signatureVerificationFailed:
|
|
57
63
|
userInfo[NSLocalizedDescriptionKey] = "Bundle signature verification failed"
|
|
58
64
|
userInfo[NSLocalizedRecoverySuggestionErrorKey] = "The bundle may be corrupted or tampered with. Rejecting update for security"
|
|
59
65
|
|
|
60
|
-
case .
|
|
61
|
-
userInfo[NSLocalizedDescriptionKey] = "
|
|
62
|
-
userInfo[NSLocalizedRecoverySuggestionErrorKey] = "The bundle file hash does not match. File may be corrupted"
|
|
66
|
+
case .fileHashMismatch:
|
|
67
|
+
userInfo[NSLocalizedDescriptionKey] = "File hash verification failed"
|
|
68
|
+
userInfo[NSLocalizedRecoverySuggestionErrorKey] = "The bundle file hash does not match the expected value. File may be corrupted"
|
|
63
69
|
|
|
64
|
-
case .
|
|
65
|
-
userInfo[NSLocalizedDescriptionKey] = "Failed to
|
|
70
|
+
case .fileReadFailed:
|
|
71
|
+
userInfo[NSLocalizedDescriptionKey] = "Failed to read file for verification"
|
|
66
72
|
userInfo[NSLocalizedRecoverySuggestionErrorKey] = "Could not read file for hash verification"
|
|
67
73
|
|
|
68
74
|
case .unsignedNotAllowed:
|
|
@@ -149,7 +155,7 @@ public class SignatureVerifier {
|
|
|
149
155
|
// Rule: null/empty fileHash → REJECT
|
|
150
156
|
guard let hash = fileHash, !hash.isEmpty else {
|
|
151
157
|
NSLog("[SignatureVerifier] fileHash is null or empty. Rejecting update.")
|
|
152
|
-
return .failure(.
|
|
158
|
+
return .failure(.missingFileHash)
|
|
153
159
|
}
|
|
154
160
|
|
|
155
161
|
if isSignedFormat(hash) {
|
|
@@ -192,7 +198,7 @@ public class SignatureVerifier {
|
|
|
192
198
|
|
|
193
199
|
guard HashUtils.verifyHash(fileURL: fileURL, expectedHash: expectedHash) else {
|
|
194
200
|
NSLog("[SignatureVerifier] Hash mismatch!")
|
|
195
|
-
return .failure(.
|
|
201
|
+
return .failure(.fileHashMismatch)
|
|
196
202
|
}
|
|
197
203
|
|
|
198
204
|
NSLog("[SignatureVerifier] ✅ Hash verified successfully")
|
|
@@ -228,7 +234,7 @@ public class SignatureVerifier {
|
|
|
228
234
|
// Calculate file hash
|
|
229
235
|
guard let fileHashHex = HashUtils.calculateSHA256(fileURL: fileURL) else {
|
|
230
236
|
NSLog("[SignatureVerifier] Failed to calculate file hash")
|
|
231
|
-
return .failure(.
|
|
237
|
+
return .failure(.fileReadFailed)
|
|
232
238
|
}
|
|
233
239
|
|
|
234
240
|
NSLog("[SignatureVerifier] Calculated file hash: \(fileHashHex)")
|
|
@@ -264,7 +270,7 @@ public class SignatureVerifier {
|
|
|
264
270
|
|
|
265
271
|
if let err = error?.takeRetainedValue() {
|
|
266
272
|
NSLog("[SignatureVerifier] Verification failed: \(err)")
|
|
267
|
-
return .failure(.
|
|
273
|
+
return .failure(.signatureVerificationFailed)
|
|
268
274
|
}
|
|
269
275
|
|
|
270
276
|
if verified {
|
|
@@ -272,7 +278,7 @@ public class SignatureVerifier {
|
|
|
272
278
|
return .success(())
|
|
273
279
|
} else {
|
|
274
280
|
NSLog("[SignatureVerifier] ❌ Signature verification failed")
|
|
275
|
-
return .failure(.
|
|
281
|
+
return .failure(.signatureVerificationFailed)
|
|
276
282
|
}
|
|
277
283
|
}
|
|
278
284
|
|
|
@@ -21,7 +21,7 @@ protocol DownloadService {
|
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
enum DownloadError: Error {
|
|
24
|
-
case incompleteDownload
|
|
24
|
+
case incompleteDownload(expected: Int64, actual: Int64)
|
|
25
25
|
case invalidContentLength
|
|
26
26
|
}
|
|
27
27
|
|
|
@@ -109,7 +109,7 @@ extension URLSessionDownloadService: URLSessionDownloadDelegate {
|
|
|
109
109
|
NSLog("[DownloadService] Download incomplete: \(actualSize) / \(expectedSize) bytes")
|
|
110
110
|
// Delete incomplete file
|
|
111
111
|
try? FileManager.default.removeItem(at: location)
|
|
112
|
-
completion?(.failure(DownloadError.incompleteDownload))
|
|
112
|
+
completion?(.failure(DownloadError.incompleteDownload(expected: expectedSize, actual: actualSize)))
|
|
113
113
|
return
|
|
114
114
|
}
|
|
115
115
|
|
|
@@ -10,12 +10,18 @@
|
|
|
10
10
|
#endif // RCT_NEW_ARCH_ENABLED
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* Returns the currently active bundle URL.
|
|
14
|
-
* Callable from Objective-C (e.g., AppDelegate).
|
|
13
|
+
* Returns the currently active bundle URL from the default (static) instance.
|
|
14
|
+
* Callable from Objective-C (e.g., AppDelegate).
|
|
15
15
|
* This is implemented in HotUpdater.mm and calls the Swift static method.
|
|
16
16
|
*/
|
|
17
17
|
+ (NSURL *)bundleURL;
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Returns the bundle URL for this specific instance.
|
|
21
|
+
* @return The bundle URL for this instance
|
|
22
|
+
*/
|
|
23
|
+
- (NSURL *)bundleURL;
|
|
24
|
+
|
|
19
25
|
/**
|
|
20
26
|
* 다운로드 진행 상황 업데이트 시간을 추적하는 속성
|
|
21
27
|
*/
|
|
@@ -4,11 +4,28 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.checkForUpdate = checkForUpdate;
|
|
7
|
-
exports.getUpdateSource = void 0;
|
|
8
7
|
var _reactNative = require("react-native");
|
|
9
8
|
var _error = require("./error.js");
|
|
10
9
|
var _fetchUpdateInfo = require("./fetchUpdateInfo.js");
|
|
11
10
|
var _native = require("./native.js");
|
|
11
|
+
// Internal type that includes baseURL for use within index.ts
|
|
12
|
+
|
|
13
|
+
// Internal function to build update URL (not exported)
|
|
14
|
+
function buildUpdateUrl(baseURL, updateStrategy, params) {
|
|
15
|
+
switch (updateStrategy) {
|
|
16
|
+
case "fingerprint":
|
|
17
|
+
{
|
|
18
|
+
if (!params.fingerprintHash) {
|
|
19
|
+
throw new _error.HotUpdaterError("Fingerprint hash is required");
|
|
20
|
+
}
|
|
21
|
+
return `${baseURL}/fingerprint/${params.platform}/${params.fingerprintHash}/${params.channel}/${params.minBundleId}/${params.bundleId}`;
|
|
22
|
+
}
|
|
23
|
+
case "appVersion":
|
|
24
|
+
{
|
|
25
|
+
return `${baseURL}/app-version/${params.platform}/${params.appVersion}/${params.channel}/${params.minBundleId}/${params.bundleId}`;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
12
29
|
async function checkForUpdate(options) {
|
|
13
30
|
if (__DEV__) {
|
|
14
31
|
return null;
|
|
@@ -17,6 +34,10 @@ async function checkForUpdate(options) {
|
|
|
17
34
|
options.onError?.(new _error.HotUpdaterError("HotUpdater is only supported on iOS and Android"));
|
|
18
35
|
return null;
|
|
19
36
|
}
|
|
37
|
+
if (!options.baseURL || !options.updateStrategy) {
|
|
38
|
+
options.onError?.(new _error.HotUpdaterError("'baseURL' and 'updateStrategy' are required"));
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
20
41
|
const currentAppVersion = (0, _native.getAppVersion)();
|
|
21
42
|
const platform = _reactNative.Platform.OS;
|
|
22
43
|
const currentBundleId = (0, _native.getBundleId)();
|
|
@@ -27,16 +48,16 @@ async function checkForUpdate(options) {
|
|
|
27
48
|
return null;
|
|
28
49
|
}
|
|
29
50
|
const fingerprintHash = (0, _native.getFingerprintHash)();
|
|
51
|
+
const url = buildUpdateUrl(options.baseURL, options.updateStrategy, {
|
|
52
|
+
platform,
|
|
53
|
+
appVersion: currentAppVersion,
|
|
54
|
+
fingerprintHash: fingerprintHash ?? null,
|
|
55
|
+
channel,
|
|
56
|
+
minBundleId,
|
|
57
|
+
bundleId: currentBundleId
|
|
58
|
+
});
|
|
30
59
|
return (0, _fetchUpdateInfo.fetchUpdateInfo)({
|
|
31
|
-
|
|
32
|
-
params: {
|
|
33
|
-
bundleId: currentBundleId,
|
|
34
|
-
appVersion: currentAppVersion,
|
|
35
|
-
platform,
|
|
36
|
-
minBundleId,
|
|
37
|
-
channel,
|
|
38
|
-
fingerprintHash
|
|
39
|
-
},
|
|
60
|
+
url,
|
|
40
61
|
requestHeaders: options.requestHeaders,
|
|
41
62
|
onError: options.onError,
|
|
42
63
|
requestTimeout: options.requestTimeout
|
|
@@ -57,22 +78,4 @@ async function checkForUpdate(options) {
|
|
|
57
78
|
};
|
|
58
79
|
});
|
|
59
80
|
}
|
|
60
|
-
const getUpdateSource = (baseUrl, options) => params => {
|
|
61
|
-
switch (options.updateStrategy) {
|
|
62
|
-
case "fingerprint":
|
|
63
|
-
{
|
|
64
|
-
if (!params.fingerprintHash) {
|
|
65
|
-
throw new _error.HotUpdaterError("Fingerprint hash is required");
|
|
66
|
-
}
|
|
67
|
-
return `${baseUrl}/fingerprint/${params.platform}/${params.fingerprintHash}/${params.channel}/${params.minBundleId}/${params.bundleId}`;
|
|
68
|
-
}
|
|
69
|
-
case "appVersion":
|
|
70
|
-
{
|
|
71
|
-
return `${baseUrl}/app-version/${params.platform}/${params.appVersion}/${params.channel}/${params.minBundleId}/${params.bundleId}`;
|
|
72
|
-
}
|
|
73
|
-
default:
|
|
74
|
-
return baseUrl;
|
|
75
|
-
}
|
|
76
|
-
};
|
|
77
|
-
exports.getUpdateSource = getUpdateSource;
|
|
78
81
|
//# sourceMappingURL=checkForUpdate.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["_reactNative","require","_error","_fetchUpdateInfo","_native","
|
|
1
|
+
{"version":3,"names":["_reactNative","require","_error","_fetchUpdateInfo","_native","buildUpdateUrl","baseURL","updateStrategy","params","fingerprintHash","HotUpdaterError","platform","channel","minBundleId","bundleId","appVersion","checkForUpdate","options","__DEV__","includes","Platform","OS","onError","currentAppVersion","getAppVersion","currentBundleId","getBundleId","getMinBundleId","getChannel","getFingerprintHash","url","fetchUpdateInfo","requestHeaders","requestTimeout","then","updateInfo","updateBundle","id","fileUrl","fileHash","status"],"sourceRoot":"../../src","sources":["checkForUpdate.ts"],"mappings":"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","ignoreList":[]}
|
package/lib/commonjs/error.js
CHANGED
|
@@ -3,7 +3,51 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.HotUpdaterError = void 0;
|
|
6
|
+
exports.HotUpdaterErrorCode = exports.HotUpdaterError = void 0;
|
|
7
|
+
exports.isHotUpdaterError = isHotUpdaterError;
|
|
8
|
+
/**
|
|
9
|
+
* Hot Updater Error Codes
|
|
10
|
+
*
|
|
11
|
+
* This file defines all possible error codes that can be thrown by the native
|
|
12
|
+
* updateBundle function. These error codes are shared across iOS and Android
|
|
13
|
+
* implementations to ensure consistent error handling.
|
|
14
|
+
*
|
|
15
|
+
* Error Classification:
|
|
16
|
+
* - Parameter Validation: Invalid or missing function parameters
|
|
17
|
+
* - Bundle Storage: Errors during download, extraction, and storage
|
|
18
|
+
* - Signature Verification: Cryptographic verification failures (collapsed to a single public code)
|
|
19
|
+
* - Internal: Platform-specific or unexpected errors
|
|
20
|
+
*
|
|
21
|
+
* Retryability:
|
|
22
|
+
* - Retryable: DOWNLOAD_FAILED, INCOMPLETE_DOWNLOAD
|
|
23
|
+
* - Non-retryable: Most validation and verification errors
|
|
24
|
+
*/
|
|
25
|
+
let HotUpdaterErrorCode = exports.HotUpdaterErrorCode = /*#__PURE__*/function (HotUpdaterErrorCode) {
|
|
26
|
+
HotUpdaterErrorCode["MISSING_BUNDLE_ID"] = "MISSING_BUNDLE_ID";
|
|
27
|
+
HotUpdaterErrorCode["INVALID_FILE_URL"] = "INVALID_FILE_URL";
|
|
28
|
+
HotUpdaterErrorCode["DIRECTORY_CREATION_FAILED"] = "DIRECTORY_CREATION_FAILED";
|
|
29
|
+
HotUpdaterErrorCode["DOWNLOAD_FAILED"] = "DOWNLOAD_FAILED";
|
|
30
|
+
HotUpdaterErrorCode["INCOMPLETE_DOWNLOAD"] = "INCOMPLETE_DOWNLOAD";
|
|
31
|
+
HotUpdaterErrorCode["EXTRACTION_FORMAT_ERROR"] = "EXTRACTION_FORMAT_ERROR";
|
|
32
|
+
HotUpdaterErrorCode["INVALID_BUNDLE"] = "INVALID_BUNDLE";
|
|
33
|
+
HotUpdaterErrorCode["INSUFFICIENT_DISK_SPACE"] = "INSUFFICIENT_DISK_SPACE";
|
|
34
|
+
HotUpdaterErrorCode["SIGNATURE_VERIFICATION_FAILED"] = "SIGNATURE_VERIFICATION_FAILED";
|
|
35
|
+
HotUpdaterErrorCode["MOVE_OPERATION_FAILED"] = "MOVE_OPERATION_FAILED";
|
|
36
|
+
HotUpdaterErrorCode["BUNDLE_IN_CRASHED_HISTORY"] = "BUNDLE_IN_CRASHED_HISTORY";
|
|
37
|
+
HotUpdaterErrorCode["SELF_DEALLOCATED"] = "SELF_DEALLOCATED";
|
|
38
|
+
HotUpdaterErrorCode["UNKNOWN_ERROR"] = "UNKNOWN_ERROR";
|
|
39
|
+
return HotUpdaterErrorCode;
|
|
40
|
+
}({});
|
|
41
|
+
/**
|
|
42
|
+
* Type guard to check if an error is a HotUpdaterError
|
|
43
|
+
*/
|
|
44
|
+
function isHotUpdaterError(error) {
|
|
45
|
+
return typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" && Object.values(HotUpdaterErrorCode).includes(error.code);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Base error class for Hot Updater
|
|
50
|
+
*/
|
|
7
51
|
class HotUpdaterError extends Error {
|
|
8
52
|
constructor(message) {
|
|
9
53
|
super(message);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["HotUpdaterError","Error","constructor","message","name"
|
|
1
|
+
{"version":3,"names":["HotUpdaterErrorCode","exports","isHotUpdaterError","error","code","Object","values","includes","HotUpdaterError","Error","constructor","message","name"],"sourceRoot":"../../src","sources":["error.ts"],"mappings":";;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAhBA,IAkBYA,mBAAmB,GAAAC,OAAA,CAAAD,mBAAA,0BAAnBA,mBAAmB;EAAnBA,mBAAmB;EAAnBA,mBAAmB;EAAnBA,mBAAmB;EAAnBA,mBAAmB;EAAnBA,mBAAmB;EAAnBA,mBAAmB;EAAnBA,mBAAmB;EAAnBA,mBAAmB;EAAnBA,mBAAmB;EAAnBA,mBAAmB;EAAnBA,mBAAmB;EAAnBA,mBAAmB;EAAnBA,mBAAmB;EAAA,OAAnBA,mBAAmB;AAAA;AAmH/B;AACA;AACA;AACO,SAASE,iBAAiBA,CAC/BC,KAAc,EAC2C;EACzD,OACE,OAAOA,KAAK,KAAK,QAAQ,IACzBA,KAAK,KAAK,IAAI,IACd,MAAM,IAAIA,KAAK,IACf,OAAOA,KAAK,CAACC,IAAI,KAAK,QAAQ,IAC9BC,MAAM,CAACC,MAAM,CAACN,mBAAmB,CAAC,CAACO,QAAQ,CACzCJ,KAAK,CAACC,IACR,CAAC;AAEL;;AAEA;AACA;AACA;AACO,MAAMI,eAAe,SAASC,KAAK,CAAC;EACzCC,WAAWA,CAACC,OAAe,EAAE;IAC3B,KAAK,CAACA,OAAO,CAAC;IACd,IAAI,CAACC,IAAI,GAAG,iBAAiB;EAC/B;AACF;AAACX,OAAA,CAAAO,eAAA,GAAAA,eAAA","ignoreList":[]}
|
|
@@ -4,60 +4,22 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.fetchUpdateInfo = void 0;
|
|
7
|
-
function buildRequestHeaders(params, requestHeaders) {
|
|
8
|
-
const updateStrategy = params.fingerprintHash ? "fingerprint" : "appVersion";
|
|
9
|
-
return {
|
|
10
|
-
"Content-Type": "application/json",
|
|
11
|
-
"x-app-platform": params.platform,
|
|
12
|
-
"x-bundle-id": params.bundleId,
|
|
13
|
-
...(updateStrategy === "fingerprint" ? {
|
|
14
|
-
"x-fingerprint-hash": params.fingerprintHash
|
|
15
|
-
} : {
|
|
16
|
-
"x-app-version": params.appVersion
|
|
17
|
-
}),
|
|
18
|
-
...(params.minBundleId && {
|
|
19
|
-
"x-min-bundle-id": params.minBundleId
|
|
20
|
-
}),
|
|
21
|
-
...(params.channel && {
|
|
22
|
-
"x-channel": params.channel
|
|
23
|
-
}),
|
|
24
|
-
...requestHeaders
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
async function resolveSource(source, params) {
|
|
28
|
-
if (typeof source !== "function") {
|
|
29
|
-
return {
|
|
30
|
-
url: source
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
const result = source(params);
|
|
34
|
-
if (typeof result === "string") {
|
|
35
|
-
return {
|
|
36
|
-
url: result
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
return {
|
|
40
|
-
info: await result
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
7
|
const fetchUpdateInfo = async ({
|
|
44
|
-
|
|
45
|
-
params,
|
|
8
|
+
url,
|
|
46
9
|
requestHeaders,
|
|
47
10
|
onError,
|
|
48
11
|
requestTimeout = 5000
|
|
49
12
|
}) => {
|
|
50
13
|
try {
|
|
51
|
-
const resolvedSource = await resolveSource(source, params);
|
|
52
|
-
if ("info" in resolvedSource) {
|
|
53
|
-
return resolvedSource.info;
|
|
54
|
-
}
|
|
55
14
|
const controller = new AbortController();
|
|
56
15
|
const timeoutId = setTimeout(() => {
|
|
57
16
|
controller.abort();
|
|
58
17
|
}, requestTimeout);
|
|
59
|
-
const headers =
|
|
60
|
-
|
|
18
|
+
const headers = {
|
|
19
|
+
"Content-Type": "application/json",
|
|
20
|
+
...requestHeaders
|
|
21
|
+
};
|
|
22
|
+
const response = await fetch(url, {
|
|
61
23
|
signal: controller.signal,
|
|
62
24
|
headers
|
|
63
25
|
});
|
|
@@ -67,7 +29,7 @@ const fetchUpdateInfo = async ({
|
|
|
67
29
|
}
|
|
68
30
|
return response.json();
|
|
69
31
|
} catch (error) {
|
|
70
|
-
if (error.name === "AbortError") {
|
|
32
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
71
33
|
onError?.(new Error("Request timed out"));
|
|
72
34
|
} else {
|
|
73
35
|
onError?.(error);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["
|
|
1
|
+
{"version":3,"names":["fetchUpdateInfo","url","requestHeaders","onError","requestTimeout","controller","AbortController","timeoutId","setTimeout","abort","headers","response","fetch","signal","clearTimeout","status","Error","statusText","json","error","name","exports"],"sourceRoot":"../../src","sources":["fetchUpdateInfo.ts"],"mappings":";;;;;;AAEO,MAAMA,eAAe,GAAG,MAAAA,CAAO;EACpCC,GAAG;EACHC,cAAc;EACdC,OAAO;EACPC,cAAc,GAAG;AAMnB,CAAC,KAAoC;EACnC,IAAI;IACF,MAAMC,UAAU,GAAG,IAAIC,eAAe,CAAC,CAAC;IACxC,MAAMC,SAAS,GAAGC,UAAU,CAAC,MAAM;MACjCH,UAAU,CAACI,KAAK,CAAC,CAAC;IACpB,CAAC,EAAEL,cAAc,CAAC;IAElB,MAAMM,OAAO,GAAG;MACd,cAAc,EAAE,kBAAkB;MAClC,GAAGR;IACL,CAAC;IAED,MAAMS,QAAQ,GAAG,MAAMC,KAAK,CAACX,GAAG,EAAE;MAChCY,MAAM,EAAER,UAAU,CAACQ,MAAM;MACzBH;IACF,CAAC,CAAC;IACFI,YAAY,CAACP,SAAS,CAAC;IAEvB,IAAII,QAAQ,CAACI,MAAM,KAAK,GAAG,EAAE;MAC3B,MAAM,IAAIC,KAAK,CAACL,QAAQ,CAACM,UAAU,CAAC;IACtC;IACA,OAAON,QAAQ,CAACO,IAAI,CAAC,CAAC;EACxB,CAAC,CAAC,OAAOC,KAAc,EAAE;IACvB,IAAIA,KAAK,YAAYH,KAAK,IAAIG,KAAK,CAACC,IAAI,KAAK,YAAY,EAAE;MACzDjB,OAAO,GAAG,IAAIa,KAAK,CAAC,mBAAmB,CAAC,CAAC;IAC3C,CAAC,MAAM;MACLb,OAAO,GAAGgB,KAAc,CAAC;IAC3B;IACA,OAAO,IAAI;EACb;AACF,CAAC;AAACE,OAAA,CAAArB,eAAA,GAAAA,eAAA","ignoreList":[]}
|