@otakit/capacitor-updater 1.1.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 +18 -0
- package/android/src/main/java/com/updatekit/updater/BundleInfo.java +34 -2
- package/android/src/main/java/com/updatekit/updater/BundleStore.java +9 -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 +62 -6
- package/dist/esm/definitions.d.ts +6 -2
- 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 +55 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,6 +10,7 @@ Capacitor OTA updater plugin for OtaKit.
|
|
|
10
10
|
- activates them on the next launch, next resume, or immediately
|
|
11
11
|
- checks for updates on cold start and app resume (configurable interval)
|
|
12
12
|
- also supports fully manual update prompts when the app wants control
|
|
13
|
+
- supports optional `runtimeVersion` lanes for native compatibility boundaries
|
|
13
14
|
- requires `notifyAppReady()` as the success handshake
|
|
14
15
|
- rolls back automatically if the new bundle does not prove healthy
|
|
15
16
|
|
|
@@ -46,6 +47,7 @@ plugins: {
|
|
|
46
47
|
appReadyTimeout: 10000,
|
|
47
48
|
// Optional:
|
|
48
49
|
// channel: "staging",
|
|
50
|
+
// runtimeVersion: "2026.04",
|
|
49
51
|
// updateMode: "next-resume",
|
|
50
52
|
// updateMode: "manual",
|
|
51
53
|
// updateMode: "immediate",
|
|
@@ -62,6 +64,22 @@ Advanced overrides for self-hosting or custom trust only:
|
|
|
62
64
|
Hosted OtaKit already points at `https://otakit.app/api/v1` and already trusts
|
|
63
65
|
the managed manifest signing keys.
|
|
64
66
|
|
|
67
|
+
## Channels vs runtimeVersion
|
|
68
|
+
|
|
69
|
+
- `channel` answers "who should get this rollout?"
|
|
70
|
+
- `runtimeVersion` answers "which native app shell can safely run this bundle?"
|
|
71
|
+
|
|
72
|
+
Use channels for rollout tracks such as `beta`, `staging`, or `production`.
|
|
73
|
+
|
|
74
|
+
Use `runtimeVersion` when a new store build creates a new compatibility boundary and you do not
|
|
75
|
+
want devices on that new native shell to keep receiving older OTA bundles.
|
|
76
|
+
|
|
77
|
+
When `runtimeVersion` is set:
|
|
78
|
+
|
|
79
|
+
- the plugin requests bundle updates only for that runtime lane
|
|
80
|
+
- bundle uploads inherit the same runtime value automatically through the CLI
|
|
81
|
+
- releases stay simple: publish the bundle, and it naturally stays inside its own runtime lane
|
|
82
|
+
|
|
65
83
|
## Trust model
|
|
66
84
|
|
|
67
85
|
The plugin does not just download from a URL and trust the result.
|
|
@@ -8,6 +8,7 @@ class BundleInfo {
|
|
|
8
8
|
|
|
9
9
|
final String id;
|
|
10
10
|
final String version;
|
|
11
|
+
final String runtimeVersion;
|
|
11
12
|
final BundleStatus status;
|
|
12
13
|
final Long downloadedAt;
|
|
13
14
|
final String sha256;
|
|
@@ -18,6 +19,7 @@ class BundleInfo {
|
|
|
18
19
|
BundleInfo(
|
|
19
20
|
String id,
|
|
20
21
|
String version,
|
|
22
|
+
String runtimeVersion,
|
|
21
23
|
BundleStatus status,
|
|
22
24
|
Long downloadedAt,
|
|
23
25
|
String sha256,
|
|
@@ -27,6 +29,7 @@ class BundleInfo {
|
|
|
27
29
|
) {
|
|
28
30
|
this.id = id;
|
|
29
31
|
this.version = version;
|
|
32
|
+
this.runtimeVersion = runtimeVersion;
|
|
30
33
|
this.status = status;
|
|
31
34
|
this.downloadedAt = downloadedAt;
|
|
32
35
|
this.sha256 = sha256;
|
|
@@ -43,6 +46,9 @@ class BundleInfo {
|
|
|
43
46
|
JSObject object = new JSObject();
|
|
44
47
|
object.put("id", id);
|
|
45
48
|
object.put("version", version);
|
|
49
|
+
if (runtimeVersion != null) {
|
|
50
|
+
object.put("runtimeVersion", runtimeVersion);
|
|
51
|
+
}
|
|
46
52
|
object.put("status", status.value());
|
|
47
53
|
if (downloadedAt != null) {
|
|
48
54
|
object.put("downloadedAt", DateUtils.toIsoString(downloadedAt));
|
|
@@ -63,6 +69,9 @@ class BundleInfo {
|
|
|
63
69
|
JSONObject object = new JSONObject();
|
|
64
70
|
object.put("id", id);
|
|
65
71
|
object.put("version", version);
|
|
72
|
+
if (runtimeVersion != null) {
|
|
73
|
+
object.put("runtimeVersion", runtimeVersion);
|
|
74
|
+
}
|
|
66
75
|
object.put("status", status.value());
|
|
67
76
|
if (downloadedAt != null) {
|
|
68
77
|
object.put("downloadedAt", downloadedAt);
|
|
@@ -85,16 +94,39 @@ class BundleInfo {
|
|
|
85
94
|
static BundleInfo fromJSONObject(JSONObject object) {
|
|
86
95
|
String id = object.optString("id", "builtin");
|
|
87
96
|
String version = object.optString("version", "0.0.0");
|
|
97
|
+
String runtimeVersion = object.has("runtimeVersion")
|
|
98
|
+
? object.optString("runtimeVersion", null)
|
|
99
|
+
: null;
|
|
88
100
|
BundleStatus status = BundleStatus.from(object.optString("status", "pending"));
|
|
89
101
|
Long downloadedAt = object.has("downloadedAt") ? object.optLong("downloadedAt") : null;
|
|
90
102
|
String sha256 = object.has("sha256") ? object.optString("sha256", null) : null;
|
|
91
103
|
String channel = object.has("channel") ? object.optString("channel", null) : null;
|
|
92
104
|
String releaseId = object.has("releaseId") ? object.optString("releaseId", null) : null;
|
|
93
105
|
String path = object.has("path") ? object.optString("path", null) : null;
|
|
94
|
-
return new BundleInfo(
|
|
106
|
+
return new BundleInfo(
|
|
107
|
+
id,
|
|
108
|
+
version,
|
|
109
|
+
runtimeVersion,
|
|
110
|
+
status,
|
|
111
|
+
downloadedAt,
|
|
112
|
+
sha256,
|
|
113
|
+
path,
|
|
114
|
+
channel,
|
|
115
|
+
releaseId
|
|
116
|
+
);
|
|
95
117
|
}
|
|
96
118
|
|
|
97
119
|
BundleInfo withStatus(BundleStatus nextStatus) {
|
|
98
|
-
return new BundleInfo(
|
|
120
|
+
return new BundleInfo(
|
|
121
|
+
id,
|
|
122
|
+
version,
|
|
123
|
+
runtimeVersion,
|
|
124
|
+
nextStatus,
|
|
125
|
+
downloadedAt,
|
|
126
|
+
sha256,
|
|
127
|
+
path,
|
|
128
|
+
channel,
|
|
129
|
+
releaseId
|
|
130
|
+
);
|
|
99
131
|
}
|
|
100
132
|
}
|
|
@@ -27,12 +27,14 @@ final class BundleStore {
|
|
|
27
27
|
private final File bundlesDirectory;
|
|
28
28
|
private final String builtinVersion;
|
|
29
29
|
private final String nativeBuild;
|
|
30
|
+
private final String appRuntimeVersion;
|
|
30
31
|
|
|
31
|
-
BundleStore(Context context, String builtinVersion, String nativeBuild) {
|
|
32
|
+
BundleStore(Context context, String builtinVersion, String nativeBuild, String appRuntimeVersion) {
|
|
32
33
|
this.context = context.getApplicationContext();
|
|
33
34
|
this.prefs = this.context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
|
34
35
|
this.builtinVersion = builtinVersion;
|
|
35
36
|
this.nativeBuild = nativeBuild;
|
|
37
|
+
this.appRuntimeVersion = appRuntimeVersion;
|
|
36
38
|
this.bundlesDirectory = new File(this.context.getFilesDir(), "otakit_bundles");
|
|
37
39
|
if (!bundlesDirectory.exists()) {
|
|
38
40
|
//noinspection ResultOfMethodCallIgnored
|
|
@@ -56,6 +58,7 @@ final class BundleStore {
|
|
|
56
58
|
return new BundleInfo(
|
|
57
59
|
"builtin",
|
|
58
60
|
builtinVersion,
|
|
61
|
+
appRuntimeVersion,
|
|
59
62
|
BundleStatus.BUILTIN,
|
|
60
63
|
null,
|
|
61
64
|
null,
|
|
@@ -229,7 +232,7 @@ final class BundleStore {
|
|
|
229
232
|
}
|
|
230
233
|
}
|
|
231
234
|
|
|
232
|
-
synchronized
|
|
235
|
+
synchronized List<BundleInfo> listDownloadedBundleInfos() {
|
|
233
236
|
List<BundleInfo> result = new ArrayList<>();
|
|
234
237
|
File[] entries = bundlesDirectory.listFiles();
|
|
235
238
|
if (entries != null) {
|
|
@@ -241,7 +244,11 @@ final class BundleStore {
|
|
|
241
244
|
}
|
|
242
245
|
}
|
|
243
246
|
}
|
|
247
|
+
return result;
|
|
248
|
+
}
|
|
244
249
|
|
|
250
|
+
synchronized JSArray listDownloadedBundles() {
|
|
251
|
+
List<BundleInfo> result = listDownloadedBundleInfos();
|
|
245
252
|
JSArray array = new JSArray();
|
|
246
253
|
for (BundleInfo bundle : result) {
|
|
247
254
|
array.put(bundle.toJSObject());
|
|
@@ -30,26 +30,23 @@ final class ManifestClient {
|
|
|
30
30
|
final String url;
|
|
31
31
|
final String sha256;
|
|
32
32
|
final int size;
|
|
33
|
-
final
|
|
33
|
+
final String runtimeVersion;
|
|
34
34
|
final String releaseId;
|
|
35
|
-
final ManifestSignature signature;
|
|
36
35
|
|
|
37
36
|
LatestManifest(
|
|
38
37
|
String version,
|
|
39
38
|
String url,
|
|
40
39
|
String sha256,
|
|
41
40
|
int size,
|
|
42
|
-
|
|
43
|
-
String releaseId
|
|
44
|
-
ManifestSignature signature
|
|
41
|
+
String runtimeVersion,
|
|
42
|
+
String releaseId
|
|
45
43
|
) {
|
|
46
44
|
this.version = version;
|
|
47
45
|
this.url = url;
|
|
48
46
|
this.sha256 = sha256;
|
|
49
47
|
this.size = size;
|
|
50
|
-
this.
|
|
48
|
+
this.runtimeVersion = runtimeVersion;
|
|
51
49
|
this.releaseId = releaseId;
|
|
52
|
-
this.signature = signature;
|
|
53
50
|
}
|
|
54
51
|
}
|
|
55
52
|
|
|
@@ -71,7 +68,7 @@ final class ManifestClient {
|
|
|
71
68
|
String channel,
|
|
72
69
|
String currentVersion,
|
|
73
70
|
String currentReleaseId,
|
|
74
|
-
String
|
|
71
|
+
String runtimeVersion,
|
|
75
72
|
String platform,
|
|
76
73
|
boolean allowInsecureUrls,
|
|
77
74
|
java.util.List<ManifestVerifier.KeyEntry> manifestKeys
|
|
@@ -93,7 +90,9 @@ final class ManifestClient {
|
|
|
93
90
|
if (currentReleaseId != null && !currentReleaseId.trim().isEmpty()) {
|
|
94
91
|
connection.setRequestProperty("X-Release-Id", currentReleaseId);
|
|
95
92
|
}
|
|
96
|
-
|
|
93
|
+
if (runtimeVersion != null && !runtimeVersion.trim().isEmpty()) {
|
|
94
|
+
connection.setRequestProperty("X-Runtime-Version", runtimeVersion);
|
|
95
|
+
}
|
|
97
96
|
connection.setConnectTimeout(15_000);
|
|
98
97
|
connection.setReadTimeout(30_000);
|
|
99
98
|
|
|
@@ -118,31 +117,15 @@ final class ManifestClient {
|
|
|
118
117
|
String sha256 = json.getString("sha256");
|
|
119
118
|
int size = json.getInt("size");
|
|
120
119
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
} else if (raw instanceof String) {
|
|
127
|
-
String value = ((String) raw).trim();
|
|
128
|
-
if (!value.isEmpty()) {
|
|
129
|
-
minNativeBuild = Integer.parseInt(value);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
120
|
+
String responseRuntimeVersion = json.has("runtimeVersion") && !json.isNull("runtimeVersion")
|
|
121
|
+
? json.getString("runtimeVersion").trim()
|
|
122
|
+
: null;
|
|
123
|
+
if (responseRuntimeVersion != null && responseRuntimeVersion.isEmpty()) {
|
|
124
|
+
responseRuntimeVersion = null;
|
|
132
125
|
}
|
|
133
126
|
|
|
134
|
-
ManifestSignature signature =
|
|
135
|
-
|
|
136
|
-
JSONObject sigObj = json.getJSONObject("signature");
|
|
137
|
-
if (sigObj.has("kid") && sigObj.has("sig") && sigObj.has("iat") && sigObj.has("exp")) {
|
|
138
|
-
signature = new ManifestSignature(
|
|
139
|
-
sigObj.getString("kid"),
|
|
140
|
-
sigObj.getString("sig"),
|
|
141
|
-
sigObj.getInt("iat"),
|
|
142
|
-
sigObj.getInt("exp")
|
|
143
|
-
);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
127
|
+
ManifestSignature signature = parseSignature(json.optJSONObject("signature"));
|
|
128
|
+
ManifestSignature signatureV2 = parseSignature(json.optJSONObject("signatureV2"));
|
|
146
129
|
|
|
147
130
|
String releaseId = null;
|
|
148
131
|
if (json.has("releaseId") && !json.isNull("releaseId")) {
|
|
@@ -161,22 +144,34 @@ final class ManifestClient {
|
|
|
161
144
|
|
|
162
145
|
// Verify manifest signature if signing keys are configured
|
|
163
146
|
if (manifestKeys != null && !manifestKeys.isEmpty()) {
|
|
164
|
-
if (
|
|
147
|
+
if (signatureV2 != null) {
|
|
148
|
+
ManifestVerifier.verify(
|
|
149
|
+
appId,
|
|
150
|
+
channel,
|
|
151
|
+
platform,
|
|
152
|
+
version,
|
|
153
|
+
sha256,
|
|
154
|
+
size,
|
|
155
|
+
responseRuntimeVersion,
|
|
156
|
+
signatureV2,
|
|
157
|
+
manifestKeys
|
|
158
|
+
);
|
|
159
|
+
} else if (signature != null) {
|
|
160
|
+
ManifestVerifier.verifyLegacy(
|
|
161
|
+
appId,
|
|
162
|
+
channel,
|
|
163
|
+
platform,
|
|
164
|
+
version,
|
|
165
|
+
sha256,
|
|
166
|
+
size,
|
|
167
|
+
signature,
|
|
168
|
+
manifestKeys
|
|
169
|
+
);
|
|
170
|
+
} else {
|
|
165
171
|
throw new IllegalStateException(
|
|
166
172
|
"Manifest signature missing but signing keys are configured"
|
|
167
173
|
);
|
|
168
174
|
}
|
|
169
|
-
ManifestVerifier.verify(
|
|
170
|
-
appId,
|
|
171
|
-
channel,
|
|
172
|
-
platform,
|
|
173
|
-
version,
|
|
174
|
-
sha256,
|
|
175
|
-
size,
|
|
176
|
-
minNativeBuild,
|
|
177
|
-
signature,
|
|
178
|
-
manifestKeys
|
|
179
|
-
);
|
|
180
175
|
}
|
|
181
176
|
|
|
182
177
|
return new LatestManifest(
|
|
@@ -184,9 +179,8 @@ final class ManifestClient {
|
|
|
184
179
|
downloadUrl,
|
|
185
180
|
sha256,
|
|
186
181
|
size,
|
|
187
|
-
|
|
188
|
-
releaseId
|
|
189
|
-
signature
|
|
182
|
+
responseRuntimeVersion,
|
|
183
|
+
releaseId
|
|
190
184
|
);
|
|
191
185
|
} finally {
|
|
192
186
|
connection.disconnect();
|
|
@@ -206,4 +200,19 @@ final class ManifestClient {
|
|
|
206
200
|
return new String(out.toByteArray(), StandardCharsets.UTF_8);
|
|
207
201
|
}
|
|
208
202
|
}
|
|
203
|
+
|
|
204
|
+
private static ManifestSignature parseSignature(JSONObject sigObj) {
|
|
205
|
+
if (sigObj == null) {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
if (!sigObj.has("kid") || !sigObj.has("sig") || !sigObj.has("iat") || !sigObj.has("exp")) {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
return new ManifestSignature(
|
|
212
|
+
sigObj.getString("kid"),
|
|
213
|
+
sigObj.getString("sig"),
|
|
214
|
+
sigObj.getInt("iat"),
|
|
215
|
+
sigObj.getInt("exp")
|
|
216
|
+
);
|
|
217
|
+
}
|
|
209
218
|
}
|
|
@@ -34,7 +34,51 @@ final class ManifestVerifier {
|
|
|
34
34
|
String version,
|
|
35
35
|
String sha256,
|
|
36
36
|
int size,
|
|
37
|
-
|
|
37
|
+
String runtimeVersion,
|
|
38
|
+
ManifestClient.ManifestSignature signature,
|
|
39
|
+
List<KeyEntry> trustedKeys
|
|
40
|
+
) throws Exception {
|
|
41
|
+
String payload = buildCanonicalPayload(
|
|
42
|
+
appId,
|
|
43
|
+
channel,
|
|
44
|
+
platform,
|
|
45
|
+
version,
|
|
46
|
+
sha256,
|
|
47
|
+
size,
|
|
48
|
+
runtimeVersion,
|
|
49
|
+
signature.kid,
|
|
50
|
+
signature.iat,
|
|
51
|
+
signature.exp
|
|
52
|
+
);
|
|
53
|
+
verifyPayload(payload, signature, trustedKeys);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
static void verifyLegacy(
|
|
57
|
+
String appId,
|
|
58
|
+
String channel,
|
|
59
|
+
String platform,
|
|
60
|
+
String version,
|
|
61
|
+
String sha256,
|
|
62
|
+
int size,
|
|
63
|
+
ManifestClient.ManifestSignature signature,
|
|
64
|
+
List<KeyEntry> trustedKeys
|
|
65
|
+
) throws Exception {
|
|
66
|
+
String payload = buildLegacyCanonicalPayload(
|
|
67
|
+
appId,
|
|
68
|
+
channel,
|
|
69
|
+
platform,
|
|
70
|
+
version,
|
|
71
|
+
sha256,
|
|
72
|
+
size,
|
|
73
|
+
signature.kid,
|
|
74
|
+
signature.iat,
|
|
75
|
+
signature.exp
|
|
76
|
+
);
|
|
77
|
+
verifyPayload(payload, signature, trustedKeys);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private static void verifyPayload(
|
|
81
|
+
String payload,
|
|
38
82
|
ManifestClient.ManifestSignature signature,
|
|
39
83
|
List<KeyEntry> trustedKeys
|
|
40
84
|
) throws Exception {
|
|
@@ -56,20 +100,6 @@ final class ManifestVerifier {
|
|
|
56
100
|
throw new IllegalStateException("Unknown signing key ID: " + signature.kid);
|
|
57
101
|
}
|
|
58
102
|
|
|
59
|
-
// Build canonical payload (must match server exactly)
|
|
60
|
-
String payload = buildCanonicalPayload(
|
|
61
|
-
appId,
|
|
62
|
-
channel,
|
|
63
|
-
platform,
|
|
64
|
-
version,
|
|
65
|
-
sha256,
|
|
66
|
-
size,
|
|
67
|
-
minNativeBuild,
|
|
68
|
-
signature.kid,
|
|
69
|
-
signature.iat,
|
|
70
|
-
signature.exp
|
|
71
|
-
);
|
|
72
|
-
|
|
73
103
|
// Decode base64url signature
|
|
74
104
|
byte[] sigBytes = base64UrlDecode(signature.sig);
|
|
75
105
|
|
|
@@ -94,12 +124,56 @@ final class ManifestVerifier {
|
|
|
94
124
|
String version,
|
|
95
125
|
String sha256,
|
|
96
126
|
int size,
|
|
97
|
-
|
|
127
|
+
String runtimeVersion,
|
|
128
|
+
String kid,
|
|
129
|
+
int iat,
|
|
130
|
+
int exp
|
|
131
|
+
) {
|
|
132
|
+
return (
|
|
133
|
+
"MANIFEST_V2\n" +
|
|
134
|
+
"appId:" +
|
|
135
|
+
appId +
|
|
136
|
+
"\n" +
|
|
137
|
+
"channel:" +
|
|
138
|
+
(channel != null ? channel : "null") +
|
|
139
|
+
"\n" +
|
|
140
|
+
"platform:" +
|
|
141
|
+
platform +
|
|
142
|
+
"\n" +
|
|
143
|
+
"version:" +
|
|
144
|
+
version +
|
|
145
|
+
"\n" +
|
|
146
|
+
"sha256:" +
|
|
147
|
+
sha256 +
|
|
148
|
+
"\n" +
|
|
149
|
+
"size:" +
|
|
150
|
+
size +
|
|
151
|
+
"\n" +
|
|
152
|
+
"runtimeVersion:" +
|
|
153
|
+
(runtimeVersion != null ? runtimeVersion : "null") +
|
|
154
|
+
"\n" +
|
|
155
|
+
"kid:" +
|
|
156
|
+
kid +
|
|
157
|
+
"\n" +
|
|
158
|
+
"iat:" +
|
|
159
|
+
iat +
|
|
160
|
+
"\n" +
|
|
161
|
+
"exp:" +
|
|
162
|
+
exp
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private static String buildLegacyCanonicalPayload(
|
|
167
|
+
String appId,
|
|
168
|
+
String channel,
|
|
169
|
+
String platform,
|
|
170
|
+
String version,
|
|
171
|
+
String sha256,
|
|
172
|
+
int size,
|
|
98
173
|
String kid,
|
|
99
174
|
int iat,
|
|
100
175
|
int exp
|
|
101
176
|
) {
|
|
102
|
-
String minBuildStr = minNativeBuild != null ? String.valueOf(minNativeBuild) : "null";
|
|
103
177
|
return (
|
|
104
178
|
"MANIFEST_V1\n" +
|
|
105
179
|
"appId:" +
|
|
@@ -120,8 +194,7 @@ final class ManifestVerifier {
|
|
|
120
194
|
"size:" +
|
|
121
195
|
size +
|
|
122
196
|
"\n" +
|
|
123
|
-
"minNativeBuild:" +
|
|
124
|
-
minBuildStr +
|
|
197
|
+
"minNativeBuild:null" +
|
|
125
198
|
"\n" +
|
|
126
199
|
"kid:" +
|
|
127
200
|
kid +
|
|
@@ -39,6 +39,7 @@ public class UpdaterPlugin extends Plugin {
|
|
|
39
39
|
private String updateUrl;
|
|
40
40
|
private String appId;
|
|
41
41
|
private String channel;
|
|
42
|
+
private String runtimeVersion;
|
|
42
43
|
private java.util.List<ManifestVerifier.KeyEntry> manifestKeys = new java.util.ArrayList<>();
|
|
43
44
|
private long checkIntervalMs = 600_000;
|
|
44
45
|
private final AtomicBoolean checkInProgress = new AtomicBoolean(false);
|
|
@@ -71,13 +72,14 @@ public class UpdaterPlugin extends Plugin {
|
|
|
71
72
|
}
|
|
72
73
|
} catch (PackageManager.NameNotFoundException ignored) {}
|
|
73
74
|
|
|
74
|
-
this.store = new BundleStore(getContext(), builtinVersion, nativeBuild);
|
|
75
75
|
this.updateUrl = resolveUpdateUrl(
|
|
76
76
|
getConfig().getString("serverUrl"),
|
|
77
77
|
System.getenv("OTAKIT_SERVER_URL")
|
|
78
78
|
);
|
|
79
79
|
this.appId = getConfig().getString("appId");
|
|
80
80
|
this.channel = trimToNull(getConfig().getString("channel"));
|
|
81
|
+
this.runtimeVersion = trimToNull(getConfig().getString("runtimeVersion"));
|
|
82
|
+
this.store = new BundleStore(getContext(), builtinVersion, nativeBuild, this.runtimeVersion);
|
|
81
83
|
this.allowInsecureUrls = getConfig().getBoolean("allowInsecureUrls", false);
|
|
82
84
|
String configuredUpdateMode = getConfig().getString("updateMode", UPDATE_MODE_NEXT_LAUNCH);
|
|
83
85
|
this.updateMode = resolveUpdateMode(configuredUpdateMode);
|
|
@@ -121,6 +123,8 @@ public class UpdaterPlugin extends Plugin {
|
|
|
121
123
|
this.appReadyTimeoutMs = Math.max(1000, getConfig().getInt("appReadyTimeout", 10_000));
|
|
122
124
|
this.checkIntervalMs = Math.max(600_000, getConfig().getInt("checkInterval", 600_000));
|
|
123
125
|
|
|
126
|
+
pruneIncompatibleBundles();
|
|
127
|
+
|
|
124
128
|
BundleInfo current = store.getCurrentBundle();
|
|
125
129
|
if (current.status == BundleStatus.TRIAL) {
|
|
126
130
|
rollbackCurrentBundle("app_restarted_before_notify");
|
|
@@ -290,6 +294,9 @@ public class UpdaterPlugin extends Plugin {
|
|
|
290
294
|
result.put("sha256", staged.sha256 != null ? staged.sha256 : "");
|
|
291
295
|
result.put("size", 0);
|
|
292
296
|
result.put("downloaded", true);
|
|
297
|
+
if (staged.runtimeVersion != null) {
|
|
298
|
+
result.put("runtimeVersion", staged.runtimeVersion);
|
|
299
|
+
}
|
|
293
300
|
if (staged.releaseId != null) {
|
|
294
301
|
result.put("releaseId", staged.releaseId);
|
|
295
302
|
}
|
|
@@ -513,7 +520,7 @@ public class UpdaterPlugin extends Plugin {
|
|
|
513
520
|
channel,
|
|
514
521
|
current.version,
|
|
515
522
|
current.releaseId,
|
|
516
|
-
|
|
523
|
+
runtimeVersion,
|
|
517
524
|
"android",
|
|
518
525
|
allowInsecureUrls,
|
|
519
526
|
manifestKeys
|
|
@@ -530,6 +537,12 @@ public class UpdaterPlugin extends Plugin {
|
|
|
530
537
|
return null;
|
|
531
538
|
}
|
|
532
539
|
|
|
540
|
+
if (!isCompatibleRuntime(latest.runtimeVersion)) {
|
|
541
|
+
throw new IllegalStateException(
|
|
542
|
+
"Manifest runtimeVersion does not match the installed app runtime"
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
|
|
533
546
|
BundleInfo staged = findMatchingStagedBundle(latest, targetChannel);
|
|
534
547
|
if (emitEvents) {
|
|
535
548
|
notifyListeners("updateAvailable", manifestToJSObject(latest, staged != null));
|
|
@@ -545,6 +558,7 @@ public class UpdaterPlugin extends Plugin {
|
|
|
545
558
|
latest.version,
|
|
546
559
|
latest.sha256,
|
|
547
560
|
latest.size,
|
|
561
|
+
latest.runtimeVersion,
|
|
548
562
|
targetChannel,
|
|
549
563
|
latest.releaseId
|
|
550
564
|
);
|
|
@@ -558,6 +572,7 @@ public class UpdaterPlugin extends Plugin {
|
|
|
558
572
|
refreshed.version,
|
|
559
573
|
refreshed.sha256,
|
|
560
574
|
refreshed.size,
|
|
575
|
+
refreshed.runtimeVersion,
|
|
561
576
|
targetChannel,
|
|
562
577
|
refreshed.releaseId
|
|
563
578
|
);
|
|
@@ -580,7 +595,7 @@ public class UpdaterPlugin extends Plugin {
|
|
|
580
595
|
|
|
581
596
|
private BundleInfo downloadAndStage(URL url, String version, String expectedSha256)
|
|
582
597
|
throws Exception {
|
|
583
|
-
return downloadAndStage(url, version, expectedSha256, 0, null, null);
|
|
598
|
+
return downloadAndStage(url, version, expectedSha256, 0, null, null, null);
|
|
584
599
|
}
|
|
585
600
|
|
|
586
601
|
private BundleInfo downloadAndStage(
|
|
@@ -588,6 +603,7 @@ public class UpdaterPlugin extends Plugin {
|
|
|
588
603
|
String version,
|
|
589
604
|
String expectedSha256,
|
|
590
605
|
int expectedSize,
|
|
606
|
+
String runtimeVersion,
|
|
591
607
|
String channel,
|
|
592
608
|
String releaseId
|
|
593
609
|
) throws Exception {
|
|
@@ -635,6 +651,7 @@ public class UpdaterPlugin extends Plugin {
|
|
|
635
651
|
BundleInfo info = new BundleInfo(
|
|
636
652
|
bundleId,
|
|
637
653
|
version,
|
|
654
|
+
runtimeVersion,
|
|
638
655
|
BundleStatus.PENDING,
|
|
639
656
|
System.currentTimeMillis(),
|
|
640
657
|
expectedSha256,
|
|
@@ -730,6 +747,12 @@ public class UpdaterPlugin extends Plugin {
|
|
|
730
747
|
store.setStagedBundleId(null);
|
|
731
748
|
return store.getCurrentBundle();
|
|
732
749
|
}
|
|
750
|
+
if (!isCompatibleRuntime(staged)) {
|
|
751
|
+
try {
|
|
752
|
+
store.deleteBundle(staged.id);
|
|
753
|
+
} catch (Exception ignored) {}
|
|
754
|
+
return store.getCurrentBundle();
|
|
755
|
+
}
|
|
733
756
|
|
|
734
757
|
store.setCurrentBundleId(staged.id);
|
|
735
758
|
store.setStagedBundleId(null);
|
|
@@ -747,6 +770,12 @@ public class UpdaterPlugin extends Plugin {
|
|
|
747
770
|
store.setStagedBundleId(null);
|
|
748
771
|
return;
|
|
749
772
|
}
|
|
773
|
+
if (!isCompatibleRuntime(staged)) {
|
|
774
|
+
try {
|
|
775
|
+
store.deleteBundle(staged.id);
|
|
776
|
+
} catch (Exception ignored) {}
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
750
779
|
|
|
751
780
|
store.setCurrentBundleId(staged.id);
|
|
752
781
|
store.setStagedBundleId(null);
|
|
@@ -874,12 +903,12 @@ public class UpdaterPlugin extends Plugin {
|
|
|
874
903
|
object.put("sha256", latest.sha256);
|
|
875
904
|
object.put("size", latest.size);
|
|
876
905
|
object.put("downloaded", downloaded);
|
|
906
|
+
if (latest.runtimeVersion != null) {
|
|
907
|
+
object.put("runtimeVersion", latest.runtimeVersion);
|
|
908
|
+
}
|
|
877
909
|
if (latest.releaseId != null) {
|
|
878
910
|
object.put("releaseId", latest.releaseId);
|
|
879
911
|
}
|
|
880
|
-
if (latest.minNativeBuild != null) {
|
|
881
|
-
object.put("minNativeBuild", latest.minNativeBuild);
|
|
882
|
-
}
|
|
883
912
|
return object;
|
|
884
913
|
}
|
|
885
914
|
|
|
@@ -902,6 +931,10 @@ public class UpdaterPlugin extends Plugin {
|
|
|
902
931
|
return null;
|
|
903
932
|
}
|
|
904
933
|
|
|
934
|
+
if (!java.util.Objects.equals(trimToNull(staged.runtimeVersion), trimToNull(latest.runtimeVersion))) {
|
|
935
|
+
return null;
|
|
936
|
+
}
|
|
937
|
+
|
|
905
938
|
if (
|
|
906
939
|
latest.releaseId != null &&
|
|
907
940
|
staged.releaseId != null &&
|
|
@@ -1061,6 +1094,29 @@ public class UpdaterPlugin extends Plugin {
|
|
|
1061
1094
|
);
|
|
1062
1095
|
}
|
|
1063
1096
|
|
|
1097
|
+
private void pruneIncompatibleBundles() {
|
|
1098
|
+
for (BundleInfo bundle : store.listDownloadedBundleInfos()) {
|
|
1099
|
+
if (!isCompatibleRuntime(bundle)) {
|
|
1100
|
+
try {
|
|
1101
|
+
store.deleteBundle(bundle.id);
|
|
1102
|
+
} catch (Exception ignored) {}
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
BundleInfo failed = store.getFailedBundle();
|
|
1107
|
+
if (failed != null && !isCompatibleRuntime(failed)) {
|
|
1108
|
+
store.setFailedBundle(null);
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
private boolean isCompatibleRuntime(String bundleRuntimeVersion) {
|
|
1113
|
+
return java.util.Objects.equals(trimToNull(bundleRuntimeVersion), runtimeVersion);
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
private boolean isCompatibleRuntime(BundleInfo bundle) {
|
|
1117
|
+
return isCompatibleRuntime(bundle.runtimeVersion);
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1064
1120
|
private long getFreeDiskSpace() {
|
|
1065
1121
|
try {
|
|
1066
1122
|
android.os.StatFs statFs = new android.os.StatFs(
|
|
@@ -19,6 +19,8 @@ export interface BundleInfo {
|
|
|
19
19
|
id: string;
|
|
20
20
|
/** Semantic version string */
|
|
21
21
|
version: string;
|
|
22
|
+
/** Native compatibility lane for this bundle. */
|
|
23
|
+
runtimeVersion?: string;
|
|
22
24
|
/** Current status of the bundle */
|
|
23
25
|
status: BundleStatus;
|
|
24
26
|
/** ISO timestamp when bundle was downloaded */
|
|
@@ -33,6 +35,8 @@ export interface BundleInfo {
|
|
|
33
35
|
export interface LatestVersion {
|
|
34
36
|
/** Version string */
|
|
35
37
|
version: string;
|
|
38
|
+
/** Native compatibility lane for this update. */
|
|
39
|
+
runtimeVersion?: string;
|
|
36
40
|
/** Download URL */
|
|
37
41
|
url: string;
|
|
38
42
|
/** SHA-256 checksum */
|
|
@@ -43,8 +47,6 @@ export interface LatestVersion {
|
|
|
43
47
|
downloaded?: boolean;
|
|
44
48
|
/** Release history ID associated with this manifest */
|
|
45
49
|
releaseId?: string;
|
|
46
|
-
/** Minimum native build required */
|
|
47
|
-
minNativeBuild?: number;
|
|
48
50
|
}
|
|
49
51
|
export interface BundleListResult {
|
|
50
52
|
bundles: BundleInfo[];
|
|
@@ -70,6 +72,8 @@ export interface OtaKitConfig {
|
|
|
70
72
|
appId: string;
|
|
71
73
|
/** Optional named release track. Omit to use the base channel. */
|
|
72
74
|
channel?: string;
|
|
75
|
+
/** Optional native compatibility lane. Set this when a new store build should start a new OTA line. */
|
|
76
|
+
runtimeVersion?: string;
|
|
73
77
|
/** Overall update behavior. Defaults to next-launch. */
|
|
74
78
|
updateMode?: OtaKitUpdateMode;
|
|
75
79
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"definitions.d.ts","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAE5D;;GAEG;AACH,oBAAY,YAAY;IACtB,+BAA+B;IAC/B,OAAO,YAAY;IACnB,iDAAiD;IACjD,OAAO,YAAY;IACnB,mCAAmC;IACnC,KAAK,UAAU;IACf,wBAAwB;IACxB,OAAO,YAAY;IACnB,yDAAyD;IACzD,KAAK,UAAU;CAChB;AAED,MAAM,WAAW,UAAU;IACzB,+BAA+B;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,8BAA8B;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,mCAAmC;IACnC,MAAM,EAAE,YAAY,CAAC;IACrB,+CAA+C;IAC/C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iCAAiC;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,qDAAqD;IACrD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,gEAAgE;IAChE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,aAAa;IAC5B,qBAAqB;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,mBAAmB;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,uBAAuB;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,2BAA2B;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,6DAA6D;IAC7D,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,uDAAuD;IACvD,SAAS,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"definitions.d.ts","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"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"}
|
|
@@ -3,6 +3,7 @@ import Foundation
|
|
|
3
3
|
struct BundleInfo: Codable {
|
|
4
4
|
let id: String
|
|
5
5
|
let version: String
|
|
6
|
+
let runtimeVersion: String?
|
|
6
7
|
let status: BundleStatus
|
|
7
8
|
let downloadedAt: Date?
|
|
8
9
|
let sha256: String?
|
|
@@ -21,6 +22,10 @@ struct BundleInfo: Codable {
|
|
|
21
22
|
"status": status.rawValue,
|
|
22
23
|
]
|
|
23
24
|
|
|
25
|
+
if let runtimeVersion {
|
|
26
|
+
result["runtimeVersion"] = runtimeVersion
|
|
27
|
+
}
|
|
28
|
+
|
|
24
29
|
if let downloadedAt {
|
|
25
30
|
result["downloadedAt"] = ISO8601DateFormatter().string(from: downloadedAt)
|
|
26
31
|
}
|
|
@@ -10,6 +10,7 @@ final class BundleStore {
|
|
|
10
10
|
|
|
11
11
|
private let defaults = UserDefaults.standard
|
|
12
12
|
private let fileManager = FileManager.default
|
|
13
|
+
var appRuntimeVersion: String?
|
|
13
14
|
|
|
14
15
|
private lazy var decoder: JSONDecoder = {
|
|
15
16
|
let decoder = JSONDecoder()
|
|
@@ -51,6 +52,7 @@ final class BundleStore {
|
|
|
51
52
|
BundleInfo(
|
|
52
53
|
id: "builtin",
|
|
53
54
|
version: builtinVersion,
|
|
55
|
+
runtimeVersion: appRuntimeVersion,
|
|
54
56
|
status: .builtin,
|
|
55
57
|
downloadedAt: nil,
|
|
56
58
|
sha256: nil,
|
|
@@ -202,6 +204,7 @@ final class BundleStore {
|
|
|
202
204
|
bundle = BundleInfo(
|
|
203
205
|
id: bundle.id,
|
|
204
206
|
version: bundle.version,
|
|
207
|
+
runtimeVersion: bundle.runtimeVersion,
|
|
205
208
|
status: status,
|
|
206
209
|
downloadedAt: bundle.downloadedAt,
|
|
207
210
|
sha256: bundle.sha256,
|
|
@@ -5,9 +5,8 @@ struct LatestManifest {
|
|
|
5
5
|
let url: String
|
|
6
6
|
let sha256: String
|
|
7
7
|
let size: Int
|
|
8
|
-
let
|
|
8
|
+
let runtimeVersion: String?
|
|
9
9
|
let releaseId: String?
|
|
10
|
-
let signature: ManifestSignature?
|
|
11
10
|
}
|
|
12
11
|
|
|
13
12
|
struct ManifestSignature {
|
|
@@ -44,7 +43,7 @@ enum ManifestClient {
|
|
|
44
43
|
channel: String?,
|
|
45
44
|
currentVersion: String,
|
|
46
45
|
currentReleaseId: String?,
|
|
47
|
-
|
|
46
|
+
runtimeVersion: String?,
|
|
48
47
|
platform: String,
|
|
49
48
|
allowInsecureUrls: Bool = false,
|
|
50
49
|
manifestKeys: [ManifestKey] = []
|
|
@@ -73,7 +72,9 @@ enum ManifestClient {
|
|
|
73
72
|
if let currentReleaseId, !currentReleaseId.isEmpty {
|
|
74
73
|
request.setValue(currentReleaseId, forHTTPHeaderField: "X-Release-Id")
|
|
75
74
|
}
|
|
76
|
-
|
|
75
|
+
if let runtimeVersion, !runtimeVersion.isEmpty {
|
|
76
|
+
request.setValue(runtimeVersion, forHTTPHeaderField: "X-Runtime-Version")
|
|
77
|
+
}
|
|
77
78
|
request.timeoutInterval = 30
|
|
78
79
|
|
|
79
80
|
let (data, response) = try await URLSession.shared.data(for: request)
|
|
@@ -102,21 +103,12 @@ enum ManifestClient {
|
|
|
102
103
|
throw ManifestClientError.invalidResponse
|
|
103
104
|
}
|
|
104
105
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
} else if let stringValue = object["minNativeBuild"] as? String {
|
|
109
|
-
minNativeBuild = Int(stringValue)
|
|
110
|
-
}
|
|
106
|
+
let runtimeVersion = (object["runtimeVersion"] as? String)?
|
|
107
|
+
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
108
|
+
.nilIfEmpty
|
|
111
109
|
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
}
|
|
110
|
+
let signature = parseSignature(object["signature"])
|
|
111
|
+
let signatureV2 = parseSignature(object["signatureV2"])
|
|
120
112
|
|
|
121
113
|
let releaseId = object["releaseId"] as? String
|
|
122
114
|
|
|
@@ -130,22 +122,36 @@ enum ManifestClient {
|
|
|
130
122
|
print("[UpdateKit] WARNING: No manifest signing keys configured — signature verification is disabled for this request.")
|
|
131
123
|
}
|
|
132
124
|
|
|
133
|
-
// Verify manifest signature if signing keys are configured
|
|
125
|
+
// Verify manifest signature if signing keys are configured.
|
|
126
|
+
// New manifests carry signatureV2 with runtimeVersion in the signed payload.
|
|
127
|
+
// Older servers only return the legacy signature field.
|
|
134
128
|
if !manifestKeys.isEmpty {
|
|
135
|
-
|
|
129
|
+
if let sigV2 = signatureV2 {
|
|
130
|
+
try ManifestVerifier.verify(
|
|
131
|
+
appId: appId,
|
|
132
|
+
channel: channel,
|
|
133
|
+
platform: platform,
|
|
134
|
+
version: version,
|
|
135
|
+
sha256: sha256,
|
|
136
|
+
size: size,
|
|
137
|
+
runtimeVersion: runtimeVersion,
|
|
138
|
+
signature: sigV2,
|
|
139
|
+
trustedKeys: manifestKeys
|
|
140
|
+
)
|
|
141
|
+
} else if let sig = signature {
|
|
142
|
+
try ManifestVerifier.verifyLegacy(
|
|
143
|
+
appId: appId,
|
|
144
|
+
channel: channel,
|
|
145
|
+
platform: platform,
|
|
146
|
+
version: version,
|
|
147
|
+
sha256: sha256,
|
|
148
|
+
size: size,
|
|
149
|
+
signature: sig,
|
|
150
|
+
trustedKeys: manifestKeys
|
|
151
|
+
)
|
|
152
|
+
} else {
|
|
136
153
|
throw ManifestVerifierError.missingSignature
|
|
137
154
|
}
|
|
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
155
|
}
|
|
150
156
|
|
|
151
157
|
return LatestManifest(
|
|
@@ -153,9 +159,26 @@ enum ManifestClient {
|
|
|
153
159
|
url: downloadUrl,
|
|
154
160
|
sha256: sha256,
|
|
155
161
|
size: size,
|
|
156
|
-
|
|
157
|
-
releaseId: releaseId
|
|
158
|
-
signature: signature
|
|
162
|
+
runtimeVersion: runtimeVersion,
|
|
163
|
+
releaseId: releaseId
|
|
159
164
|
)
|
|
160
165
|
}
|
|
166
|
+
|
|
167
|
+
private static func parseSignature(_ rawValue: Any?) -> ManifestSignature? {
|
|
168
|
+
guard let sigObj = rawValue as? [String: Any],
|
|
169
|
+
let kid = sigObj["kid"] as? String,
|
|
170
|
+
let sig = sigObj["sig"] as? String,
|
|
171
|
+
let iat = sigObj["iat"] as? Int,
|
|
172
|
+
let exp = sigObj["exp"] as? Int else {
|
|
173
|
+
return nil
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return ManifestSignature(kid: kid, sig: sig, iat: iat, exp: exp)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private extension String {
|
|
181
|
+
var nilIfEmpty: String? {
|
|
182
|
+
isEmpty ? nil : self
|
|
183
|
+
}
|
|
161
184
|
}
|
|
@@ -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)",
|
|
@@ -43,6 +43,7 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
43
43
|
private var updateUrl = UpdaterPlugin.defaultUpdateURL
|
|
44
44
|
private var appId: String?
|
|
45
45
|
private var channel: String?
|
|
46
|
+
private var runtimeVersion: String?
|
|
46
47
|
private var manifestKeys: [(kid: String, key: Data)] = []
|
|
47
48
|
private var trialTimeoutWorkItem: DispatchWorkItem?
|
|
48
49
|
private var checkIntervalMs: Int = 600_000
|
|
@@ -61,6 +62,8 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
61
62
|
)
|
|
62
63
|
appId = getConfig().getString("appId")
|
|
63
64
|
channel = trimToNil(getConfig().getString("channel"))
|
|
65
|
+
runtimeVersion = trimToNil(getConfig().getString("runtimeVersion"))
|
|
66
|
+
store.appRuntimeVersion = runtimeVersion
|
|
64
67
|
allowInsecureUrls = getConfig().getBoolean("allowInsecureUrls", false)
|
|
65
68
|
let updateModeRaw = getConfig().getString("updateMode", UpdateMode.nextLaunch.rawValue)
|
|
66
69
|
updateMode = resolveUpdateMode(configured: updateModeRaw)
|
|
@@ -89,6 +92,8 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
89
92
|
manifestKeys = HostedManifestKeys.defaults
|
|
90
93
|
}
|
|
91
94
|
|
|
95
|
+
pruneIncompatibleBundles()
|
|
96
|
+
|
|
92
97
|
var current = store.getCurrentBundle()
|
|
93
98
|
if current.status == .trial {
|
|
94
99
|
rollbackCurrentBundle(reason: "app_restarted_before_notify")
|
|
@@ -307,6 +312,9 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
307
312
|
"size": 0,
|
|
308
313
|
"downloaded": true,
|
|
309
314
|
]
|
|
315
|
+
if let runtimeVersion = staged.runtimeVersion {
|
|
316
|
+
payload["runtimeVersion"] = runtimeVersion
|
|
317
|
+
}
|
|
310
318
|
if let releaseId = staged.releaseId {
|
|
311
319
|
payload["releaseId"] = releaseId
|
|
312
320
|
}
|
|
@@ -503,7 +511,7 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
503
511
|
channel: channel,
|
|
504
512
|
currentVersion: current.version,
|
|
505
513
|
currentReleaseId: current.releaseId,
|
|
506
|
-
|
|
514
|
+
runtimeVersion: runtimeVersion,
|
|
507
515
|
platform: "ios",
|
|
508
516
|
allowInsecureUrls: allowInsecureUrls,
|
|
509
517
|
manifestKeys: manifestKeys.map {
|
|
@@ -526,6 +534,14 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
526
534
|
return nil
|
|
527
535
|
}
|
|
528
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
|
+
|
|
529
545
|
let staged = findMatchingStagedBundle(latest: manifest, targetChannel: targetChannel)
|
|
530
546
|
if emitEvents {
|
|
531
547
|
notifyListeners("updateAvailable", data: manifestToDictionary(manifest, downloaded: staged != nil))
|
|
@@ -549,6 +565,7 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
549
565
|
version: manifest.version,
|
|
550
566
|
expectedSha256: manifest.sha256,
|
|
551
567
|
expectedSize: manifest.size,
|
|
568
|
+
runtimeVersion: manifest.runtimeVersion,
|
|
552
569
|
channel: targetChannel,
|
|
553
570
|
releaseId: manifest.releaseId
|
|
554
571
|
)
|
|
@@ -568,6 +585,7 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
568
585
|
version: manifest.version,
|
|
569
586
|
expectedSha256: manifest.sha256,
|
|
570
587
|
expectedSize: manifest.size,
|
|
588
|
+
runtimeVersion: manifest.runtimeVersion,
|
|
571
589
|
channel: targetChannel,
|
|
572
590
|
releaseId: manifest.releaseId
|
|
573
591
|
)
|
|
@@ -588,6 +606,7 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
588
606
|
version: String,
|
|
589
607
|
expectedSha256: String,
|
|
590
608
|
expectedSize: Int? = nil,
|
|
609
|
+
runtimeVersion: String? = nil,
|
|
591
610
|
channel: String? = nil,
|
|
592
611
|
releaseId: String? = nil
|
|
593
612
|
) async throws -> BundleInfo {
|
|
@@ -652,6 +671,7 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
652
671
|
let info = BundleInfo(
|
|
653
672
|
id: bundleId,
|
|
654
673
|
version: version,
|
|
674
|
+
runtimeVersion: runtimeVersion,
|
|
655
675
|
status: .pending,
|
|
656
676
|
downloadedAt: Date(),
|
|
657
677
|
sha256: expectedSha256,
|
|
@@ -723,6 +743,10 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
723
743
|
store.setStagedBundleId(nil)
|
|
724
744
|
return store.getCurrentBundle()
|
|
725
745
|
}
|
|
746
|
+
guard isCompatibleRuntime(staged) else {
|
|
747
|
+
try? store.deleteBundle(id: staged.id)
|
|
748
|
+
return store.getCurrentBundle()
|
|
749
|
+
}
|
|
726
750
|
|
|
727
751
|
store.setCurrentBundleId(staged.id)
|
|
728
752
|
store.setStagedBundleId(nil)
|
|
@@ -737,6 +761,10 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
737
761
|
store.setStagedBundleId(nil)
|
|
738
762
|
return
|
|
739
763
|
}
|
|
764
|
+
guard isCompatibleRuntime(staged) else {
|
|
765
|
+
try? store.deleteBundle(id: staged.id)
|
|
766
|
+
return
|
|
767
|
+
}
|
|
740
768
|
|
|
741
769
|
store.setCurrentBundleId(staged.id)
|
|
742
770
|
store.setStagedBundleId(nil)
|
|
@@ -795,6 +823,7 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
795
823
|
let failed = BundleInfo(
|
|
796
824
|
id: current.id,
|
|
797
825
|
version: current.version,
|
|
826
|
+
runtimeVersion: current.runtimeVersion,
|
|
798
827
|
status: .error,
|
|
799
828
|
downloadedAt: current.downloadedAt,
|
|
800
829
|
sha256: current.sha256,
|
|
@@ -901,12 +930,12 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
901
930
|
"size": latest.size,
|
|
902
931
|
"downloaded": downloaded,
|
|
903
932
|
]
|
|
933
|
+
if let runtimeVersion = latest.runtimeVersion {
|
|
934
|
+
payload["runtimeVersion"] = runtimeVersion
|
|
935
|
+
}
|
|
904
936
|
if let releaseId = latest.releaseId {
|
|
905
937
|
payload["releaseId"] = releaseId
|
|
906
938
|
}
|
|
907
|
-
if let minNativeBuild = latest.minNativeBuild {
|
|
908
|
-
payload["minNativeBuild"] = minNativeBuild
|
|
909
|
-
}
|
|
910
939
|
return payload
|
|
911
940
|
}
|
|
912
941
|
|
|
@@ -926,6 +955,10 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
926
955
|
return nil
|
|
927
956
|
}
|
|
928
957
|
|
|
958
|
+
if trimToNil(staged.runtimeVersion) != trimToNil(latest.runtimeVersion) {
|
|
959
|
+
return nil
|
|
960
|
+
}
|
|
961
|
+
|
|
929
962
|
if let latestReleaseId = latest.releaseId,
|
|
930
963
|
let stagedReleaseId = staged.releaseId,
|
|
931
964
|
latestReleaseId == stagedReleaseId {
|
|
@@ -940,6 +973,24 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
940
973
|
return nil
|
|
941
974
|
}
|
|
942
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
|
+
|
|
943
994
|
private func buildBundleId(from version: String) -> String {
|
|
944
995
|
let trimmed = version.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
945
996
|
let allowed = CharacterSet(
|