@otakit/capacitor-updater 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +183 -0
  3. package/UpdatekitUpdater.podspec +18 -0
  4. package/android/build.gradle +49 -0
  5. package/android/src/main/AndroidManifest.xml +3 -0
  6. package/android/src/main/java/com/updatekit/updater/BundleInfo.java +100 -0
  7. package/android/src/main/java/com/updatekit/updater/BundleStatus.java +28 -0
  8. package/android/src/main/java/com/updatekit/updater/BundleStore.java +264 -0
  9. package/android/src/main/java/com/updatekit/updater/DateUtils.java +17 -0
  10. package/android/src/main/java/com/updatekit/updater/HashUtils.java +32 -0
  11. package/android/src/main/java/com/updatekit/updater/HostedManifestKeys.java +37 -0
  12. package/android/src/main/java/com/updatekit/updater/ManifestClient.java +209 -0
  13. package/android/src/main/java/com/updatekit/updater/ManifestVerifier.java +146 -0
  14. package/android/src/main/java/com/updatekit/updater/StatsClient.java +80 -0
  15. package/android/src/main/java/com/updatekit/updater/UpdaterPlugin.java +963 -0
  16. package/android/src/main/java/com/updatekit/updater/ZipUtils.java +72 -0
  17. package/dist/esm/definitions.d.ts +229 -0
  18. package/dist/esm/definitions.d.ts.map +1 -0
  19. package/dist/esm/definitions.js +17 -0
  20. package/dist/esm/definitions.js.map +1 -0
  21. package/dist/esm/index.d.ts +5 -0
  22. package/dist/esm/index.d.ts.map +1 -0
  23. package/dist/esm/index.js +85 -0
  24. package/dist/esm/index.js.map +1 -0
  25. package/dist/esm/web.d.ts +25 -0
  26. package/dist/esm/web.d.ts.map +1 -0
  27. package/dist/esm/web.js +56 -0
  28. package/dist/esm/web.js.map +1 -0
  29. package/dist/plugin.cjs.js +165 -0
  30. package/dist/plugin.cjs.js.map +1 -0
  31. package/dist/plugin.js +168 -0
  32. package/dist/plugin.js.map +1 -0
  33. package/ios/Sources/UpdaterPlugin/BundleInfo.swift +39 -0
  34. package/ios/Sources/UpdaterPlugin/BundleStatus.swift +9 -0
  35. package/ios/Sources/UpdaterPlugin/BundleStore.swift +233 -0
  36. package/ios/Sources/UpdaterPlugin/Downloader.swift +120 -0
  37. package/ios/Sources/UpdaterPlugin/HashUtils.swift +33 -0
  38. package/ios/Sources/UpdaterPlugin/HostedManifestKeys.swift +23 -0
  39. package/ios/Sources/UpdaterPlugin/ManifestClient.swift +161 -0
  40. package/ios/Sources/UpdaterPlugin/ManifestVerifier.swift +115 -0
  41. package/ios/Sources/UpdaterPlugin/StatsClient.swift +66 -0
  42. package/ios/Sources/UpdaterPlugin/UpdaterPlugin.m +15 -0
  43. package/ios/Sources/UpdaterPlugin/UpdaterPlugin.swift +913 -0
  44. package/ios/Sources/UpdaterPlugin/ZipUtils.swift +90 -0
  45. package/package.json +85 -0
@@ -0,0 +1,32 @@
1
+ package com.updatekit.updater;
2
+
3
+ import java.io.File;
4
+ import java.io.FileInputStream;
5
+ import java.security.MessageDigest;
6
+
7
+ final class HashUtils {
8
+
9
+ private HashUtils() {}
10
+
11
+ static String sha256(File file) throws Exception {
12
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
13
+ try (FileInputStream input = new FileInputStream(file)) {
14
+ byte[] buffer = new byte[1024 * 1024];
15
+ int read;
16
+ while ((read = input.read(buffer)) > 0) {
17
+ digest.update(buffer, 0, read);
18
+ }
19
+ }
20
+
21
+ byte[] hash = digest.digest();
22
+ StringBuilder builder = new StringBuilder();
23
+ for (byte b : hash) {
24
+ builder.append(String.format("%02x", b));
25
+ }
26
+ return builder.toString();
27
+ }
28
+
29
+ static boolean verify(File file, String expectedSha256) throws Exception {
30
+ return sha256(file).equalsIgnoreCase(expectedSha256);
31
+ }
32
+ }
@@ -0,0 +1,37 @@
1
+ package com.updatekit.updater;
2
+
3
+ import android.util.Base64;
4
+ import java.util.ArrayList;
5
+ import java.util.List;
6
+ import java.util.Locale;
7
+
8
+ final class HostedManifestKeys {
9
+
10
+ private static final String MANAGED_SERVER_URL = "https://www.otakit.app/api/v1";
11
+
12
+ private HostedManifestKeys() {}
13
+
14
+ static boolean matchesManagedServer(String updateUrl) {
15
+ if (updateUrl == null) {
16
+ return false;
17
+ }
18
+
19
+ String normalized = updateUrl.trim().replaceAll("/+$", "").toLowerCase(Locale.ROOT);
20
+ return normalized.equals(MANAGED_SERVER_URL)
21
+ || normalized.equals("https://otakit.app/api/v1");
22
+ }
23
+
24
+ static List<ManifestVerifier.KeyEntry> createDefaultKeys() {
25
+ ArrayList<ManifestVerifier.KeyEntry> keys = new ArrayList<>();
26
+ keys.add(
27
+ new ManifestVerifier.KeyEntry(
28
+ "hosted-2026-04-02-ce611e6d",
29
+ Base64.decode(
30
+ "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAELg6eAj2+7aZ1FJnYUNMjOtWuQLJMomXkPvmeTQ3gXyabpLTDX0m3iWYO3cEOXqIR6NphGC6csS2T5bCtXwIBFw==",
31
+ Base64.DEFAULT
32
+ )
33
+ )
34
+ );
35
+ return keys;
36
+ }
37
+ }
@@ -0,0 +1,209 @@
1
+ package com.updatekit.updater;
2
+
3
+ import java.io.ByteArrayOutputStream;
4
+ import java.io.InputStream;
5
+ import java.net.HttpURLConnection;
6
+ import java.net.URL;
7
+ import java.nio.charset.StandardCharsets;
8
+ import org.json.JSONObject;
9
+
10
+ final class ManifestClient {
11
+
12
+ static final class ManifestSignature {
13
+
14
+ final String kid;
15
+ final String sig;
16
+ final int iat;
17
+ final int exp;
18
+
19
+ ManifestSignature(String kid, String sig, int iat, int exp) {
20
+ this.kid = kid;
21
+ this.sig = sig;
22
+ this.iat = iat;
23
+ this.exp = exp;
24
+ }
25
+ }
26
+
27
+ static final class LatestManifest {
28
+
29
+ final String version;
30
+ final String url;
31
+ final String sha256;
32
+ final int size;
33
+ final Integer minNativeBuild;
34
+ final String releaseId;
35
+ final ManifestSignature signature;
36
+
37
+ LatestManifest(
38
+ String version,
39
+ String url,
40
+ String sha256,
41
+ int size,
42
+ Integer minNativeBuild,
43
+ String releaseId,
44
+ ManifestSignature signature
45
+ ) {
46
+ this.version = version;
47
+ this.url = url;
48
+ this.sha256 = sha256;
49
+ this.size = size;
50
+ this.minNativeBuild = minNativeBuild;
51
+ this.releaseId = releaseId;
52
+ this.signature = signature;
53
+ }
54
+ }
55
+
56
+ private ManifestClient() {}
57
+
58
+ static void requireHTTPS(URL url, boolean allowInsecure) throws Exception {
59
+ String protocol = url.getProtocol().toLowerCase();
60
+ if ("https".equals(protocol)) return;
61
+ if (allowInsecure) {
62
+ String host = url.getHost().toLowerCase();
63
+ if ("localhost".equals(host) || "127.0.0.1".equals(host)) return;
64
+ }
65
+ throw new IllegalStateException("URL must use HTTPS: " + url.toString());
66
+ }
67
+
68
+ static LatestManifest fetchLatest(
69
+ String updateUrl,
70
+ String appId,
71
+ String channel,
72
+ String currentVersion,
73
+ String currentReleaseId,
74
+ String nativeBuild,
75
+ String platform,
76
+ boolean allowInsecureUrls,
77
+ java.util.List<ManifestVerifier.KeyEntry> manifestKeys
78
+ ) throws Exception {
79
+ String base = updateUrl.replaceAll("/+$", "");
80
+ URL url = new URL(base + "/manifest");
81
+
82
+ requireHTTPS(url, allowInsecureUrls);
83
+
84
+ HttpURLConnection connection = (HttpURLConnection) url.openConnection();
85
+ try {
86
+ connection.setRequestMethod("GET");
87
+ connection.setRequestProperty("X-App-Id", appId);
88
+ connection.setRequestProperty("X-Platform", platform);
89
+ if (channel != null && !channel.trim().isEmpty()) {
90
+ connection.setRequestProperty("X-Channel", channel);
91
+ }
92
+ connection.setRequestProperty("X-Current-Version", currentVersion);
93
+ if (currentReleaseId != null && !currentReleaseId.trim().isEmpty()) {
94
+ connection.setRequestProperty("X-Release-Id", currentReleaseId);
95
+ }
96
+ connection.setRequestProperty("X-Native-Build", nativeBuild);
97
+ connection.setConnectTimeout(15_000);
98
+ connection.setReadTimeout(30_000);
99
+
100
+ int status = connection.getResponseCode();
101
+ if (status == 204) {
102
+ return null;
103
+ }
104
+ if (status != 200) {
105
+ String body = readStream(
106
+ connection.getErrorStream() != null
107
+ ? connection.getErrorStream()
108
+ : connection.getInputStream()
109
+ );
110
+ throw new IllegalStateException("Latest request failed (" + status + "): " + body);
111
+ }
112
+
113
+ String payload = readStream(connection.getInputStream());
114
+ JSONObject json = new JSONObject(payload);
115
+
116
+ String version = json.getString("version");
117
+ String downloadUrl = json.getString("url");
118
+ String sha256 = json.getString("sha256");
119
+ int size = json.getInt("size");
120
+
121
+ Integer minNativeBuild = null;
122
+ if (json.has("minNativeBuild") && !json.isNull("minNativeBuild")) {
123
+ Object raw = json.get("minNativeBuild");
124
+ if (raw instanceof Number) {
125
+ minNativeBuild = ((Number) raw).intValue();
126
+ } else if (raw instanceof String) {
127
+ String value = ((String) raw).trim();
128
+ if (!value.isEmpty()) {
129
+ minNativeBuild = Integer.parseInt(value);
130
+ }
131
+ }
132
+ }
133
+
134
+ ManifestSignature signature = null;
135
+ if (json.has("signature") && !json.isNull("signature")) {
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
+ }
146
+
147
+ String releaseId = null;
148
+ if (json.has("releaseId") && !json.isNull("releaseId")) {
149
+ releaseId = json.getString("releaseId");
150
+ }
151
+
152
+ // Validate download URL scheme
153
+ requireHTTPS(new URL(downloadUrl), allowInsecureUrls);
154
+
155
+ if (manifestKeys == null || manifestKeys.isEmpty()) {
156
+ android.util.Log.w(
157
+ "UpdateKit",
158
+ "No manifest signing keys configured — signature verification is disabled for this request."
159
+ );
160
+ }
161
+
162
+ // Verify manifest signature if signing keys are configured
163
+ if (manifestKeys != null && !manifestKeys.isEmpty()) {
164
+ if (signature == null) {
165
+ throw new IllegalStateException(
166
+ "Manifest signature missing but signing keys are configured"
167
+ );
168
+ }
169
+ ManifestVerifier.verify(
170
+ appId,
171
+ channel,
172
+ platform,
173
+ version,
174
+ sha256,
175
+ size,
176
+ minNativeBuild,
177
+ signature,
178
+ manifestKeys
179
+ );
180
+ }
181
+
182
+ return new LatestManifest(
183
+ version,
184
+ downloadUrl,
185
+ sha256,
186
+ size,
187
+ minNativeBuild,
188
+ releaseId,
189
+ signature
190
+ );
191
+ } finally {
192
+ connection.disconnect();
193
+ }
194
+ }
195
+
196
+ private static String readStream(InputStream input) throws Exception {
197
+ if (input == null) {
198
+ return "";
199
+ }
200
+ try (InputStream stream = input; ByteArrayOutputStream out = new ByteArrayOutputStream()) {
201
+ byte[] buffer = new byte[8192];
202
+ int read;
203
+ while ((read = stream.read(buffer)) > 0) {
204
+ out.write(buffer, 0, read);
205
+ }
206
+ return new String(out.toByteArray(), StandardCharsets.UTF_8);
207
+ }
208
+ }
209
+ }
@@ -0,0 +1,146 @@
1
+ package com.updatekit.updater;
2
+
3
+ import android.util.Base64;
4
+ import java.security.KeyFactory;
5
+ import java.security.PublicKey;
6
+ import java.security.Signature;
7
+ import java.security.spec.X509EncodedKeySpec;
8
+ import java.util.List;
9
+
10
+ final class ManifestVerifier {
11
+
12
+ static final class KeyEntry {
13
+
14
+ final String kid;
15
+ final byte[] derData;
16
+
17
+ KeyEntry(String kid, byte[] derData) {
18
+ this.kid = kid;
19
+ this.derData = derData;
20
+ }
21
+ }
22
+
23
+ private ManifestVerifier() {}
24
+
25
+ /**
26
+ * Verify a manifest signature using ES256 (ECDSA P-256 + SHA-256).
27
+ *
28
+ * @throws Exception on verification failure (unknown kid, expired, invalid signature).
29
+ */
30
+ static void verify(
31
+ String appId,
32
+ String channel,
33
+ String platform,
34
+ String version,
35
+ String sha256,
36
+ int size,
37
+ Integer minNativeBuild,
38
+ ManifestClient.ManifestSignature signature,
39
+ List<KeyEntry> trustedKeys
40
+ ) throws Exception {
41
+ // Check expiry
42
+ long now = System.currentTimeMillis() / 1000;
43
+ if (signature.exp <= now) {
44
+ throw new IllegalStateException("Manifest signature expired");
45
+ }
46
+
47
+ // Find matching key
48
+ KeyEntry keyEntry = null;
49
+ for (KeyEntry entry : trustedKeys) {
50
+ if (entry.kid.equals(signature.kid)) {
51
+ keyEntry = entry;
52
+ break;
53
+ }
54
+ }
55
+ if (keyEntry == null) {
56
+ throw new IllegalStateException("Unknown signing key ID: " + signature.kid);
57
+ }
58
+
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
+ // Decode base64url signature
74
+ byte[] sigBytes = base64UrlDecode(signature.sig);
75
+
76
+ // Verify with java.security
77
+ X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyEntry.derData);
78
+ KeyFactory keyFactory = KeyFactory.getInstance("EC");
79
+ PublicKey verificationKey = keyFactory.generatePublic(keySpec);
80
+
81
+ Signature verifier = Signature.getInstance("SHA256withECDSA");
82
+ verifier.initVerify(verificationKey);
83
+ verifier.update(payload.getBytes(java.nio.charset.StandardCharsets.UTF_8));
84
+
85
+ if (!verifier.verify(sigBytes)) {
86
+ throw new IllegalStateException("Manifest signature verification failed");
87
+ }
88
+ }
89
+
90
+ private static String buildCanonicalPayload(
91
+ String appId,
92
+ String channel,
93
+ String platform,
94
+ String version,
95
+ String sha256,
96
+ int size,
97
+ Integer minNativeBuild,
98
+ String kid,
99
+ int iat,
100
+ int exp
101
+ ) {
102
+ String minBuildStr = minNativeBuild != null ? String.valueOf(minNativeBuild) : "null";
103
+ return (
104
+ "MANIFEST_V1\n" +
105
+ "appId:" +
106
+ appId +
107
+ "\n" +
108
+ "channel:" +
109
+ (channel != null ? channel : "null") +
110
+ "\n" +
111
+ "platform:" +
112
+ platform +
113
+ "\n" +
114
+ "version:" +
115
+ version +
116
+ "\n" +
117
+ "sha256:" +
118
+ sha256 +
119
+ "\n" +
120
+ "size:" +
121
+ size +
122
+ "\n" +
123
+ "minNativeBuild:" +
124
+ minBuildStr +
125
+ "\n" +
126
+ "kid:" +
127
+ kid +
128
+ "\n" +
129
+ "iat:" +
130
+ iat +
131
+ "\n" +
132
+ "exp:" +
133
+ exp
134
+ );
135
+ }
136
+
137
+ private static byte[] base64UrlDecode(String input) {
138
+ // Convert base64url to standard base64
139
+ String base64 = input.replace('-', '+').replace('_', '/');
140
+ int remainder = base64.length() % 4;
141
+ if (remainder > 0) {
142
+ base64 += "====".substring(remainder);
143
+ }
144
+ return Base64.decode(base64, Base64.DEFAULT);
145
+ }
146
+ }
@@ -0,0 +1,80 @@
1
+ package com.updatekit.updater;
2
+
3
+ import java.io.OutputStream;
4
+ import java.net.HttpURLConnection;
5
+ import java.net.URL;
6
+ import java.nio.charset.StandardCharsets;
7
+ import java.util.concurrent.ExecutorService;
8
+ import java.util.concurrent.Executors;
9
+ import org.json.JSONObject;
10
+
11
+ final class StatsClient {
12
+
13
+ private static final ExecutorService executor = Executors.newSingleThreadExecutor();
14
+
15
+ private StatsClient() {}
16
+
17
+ static void send(
18
+ String updateUrl,
19
+ String appId,
20
+ String platform,
21
+ String action,
22
+ String bundleVersion,
23
+ String channel,
24
+ String releaseId,
25
+ String nativeBuild,
26
+ String errorMessage
27
+ ) {
28
+ executor.execute(() -> {
29
+ HttpURLConnection connection = null;
30
+ try {
31
+ String base = updateUrl.replaceAll("/+$", "");
32
+ URL url = new URL(base + "/stats");
33
+
34
+ JSONObject payload = new JSONObject();
35
+ payload.put("platform", platform);
36
+ payload.put("action", action);
37
+ if (bundleVersion != null) {
38
+ payload.put("bundleVersion", bundleVersion);
39
+ }
40
+ if (channel != null && !channel.isEmpty()) {
41
+ payload.put("channel", channel);
42
+ }
43
+ if (releaseId != null && !releaseId.isEmpty()) {
44
+ payload.put("releaseId", releaseId);
45
+ }
46
+ if (nativeBuild != null) {
47
+ payload.put("nativeBuild", nativeBuild);
48
+ }
49
+ if (errorMessage != null) {
50
+ String truncated =
51
+ errorMessage.length() > 500 ? errorMessage.substring(0, 500) : errorMessage;
52
+ payload.put("errorMessage", truncated);
53
+ }
54
+
55
+ byte[] body = payload.toString().getBytes(StandardCharsets.UTF_8);
56
+
57
+ connection = (HttpURLConnection) url.openConnection();
58
+ connection.setRequestMethod("POST");
59
+ connection.setRequestProperty("X-App-Id", appId);
60
+ connection.setRequestProperty("Content-Type", "application/json");
61
+ connection.setConnectTimeout(10_000);
62
+ connection.setReadTimeout(10_000);
63
+ connection.setDoOutput(true);
64
+
65
+ try (OutputStream output = connection.getOutputStream()) {
66
+ output.write(body);
67
+ }
68
+
69
+ // Fire and forget - just trigger the request
70
+ connection.getResponseCode();
71
+ } catch (Exception ignored) {
72
+ // Stats are best-effort, don't fail on errors
73
+ } finally {
74
+ if (connection != null) {
75
+ connection.disconnect();
76
+ }
77
+ }
78
+ });
79
+ }
80
+ }