@otakit/capacitor-updater 1.2.0 → 2.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/README.md +21 -14
- package/android/src/main/java/com/updatekit/updater/HostedManifestKeys.java +5 -6
- package/android/src/main/java/com/updatekit/updater/ManifestClient.java +26 -46
- package/android/src/main/java/com/updatekit/updater/ManifestVerifier.java +1 -75
- package/android/src/main/java/com/updatekit/updater/UpdaterPlugin.java +104 -70
- package/dist/esm/definitions.d.ts +5 -23
- package/dist/esm/definitions.d.ts.map +1 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +0 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/web.d.ts +0 -4
- package/dist/esm/web.d.ts.map +1 -1
- package/dist/esm/web.js +0 -8
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +0 -10
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +0 -10
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/UpdaterPlugin/HostedManifestKeys.swift +5 -5
- package/ios/Sources/UpdaterPlugin/ManifestClient.swift +26 -50
- package/ios/Sources/UpdaterPlugin/ManifestVerifier.swift +2 -56
- package/ios/Sources/UpdaterPlugin/UpdaterPlugin.m +0 -2
- package/ios/Sources/UpdaterPlugin/UpdaterPlugin.swift +90 -67
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Capacitor OTA updater plugin for OtaKit.
|
|
|
4
4
|
|
|
5
5
|
## What it does
|
|
6
6
|
|
|
7
|
-
-
|
|
7
|
+
- fetches the latest manifest for its release lane from the CDN
|
|
8
8
|
- downloads and verifies OTA bundles
|
|
9
9
|
- stages updates safely
|
|
10
10
|
- activates them on the next launch, next resume, or immediately
|
|
@@ -14,6 +14,11 @@ Capacitor OTA updater plugin for OtaKit.
|
|
|
14
14
|
- requires `notifyAppReady()` as the success handshake
|
|
15
15
|
- rolls back automatically if the new bundle does not prove healthy
|
|
16
16
|
|
|
17
|
+
OtaKit publishes signed static manifests into object storage behind a CDN. The
|
|
18
|
+
plugin fetches the manifest for its `appId + channel + runtimeVersion` lane,
|
|
19
|
+
verifies it, compares it against the current and staged bundle locally, and
|
|
20
|
+
only downloads when the manifest actually points at something newer.
|
|
21
|
+
|
|
17
22
|
For normal app code, the main public methods are:
|
|
18
23
|
|
|
19
24
|
```ts
|
|
@@ -57,12 +62,13 @@ plugins: {
|
|
|
57
62
|
|
|
58
63
|
Advanced overrides for self-hosting or custom trust only:
|
|
59
64
|
|
|
60
|
-
- `
|
|
65
|
+
- `cdnUrl` for manifest and bundle delivery
|
|
66
|
+
- `serverUrl` for stats and control-plane requests
|
|
61
67
|
- `manifestKeys`
|
|
62
68
|
- `allowInsecureUrls`
|
|
63
69
|
|
|
64
|
-
Hosted OtaKit already points at
|
|
65
|
-
the managed manifest signing keys.
|
|
70
|
+
Hosted OtaKit already points at the managed control-plane API and CDN and already
|
|
71
|
+
trusts the managed manifest signing keys.
|
|
66
72
|
|
|
67
73
|
## Channels vs runtimeVersion
|
|
68
74
|
|
|
@@ -84,11 +90,12 @@ When `runtimeVersion` is set:
|
|
|
84
90
|
|
|
85
91
|
The plugin does not just download from a URL and trust the result.
|
|
86
92
|
|
|
87
|
-
1. it fetches
|
|
93
|
+
1. it fetches the latest manifest from the CDN for its app + channel + runtimeVersion lane
|
|
88
94
|
2. it verifies the manifest signature when manifest keys are configured
|
|
89
|
-
3. it
|
|
90
|
-
4.
|
|
91
|
-
5. it
|
|
95
|
+
3. it compares that manifest against the current and staged bundle already on the device
|
|
96
|
+
4. if the manifest is newer, it downloads the bundle zip
|
|
97
|
+
5. it verifies the zip against the manifest `sha256`
|
|
98
|
+
6. it stages and activates the bundle
|
|
92
99
|
|
|
93
100
|
In the hosted path, managed signing keys are already built in.
|
|
94
101
|
|
|
@@ -165,8 +172,9 @@ await OtaKit.notifyAppReady();
|
|
|
165
172
|
```
|
|
166
173
|
|
|
167
174
|
The plugin handles checking, downloading, activation, and rollback based on
|
|
168
|
-
`updateMode`.
|
|
169
|
-
the background
|
|
175
|
+
`updateMode`. In `next-launch` and `next-resume`, it checks on cold start and
|
|
176
|
+
every time the app comes back from the background, throttled by `checkInterval`.
|
|
177
|
+
`immediate` bypasses that throttle.
|
|
170
178
|
|
|
171
179
|
For most apps, this is the entire runtime integration.
|
|
172
180
|
|
|
@@ -195,10 +203,9 @@ only after explicit user confirmation.
|
|
|
195
203
|
|
|
196
204
|
## Throttle
|
|
197
205
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
null.
|
|
206
|
+
`checkInterval` (default 10 min) only applies to automatic checks in
|
|
207
|
+
`next-launch` and `next-resume`. Manual `check()` / `download()` calls are
|
|
208
|
+
always live, and `immediate` mode ignores the interval entirely.
|
|
202
209
|
|
|
203
210
|
## Retention and deletion
|
|
204
211
|
|
|
@@ -7,18 +7,17 @@ import java.util.Locale;
|
|
|
7
7
|
|
|
8
8
|
final class HostedManifestKeys {
|
|
9
9
|
|
|
10
|
-
private static final String
|
|
10
|
+
private static final String MANAGED_CDN_URL = "https://cdn.otakit.app";
|
|
11
11
|
|
|
12
12
|
private HostedManifestKeys() {}
|
|
13
13
|
|
|
14
|
-
static boolean
|
|
15
|
-
if (
|
|
14
|
+
static boolean matchesManagedManifestUrl(String cdnUrl) {
|
|
15
|
+
if (cdnUrl == null) {
|
|
16
16
|
return false;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
String normalized =
|
|
20
|
-
return normalized.equals(
|
|
21
|
-
|| normalized.equals("https://otakit.app/api/v1");
|
|
19
|
+
String normalized = cdnUrl.trim().replaceAll("/+$", "").toLowerCase(Locale.ROOT);
|
|
20
|
+
return normalized.equals(MANAGED_CDN_URL) || normalized.equals("https://www.otakit.app");
|
|
22
21
|
}
|
|
23
22
|
|
|
24
23
|
static List<ManifestVerifier.KeyEntry> createDefaultKeys() {
|
|
@@ -9,6 +9,9 @@ import org.json.JSONObject;
|
|
|
9
9
|
|
|
10
10
|
final class ManifestClient {
|
|
11
11
|
|
|
12
|
+
private static final String BASE_CHANNEL_KEY = "__base__";
|
|
13
|
+
private static final String DEFAULT_RUNTIME_KEY = "__default__";
|
|
14
|
+
|
|
12
15
|
static final class ManifestSignature {
|
|
13
16
|
|
|
14
17
|
final String kid;
|
|
@@ -63,41 +66,33 @@ final class ManifestClient {
|
|
|
63
66
|
}
|
|
64
67
|
|
|
65
68
|
static LatestManifest fetchLatest(
|
|
66
|
-
String
|
|
69
|
+
String cdnUrl,
|
|
67
70
|
String appId,
|
|
68
71
|
String channel,
|
|
69
|
-
String currentVersion,
|
|
70
|
-
String currentReleaseId,
|
|
71
72
|
String runtimeVersion,
|
|
72
|
-
String platform,
|
|
73
73
|
boolean allowInsecureUrls,
|
|
74
74
|
java.util.List<ManifestVerifier.KeyEntry> manifestKeys
|
|
75
75
|
) throws Exception {
|
|
76
|
-
String base =
|
|
77
|
-
|
|
76
|
+
String base = cdnUrl.replaceAll("/+$", "");
|
|
77
|
+
String channelKey = channel != null && !channel.trim().isEmpty() ? channel.trim() : BASE_CHANNEL_KEY;
|
|
78
|
+
String runtimeKey =
|
|
79
|
+
runtimeVersion != null && !runtimeVersion.trim().isEmpty()
|
|
80
|
+
? runtimeVersion.trim()
|
|
81
|
+
: DEFAULT_RUNTIME_KEY;
|
|
82
|
+
URL url = new URL(
|
|
83
|
+
base + "/manifests/" + appId + "/" + channelKey + "/" + runtimeKey + "/manifest.json"
|
|
84
|
+
);
|
|
78
85
|
|
|
79
86
|
requireHTTPS(url, allowInsecureUrls);
|
|
80
87
|
|
|
81
88
|
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
|
82
89
|
try {
|
|
83
90
|
connection.setRequestMethod("GET");
|
|
84
|
-
connection.setRequestProperty("X-App-Id", appId);
|
|
85
|
-
connection.setRequestProperty("X-Platform", platform);
|
|
86
|
-
if (channel != null && !channel.trim().isEmpty()) {
|
|
87
|
-
connection.setRequestProperty("X-Channel", channel);
|
|
88
|
-
}
|
|
89
|
-
connection.setRequestProperty("X-Current-Version", currentVersion);
|
|
90
|
-
if (currentReleaseId != null && !currentReleaseId.trim().isEmpty()) {
|
|
91
|
-
connection.setRequestProperty("X-Release-Id", currentReleaseId);
|
|
92
|
-
}
|
|
93
|
-
if (runtimeVersion != null && !runtimeVersion.trim().isEmpty()) {
|
|
94
|
-
connection.setRequestProperty("X-Runtime-Version", runtimeVersion);
|
|
95
|
-
}
|
|
96
91
|
connection.setConnectTimeout(15_000);
|
|
97
92
|
connection.setReadTimeout(30_000);
|
|
98
93
|
|
|
99
94
|
int status = connection.getResponseCode();
|
|
100
|
-
if (status == 204) {
|
|
95
|
+
if (status == 404 || status == 204) {
|
|
101
96
|
return null;
|
|
102
97
|
}
|
|
103
98
|
if (status != 200) {
|
|
@@ -125,14 +120,12 @@ final class ManifestClient {
|
|
|
125
120
|
}
|
|
126
121
|
|
|
127
122
|
ManifestSignature signature = parseSignature(json.optJSONObject("signature"));
|
|
128
|
-
ManifestSignature signatureV2 = parseSignature(json.optJSONObject("signatureV2"));
|
|
129
123
|
|
|
130
124
|
String releaseId = null;
|
|
131
125
|
if (json.has("releaseId") && !json.isNull("releaseId")) {
|
|
132
126
|
releaseId = json.getString("releaseId");
|
|
133
127
|
}
|
|
134
128
|
|
|
135
|
-
// Validate download URL scheme
|
|
136
129
|
requireHTTPS(new URL(downloadUrl), allowInsecureUrls);
|
|
137
130
|
|
|
138
131
|
if (manifestKeys == null || manifestKeys.isEmpty()) {
|
|
@@ -142,36 +135,23 @@ final class ManifestClient {
|
|
|
142
135
|
);
|
|
143
136
|
}
|
|
144
137
|
|
|
145
|
-
// Verify manifest signature if signing keys are configured
|
|
146
138
|
if (manifestKeys != null && !manifestKeys.isEmpty()) {
|
|
147
|
-
if (
|
|
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 {
|
|
139
|
+
if (signature == null) {
|
|
171
140
|
throw new IllegalStateException(
|
|
172
141
|
"Manifest signature missing but signing keys are configured"
|
|
173
142
|
);
|
|
174
143
|
}
|
|
144
|
+
|
|
145
|
+
ManifestVerifier.verify(
|
|
146
|
+
appId,
|
|
147
|
+
channel,
|
|
148
|
+
version,
|
|
149
|
+
sha256,
|
|
150
|
+
size,
|
|
151
|
+
responseRuntimeVersion,
|
|
152
|
+
signature,
|
|
153
|
+
manifestKeys
|
|
154
|
+
);
|
|
175
155
|
}
|
|
176
156
|
|
|
177
157
|
return new LatestManifest(
|
|
@@ -30,7 +30,6 @@ final class ManifestVerifier {
|
|
|
30
30
|
static void verify(
|
|
31
31
|
String appId,
|
|
32
32
|
String channel,
|
|
33
|
-
String platform,
|
|
34
33
|
String version,
|
|
35
34
|
String sha256,
|
|
36
35
|
int size,
|
|
@@ -41,7 +40,6 @@ final class ManifestVerifier {
|
|
|
41
40
|
String payload = buildCanonicalPayload(
|
|
42
41
|
appId,
|
|
43
42
|
channel,
|
|
44
|
-
platform,
|
|
45
43
|
version,
|
|
46
44
|
sha256,
|
|
47
45
|
size,
|
|
@@ -53,30 +51,6 @@ final class ManifestVerifier {
|
|
|
53
51
|
verifyPayload(payload, signature, trustedKeys);
|
|
54
52
|
}
|
|
55
53
|
|
|
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
54
|
private static void verifyPayload(
|
|
81
55
|
String payload,
|
|
82
56
|
ManifestClient.ManifestSignature signature,
|
|
@@ -120,7 +94,6 @@ final class ManifestVerifier {
|
|
|
120
94
|
private static String buildCanonicalPayload(
|
|
121
95
|
String appId,
|
|
122
96
|
String channel,
|
|
123
|
-
String platform,
|
|
124
97
|
String version,
|
|
125
98
|
String sha256,
|
|
126
99
|
int size,
|
|
@@ -130,16 +103,13 @@ final class ManifestVerifier {
|
|
|
130
103
|
int exp
|
|
131
104
|
) {
|
|
132
105
|
return (
|
|
133
|
-
"
|
|
106
|
+
"MANIFEST\n" +
|
|
134
107
|
"appId:" +
|
|
135
108
|
appId +
|
|
136
109
|
"\n" +
|
|
137
110
|
"channel:" +
|
|
138
111
|
(channel != null ? channel : "null") +
|
|
139
112
|
"\n" +
|
|
140
|
-
"platform:" +
|
|
141
|
-
platform +
|
|
142
|
-
"\n" +
|
|
143
113
|
"version:" +
|
|
144
114
|
version +
|
|
145
115
|
"\n" +
|
|
@@ -163,50 +133,6 @@ final class ManifestVerifier {
|
|
|
163
133
|
);
|
|
164
134
|
}
|
|
165
135
|
|
|
166
|
-
private static String buildLegacyCanonicalPayload(
|
|
167
|
-
String appId,
|
|
168
|
-
String channel,
|
|
169
|
-
String platform,
|
|
170
|
-
String version,
|
|
171
|
-
String sha256,
|
|
172
|
-
int size,
|
|
173
|
-
String kid,
|
|
174
|
-
int iat,
|
|
175
|
-
int exp
|
|
176
|
-
) {
|
|
177
|
-
return (
|
|
178
|
-
"MANIFEST_V1\n" +
|
|
179
|
-
"appId:" +
|
|
180
|
-
appId +
|
|
181
|
-
"\n" +
|
|
182
|
-
"channel:" +
|
|
183
|
-
(channel != null ? channel : "null") +
|
|
184
|
-
"\n" +
|
|
185
|
-
"platform:" +
|
|
186
|
-
platform +
|
|
187
|
-
"\n" +
|
|
188
|
-
"version:" +
|
|
189
|
-
version +
|
|
190
|
-
"\n" +
|
|
191
|
-
"sha256:" +
|
|
192
|
-
sha256 +
|
|
193
|
-
"\n" +
|
|
194
|
-
"size:" +
|
|
195
|
-
size +
|
|
196
|
-
"\n" +
|
|
197
|
-
"minNativeBuild:null" +
|
|
198
|
-
"\n" +
|
|
199
|
-
"kid:" +
|
|
200
|
-
kid +
|
|
201
|
-
"\n" +
|
|
202
|
-
"iat:" +
|
|
203
|
-
iat +
|
|
204
|
-
"\n" +
|
|
205
|
-
"exp:" +
|
|
206
|
-
exp
|
|
207
|
-
);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
136
|
private static byte[] base64UrlDecode(String input) {
|
|
211
137
|
// Convert base64url to standard base64
|
|
212
138
|
String base64 = input.replace('-', '+').replace('_', '/');
|
|
@@ -37,6 +37,7 @@ public class UpdaterPlugin extends Plugin {
|
|
|
37
37
|
private boolean allowInsecureUrls = false;
|
|
38
38
|
private String updateMode = UPDATE_MODE_NEXT_LAUNCH;
|
|
39
39
|
private String updateUrl;
|
|
40
|
+
private String cdnUrl;
|
|
40
41
|
private String appId;
|
|
41
42
|
private String channel;
|
|
42
43
|
private String runtimeVersion;
|
|
@@ -44,6 +45,7 @@ public class UpdaterPlugin extends Plugin {
|
|
|
44
45
|
private long checkIntervalMs = 600_000;
|
|
45
46
|
private final AtomicBoolean checkInProgress = new AtomicBoolean(false);
|
|
46
47
|
private static final String DEFAULT_UPDATE_URL = "https://www.otakit.app/api/v1";
|
|
48
|
+
private static final String DEFAULT_CDN_URL = "https://cdn.otakit.app";
|
|
47
49
|
private static final String API_PATH_SUFFIX = "/api/v1";
|
|
48
50
|
private static final String UPDATE_MODE_MANUAL = "manual";
|
|
49
51
|
private static final String UPDATE_MODE_NEXT_LAUNCH = "next-launch";
|
|
@@ -76,6 +78,11 @@ public class UpdaterPlugin extends Plugin {
|
|
|
76
78
|
getConfig().getString("serverUrl"),
|
|
77
79
|
System.getenv("OTAKIT_SERVER_URL")
|
|
78
80
|
);
|
|
81
|
+
this.cdnUrl = resolveCdnUrl(
|
|
82
|
+
getConfig().getString("cdnUrl"),
|
|
83
|
+
System.getenv("OTAKIT_CDN_URL"),
|
|
84
|
+
this.updateUrl
|
|
85
|
+
);
|
|
79
86
|
this.appId = getConfig().getString("appId");
|
|
80
87
|
this.channel = trimToNull(getConfig().getString("channel"));
|
|
81
88
|
this.runtimeVersion = trimToNull(getConfig().getString("runtimeVersion"));
|
|
@@ -116,7 +123,7 @@ public class UpdaterPlugin extends Plugin {
|
|
|
116
123
|
manifestKeys.add(new ManifestVerifier.KeyEntry("_invalid_", new byte[0]));
|
|
117
124
|
}
|
|
118
125
|
|
|
119
|
-
if (manifestKeys.isEmpty() && HostedManifestKeys.
|
|
126
|
+
if (manifestKeys.isEmpty() && HostedManifestKeys.matchesManagedManifestUrl(cdnUrl)) {
|
|
120
127
|
manifestKeys.addAll(HostedManifestKeys.createDefaultKeys());
|
|
121
128
|
}
|
|
122
129
|
|
|
@@ -174,7 +181,15 @@ public class UpdaterPlugin extends Plugin {
|
|
|
174
181
|
}
|
|
175
182
|
}
|
|
176
183
|
|
|
177
|
-
if (shouldThrottleCheck()) return;
|
|
184
|
+
if (!UPDATE_MODE_IMMEDIATE.equals(updateMode) && shouldThrottleCheck()) return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (
|
|
188
|
+
TRIGGER_LAUNCH.equals(trigger) &&
|
|
189
|
+
!UPDATE_MODE_IMMEDIATE.equals(updateMode) &&
|
|
190
|
+
shouldThrottleCheck()
|
|
191
|
+
) {
|
|
192
|
+
return;
|
|
178
193
|
}
|
|
179
194
|
|
|
180
195
|
// Acquire in-flight guard (submit-time)
|
|
@@ -185,7 +200,6 @@ public class UpdaterPlugin extends Plugin {
|
|
|
185
200
|
executor.execute(() -> {
|
|
186
201
|
try {
|
|
187
202
|
BundleInfo result = performCheckAndDownload(null, true);
|
|
188
|
-
recordCheckTimestamp();
|
|
189
203
|
if (result != null) {
|
|
190
204
|
activateStagedBundleForReload();
|
|
191
205
|
reloadWebView();
|
|
@@ -282,8 +296,7 @@ public class UpdaterPlugin extends Plugin {
|
|
|
282
296
|
|
|
283
297
|
@PluginMethod
|
|
284
298
|
public void check(PluginCall call) {
|
|
285
|
-
|
|
286
|
-
if (shouldThrottleCheck() || !checkInProgress.compareAndSet(false, true)) {
|
|
299
|
+
if (!checkInProgress.compareAndSet(false, true)) {
|
|
287
300
|
String stagedId = store.getStagedBundleId();
|
|
288
301
|
if (stagedId != null) {
|
|
289
302
|
BundleInfo staged = store.getBundle(stagedId);
|
|
@@ -311,10 +324,10 @@ public class UpdaterPlugin extends Plugin {
|
|
|
311
324
|
executor.execute(() -> {
|
|
312
325
|
try {
|
|
313
326
|
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().
|
|
316
327
|
if (latest == null) {
|
|
317
328
|
call.resolve((JSObject) null);
|
|
329
|
+
} else if (isCurrentBundleLatest(latest, targetChannel)) {
|
|
330
|
+
call.resolve((JSObject) null);
|
|
318
331
|
} else {
|
|
319
332
|
BundleInfo staged = findMatchingStagedBundle(latest, targetChannel);
|
|
320
333
|
call.resolve(manifestToJSObject(latest, staged != null));
|
|
@@ -329,8 +342,7 @@ public class UpdaterPlugin extends Plugin {
|
|
|
329
342
|
|
|
330
343
|
@PluginMethod
|
|
331
344
|
public void download(PluginCall call) {
|
|
332
|
-
|
|
333
|
-
if (shouldThrottleCheck() || !checkInProgress.compareAndSet(false, true)) {
|
|
345
|
+
if (!checkInProgress.compareAndSet(false, true)) {
|
|
334
346
|
String stagedId = store.getStagedBundleId();
|
|
335
347
|
if (stagedId != null) {
|
|
336
348
|
BundleInfo staged = store.getBundle(stagedId);
|
|
@@ -345,7 +357,6 @@ public class UpdaterPlugin extends Plugin {
|
|
|
345
357
|
executor.execute(() -> {
|
|
346
358
|
try {
|
|
347
359
|
BundleInfo bundle = performCheckAndDownload(null, true);
|
|
348
|
-
recordCheckTimestamp();
|
|
349
360
|
if (bundle == null) {
|
|
350
361
|
call.resolve((JSObject) null);
|
|
351
362
|
} else {
|
|
@@ -359,42 +370,6 @@ public class UpdaterPlugin extends Plugin {
|
|
|
359
370
|
});
|
|
360
371
|
}
|
|
361
372
|
|
|
362
|
-
@PluginMethod
|
|
363
|
-
public void debugCheck(PluginCall call) {
|
|
364
|
-
String requestedChannel = call.getString("channel");
|
|
365
|
-
String targetChannel = resolveTargetChannel(requestedChannel);
|
|
366
|
-
executor.execute(() -> {
|
|
367
|
-
try {
|
|
368
|
-
ManifestClient.LatestManifest latest = fetchLatest(targetChannel);
|
|
369
|
-
if (latest == null) {
|
|
370
|
-
call.resolve((JSObject) null);
|
|
371
|
-
} else {
|
|
372
|
-
BundleInfo staged = findMatchingStagedBundle(latest, targetChannel);
|
|
373
|
-
call.resolve(manifestToJSObject(latest, staged != null));
|
|
374
|
-
}
|
|
375
|
-
} catch (Exception e) {
|
|
376
|
-
call.reject("debugCheck failed: " + e.getMessage());
|
|
377
|
-
}
|
|
378
|
-
});
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
@PluginMethod
|
|
382
|
-
public void debugDownload(PluginCall call) {
|
|
383
|
-
String requestedChannel = call.getString("channel");
|
|
384
|
-
executor.execute(() -> {
|
|
385
|
-
try {
|
|
386
|
-
BundleInfo bundle = performCheckAndDownload(requestedChannel, true);
|
|
387
|
-
if (bundle == null) {
|
|
388
|
-
call.resolve((JSObject) null);
|
|
389
|
-
} else {
|
|
390
|
-
call.resolve(bundle.toJSObject());
|
|
391
|
-
}
|
|
392
|
-
} catch (Exception e) {
|
|
393
|
-
call.reject("debugDownload failed: " + e.getMessage());
|
|
394
|
-
}
|
|
395
|
-
});
|
|
396
|
-
}
|
|
397
|
-
|
|
398
373
|
@PluginMethod
|
|
399
374
|
public void apply(PluginCall call) {
|
|
400
375
|
String stagedId = store.getStagedBundleId();
|
|
@@ -512,16 +487,11 @@ public class UpdaterPlugin extends Plugin {
|
|
|
512
487
|
throw new IllegalStateException("Missing appId in plugin config");
|
|
513
488
|
}
|
|
514
489
|
|
|
515
|
-
BundleInfo current = store.getCurrentBundle();
|
|
516
|
-
|
|
517
490
|
return ManifestClient.fetchLatest(
|
|
518
|
-
|
|
491
|
+
cdnUrl,
|
|
519
492
|
appId,
|
|
520
493
|
channel,
|
|
521
|
-
current.version,
|
|
522
|
-
current.releaseId,
|
|
523
494
|
runtimeVersion,
|
|
524
|
-
"android",
|
|
525
495
|
allowInsecureUrls,
|
|
526
496
|
manifestKeys
|
|
527
497
|
);
|
|
@@ -543,6 +513,13 @@ public class UpdaterPlugin extends Plugin {
|
|
|
543
513
|
);
|
|
544
514
|
}
|
|
545
515
|
|
|
516
|
+
if (isCurrentBundleLatest(latest, targetChannel)) {
|
|
517
|
+
if (emitEvents) {
|
|
518
|
+
notifyListeners("noUpdateAvailable", new JSObject());
|
|
519
|
+
}
|
|
520
|
+
return null;
|
|
521
|
+
}
|
|
522
|
+
|
|
546
523
|
BundleInfo staged = findMatchingStagedBundle(latest, targetChannel);
|
|
547
524
|
if (emitEvents) {
|
|
548
525
|
notifyListeners("updateAvailable", manifestToJSObject(latest, staged != null));
|
|
@@ -912,6 +889,54 @@ public class UpdaterPlugin extends Plugin {
|
|
|
912
889
|
return object;
|
|
913
890
|
}
|
|
914
891
|
|
|
892
|
+
private boolean isCurrentBundleLatest(
|
|
893
|
+
ManifestClient.LatestManifest latest,
|
|
894
|
+
String targetChannel
|
|
895
|
+
) {
|
|
896
|
+
return doesBundleMatchLatest(store.getCurrentBundle(), latest, targetChannel);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
private boolean doesBundleMatchLatest(
|
|
900
|
+
BundleInfo bundle,
|
|
901
|
+
ManifestClient.LatestManifest latest,
|
|
902
|
+
String targetChannel
|
|
903
|
+
) {
|
|
904
|
+
if (bundle == null) {
|
|
905
|
+
return false;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
if (!java.util.Objects.equals(trimToNull(bundle.channel), targetChannel)) {
|
|
909
|
+
return false;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
if (
|
|
913
|
+
!java.util.Objects.equals(
|
|
914
|
+
trimToNull(bundle.runtimeVersion),
|
|
915
|
+
trimToNull(latest.runtimeVersion)
|
|
916
|
+
)
|
|
917
|
+
) {
|
|
918
|
+
return false;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
if (
|
|
922
|
+
latest.releaseId != null &&
|
|
923
|
+
bundle.releaseId != null &&
|
|
924
|
+
latest.releaseId.equals(bundle.releaseId)
|
|
925
|
+
) {
|
|
926
|
+
return true;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
if (
|
|
930
|
+
latest.sha256 != null &&
|
|
931
|
+
bundle.sha256 != null &&
|
|
932
|
+
latest.sha256.equals(bundle.sha256)
|
|
933
|
+
) {
|
|
934
|
+
return true;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
return latest.version != null && latest.version.equals(bundle.version);
|
|
938
|
+
}
|
|
939
|
+
|
|
915
940
|
private BundleInfo findMatchingStagedBundle(
|
|
916
941
|
ManifestClient.LatestManifest latest,
|
|
917
942
|
String targetChannel
|
|
@@ -935,24 +960,7 @@ public class UpdaterPlugin extends Plugin {
|
|
|
935
960
|
return null;
|
|
936
961
|
}
|
|
937
962
|
|
|
938
|
-
|
|
939
|
-
latest.releaseId != null &&
|
|
940
|
-
staged.releaseId != null &&
|
|
941
|
-
latest.releaseId.equals(staged.releaseId)
|
|
942
|
-
) {
|
|
943
|
-
return staged;
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
if (
|
|
947
|
-
latest.version != null &&
|
|
948
|
-
latest.version.equals(staged.version) &&
|
|
949
|
-
latest.sha256 != null &&
|
|
950
|
-
latest.sha256.equals(staged.sha256)
|
|
951
|
-
) {
|
|
952
|
-
return staged;
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
return null;
|
|
963
|
+
return doesBundleMatchLatest(staged, latest, targetChannel) ? staged : null;
|
|
956
964
|
}
|
|
957
965
|
|
|
958
966
|
private void moveDirectory(File source, File destination) throws Exception {
|
|
@@ -1049,6 +1057,28 @@ public class UpdaterPlugin extends Plugin {
|
|
|
1049
1057
|
return DEFAULT_UPDATE_URL;
|
|
1050
1058
|
}
|
|
1051
1059
|
|
|
1060
|
+
private String resolveCdnUrl(String configured, String env, String resolvedUpdateUrl) {
|
|
1061
|
+
String configuredValue = trimToNull(configured);
|
|
1062
|
+
if (configuredValue != null) {
|
|
1063
|
+
return normalizeCdnUrl(configuredValue);
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
String envValue = trimToNull(env);
|
|
1067
|
+
if (envValue != null) {
|
|
1068
|
+
return normalizeCdnUrl(envValue);
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
if (
|
|
1072
|
+
resolvedUpdateUrl != null &&
|
|
1073
|
+
!DEFAULT_UPDATE_URL.equalsIgnoreCase(resolvedUpdateUrl) &&
|
|
1074
|
+
resolvedUpdateUrl.toLowerCase(java.util.Locale.ROOT).endsWith(API_PATH_SUFFIX)
|
|
1075
|
+
) {
|
|
1076
|
+
return resolvedUpdateUrl.substring(0, resolvedUpdateUrl.length() - API_PATH_SUFFIX.length());
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
return DEFAULT_CDN_URL;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1052
1082
|
private String normalizeUpdateUrl(String raw) {
|
|
1053
1083
|
String trimmed = raw.trim().replaceAll("/+$", "");
|
|
1054
1084
|
if (trimmed.toLowerCase(java.util.Locale.ROOT).endsWith(API_PATH_SUFFIX)) {
|
|
@@ -1057,6 +1087,10 @@ public class UpdaterPlugin extends Plugin {
|
|
|
1057
1087
|
return trimmed + API_PATH_SUFFIX;
|
|
1058
1088
|
}
|
|
1059
1089
|
|
|
1090
|
+
private String normalizeCdnUrl(String raw) {
|
|
1091
|
+
return raw.trim().replaceAll("/+$", "");
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1060
1094
|
private String trimToNull(String value) {
|
|
1061
1095
|
if (value == null) {
|
|
1062
1096
|
return null;
|
|
@@ -78,34 +78,22 @@ export interface OtaKitConfig {
|
|
|
78
78
|
updateMode?: OtaKitUpdateMode;
|
|
79
79
|
/**
|
|
80
80
|
* Minimum milliseconds between automatic update checks.
|
|
81
|
-
* Applies to
|
|
82
|
-
*
|
|
81
|
+
* Applies only to automatic checks in `next-launch` and `next-resume`.
|
|
82
|
+
* Manual APIs and `immediate` mode bypass this throttle. Defaults to 600000 (10 min).
|
|
83
83
|
*/
|
|
84
84
|
checkInterval?: number;
|
|
85
85
|
/** Milliseconds to wait for notifyAppReady(). Defaults to 10000. */
|
|
86
86
|
appReadyTimeout?: number;
|
|
87
|
-
/** Custom API base URL for
|
|
87
|
+
/** Custom API base URL for stats and other control-plane requests. */
|
|
88
88
|
serverUrl?: string;
|
|
89
|
+
/** Custom CDN base URL for manifest and bundle delivery. */
|
|
90
|
+
cdnUrl?: string;
|
|
89
91
|
/** Custom manifest verification keys for self-hosted or custom trust. */
|
|
90
92
|
manifestKeys?: OtaKitManifestKey[];
|
|
91
93
|
/** Allow HTTP only for localhost development. Defaults to false. */
|
|
92
94
|
allowInsecureUrls?: boolean;
|
|
93
95
|
}
|
|
94
96
|
export interface OtaKitDebugApi {
|
|
95
|
-
/**
|
|
96
|
-
* Check the server for a newer version without downloading it.
|
|
97
|
-
* You can optionally pass { channel } for a one-off debug override.
|
|
98
|
-
*/
|
|
99
|
-
check(options?: {
|
|
100
|
-
channel?: string;
|
|
101
|
-
}): Promise<LatestVersion | null>;
|
|
102
|
-
/**
|
|
103
|
-
* Check the server and download the latest bundle if available.
|
|
104
|
-
* Ensures the latest bundle is staged for later activation.
|
|
105
|
-
*/
|
|
106
|
-
download(options?: {
|
|
107
|
-
channel?: string;
|
|
108
|
-
}): Promise<BundleInfo | null>;
|
|
109
97
|
/**
|
|
110
98
|
* Reset to the builtin bundle and reload the WebView.
|
|
111
99
|
*
|
|
@@ -206,12 +194,6 @@ export interface OtaKitBridgePlugin {
|
|
|
206
194
|
apply(): Promise<void>;
|
|
207
195
|
notifyAppReady(): Promise<void>;
|
|
208
196
|
debugGetState(): Promise<OtaKitDebugState>;
|
|
209
|
-
debugCheck(options?: {
|
|
210
|
-
channel?: string;
|
|
211
|
-
}): Promise<LatestVersion | null>;
|
|
212
|
-
debugDownload(options?: {
|
|
213
|
-
channel?: string;
|
|
214
|
-
}): Promise<BundleInfo | null>;
|
|
215
197
|
debugReset(): Promise<void>;
|
|
216
198
|
debugListBundles(): Promise<BundleListResult>;
|
|
217
199
|
debugDeleteBundle(options: {
|