@otakit/capacitor-updater 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -7
- package/android/src/main/java/com/updatekit/updater/BundleInfo.java +34 -2
- package/android/src/main/java/com/updatekit/updater/BundleStore.java +13 -2
- package/android/src/main/java/com/updatekit/updater/ManifestClient.java +56 -47
- package/android/src/main/java/com/updatekit/updater/ManifestVerifier.java +92 -19
- package/android/src/main/java/com/updatekit/updater/UpdaterPlugin.java +183 -16
- package/dist/esm/definitions.d.ts +13 -3
- package/dist/esm/definitions.d.ts.map +1 -1
- package/ios/Sources/UpdaterPlugin/BundleInfo.swift +5 -0
- package/ios/Sources/UpdaterPlugin/BundleStore.swift +3 -0
- package/ios/Sources/UpdaterPlugin/ManifestClient.swift +57 -34
- package/ios/Sources/UpdaterPlugin/ManifestVerifier.swift +73 -18
- package/ios/Sources/UpdaterPlugin/UpdaterPlugin.swift +209 -16
- package/package.json +1 -1
|
@@ -21,6 +21,7 @@ import java.nio.charset.StandardCharsets;
|
|
|
21
21
|
import java.security.MessageDigest;
|
|
22
22
|
import java.util.concurrent.ExecutorService;
|
|
23
23
|
import java.util.concurrent.Executors;
|
|
24
|
+
import java.util.concurrent.atomic.AtomicBoolean;
|
|
24
25
|
|
|
25
26
|
@CapacitorPlugin(name = "OtaKit")
|
|
26
27
|
public class UpdaterPlugin extends Plugin {
|
|
@@ -38,12 +39,19 @@ public class UpdaterPlugin extends Plugin {
|
|
|
38
39
|
private String updateUrl;
|
|
39
40
|
private String appId;
|
|
40
41
|
private String channel;
|
|
42
|
+
private String runtimeVersion;
|
|
41
43
|
private java.util.List<ManifestVerifier.KeyEntry> manifestKeys = new java.util.ArrayList<>();
|
|
44
|
+
private long checkIntervalMs = 600_000;
|
|
45
|
+
private final AtomicBoolean checkInProgress = new AtomicBoolean(false);
|
|
42
46
|
private static final String DEFAULT_UPDATE_URL = "https://www.otakit.app/api/v1";
|
|
43
47
|
private static final String API_PATH_SUFFIX = "/api/v1";
|
|
44
48
|
private static final String UPDATE_MODE_MANUAL = "manual";
|
|
45
49
|
private static final String UPDATE_MODE_NEXT_LAUNCH = "next-launch";
|
|
50
|
+
private static final String UPDATE_MODE_NEXT_RESUME = "next-resume";
|
|
46
51
|
private static final String UPDATE_MODE_IMMEDIATE = "immediate";
|
|
52
|
+
private static final String KEY_LAST_CHECK_TIMESTAMP = "last_check_timestamp";
|
|
53
|
+
private static final String TRIGGER_LAUNCH = "launch";
|
|
54
|
+
private static final String TRIGGER_RESUME = "resume";
|
|
47
55
|
|
|
48
56
|
@Override
|
|
49
57
|
public void load() {
|
|
@@ -64,13 +72,14 @@ public class UpdaterPlugin extends Plugin {
|
|
|
64
72
|
}
|
|
65
73
|
} catch (PackageManager.NameNotFoundException ignored) {}
|
|
66
74
|
|
|
67
|
-
this.store = new BundleStore(getContext(), builtinVersion, nativeBuild);
|
|
68
75
|
this.updateUrl = resolveUpdateUrl(
|
|
69
76
|
getConfig().getString("serverUrl"),
|
|
70
77
|
System.getenv("OTAKIT_SERVER_URL")
|
|
71
78
|
);
|
|
72
79
|
this.appId = getConfig().getString("appId");
|
|
73
80
|
this.channel = trimToNull(getConfig().getString("channel"));
|
|
81
|
+
this.runtimeVersion = trimToNull(getConfig().getString("runtimeVersion"));
|
|
82
|
+
this.store = new BundleStore(getContext(), builtinVersion, nativeBuild, this.runtimeVersion);
|
|
74
83
|
this.allowInsecureUrls = getConfig().getBoolean("allowInsecureUrls", false);
|
|
75
84
|
String configuredUpdateMode = getConfig().getString("updateMode", UPDATE_MODE_NEXT_LAUNCH);
|
|
76
85
|
this.updateMode = resolveUpdateMode(configuredUpdateMode);
|
|
@@ -112,6 +121,9 @@ public class UpdaterPlugin extends Plugin {
|
|
|
112
121
|
}
|
|
113
122
|
|
|
114
123
|
this.appReadyTimeoutMs = Math.max(1000, getConfig().getInt("appReadyTimeout", 10_000));
|
|
124
|
+
this.checkIntervalMs = Math.max(600_000, getConfig().getInt("checkInterval", 600_000));
|
|
125
|
+
|
|
126
|
+
pruneIncompatibleBundles();
|
|
115
127
|
|
|
116
128
|
BundleInfo current = store.getCurrentBundle();
|
|
117
129
|
if (current.status == BundleStatus.TRIAL) {
|
|
@@ -135,26 +147,84 @@ public class UpdaterPlugin extends Plugin {
|
|
|
135
147
|
}
|
|
136
148
|
|
|
137
149
|
if (isAutomaticUpdateMode()) {
|
|
138
|
-
|
|
150
|
+
runAutomaticUpdate(TRIGGER_LAUNCH);
|
|
139
151
|
}
|
|
140
152
|
}
|
|
141
153
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
154
|
+
@Override
|
|
155
|
+
protected void handleOnResume() {
|
|
156
|
+
super.handleOnResume();
|
|
157
|
+
if (isAutomaticUpdateMode()) {
|
|
158
|
+
runAutomaticUpdate(TRIGGER_RESUME);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private void runAutomaticUpdate(String trigger) {
|
|
163
|
+
// Resume-only guards
|
|
164
|
+
if (TRIGGER_RESUME.equals(trigger)) {
|
|
165
|
+
if (UPDATE_MODE_MANUAL.equals(updateMode)) return;
|
|
166
|
+
|
|
167
|
+
// next-resume and immediate: activate staged bundle on resume without server check
|
|
168
|
+
if ((UPDATE_MODE_NEXT_RESUME.equals(updateMode) || UPDATE_MODE_IMMEDIATE.equals(updateMode))) {
|
|
169
|
+
String stagedId = store.getStagedBundleId();
|
|
170
|
+
if (stagedId != null && store.getBundle(stagedId) != null) {
|
|
147
171
|
activateStagedBundleForReload();
|
|
148
172
|
reloadWebView();
|
|
173
|
+
return;
|
|
149
174
|
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (shouldThrottleCheck()) return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Acquire in-flight guard (submit-time)
|
|
181
|
+
if (!checkInProgress.compareAndSet(false, true)) return;
|
|
182
|
+
|
|
183
|
+
if (UPDATE_MODE_IMMEDIATE.equals(updateMode)) {
|
|
184
|
+
// Immediate: check+download, activate if found
|
|
185
|
+
executor.execute(() -> {
|
|
186
|
+
try {
|
|
187
|
+
BundleInfo result = performCheckAndDownload(null, true);
|
|
188
|
+
recordCheckTimestamp();
|
|
189
|
+
if (result != null) {
|
|
190
|
+
activateStagedBundleForReload();
|
|
191
|
+
reloadWebView();
|
|
192
|
+
}
|
|
193
|
+
} catch (Exception e) {
|
|
194
|
+
android.util.Log.w("OtaKit", "immediate update failed (" + trigger + ")", e);
|
|
195
|
+
} finally {
|
|
196
|
+
checkInProgress.set(false);
|
|
153
197
|
}
|
|
198
|
+
});
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// next-launch / next-resume: background check+download (fire-and-forget)
|
|
203
|
+
executor.execute(() -> {
|
|
204
|
+
try {
|
|
205
|
+
performCheckAndDownload(null, true);
|
|
206
|
+
recordCheckTimestamp();
|
|
207
|
+
} catch (Exception ignored) {
|
|
208
|
+
// Check failed — timestamp not recorded, will retry on next trigger
|
|
209
|
+
} finally {
|
|
210
|
+
checkInProgress.set(false);
|
|
154
211
|
}
|
|
155
212
|
});
|
|
156
213
|
}
|
|
157
214
|
|
|
215
|
+
private boolean shouldThrottleCheck() {
|
|
216
|
+
long lastCheck = store.getPrefs().getLong(KEY_LAST_CHECK_TIMESTAMP, 0);
|
|
217
|
+
if (lastCheck <= 0) return false;
|
|
218
|
+
long elapsed = System.currentTimeMillis() - lastCheck;
|
|
219
|
+
return elapsed < checkIntervalMs;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private void recordCheckTimestamp() {
|
|
223
|
+
store.getPrefs().edit()
|
|
224
|
+
.putLong(KEY_LAST_CHECK_TIMESTAMP, System.currentTimeMillis())
|
|
225
|
+
.apply();
|
|
226
|
+
}
|
|
227
|
+
|
|
158
228
|
private boolean shouldActivateStagedOnLaunch() {
|
|
159
229
|
return !UPDATE_MODE_MANUAL.equals(updateMode);
|
|
160
230
|
}
|
|
@@ -175,12 +245,15 @@ public class UpdaterPlugin extends Plugin {
|
|
|
175
245
|
if (UPDATE_MODE_MANUAL.equals(raw)) {
|
|
176
246
|
return UPDATE_MODE_MANUAL;
|
|
177
247
|
}
|
|
248
|
+
if (UPDATE_MODE_NEXT_RESUME.equals(raw)) {
|
|
249
|
+
return UPDATE_MODE_NEXT_RESUME;
|
|
250
|
+
}
|
|
178
251
|
if (UPDATE_MODE_IMMEDIATE.equals(raw)) {
|
|
179
252
|
return UPDATE_MODE_IMMEDIATE;
|
|
180
253
|
}
|
|
181
254
|
|
|
182
255
|
android.util.Log.w(
|
|
183
|
-
"
|
|
256
|
+
"OtaKit",
|
|
184
257
|
"Unknown updateMode '" + raw + "', defaulting to 'next-launch'"
|
|
185
258
|
);
|
|
186
259
|
return UPDATE_MODE_NEXT_LAUNCH;
|
|
@@ -209,10 +282,37 @@ public class UpdaterPlugin extends Plugin {
|
|
|
209
282
|
|
|
210
283
|
@PluginMethod
|
|
211
284
|
public void check(PluginCall call) {
|
|
285
|
+
// Respect throttle — return staged bundle as a LatestVersion if available, else null
|
|
286
|
+
if (shouldThrottleCheck() || !checkInProgress.compareAndSet(false, true)) {
|
|
287
|
+
String stagedId = store.getStagedBundleId();
|
|
288
|
+
if (stagedId != null) {
|
|
289
|
+
BundleInfo staged = store.getBundle(stagedId);
|
|
290
|
+
if (staged != null) {
|
|
291
|
+
JSObject result = new JSObject();
|
|
292
|
+
result.put("version", staged.version);
|
|
293
|
+
result.put("url", "");
|
|
294
|
+
result.put("sha256", staged.sha256 != null ? staged.sha256 : "");
|
|
295
|
+
result.put("size", 0);
|
|
296
|
+
result.put("downloaded", true);
|
|
297
|
+
if (staged.runtimeVersion != null) {
|
|
298
|
+
result.put("runtimeVersion", staged.runtimeVersion);
|
|
299
|
+
}
|
|
300
|
+
if (staged.releaseId != null) {
|
|
301
|
+
result.put("releaseId", staged.releaseId);
|
|
302
|
+
}
|
|
303
|
+
call.resolve(result);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
call.resolve((JSObject) null);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
212
310
|
String targetChannel = resolveTargetChannel(null);
|
|
213
311
|
executor.execute(() -> {
|
|
214
312
|
try {
|
|
215
313
|
ManifestClient.LatestManifest latest = fetchLatest(targetChannel);
|
|
314
|
+
// check() does NOT record timestamp — only download() and automatic paths do.
|
|
315
|
+
// This keeps check() -> download() working without the throttle blocking download().
|
|
216
316
|
if (latest == null) {
|
|
217
317
|
call.resolve((JSObject) null);
|
|
218
318
|
} else {
|
|
@@ -221,15 +321,31 @@ public class UpdaterPlugin extends Plugin {
|
|
|
221
321
|
}
|
|
222
322
|
} catch (Exception e) {
|
|
223
323
|
call.reject("check failed: " + e.getMessage());
|
|
324
|
+
} finally {
|
|
325
|
+
checkInProgress.set(false);
|
|
224
326
|
}
|
|
225
327
|
});
|
|
226
328
|
}
|
|
227
329
|
|
|
228
330
|
@PluginMethod
|
|
229
331
|
public void download(PluginCall call) {
|
|
332
|
+
// Respect throttle — return staged info if available, else null
|
|
333
|
+
if (shouldThrottleCheck() || !checkInProgress.compareAndSet(false, true)) {
|
|
334
|
+
String stagedId = store.getStagedBundleId();
|
|
335
|
+
if (stagedId != null) {
|
|
336
|
+
BundleInfo staged = store.getBundle(stagedId);
|
|
337
|
+
if (staged != null) {
|
|
338
|
+
call.resolve(staged.toJSObject());
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
call.resolve((JSObject) null);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
230
345
|
executor.execute(() -> {
|
|
231
346
|
try {
|
|
232
347
|
BundleInfo bundle = performCheckAndDownload(null, true);
|
|
348
|
+
recordCheckTimestamp();
|
|
233
349
|
if (bundle == null) {
|
|
234
350
|
call.resolve((JSObject) null);
|
|
235
351
|
} else {
|
|
@@ -237,6 +353,8 @@ public class UpdaterPlugin extends Plugin {
|
|
|
237
353
|
}
|
|
238
354
|
} catch (Exception e) {
|
|
239
355
|
call.reject("download failed: " + e.getMessage());
|
|
356
|
+
} finally {
|
|
357
|
+
checkInProgress.set(false);
|
|
240
358
|
}
|
|
241
359
|
});
|
|
242
360
|
}
|
|
@@ -402,7 +520,7 @@ public class UpdaterPlugin extends Plugin {
|
|
|
402
520
|
channel,
|
|
403
521
|
current.version,
|
|
404
522
|
current.releaseId,
|
|
405
|
-
|
|
523
|
+
runtimeVersion,
|
|
406
524
|
"android",
|
|
407
525
|
allowInsecureUrls,
|
|
408
526
|
manifestKeys
|
|
@@ -419,6 +537,12 @@ public class UpdaterPlugin extends Plugin {
|
|
|
419
537
|
return null;
|
|
420
538
|
}
|
|
421
539
|
|
|
540
|
+
if (!isCompatibleRuntime(latest.runtimeVersion)) {
|
|
541
|
+
throw new IllegalStateException(
|
|
542
|
+
"Manifest runtimeVersion does not match the installed app runtime"
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
|
|
422
546
|
BundleInfo staged = findMatchingStagedBundle(latest, targetChannel);
|
|
423
547
|
if (emitEvents) {
|
|
424
548
|
notifyListeners("updateAvailable", manifestToJSObject(latest, staged != null));
|
|
@@ -434,6 +558,7 @@ public class UpdaterPlugin extends Plugin {
|
|
|
434
558
|
latest.version,
|
|
435
559
|
latest.sha256,
|
|
436
560
|
latest.size,
|
|
561
|
+
latest.runtimeVersion,
|
|
437
562
|
targetChannel,
|
|
438
563
|
latest.releaseId
|
|
439
564
|
);
|
|
@@ -447,6 +572,7 @@ public class UpdaterPlugin extends Plugin {
|
|
|
447
572
|
refreshed.version,
|
|
448
573
|
refreshed.sha256,
|
|
449
574
|
refreshed.size,
|
|
575
|
+
refreshed.runtimeVersion,
|
|
450
576
|
targetChannel,
|
|
451
577
|
refreshed.releaseId
|
|
452
578
|
);
|
|
@@ -469,7 +595,7 @@ public class UpdaterPlugin extends Plugin {
|
|
|
469
595
|
|
|
470
596
|
private BundleInfo downloadAndStage(URL url, String version, String expectedSha256)
|
|
471
597
|
throws Exception {
|
|
472
|
-
return downloadAndStage(url, version, expectedSha256, 0, null, null);
|
|
598
|
+
return downloadAndStage(url, version, expectedSha256, 0, null, null, null);
|
|
473
599
|
}
|
|
474
600
|
|
|
475
601
|
private BundleInfo downloadAndStage(
|
|
@@ -477,6 +603,7 @@ public class UpdaterPlugin extends Plugin {
|
|
|
477
603
|
String version,
|
|
478
604
|
String expectedSha256,
|
|
479
605
|
int expectedSize,
|
|
606
|
+
String runtimeVersion,
|
|
480
607
|
String channel,
|
|
481
608
|
String releaseId
|
|
482
609
|
) throws Exception {
|
|
@@ -524,6 +651,7 @@ public class UpdaterPlugin extends Plugin {
|
|
|
524
651
|
BundleInfo info = new BundleInfo(
|
|
525
652
|
bundleId,
|
|
526
653
|
version,
|
|
654
|
+
runtimeVersion,
|
|
527
655
|
BundleStatus.PENDING,
|
|
528
656
|
System.currentTimeMillis(),
|
|
529
657
|
expectedSha256,
|
|
@@ -619,6 +747,12 @@ public class UpdaterPlugin extends Plugin {
|
|
|
619
747
|
store.setStagedBundleId(null);
|
|
620
748
|
return store.getCurrentBundle();
|
|
621
749
|
}
|
|
750
|
+
if (!isCompatibleRuntime(staged)) {
|
|
751
|
+
try {
|
|
752
|
+
store.deleteBundle(staged.id);
|
|
753
|
+
} catch (Exception ignored) {}
|
|
754
|
+
return store.getCurrentBundle();
|
|
755
|
+
}
|
|
622
756
|
|
|
623
757
|
store.setCurrentBundleId(staged.id);
|
|
624
758
|
store.setStagedBundleId(null);
|
|
@@ -636,6 +770,12 @@ public class UpdaterPlugin extends Plugin {
|
|
|
636
770
|
store.setStagedBundleId(null);
|
|
637
771
|
return;
|
|
638
772
|
}
|
|
773
|
+
if (!isCompatibleRuntime(staged)) {
|
|
774
|
+
try {
|
|
775
|
+
store.deleteBundle(staged.id);
|
|
776
|
+
} catch (Exception ignored) {}
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
639
779
|
|
|
640
780
|
store.setCurrentBundleId(staged.id);
|
|
641
781
|
store.setStagedBundleId(null);
|
|
@@ -763,12 +903,12 @@ public class UpdaterPlugin extends Plugin {
|
|
|
763
903
|
object.put("sha256", latest.sha256);
|
|
764
904
|
object.put("size", latest.size);
|
|
765
905
|
object.put("downloaded", downloaded);
|
|
906
|
+
if (latest.runtimeVersion != null) {
|
|
907
|
+
object.put("runtimeVersion", latest.runtimeVersion);
|
|
908
|
+
}
|
|
766
909
|
if (latest.releaseId != null) {
|
|
767
910
|
object.put("releaseId", latest.releaseId);
|
|
768
911
|
}
|
|
769
|
-
if (latest.minNativeBuild != null) {
|
|
770
|
-
object.put("minNativeBuild", latest.minNativeBuild);
|
|
771
|
-
}
|
|
772
912
|
return object;
|
|
773
913
|
}
|
|
774
914
|
|
|
@@ -791,6 +931,10 @@ public class UpdaterPlugin extends Plugin {
|
|
|
791
931
|
return null;
|
|
792
932
|
}
|
|
793
933
|
|
|
934
|
+
if (!java.util.Objects.equals(trimToNull(staged.runtimeVersion), trimToNull(latest.runtimeVersion))) {
|
|
935
|
+
return null;
|
|
936
|
+
}
|
|
937
|
+
|
|
794
938
|
if (
|
|
795
939
|
latest.releaseId != null &&
|
|
796
940
|
staged.releaseId != null &&
|
|
@@ -950,6 +1094,29 @@ public class UpdaterPlugin extends Plugin {
|
|
|
950
1094
|
);
|
|
951
1095
|
}
|
|
952
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
|
+
|
|
953
1120
|
private long getFreeDiskSpace() {
|
|
954
1121
|
try {
|
|
955
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[];
|
|
@@ -57,7 +59,7 @@ export interface OtaKitState {
|
|
|
57
59
|
export interface OtaKitDebugState extends OtaKitState {
|
|
58
60
|
fallback: BundleInfo;
|
|
59
61
|
}
|
|
60
|
-
export type OtaKitUpdateMode = 'manual' | 'next-launch' | 'immediate';
|
|
62
|
+
export type OtaKitUpdateMode = 'manual' | 'next-launch' | 'next-resume' | 'immediate';
|
|
61
63
|
export interface OtaKitManifestKey {
|
|
62
64
|
kid: string;
|
|
63
65
|
key: string;
|
|
@@ -70,8 +72,16 @@ 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;
|
|
79
|
+
/**
|
|
80
|
+
* Minimum milliseconds between automatic update checks.
|
|
81
|
+
* Applies to both resume-triggered and manual check()/download() calls.
|
|
82
|
+
* Debug APIs bypass this throttle. Defaults to 600000 (10 min).
|
|
83
|
+
*/
|
|
84
|
+
checkInterval?: number;
|
|
75
85
|
/** Milliseconds to wait for notifyAppReady(). Defaults to 10000. */
|
|
76
86
|
appReadyTimeout?: number;
|
|
77
87
|
/** Custom API base URL for self-hosted or custom servers. */
|
|
@@ -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
|
}
|