@otakit/capacitor-updater 2.3.2 → 3.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.
Files changed (57) hide show
  1. package/README.md +46 -5
  2. package/android/build.gradle +16 -0
  3. package/android/src/main/java/com/otakit/updater/BundleCrypto.java +38 -17
  4. package/android/src/main/java/com/otakit/updater/BundleInfo.java +10 -0
  5. package/android/src/main/java/com/otakit/updater/BundleStore.java +134 -86
  6. package/android/src/main/java/com/otakit/updater/CheckFailure.java +53 -0
  7. package/android/src/main/java/com/otakit/updater/DeltaAssembler.java +46 -41
  8. package/android/src/main/java/com/otakit/updater/DeviceEventClient.java +125 -26
  9. package/android/src/main/java/com/otakit/updater/DocumentReadyBridge.java +71 -0
  10. package/android/src/main/java/com/otakit/updater/DownloadRetry.java +144 -0
  11. package/android/src/main/java/com/otakit/updater/EventOutbox.java +174 -0
  12. package/android/src/main/java/com/otakit/updater/FileDownloader.java +80 -0
  13. package/android/src/main/java/com/otakit/updater/ForegroundDeadline.java +84 -0
  14. package/android/src/main/java/com/otakit/updater/HashUtils.java +26 -0
  15. package/android/src/main/java/com/otakit/updater/ManifestClient.java +12 -9
  16. package/android/src/main/java/com/otakit/updater/ManifestKeyConfig.java +47 -0
  17. package/android/src/main/java/com/otakit/updater/ManifestVerifier.java +22 -4
  18. package/android/src/main/java/com/otakit/updater/SDKVersion.java +9 -0
  19. package/android/src/main/java/com/otakit/updater/UpdateOwner.java +27 -0
  20. package/android/src/main/java/com/otakit/updater/UpdaterCoordinator.java +154 -59
  21. package/android/src/main/java/com/otakit/updater/UpdaterPlugin.java +284 -209
  22. package/android/src/main/java/com/otakit/updater/WebViewActivation.java +26 -0
  23. package/dist/esm/definitions.d.ts +3 -3
  24. package/dist/esm/definitions.d.ts.map +1 -1
  25. package/ios/Sources/UpdaterPlugin/BundleCrypto.swift +35 -3
  26. package/ios/Sources/UpdaterPlugin/BundleInfo.swift +13 -0
  27. package/ios/Sources/UpdaterPlugin/BundleStore.swift +103 -77
  28. package/ios/Sources/UpdaterPlugin/CheckFailure.swift +41 -0
  29. package/ios/Sources/UpdaterPlugin/DeltaAssembler.swift +29 -17
  30. package/ios/Sources/UpdaterPlugin/DeviceEventClient.swift +12 -10
  31. package/ios/Sources/UpdaterPlugin/DocumentReadyBridge.swift +43 -0
  32. package/ios/Sources/UpdaterPlugin/DownloadRetry.swift +51 -0
  33. package/ios/Sources/UpdaterPlugin/Downloader.swift +97 -39
  34. package/ios/Sources/UpdaterPlugin/EventOutbox.swift +183 -0
  35. package/ios/Sources/UpdaterPlugin/ForegroundDeadline.swift +94 -0
  36. package/ios/Sources/UpdaterPlugin/HashUtils.swift +16 -0
  37. package/ios/Sources/UpdaterPlugin/ManifestClient.swift +2 -4
  38. package/ios/Sources/UpdaterPlugin/ManifestKeyConfig.swift +20 -0
  39. package/ios/Sources/UpdaterPlugin/ManifestVerifier.swift +7 -1
  40. package/ios/Sources/UpdaterPlugin/SDKVersion.swift +4 -0
  41. package/ios/Sources/UpdaterPlugin/UpdateOwner.swift +12 -0
  42. package/ios/Sources/UpdaterPlugin/UpdaterCoordinator.swift +146 -110
  43. package/ios/Sources/UpdaterPlugin/UpdaterPlugin.swift +206 -202
  44. package/ios/Tests/UpdaterPluginTests/BundleCryptoTests.swift +80 -0
  45. package/ios/Tests/UpdaterPluginTests/BundlePersistenceTests.swift +102 -0
  46. package/ios/Tests/UpdaterPluginTests/CheckFailureTests.swift +55 -0
  47. package/ios/Tests/UpdaterPluginTests/DeltaCacheIntegrityTests.swift +69 -0
  48. package/ios/Tests/UpdaterPluginTests/DocumentReadyBridgeTests.swift +102 -0
  49. package/ios/Tests/UpdaterPluginTests/DownloadIntegrityTests.swift +40 -0
  50. package/ios/Tests/UpdaterPluginTests/DownloadRetryTests.swift +189 -0
  51. package/ios/Tests/UpdaterPluginTests/EventDeliveryTests.swift +56 -0
  52. package/ios/Tests/UpdaterPluginTests/EventOutboxTests.swift +86 -0
  53. package/ios/Tests/UpdaterPluginTests/ForegroundDeadlineTests.swift +144 -0
  54. package/ios/Tests/UpdaterPluginTests/ManifestKeyConfigTests.swift +48 -0
  55. package/ios/Tests/UpdaterPluginTests/UpdateOwnerTests.swift +34 -0
  56. package/ios/Tests/UpdaterPluginTests/UpdaterCoordinatorTests.swift +394 -0
  57. package/package.json +7 -3
@@ -0,0 +1,174 @@
1
+ package com.otakit.updater;
2
+
3
+ import android.util.AtomicFile;
4
+ import java.io.File;
5
+ import java.io.FileOutputStream;
6
+ import java.io.IOException;
7
+ import java.nio.charset.StandardCharsets;
8
+ import java.util.ArrayList;
9
+ import java.util.List;
10
+ import org.json.JSONArray;
11
+ import org.json.JSONObject;
12
+
13
+ /** A bounded, atomic snapshot. Transport never owns or changes event identity. */
14
+ final class EventOutbox {
15
+
16
+ static final int LIMIT = 256;
17
+ static final long TTL = 7L * 24 * 60 * 60 * 1000;
18
+
19
+ static final class Entry {
20
+
21
+ final String id, url, appId, body;
22
+ final long createdAt, nextAt;
23
+ final int attempts;
24
+
25
+ Entry(String url, String appId, String body, long createdAt, int attempts, long nextAt)
26
+ throws Exception {
27
+ this.id = new JSONObject(body).getString("eventId");
28
+ this.url = url;
29
+ this.appId = appId;
30
+ this.body = body;
31
+ this.createdAt = createdAt;
32
+ this.attempts = attempts;
33
+ this.nextAt = nextAt;
34
+ }
35
+
36
+ JSONObject json() throws Exception {
37
+ return new JSONObject()
38
+ .put("url", url)
39
+ .put("appId", appId)
40
+ .put("body", body)
41
+ .put("createdAt", createdAt)
42
+ .put("attempts", attempts)
43
+ .put("nextAt", nextAt);
44
+ }
45
+ }
46
+
47
+ private final AtomicFile file;
48
+ private List<Entry> entries = new ArrayList<>();
49
+
50
+ EventOutbox(File directory) throws Exception {
51
+ if (!directory.isDirectory() && !directory.mkdirs()) throw new IOException(
52
+ "Cannot create event outbox"
53
+ );
54
+ file = new AtomicFile(new File(directory, "events.json"));
55
+ if (!file.getBaseFile().exists() && !new File(file.getBaseFile() + ".bak").exists()) return;
56
+ byte[] bytes;
57
+ try (java.io.InputStream input = file.openRead()) {
58
+ if (file.getBaseFile().length() > 4 * 1024 * 1024) throw new IOException(
59
+ "Event outbox too large"
60
+ );
61
+ java.io.ByteArrayOutputStream buffer = new java.io.ByteArrayOutputStream();
62
+ byte[] chunk = new byte[8192];
63
+ int count;
64
+ while ((count = input.read(chunk)) != -1) {
65
+ if (buffer.size() + count > 4 * 1024 * 1024) throw new IOException(
66
+ "Event outbox too large"
67
+ );
68
+ buffer.write(chunk, 0, count);
69
+ }
70
+ bytes = buffer.toByteArray();
71
+ }
72
+ try {
73
+ JSONArray stored = new JSONArray(new String(bytes, StandardCharsets.UTF_8));
74
+ for (int i = 0; i < stored.length(); i++) {
75
+ JSONObject item = stored.getJSONObject(i);
76
+ entries.add(
77
+ new Entry(
78
+ item.getString("url"),
79
+ item.getString("appId"),
80
+ item.getString("body"),
81
+ item.getLong("createdAt"),
82
+ item.getInt("attempts"),
83
+ item.getLong("nextAt")
84
+ )
85
+ );
86
+ }
87
+ if (entries.size() > LIMIT) throw new IllegalStateException("Invalid outbox length");
88
+ } catch (Exception corrupted) {
89
+ android.util.Log.w("OtaKit", "Discarding unreadable event outbox");
90
+ save(new ArrayList<>());
91
+ }
92
+ }
93
+
94
+ synchronized void enqueue(String url, String appId, String body, long now) throws Exception {
95
+ if (body.getBytes(StandardCharsets.UTF_8).length > 8192) throw new IOException(
96
+ "Device event too large"
97
+ );
98
+ List<Entry> next = live(now);
99
+ Entry entry = new Entry(url, appId, body, now, 0, now);
100
+ for (Entry existing : next) if (existing.id.equals(entry.id)) return;
101
+ while (next.size() >= LIMIT) next.remove(0);
102
+ next.add(entry);
103
+ save(next);
104
+ }
105
+
106
+ synchronized Entry ready(long now) throws Exception {
107
+ List<Entry> next = live(now);
108
+ if (next.size() != entries.size()) save(next);
109
+ for (Entry entry : entries) if (entry.nextAt <= now) return entry;
110
+ return null;
111
+ }
112
+
113
+ synchronized Long waitMilliseconds(long now) {
114
+ Long delay = null;
115
+ for (Entry entry : entries) {
116
+ long due = Math.min(entry.nextAt, entry.createdAt + TTL);
117
+ long candidate = Math.max(0, due - now);
118
+ delay = delay == null ? candidate : Math.min(delay, candidate);
119
+ }
120
+ return delay;
121
+ }
122
+
123
+ synchronized void complete(String id, int status, String retryAfter, long now, double random)
124
+ throws Exception {
125
+ List<Entry> next = live(now);
126
+ for (int i = 0; i < next.size(); i++) {
127
+ Entry entry = next.get(i);
128
+ if (!entry.id.equals(id)) continue;
129
+ if (
130
+ (status >= 200 && status < 300) ||
131
+ (status >= 300 && status < 500 && status != 408 && status != 425 && status != 429)
132
+ ) {
133
+ next.remove(i);
134
+ } else {
135
+ int attempts = Math.min(entry.attempts + 1, 20);
136
+ long backoff = (long) (Math.min(3_600_000, 5000L << Math.min(attempts - 1, 10)) *
137
+ (0.5 + random));
138
+ Long serverDelay = DownloadRetry.retryAfterMilliseconds(retryAfter, now);
139
+ long delay = serverDelay == null ? backoff : Math.max(backoff, serverDelay);
140
+ long due = delay > Long.MAX_VALUE - now ? Long.MAX_VALUE : now + delay;
141
+ next.set(i, new Entry(entry.url, entry.appId, entry.body, entry.createdAt, attempts, due));
142
+ }
143
+ break;
144
+ }
145
+ save(next);
146
+ }
147
+
148
+ private List<Entry> live(long now) {
149
+ List<Entry> result = new ArrayList<>();
150
+ for (Entry entry : entries) if (now < entry.createdAt + TTL) result.add(entry);
151
+ return result;
152
+ }
153
+
154
+ private void save(List<Entry> next) throws Exception {
155
+ JSONArray data = new JSONArray();
156
+ for (Entry entry : next) data.put(entry.json());
157
+ FileOutputStream output = null;
158
+ byte[] bytes = data.toString().getBytes(StandardCharsets.UTF_8);
159
+ try {
160
+ output = file.startWrite();
161
+ output.write(bytes);
162
+ output.getFD().sync();
163
+ file.finishWrite(output);
164
+ output = null;
165
+ if (!java.util.Arrays.equals(bytes, file.readFully())) throw new IOException(
166
+ "Event outbox commit failed"
167
+ );
168
+ entries = next;
169
+ } catch (Exception error) {
170
+ if (output != null) file.failWrite(output);
171
+ throw error;
172
+ }
173
+ }
174
+ }
@@ -0,0 +1,80 @@
1
+ package com.otakit.updater;
2
+
3
+ import java.io.File;
4
+ import java.io.FileOutputStream;
5
+ import java.io.InputStream;
6
+ import java.net.HttpURLConnection;
7
+ import java.net.URL;
8
+
9
+ final class FileDownloader {
10
+
11
+ static File download(URL url, File cacheDirectory, boolean allowInsecureUrls) throws Exception {
12
+ return download(url, cacheDirectory, allowInsecureUrls, new DownloadRetry());
13
+ }
14
+
15
+ static File download(URL url, File cacheDirectory, boolean allowInsecureUrls, DownloadRetry retry)
16
+ throws Exception {
17
+ ManifestClient.requireHTTPS(url, allowInsecureUrls);
18
+ return retry.run(() -> downloadOnce(url, cacheDirectory));
19
+ }
20
+
21
+ private static File downloadOnce(URL url, File cacheDirectory) throws Exception {
22
+ HttpURLConnection connection = (HttpURLConnection) DownloadRetry.network(url::openConnection);
23
+ File destination = null;
24
+ boolean complete = false;
25
+ try {
26
+ connection.setRequestMethod("GET");
27
+ connection.setConnectTimeout(15_000);
28
+ connection.setReadTimeout(60_000);
29
+ connection.setRequestProperty("Accept-Encoding", "identity");
30
+ int status = DownloadRetry.network(connection::getResponseCode);
31
+ if (status != 200) {
32
+ throw new DownloadRetry.HttpFailure(status, connection.getHeaderField("Retry-After"));
33
+ }
34
+ destination = File.createTempFile("otakit-", ".zip", cacheDirectory);
35
+ long receivedBytes = 0;
36
+ try (
37
+ InputStream input = DownloadRetry.network(connection::getInputStream);
38
+ FileOutputStream output = new FileOutputStream(destination)
39
+ ) {
40
+ byte[] buffer = new byte[8192];
41
+ int read;
42
+ while ((read = DownloadRetry.network(() -> input.read(buffer))) != -1) {
43
+ if (
44
+ Thread.currentThread().isInterrupted()
45
+ ) throw new java.util.concurrent.CancellationException("Download cancelled");
46
+ output.write(buffer, 0, read);
47
+ receivedBytes += read;
48
+ }
49
+ }
50
+ String declaredLength = connection.getHeaderField("Content-Length");
51
+ if (declaredLength != null && declaredLength.matches("[0-9]+")) {
52
+ long expectedBytes;
53
+ try {
54
+ expectedBytes = Long.parseLong(declaredLength);
55
+ } catch (NumberFormatException invalid) {
56
+ throw new java.io.IOException("Invalid download Content-Length");
57
+ }
58
+ if (receivedBytes != expectedBytes) {
59
+ throw new DownloadRetry.NetworkFailure(
60
+ new java.io.EOFException(
61
+ "body length mismatch; expectedBytes=" +
62
+ expectedBytes +
63
+ "; receivedBytes=" +
64
+ receivedBytes
65
+ )
66
+ );
67
+ }
68
+ }
69
+ complete = true;
70
+ return destination;
71
+ } finally {
72
+ // The caller cannot own the file until this method returns successfully.
73
+ if (!complete && destination != null) {
74
+ //noinspection ResultOfMethodCallIgnored
75
+ destination.delete();
76
+ }
77
+ connection.disconnect();
78
+ }
79
+ }
80
+ }
@@ -0,0 +1,84 @@
1
+ package com.otakit.updater;
2
+
3
+ import android.os.Handler;
4
+ import android.os.SystemClock;
5
+ import java.util.function.LongSupplier;
6
+
7
+ /** Owned by the main thread. Only foreground time consumes the readiness budget. */
8
+ final class ForegroundDeadline {
9
+
10
+ interface Scheduler {
11
+ Runnable schedule(long delayMs, Runnable callback);
12
+ }
13
+
14
+ private final LongSupplier now;
15
+ private final Scheduler scheduler;
16
+ private boolean foreground;
17
+ private long remainingMs;
18
+ private long startedAt;
19
+ private long generation;
20
+ private Runnable action;
21
+ private Runnable cancelScheduled;
22
+
23
+ ForegroundDeadline(Handler handler) {
24
+ this(SystemClock::uptimeMillis, (delay, callback) -> {
25
+ handler.postDelayed(callback, delay);
26
+ return () -> handler.removeCallbacks(callback);
27
+ });
28
+ }
29
+
30
+ ForegroundDeadline(LongSupplier now, Scheduler scheduler) {
31
+ this.now = now;
32
+ this.scheduler = scheduler;
33
+ }
34
+
35
+ void start(long timeoutMs, Runnable action) {
36
+ cancel();
37
+ remainingMs = Math.max(0, timeoutMs);
38
+ this.action = action;
39
+ arm();
40
+ }
41
+
42
+ void setForeground(boolean value) {
43
+ if (foreground == value) return;
44
+ if (foreground && action != null) {
45
+ remainingMs = Math.max(0, remainingMs - Math.max(0, now.getAsLong() - startedAt));
46
+ }
47
+ disarm();
48
+ foreground = value;
49
+ arm();
50
+ }
51
+
52
+ void cancel() {
53
+ disarm();
54
+ action = null;
55
+ remainingMs = 0;
56
+ }
57
+
58
+ private void disarm() {
59
+ generation++;
60
+ if (cancelScheduled != null) cancelScheduled.run();
61
+ cancelScheduled = null;
62
+ }
63
+
64
+ private void arm() {
65
+ if (!foreground || action == null) return;
66
+ startedAt = now.getAsLong();
67
+ long expectedGeneration = ++generation;
68
+ cancelScheduled = scheduler.schedule(remainingMs, () -> fire(expectedGeneration));
69
+ }
70
+
71
+ private void fire(long expectedGeneration) {
72
+ if (generation != expectedGeneration || !foreground || action == null) return;
73
+ remainingMs = Math.max(0, remainingMs - Math.max(0, now.getAsLong() - startedAt));
74
+ cancelScheduled = null;
75
+ if (remainingMs > 0) {
76
+ arm();
77
+ return;
78
+ }
79
+ Runnable expired = action;
80
+ action = null;
81
+ generation++;
82
+ expired.run();
83
+ }
84
+ }
@@ -45,4 +45,30 @@ final class HashUtils {
45
45
  static boolean verify(File file, String expectedSha256) throws Exception {
46
46
  return sha256(file).equalsIgnoreCase(expectedSha256);
47
47
  }
48
+
49
+ static void verifyDownload(File file, String expectedSha256, long expectedBytes, String kind)
50
+ throws Exception {
51
+ String actual = sha256(file);
52
+ boolean hashMatches = actual.equalsIgnoreCase(expectedSha256);
53
+ boolean sizeMatches = expectedBytes < 0 || expectedBytes == file.length();
54
+ if (!hashMatches || !sizeMatches) {
55
+ String safeExpected =
56
+ expectedSha256 != null && expectedSha256.matches("[a-fA-F0-9]{64}")
57
+ ? expectedSha256.toLowerCase(java.util.Locale.ROOT)
58
+ : "invalid";
59
+ throw new IllegalStateException(
60
+ "Downloaded " +
61
+ kind +
62
+ (hashMatches ? " size mismatch" : " hash mismatch") +
63
+ "; expectedSha256=" +
64
+ safeExpected +
65
+ "; actualSha256=" +
66
+ actual +
67
+ "; expectedBytes=" +
68
+ expectedBytes +
69
+ "; receivedBytes=" +
70
+ file.length()
71
+ );
72
+ }
73
+ }
48
74
  }
@@ -10,6 +10,16 @@ import org.json.JSONObject;
10
10
 
11
11
  final class ManifestClient {
12
12
 
13
+ static final class HttpFailure extends Exception {
14
+
15
+ final int status;
16
+
17
+ HttpFailure(int status) {
18
+ super("Latest request failed (" + status + ")");
19
+ this.status = status;
20
+ }
21
+ }
22
+
13
23
  private static final String BASE_CHANNEL_KEY = "__base__";
14
24
  private static final String DEFAULT_RUNTIME_KEY = "__default__";
15
25
 
@@ -155,12 +165,7 @@ final class ManifestClient {
155
165
  return null;
156
166
  }
157
167
  if (status != 200) {
158
- String body = readStream(
159
- connection.getErrorStream() != null
160
- ? connection.getErrorStream()
161
- : connection.getInputStream()
162
- );
163
- throw new IllegalStateException("Latest request failed (" + status + "): " + body);
168
+ throw new HttpFailure(status);
164
169
  }
165
170
 
166
171
  String payload = readStream(connection.getInputStream());
@@ -230,9 +235,7 @@ final class ManifestClient {
230
235
 
231
236
  if (manifestKeys != null && !manifestKeys.isEmpty()) {
232
237
  if (signature == null) {
233
- throw new IllegalStateException(
234
- "Manifest signature missing but signing keys are configured"
235
- );
238
+ throw new ManifestVerifier.VerificationException("signature_missing");
236
239
  }
237
240
 
238
241
  ManifestVerifier.verify(
@@ -0,0 +1,47 @@
1
+ package com.otakit.updater;
2
+
3
+ import android.util.Base64;
4
+ import java.util.ArrayList;
5
+ import java.util.List;
6
+ import org.json.JSONArray;
7
+ import org.json.JSONObject;
8
+
9
+ /** Empty means intentionally unconfigured; malformed explicit settings must never return empty. */
10
+ final class ManifestKeyConfig {
11
+
12
+ private ManifestKeyConfig() {}
13
+
14
+ static List<ManifestVerifier.KeyEntry> parse(JSONObject config) {
15
+ List<ManifestVerifier.KeyEntry> keys = new ArrayList<>();
16
+ try {
17
+ Object raw = config.opt("manifestKeys");
18
+ if (raw == null) return keys;
19
+ if (!(raw instanceof JSONArray)) return invalid();
20
+ JSONArray entries = (JSONArray) raw;
21
+ if (entries.length() == 0) return keys;
22
+ for (int i = 0; i < entries.length(); i++) {
23
+ JSONObject entry = entries.optJSONObject(i);
24
+ if (entry == null) continue;
25
+ String kid = entry.optString("kid", null);
26
+ String key = entry.optString("key", null);
27
+ if (kid != null && key != null) {
28
+ keys.add(new ManifestVerifier.KeyEntry(kid, Base64.decode(key, Base64.DEFAULT)));
29
+ }
30
+ }
31
+ if (keys.isEmpty()) return invalid();
32
+ return keys;
33
+ } catch (Exception error) {
34
+ return invalid();
35
+ }
36
+ }
37
+
38
+ private static List<ManifestVerifier.KeyEntry> invalid() {
39
+ android.util.Log.e(
40
+ "OtaKit",
41
+ "Invalid manifestKeys configuration. Manifest verification will reject all updates."
42
+ );
43
+ return java.util.Collections.singletonList(
44
+ new ManifestVerifier.KeyEntry("_invalid_", new byte[0])
45
+ );
46
+ }
47
+ }
@@ -22,6 +22,16 @@ final class ManifestVerifier {
22
22
 
23
23
  private ManifestVerifier() {}
24
24
 
25
+ static final class VerificationException extends Exception {
26
+
27
+ final String reason;
28
+
29
+ VerificationException(String reason) {
30
+ super("Manifest verification failed: " + reason);
31
+ this.reason = reason;
32
+ }
33
+ }
34
+
25
35
  /**
26
36
  * Verify a manifest signature using ES256 (ECDSA P-256 + SHA-256).
27
37
  *
@@ -54,7 +64,15 @@ final class ManifestVerifier {
54
64
  signature.iat,
55
65
  signature.exp
56
66
  );
57
- verifyPayload(payload, signature, trustedKeys);
67
+ try {
68
+ verifyPayload(payload, signature, trustedKeys);
69
+ } catch (VerificationException error) {
70
+ throw error;
71
+ } catch (Exception error) {
72
+ VerificationException failure = new VerificationException("signature_invalid");
73
+ failure.initCause(error);
74
+ throw failure;
75
+ }
58
76
  }
59
77
 
60
78
  private static void verifyPayload(
@@ -65,7 +83,7 @@ final class ManifestVerifier {
65
83
  // Check expiry
66
84
  long now = System.currentTimeMillis() / 1000;
67
85
  if (signature.exp <= now) {
68
- throw new IllegalStateException("Manifest signature expired");
86
+ throw new VerificationException("signature_expired");
69
87
  }
70
88
 
71
89
  // Find matching key
@@ -77,7 +95,7 @@ final class ManifestVerifier {
77
95
  }
78
96
  }
79
97
  if (keyEntry == null) {
80
- throw new IllegalStateException("Unknown signing key ID: " + signature.kid);
98
+ throw new VerificationException("signature_unknown_key");
81
99
  }
82
100
 
83
101
  // Decode base64url signature
@@ -93,7 +111,7 @@ final class ManifestVerifier {
93
111
  verifier.update(payload.getBytes(java.nio.charset.StandardCharsets.UTF_8));
94
112
 
95
113
  if (!verifier.verify(sigBytes)) {
96
- throw new IllegalStateException("Manifest signature verification failed");
114
+ throw new VerificationException("signature_invalid");
97
115
  }
98
116
  }
99
117
 
@@ -0,0 +1,9 @@
1
+ package com.otakit.updater;
2
+
3
+ // Generated from package.json by scripts/generate-native-sdk-version.mjs.
4
+ final class SDKVersion {
5
+
6
+ static final String VALUE = "3.0.0";
7
+
8
+ private SDKVersion() {}
9
+ }
@@ -0,0 +1,27 @@
1
+ package com.otakit.updater;
2
+
3
+ import java.util.concurrent.Callable;
4
+ import java.util.concurrent.CancellationException;
5
+ import java.util.function.BooleanSupplier;
6
+
7
+ /** Checked on the main thread together with state publication and activation. */
8
+ final class UpdateOwner {
9
+
10
+ private final BooleanSupplier isAvailable;
11
+ private boolean closed;
12
+
13
+ UpdateOwner(BooleanSupplier isAvailable) {
14
+ this.isAvailable = isAvailable;
15
+ }
16
+
17
+ void close() {
18
+ closed = true;
19
+ }
20
+
21
+ <T> T run(Callable<T> action) throws Exception {
22
+ if (closed || !isAvailable.getAsBoolean()) throw new CancellationException(
23
+ "Updater bridge is no longer active"
24
+ );
25
+ return action.call();
26
+ }
27
+ }