@otakit/capacitor-updater 1.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.
- package/LICENSE +21 -0
- package/README.md +183 -0
- package/UpdatekitUpdater.podspec +18 -0
- package/android/build.gradle +49 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/java/com/updatekit/updater/BundleInfo.java +100 -0
- package/android/src/main/java/com/updatekit/updater/BundleStatus.java +28 -0
- package/android/src/main/java/com/updatekit/updater/BundleStore.java +264 -0
- package/android/src/main/java/com/updatekit/updater/DateUtils.java +17 -0
- package/android/src/main/java/com/updatekit/updater/HashUtils.java +32 -0
- package/android/src/main/java/com/updatekit/updater/HostedManifestKeys.java +37 -0
- package/android/src/main/java/com/updatekit/updater/ManifestClient.java +209 -0
- package/android/src/main/java/com/updatekit/updater/ManifestVerifier.java +146 -0
- package/android/src/main/java/com/updatekit/updater/StatsClient.java +80 -0
- package/android/src/main/java/com/updatekit/updater/UpdaterPlugin.java +963 -0
- package/android/src/main/java/com/updatekit/updater/ZipUtils.java +72 -0
- package/dist/esm/definitions.d.ts +229 -0
- package/dist/esm/definitions.d.ts.map +1 -0
- package/dist/esm/definitions.js +17 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +5 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +85 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/web.d.ts +25 -0
- package/dist/esm/web.d.ts.map +1 -0
- package/dist/esm/web.js +56 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +165 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +168 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Sources/UpdaterPlugin/BundleInfo.swift +39 -0
- package/ios/Sources/UpdaterPlugin/BundleStatus.swift +9 -0
- package/ios/Sources/UpdaterPlugin/BundleStore.swift +233 -0
- package/ios/Sources/UpdaterPlugin/Downloader.swift +120 -0
- package/ios/Sources/UpdaterPlugin/HashUtils.swift +33 -0
- package/ios/Sources/UpdaterPlugin/HostedManifestKeys.swift +23 -0
- package/ios/Sources/UpdaterPlugin/ManifestClient.swift +161 -0
- package/ios/Sources/UpdaterPlugin/ManifestVerifier.swift +115 -0
- package/ios/Sources/UpdaterPlugin/StatsClient.swift +66 -0
- package/ios/Sources/UpdaterPlugin/UpdaterPlugin.m +15 -0
- package/ios/Sources/UpdaterPlugin/UpdaterPlugin.swift +913 -0
- package/ios/Sources/UpdaterPlugin/ZipUtils.swift +90 -0
- package/package.json +85 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
final class Downloader {
|
|
4
|
+
private let allowInsecureUrls: Bool
|
|
5
|
+
|
|
6
|
+
init(allowInsecureUrls: Bool = false) {
|
|
7
|
+
self.allowInsecureUrls = allowInsecureUrls
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
func download(
|
|
11
|
+
from url: URL,
|
|
12
|
+
progress: @escaping (Double, Int64, Int64) -> Void = { _, _, _ in }
|
|
13
|
+
) async throws -> URL {
|
|
14
|
+
try ManifestClient.requireHTTPS(url: url, allowInsecure: allowInsecureUrls)
|
|
15
|
+
let delegate = DownloadDelegate(progressHandler: progress)
|
|
16
|
+
return try await delegate.start(from: url)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
private final class DownloadDelegate: NSObject, URLSessionDownloadDelegate {
|
|
21
|
+
private var continuation: CheckedContinuation<URL, Error>?
|
|
22
|
+
private var session: URLSession?
|
|
23
|
+
private let progressHandler: (Double, Int64, Int64) -> Void
|
|
24
|
+
private let stateLock = NSLock()
|
|
25
|
+
private var isResolved = false
|
|
26
|
+
|
|
27
|
+
init(progressHandler: @escaping (Double, Int64, Int64) -> Void) {
|
|
28
|
+
self.progressHandler = progressHandler
|
|
29
|
+
super.init()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
func start(from url: URL) async throws -> URL {
|
|
33
|
+
try await withCheckedThrowingContinuation { continuation in
|
|
34
|
+
self.continuation = continuation
|
|
35
|
+
|
|
36
|
+
let configuration = URLSessionConfiguration.default
|
|
37
|
+
let session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
|
|
38
|
+
self.session = session
|
|
39
|
+
let task = session.downloadTask(with: url)
|
|
40
|
+
task.resume()
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private func resolve(_ result: Result<URL, Error>) {
|
|
45
|
+
stateLock.lock()
|
|
46
|
+
guard !isResolved, let continuation else {
|
|
47
|
+
stateLock.unlock()
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
isResolved = true
|
|
51
|
+
self.continuation = nil
|
|
52
|
+
stateLock.unlock()
|
|
53
|
+
|
|
54
|
+
switch result {
|
|
55
|
+
case let .success(url):
|
|
56
|
+
continuation.resume(returning: url)
|
|
57
|
+
case let .failure(error):
|
|
58
|
+
continuation.resume(throwing: error)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
func urlSession(
|
|
63
|
+
_ session: URLSession,
|
|
64
|
+
downloadTask: URLSessionDownloadTask,
|
|
65
|
+
didFinishDownloadingTo location: URL
|
|
66
|
+
) {
|
|
67
|
+
// Check HTTP status before treating the file as a valid download
|
|
68
|
+
if let httpResponse = downloadTask.response as? HTTPURLResponse,
|
|
69
|
+
!(200..<300).contains(httpResponse.statusCode) {
|
|
70
|
+
resolve(.failure(NSError(
|
|
71
|
+
domain: "Downloader",
|
|
72
|
+
code: httpResponse.statusCode,
|
|
73
|
+
userInfo: [NSLocalizedDescriptionKey: "Download failed with HTTP \(httpResponse.statusCode)"]
|
|
74
|
+
)))
|
|
75
|
+
session.finishTasksAndInvalidate()
|
|
76
|
+
self.session = nil
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let temporaryZip = FileManager.default.temporaryDirectory
|
|
81
|
+
.appendingPathComponent("updatekit-\(UUID().uuidString).zip")
|
|
82
|
+
|
|
83
|
+
do {
|
|
84
|
+
try FileManager.default.moveItem(at: location, to: temporaryZip)
|
|
85
|
+
resolve(.success(temporaryZip))
|
|
86
|
+
} catch {
|
|
87
|
+
resolve(.failure(error))
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
session.finishTasksAndInvalidate()
|
|
91
|
+
self.session = nil
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
func urlSession(
|
|
95
|
+
_ session: URLSession,
|
|
96
|
+
downloadTask: URLSessionDownloadTask,
|
|
97
|
+
didWriteData bytesWritten: Int64,
|
|
98
|
+
totalBytesWritten: Int64,
|
|
99
|
+
totalBytesExpectedToWrite: Int64
|
|
100
|
+
) {
|
|
101
|
+
guard totalBytesExpectedToWrite > 0 else {
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
let percent = (Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)) * 100
|
|
105
|
+
progressHandler(percent, totalBytesWritten, totalBytesExpectedToWrite)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
func urlSession(
|
|
109
|
+
_ session: URLSession,
|
|
110
|
+
task: URLSessionTask,
|
|
111
|
+
didCompleteWithError error: Error?
|
|
112
|
+
) {
|
|
113
|
+
if let error {
|
|
114
|
+
resolve(.failure(error))
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
session.finishTasksAndInvalidate()
|
|
118
|
+
self.session = nil
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import CryptoKit
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
4
|
+
enum HashUtilsError: Error {
|
|
5
|
+
case couldNotOpenFile
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
enum HashUtils {
|
|
9
|
+
static func sha256(fileURL: URL) throws -> String {
|
|
10
|
+
guard let handle = try? FileHandle(forReadingFrom: fileURL) else {
|
|
11
|
+
throw HashUtilsError.couldNotOpenFile
|
|
12
|
+
}
|
|
13
|
+
defer { try? handle.close() }
|
|
14
|
+
|
|
15
|
+
var hasher = SHA256()
|
|
16
|
+
|
|
17
|
+
while autoreleasepool(invoking: {
|
|
18
|
+
let data = handle.readData(ofLength: 1024 * 1024)
|
|
19
|
+
if data.isEmpty {
|
|
20
|
+
return false
|
|
21
|
+
}
|
|
22
|
+
hasher.update(data: data)
|
|
23
|
+
return true
|
|
24
|
+
}) {}
|
|
25
|
+
|
|
26
|
+
return hasher.finalize().map { String(format: "%02x", $0) }.joined()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
static func verify(fileURL: URL, expectedSha256: String) throws -> Bool {
|
|
30
|
+
let actual = try sha256(fileURL: fileURL)
|
|
31
|
+
return actual.lowercased() == expectedSha256.lowercased()
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
enum HostedManifestKeys {
|
|
4
|
+
static let managedServerURL = "https://www.otakit.app/api/v1"
|
|
5
|
+
|
|
6
|
+
static let defaults: [(kid: String, key: Data)] = [
|
|
7
|
+
(
|
|
8
|
+
kid: "hosted-2026-04-02-ce611e6d",
|
|
9
|
+
key: Data(
|
|
10
|
+
base64Encoded: "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAELg6eAj2+7aZ1FJnYUNMjOtWuQLJMomXkPvmeTQ3gXyabpLTDX0m3iWYO3cEOXqIR6NphGC6csS2T5bCtXwIBFw=="
|
|
11
|
+
)!
|
|
12
|
+
)
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
static func matchesManagedServer(_ updateUrl: String) -> Bool {
|
|
16
|
+
let normalized = updateUrl
|
|
17
|
+
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
18
|
+
.replacingOccurrences(of: "/+$", with: "", options: .regularExpression)
|
|
19
|
+
.lowercased()
|
|
20
|
+
return normalized == managedServerURL
|
|
21
|
+
|| normalized == "https://otakit.app/api/v1"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
struct LatestManifest {
|
|
4
|
+
let version: String
|
|
5
|
+
let url: String
|
|
6
|
+
let sha256: String
|
|
7
|
+
let size: Int
|
|
8
|
+
let minNativeBuild: Int?
|
|
9
|
+
let releaseId: String?
|
|
10
|
+
let signature: ManifestSignature?
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
struct ManifestSignature {
|
|
14
|
+
let kid: String
|
|
15
|
+
let sig: String
|
|
16
|
+
let iat: Int
|
|
17
|
+
let exp: Int
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
enum ManifestClientError: Error {
|
|
21
|
+
case invalidURL
|
|
22
|
+
case invalidResponse
|
|
23
|
+
case requestFailed(String)
|
|
24
|
+
case insecureURL(String)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
enum ManifestClient {
|
|
28
|
+
|
|
29
|
+
static func requireHTTPS(url: URL, allowInsecure: Bool) throws {
|
|
30
|
+
let scheme = url.scheme?.lowercased() ?? ""
|
|
31
|
+
if scheme == "https" { return }
|
|
32
|
+
if allowInsecure {
|
|
33
|
+
let host = url.host?.lowercased() ?? ""
|
|
34
|
+
if host == "localhost" || host == "127.0.0.1" { return }
|
|
35
|
+
}
|
|
36
|
+
throw ManifestClientError.insecureURL(
|
|
37
|
+
"URL must use HTTPS: \(url.absoluteString)"
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static func fetchLatest(
|
|
42
|
+
updateUrl: String,
|
|
43
|
+
appId: String,
|
|
44
|
+
channel: String?,
|
|
45
|
+
currentVersion: String,
|
|
46
|
+
currentReleaseId: String?,
|
|
47
|
+
nativeBuild: String,
|
|
48
|
+
platform: String,
|
|
49
|
+
allowInsecureUrls: Bool = false,
|
|
50
|
+
manifestKeys: [ManifestKey] = []
|
|
51
|
+
) async throws -> LatestManifest? {
|
|
52
|
+
let sanitizedBase = updateUrl.replacingOccurrences(
|
|
53
|
+
of: "/+$",
|
|
54
|
+
with: "",
|
|
55
|
+
options: .regularExpression
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
guard let baseURL = URL(string: sanitizedBase) else {
|
|
59
|
+
throw ManifestClientError.invalidURL
|
|
60
|
+
}
|
|
61
|
+
let url = baseURL.appendingPathComponent("manifest")
|
|
62
|
+
|
|
63
|
+
try requireHTTPS(url: url, allowInsecure: allowInsecureUrls)
|
|
64
|
+
|
|
65
|
+
var request = URLRequest(url: url)
|
|
66
|
+
request.httpMethod = "GET"
|
|
67
|
+
request.setValue(appId, forHTTPHeaderField: "X-App-Id")
|
|
68
|
+
request.setValue(platform, forHTTPHeaderField: "X-Platform")
|
|
69
|
+
if let channel, !channel.isEmpty {
|
|
70
|
+
request.setValue(channel, forHTTPHeaderField: "X-Channel")
|
|
71
|
+
}
|
|
72
|
+
request.setValue(currentVersion, forHTTPHeaderField: "X-Current-Version")
|
|
73
|
+
if let currentReleaseId, !currentReleaseId.isEmpty {
|
|
74
|
+
request.setValue(currentReleaseId, forHTTPHeaderField: "X-Release-Id")
|
|
75
|
+
}
|
|
76
|
+
request.setValue(nativeBuild, forHTTPHeaderField: "X-Native-Build")
|
|
77
|
+
request.timeoutInterval = 30
|
|
78
|
+
|
|
79
|
+
let (data, response) = try await URLSession.shared.data(for: request)
|
|
80
|
+
guard let httpResponse = response as? HTTPURLResponse else {
|
|
81
|
+
throw ManifestClientError.invalidResponse
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if httpResponse.statusCode == 204 {
|
|
85
|
+
return nil
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
guard httpResponse.statusCode == 200 else {
|
|
89
|
+
let body = String(data: data, encoding: .utf8) ?? "unknown"
|
|
90
|
+
throw ManifestClientError.requestFailed(
|
|
91
|
+
"HTTP \(httpResponse.statusCode): \(body)"
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
guard
|
|
96
|
+
let object = try JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
97
|
+
let version = object["version"] as? String,
|
|
98
|
+
let downloadUrl = object["url"] as? String,
|
|
99
|
+
let sha256 = object["sha256"] as? String,
|
|
100
|
+
let size = object["size"] as? Int
|
|
101
|
+
else {
|
|
102
|
+
throw ManifestClientError.invalidResponse
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
var minNativeBuild: Int?
|
|
106
|
+
if let numeric = object["minNativeBuild"] as? NSNumber {
|
|
107
|
+
minNativeBuild = numeric.intValue
|
|
108
|
+
} else if let stringValue = object["minNativeBuild"] as? String {
|
|
109
|
+
minNativeBuild = Int(stringValue)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
var signature: ManifestSignature?
|
|
113
|
+
if let sigObj = object["signature"] as? [String: Any],
|
|
114
|
+
let kid = sigObj["kid"] as? String,
|
|
115
|
+
let sig = sigObj["sig"] as? String,
|
|
116
|
+
let iat = sigObj["iat"] as? Int,
|
|
117
|
+
let exp = sigObj["exp"] as? Int {
|
|
118
|
+
signature = ManifestSignature(kid: kid, sig: sig, iat: iat, exp: exp)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let releaseId = object["releaseId"] as? String
|
|
122
|
+
|
|
123
|
+
// Validate download URL scheme
|
|
124
|
+
guard let dlURL = URL(string: downloadUrl) else {
|
|
125
|
+
throw ManifestClientError.invalidURL
|
|
126
|
+
}
|
|
127
|
+
try requireHTTPS(url: dlURL, allowInsecure: allowInsecureUrls)
|
|
128
|
+
|
|
129
|
+
if manifestKeys.isEmpty {
|
|
130
|
+
print("[UpdateKit] WARNING: No manifest signing keys configured — signature verification is disabled for this request.")
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Verify manifest signature if signing keys are configured
|
|
134
|
+
if !manifestKeys.isEmpty {
|
|
135
|
+
guard let sig = signature else {
|
|
136
|
+
throw ManifestVerifierError.missingSignature
|
|
137
|
+
}
|
|
138
|
+
try ManifestVerifier.verify(
|
|
139
|
+
appId: appId,
|
|
140
|
+
channel: channel,
|
|
141
|
+
platform: platform,
|
|
142
|
+
version: version,
|
|
143
|
+
sha256: sha256,
|
|
144
|
+
size: size,
|
|
145
|
+
minNativeBuild: minNativeBuild,
|
|
146
|
+
signature: sig,
|
|
147
|
+
trustedKeys: manifestKeys
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return LatestManifest(
|
|
152
|
+
version: version,
|
|
153
|
+
url: downloadUrl,
|
|
154
|
+
sha256: sha256,
|
|
155
|
+
size: size,
|
|
156
|
+
minNativeBuild: minNativeBuild,
|
|
157
|
+
releaseId: releaseId,
|
|
158
|
+
signature: signature
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import CryptoKit
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
4
|
+
struct ManifestKey {
|
|
5
|
+
let kid: String
|
|
6
|
+
let derData: Data
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
enum ManifestVerifierError: Error {
|
|
10
|
+
case unknownKid(String)
|
|
11
|
+
case expired
|
|
12
|
+
case invalidSignature
|
|
13
|
+
case missingSignature
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
enum ManifestVerifier {
|
|
17
|
+
|
|
18
|
+
/// Verify a manifest signature using ES256 (ECDSA P-256 + SHA-256).
|
|
19
|
+
///
|
|
20
|
+
/// - Parameters:
|
|
21
|
+
/// - appId, channel, platform: Request context (known by plugin).
|
|
22
|
+
/// - version, sha256, size, minNativeBuild: Response fields.
|
|
23
|
+
/// - signature: The signature object from the manifest response.
|
|
24
|
+
/// - trustedKeys: Array of verification keys configured in the plugin.
|
|
25
|
+
///
|
|
26
|
+
/// - Throws: `ManifestVerifierError` on failure.
|
|
27
|
+
static func verify(
|
|
28
|
+
appId: String,
|
|
29
|
+
channel: String?,
|
|
30
|
+
platform: String,
|
|
31
|
+
version: String,
|
|
32
|
+
sha256: String,
|
|
33
|
+
size: Int,
|
|
34
|
+
minNativeBuild: Int?,
|
|
35
|
+
signature: ManifestSignature,
|
|
36
|
+
trustedKeys: [ManifestKey]
|
|
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
|
+
let payload = buildCanonicalPayload(
|
|
51
|
+
appId: appId,
|
|
52
|
+
channel: channel,
|
|
53
|
+
platform: platform,
|
|
54
|
+
version: version,
|
|
55
|
+
sha256: sha256,
|
|
56
|
+
size: size,
|
|
57
|
+
minNativeBuild: minNativeBuild,
|
|
58
|
+
kid: signature.kid,
|
|
59
|
+
iat: signature.iat,
|
|
60
|
+
exp: signature.exp
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
// Decode base64url signature
|
|
64
|
+
guard let sigData = base64UrlDecode(signature.sig) else {
|
|
65
|
+
throw ManifestVerifierError.invalidSignature
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Verify with CryptoKit
|
|
69
|
+
let verificationKey = try P256.Signing.PublicKey(derRepresentation: keyEntry.derData)
|
|
70
|
+
let payloadData = Data(payload.utf8)
|
|
71
|
+
let ecdsaSignature = try P256.Signing.ECDSASignature(derRepresentation: sigData)
|
|
72
|
+
guard verificationKey.isValidSignature(ecdsaSignature, for: payloadData) else {
|
|
73
|
+
throw ManifestVerifierError.invalidSignature
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private static func buildCanonicalPayload(
|
|
78
|
+
appId: String,
|
|
79
|
+
channel: String?,
|
|
80
|
+
platform: String,
|
|
81
|
+
version: String,
|
|
82
|
+
sha256: String,
|
|
83
|
+
size: Int,
|
|
84
|
+
minNativeBuild: Int?,
|
|
85
|
+
kid: String,
|
|
86
|
+
iat: Int,
|
|
87
|
+
exp: Int
|
|
88
|
+
) -> String {
|
|
89
|
+
let minBuildStr = minNativeBuild.map { String($0) } ?? "null"
|
|
90
|
+
return [
|
|
91
|
+
"MANIFEST_V1",
|
|
92
|
+
"appId:\(appId)",
|
|
93
|
+
"channel:\(channel ?? "null")",
|
|
94
|
+
"platform:\(platform)",
|
|
95
|
+
"version:\(version)",
|
|
96
|
+
"sha256:\(sha256)",
|
|
97
|
+
"size:\(size)",
|
|
98
|
+
"minNativeBuild:\(minBuildStr)",
|
|
99
|
+
"kid:\(kid)",
|
|
100
|
+
"iat:\(iat)",
|
|
101
|
+
"exp:\(exp)",
|
|
102
|
+
].joined(separator: "\n")
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private static func base64UrlDecode(_ string: String) -> Data? {
|
|
106
|
+
var base64 = string
|
|
107
|
+
.replacingOccurrences(of: "-", with: "+")
|
|
108
|
+
.replacingOccurrences(of: "_", with: "/")
|
|
109
|
+
let remainder = base64.count % 4
|
|
110
|
+
if remainder > 0 {
|
|
111
|
+
base64 += String(repeating: "=", count: 4 - remainder)
|
|
112
|
+
}
|
|
113
|
+
return Data(base64Encoded: base64)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
enum StatsAction: String {
|
|
4
|
+
case downloaded
|
|
5
|
+
case applied
|
|
6
|
+
case downloadError = "download_error"
|
|
7
|
+
case rollback
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
enum StatsClient {
|
|
11
|
+
static func send(
|
|
12
|
+
updateUrl: String,
|
|
13
|
+
appId: String,
|
|
14
|
+
platform: String,
|
|
15
|
+
action: StatsAction,
|
|
16
|
+
bundleVersion: String?,
|
|
17
|
+
channel: String?,
|
|
18
|
+
releaseId: String?,
|
|
19
|
+
nativeBuild: String?,
|
|
20
|
+
errorMessage: String?
|
|
21
|
+
) {
|
|
22
|
+
let sanitizedBase = updateUrl.replacingOccurrences(
|
|
23
|
+
of: "/+$",
|
|
24
|
+
with: "",
|
|
25
|
+
options: .regularExpression
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
guard let url = URL(string: "\(sanitizedBase)/stats") else {
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
var payload: [String: Any] = [
|
|
33
|
+
"platform": platform,
|
|
34
|
+
"action": action.rawValue,
|
|
35
|
+
]
|
|
36
|
+
if let bundleVersion {
|
|
37
|
+
payload["bundleVersion"] = bundleVersion
|
|
38
|
+
}
|
|
39
|
+
if let channel, !channel.isEmpty {
|
|
40
|
+
payload["channel"] = channel
|
|
41
|
+
}
|
|
42
|
+
if let releaseId, !releaseId.isEmpty {
|
|
43
|
+
payload["releaseId"] = releaseId
|
|
44
|
+
}
|
|
45
|
+
if let nativeBuild {
|
|
46
|
+
payload["nativeBuild"] = nativeBuild
|
|
47
|
+
}
|
|
48
|
+
if let errorMessage {
|
|
49
|
+
payload["errorMessage"] = String(errorMessage.prefix(500))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
guard let body = try? JSONSerialization.data(withJSONObject: payload) else {
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
|
|
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
|
+
// Fire and forget - don't block on response
|
|
64
|
+
URLSession.shared.dataTask(with: request).resume()
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#import <Capacitor/Capacitor.h>
|
|
2
|
+
|
|
3
|
+
CAP_PLUGIN(UpdaterPlugin, "OtaKit",
|
|
4
|
+
CAP_PLUGIN_METHOD(check, CAPPluginReturnPromise);
|
|
5
|
+
CAP_PLUGIN_METHOD(download, CAPPluginReturnPromise);
|
|
6
|
+
CAP_PLUGIN_METHOD(apply, CAPPluginReturnPromise);
|
|
7
|
+
CAP_PLUGIN_METHOD(debugGetState, CAPPluginReturnPromise);
|
|
8
|
+
CAP_PLUGIN_METHOD(debugCheck, CAPPluginReturnPromise);
|
|
9
|
+
CAP_PLUGIN_METHOD(debugDownload, CAPPluginReturnPromise);
|
|
10
|
+
CAP_PLUGIN_METHOD(notifyAppReady, CAPPluginReturnPromise);
|
|
11
|
+
CAP_PLUGIN_METHOD(debugReset, CAPPluginReturnPromise);
|
|
12
|
+
CAP_PLUGIN_METHOD(debugListBundles, CAPPluginReturnPromise);
|
|
13
|
+
CAP_PLUGIN_METHOD(debugDeleteBundle, CAPPluginReturnPromise);
|
|
14
|
+
CAP_PLUGIN_METHOD(debugGetLastFailure, CAPPluginReturnPromise);
|
|
15
|
+
)
|