@otakit/capacitor-updater 2.0.1 → 2.1.1

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 (37) hide show
  1. package/{UpdatekitUpdater.podspec → OtaKitUpdater.podspec} +1 -1
  2. package/README.md +165 -130
  3. package/android/build.gradle +1 -1
  4. package/android/src/main/java/com/{updatekit → otakit}/updater/BundleInfo.java +1 -1
  5. package/android/src/main/java/com/{updatekit → otakit}/updater/BundleStatus.java +1 -1
  6. package/android/src/main/java/com/{updatekit → otakit}/updater/BundleStore.java +38 -9
  7. package/android/src/main/java/com/{updatekit → otakit}/updater/DateUtils.java +1 -1
  8. package/android/src/main/java/com/{updatekit → otakit}/updater/DeviceEventClient.java +3 -4
  9. package/android/src/main/java/com/{updatekit → otakit}/updater/HashUtils.java +1 -1
  10. package/android/src/main/java/com/{updatekit → otakit}/updater/HostedManifestKeys.java +1 -1
  11. package/android/src/main/java/com/{updatekit → otakit}/updater/ManifestClient.java +32 -15
  12. package/android/src/main/java/com/{updatekit → otakit}/updater/ManifestVerifier.java +1 -1
  13. package/android/src/main/java/com/otakit/updater/UpdaterCoordinator.java +726 -0
  14. package/android/src/main/java/com/otakit/updater/UpdaterPlugin.java +1191 -0
  15. package/android/src/main/java/com/{updatekit → otakit}/updater/ZipUtils.java +1 -1
  16. package/dist/esm/definitions.d.ts +48 -105
  17. package/dist/esm/definitions.d.ts.map +1 -1
  18. package/dist/esm/definitions.js.map +1 -1
  19. package/dist/esm/index.d.ts.map +1 -1
  20. package/dist/esm/index.js +6 -60
  21. package/dist/esm/index.js.map +1 -1
  22. package/dist/esm/web.d.ts +7 -11
  23. package/dist/esm/web.d.ts.map +1 -1
  24. package/dist/esm/web.js +8 -14
  25. package/dist/esm/web.js.map +1 -1
  26. package/dist/plugin.cjs.js +14 -74
  27. package/dist/plugin.cjs.js.map +1 -1
  28. package/dist/plugin.js +14 -74
  29. package/dist/plugin.js.map +1 -1
  30. package/ios/Sources/UpdaterPlugin/BundleStore.swift +28 -6
  31. package/ios/Sources/UpdaterPlugin/Downloader.swift +1 -1
  32. package/ios/Sources/UpdaterPlugin/ManifestClient.swift +8 -4
  33. package/ios/Sources/UpdaterPlugin/UpdaterCoordinator.swift +635 -0
  34. package/ios/Sources/UpdaterPlugin/UpdaterPlugin.m +3 -5
  35. package/ios/Sources/UpdaterPlugin/UpdaterPlugin.swift +506 -551
  36. package/package.json +2 -2
  37. package/android/src/main/java/com/updatekit/updater/UpdaterPlugin.java +0 -1191
@@ -3,7 +3,7 @@ require 'json'
3
3
  package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
4
4
 
5
5
  Pod::Spec.new do |s|
6
- s.name = 'UpdatekitUpdater'
6
+ s.name = 'OtaKitUpdater'
7
7
  s.version = package['version']
8
8
  s.summary = package['description']
9
9
  s.license = package['license']
package/README.md CHANGED
@@ -7,38 +7,17 @@ Capacitor OTA updater plugin for OtaKit.
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
- - activates them on the next launch, next resume, or immediately
11
- - checks for updates on cold start and app resume (configurable interval)
12
- - also supports fully manual update prompts when the app wants control
13
- - supports optional `runtimeVersion` lanes for native compatibility boundaries
14
- - requires `notifyAppReady()` as the success handshake
15
- - rolls back automatically if the new bundle does not prove healthy
10
+ - applies staged bundles on cold start, resume, or manual command
11
+ - uses `notifyAppReady()` as the health handshake
12
+ - rolls back automatically if a newly applied bundle does not prove healthy
16
13
 
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.
14
+ The plugin is intentionally small:
21
15
 
22
- For normal app code, the main public methods are:
16
+ - three automatic lifecycle entry points: runtime, launch, resume
17
+ - one shared set of update primitives: check, download, apply
18
+ - one rollback safety loop
23
19
 
24
- ```ts
25
- const state = await OtaKit.getState();
26
- const latest = await OtaKit.check();
27
-
28
- // Low-level manual flow:
29
- const bundle = await OtaKit.download();
30
- if (bundle) {
31
- await OtaKit.apply();
32
- }
33
-
34
- // Or the one-shot manual helper:
35
- await OtaKit.update();
36
- await OtaKit.notifyAppReady();
37
- ```
38
-
39
- For manual mode, `getState()` tells you if something is already staged,
40
- `check()` tells you whether a newer update exists, `download()` stages it, and
41
- `update()` is the one-shot helper that downloads and applies the newest update.
20
+ There is no built-in splash or overlay manager in this model.
42
21
 
43
22
  ## Hosted config
44
23
 
@@ -47,188 +26,244 @@ plugins: {
47
26
  OtaKit: {
48
27
  appId: "YOUR_OTAKIT_APP_ID",
49
28
  appReadyTimeout: 10000,
29
+
50
30
  // Optional:
51
31
  // channel: "staging",
52
32
  // runtimeVersion: "2026.04",
53
- // updateMode: "next-resume",
54
- // updateMode: "manual",
55
- // updateMode: "immediate",
33
+ // launchPolicy: "apply-staged",
34
+ // resumePolicy: "shadow",
35
+ // runtimePolicy: "immediate",
36
+ // checkInterval: 600000,
56
37
  }
57
38
  }
58
39
  ```
59
40
 
60
41
  Advanced overrides for self-hosting or custom trust only:
61
42
 
62
- - `cdnUrl` for manifest and bundle delivery
63
- - `ingestUrl` for event ingest requests
64
- - `serverUrl` for self-hosted control-plane tooling such as the CLI. The native runtime uses `cdnUrl` and `ingestUrl` instead.
43
+ - `cdnUrl`
44
+ - `ingestUrl`
45
+ - `serverUrl`
65
46
  - `manifestKeys`
66
47
  - `allowInsecureUrls`
67
48
 
68
49
  Hosted OtaKit already points at the managed ingest service and CDN and already
69
50
  trusts the managed manifest signing keys.
70
51
 
71
- ## Channels vs runtimeVersion
52
+ ## Policies
72
53
 
73
- - `channel` answers "who should get this rollout?"
74
- - `runtimeVersion` answers "which native app shell can safely run this bundle?"
54
+ The plugin has three automatic events:
75
55
 
76
- Use channels for rollout tracks such as `beta`, `staging`, or `production`.
56
+ - `runtime`
57
+ Cold start where the current `runtimeVersion` lane has not been resolved yet.
58
+ - `launch`
59
+ Normal cold start after runtime is already resolved.
60
+ - `resume`
61
+ App returning from background.
77
62
 
78
- Use `runtimeVersion` when a new store build creates a new compatibility boundary and you do not
79
- want devices on that new native shell to keep receiving older OTA bundles.
63
+ Each event uses the same policy names:
80
64
 
81
- When `runtimeVersion` is set:
65
+ ```ts
66
+ type OtaKitPolicy = 'off' | 'shadow' | 'apply-staged' | 'immediate';
67
+ ```
82
68
 
83
- - the plugin requests bundle updates only for that runtime lane
84
- - bundle uploads inherit the same runtime value automatically through the CLI
85
- - releases stay simple: publish the bundle, and it naturally stays inside its own runtime lane
69
+ Semantics:
86
70
 
87
- ## Trust model
71
+ - `shadow`
72
+ check + download, never apply on that event
73
+ - `apply-staged`
74
+ apply a staged bundle if one already exists, otherwise behave like `shadow`
75
+ - `immediate`
76
+ check + download + apply
88
77
 
89
- The plugin does not just download from a URL and trust the result.
78
+ Recommended defaults:
90
79
 
91
- 1. it fetches the latest manifest from the CDN for its app + channel + runtimeVersion lane
92
- 2. it verifies the manifest signature when manifest keys are configured
93
- 3. it compares that manifest against the current and staged bundle already on the device
94
- 4. if the manifest is newer, it downloads the bundle zip
95
- 5. it verifies the zip against the manifest `sha256`
96
- 6. it stages and activates the bundle
80
+ ```ts
81
+ launchPolicy = 'apply-staged';
82
+ resumePolicy = 'shadow';
83
+ runtimePolicy = 'immediate';
84
+ ```
97
85
 
98
- In the hosted path, managed signing keys are already built in.
86
+ That means:
99
87
 
100
- ## Update modes
88
+ - fresh install or new `runtimeVersion`: catch up immediately
89
+ - later cold starts: apply already staged content if present, otherwise download
90
+ the next update in the background
91
+ - resumes: periodically check and stage in the background
101
92
 
102
- All automatic modes check for updates on **cold start** (always) and **app
103
- resume**.
93
+ If an app wants full JS control:
104
94
 
105
- ### Production modes
95
+ ```ts
96
+ launchPolicy = 'off';
97
+ resumePolicy = 'off';
98
+ runtimePolicy = 'off';
99
+ ```
106
100
 
107
- - `next-launch` (default)
108
- check and download in the background on cold start and resume.
109
- activate the staged bundle only on the next cold start.
110
- zero disruption during a session — the user never sees a surprise reload.
101
+ ## Check interval
111
102
 
112
- - `next-resume` (more eager)
113
- check and download in the background on cold start and resume.
114
- activate the staged bundle on the next resume or cold start.
103
+ `checkInterval` defaults to 10 minutes and only applies to background resume
104
+ checks:
115
105
 
116
- ### Manual mode
106
+ - `resumePolicy: "shadow"`
107
+ - `resumePolicy: "apply-staged"` when there is no staged bundle to apply
117
108
 
118
- - `manual`
119
- no automatic checks, no automatic staged activation.
120
- the app integration drives everything via `check()`, `download()`, `apply()`, or `update()`.
109
+ Set `checkInterval` to `0` or a negative value to disable resume throttling.
121
110
 
122
- ### Development mode
111
+ It does not throttle:
123
112
 
113
+ - launch handling
114
+ - runtime handling
124
115
  - `immediate`
125
- checks, downloads, and activates in one shot as soon as possible on cold start and resume (the user may briefly see the previous version before a reload).
126
- primarily for development and testing — not recommended for production.
127
-
116
+ - manual JS APIs
128
117
 
129
118
  ## Runtime model
130
119
 
131
- The plugin keeps three important runtime pointers:
120
+ The plugin keeps three important pointers:
132
121
 
133
122
  - `current`
134
- the bundle the WebView is currently serving
123
+ the bundle the WebView is serving now
135
124
  - `fallback`
136
125
  the last known-good bundle used for rollback
137
126
  - `staged`
138
127
  a downloaded bundle waiting to be activated
139
128
 
140
- Bundle lifecycle is intentionally small:
129
+ Bundle lifecycle:
141
130
 
142
131
  ```text
143
132
  download -> pending -> trial -> success
144
133
  |
145
- +-> error -> rollback -> delete failed files
134
+ +-> error -> rollback
146
135
  ```
147
136
 
148
- In practice:
137
+ If a bundle is applied and never calls `notifyAppReady()`:
149
138
 
150
- - downloads start as `pending`
151
- - an activated bundle becomes `trial`
152
- - `notifyAppReady()` promotes `trial` to `success`
153
- - timeout or restart during `trial` causes rollback
139
+ - timeout triggers rollback while the app is running
140
+ - or the next cold start detects the still-trial bundle and rolls back before
141
+ boot continues
154
142
 
155
- That gives the plugin a small but important safety model:
143
+ The last failed applied bundle is persisted so the plugin does not immediately
144
+ download and apply the same broken release again.
156
145
 
157
- - `current` is what is serving now
158
- - `fallback` is the last known-good bundle
159
- - `staged` is what can be activated next
160
- - rollback always goes back to `fallback`, not to an arbitrary older bundle
146
+ ## Automatic flow
161
147
 
162
- ## Automatic and manual flows
163
-
164
- ### Automatic flow
165
-
166
- For the normal hosted path, the app usually just needs to call:
148
+ For the normal hosted path, most apps only need:
167
149
 
168
150
  ```ts
169
151
  await OtaKit.notifyAppReady();
170
152
  ```
171
153
 
172
- The plugin handles checking, downloading, activation, and rollback based on
173
- `updateMode`. In `next-launch` and `next-resume`, it checks on cold start and
174
- every time the app comes back from the background, throttled by `checkInterval`.
175
- `immediate` bypasses that throttle.
154
+ The plugin handles checking, staging, applying, rollback, and runtime-lane
155
+ catch-up based on the configured policies.
156
+
157
+ ## Loading screen recommendation
158
+
159
+ OtaKit does not manage a splash screen or loading overlay for you.
176
160
 
177
- For most apps, this is the entire runtime integration.
161
+ Recommended startup order:
178
162
 
179
- ### Manual flow
163
+ 1. keep a native splash screen or fullscreen loading view visible
164
+ 2. finish your normal app bootstrap
165
+ 3. call `notifyAppReady()`
166
+ 4. hide the splash screen or loading view
180
167
 
181
- For app-driven prompts:
168
+ For React and Next.js apps, treat this as part of the default setup, not an
169
+ optional polish step.
170
+
171
+ ## Manual APIs
172
+
173
+ The manual surface maps to the same internal engine:
182
174
 
183
175
  ```ts
184
176
  const state = await OtaKit.getState();
185
- const latest = await OtaKit.check();
177
+ const check = await OtaKit.check();
178
+ const download = await OtaKit.download();
179
+ await OtaKit.notifyAppReady();
180
+ ```
186
181
 
187
- if (latest) {
188
- await OtaKit.update();
189
- }
182
+ `check()` returns:
183
+
184
+ ```ts
185
+ type CheckResult =
186
+ | { kind: 'no_update' }
187
+ | { kind: 'already_staged'; latest: LatestVersion }
188
+ | { kind: 'update_available'; latest: LatestVersion };
190
189
  ```
191
190
 
192
- Or the split version:
191
+ `download()` returns:
193
192
 
194
193
  ```ts
195
- await OtaKit.download();
196
- await OtaKit.apply();
194
+ type DownloadResult = { kind: 'no_update' } | { kind: 'staged'; bundle: BundleInfo };
197
195
  ```
198
196
 
199
- Use the split flow when the app wants to download in the background and switch
200
- only after explicit user confirmation.
197
+ `update()` uses the same native immediate-flow operation as automatic
198
+ `"immediate"` policies:
201
199
 
202
- ## Throttle
200
+ ```ts
201
+ await OtaKit.update();
202
+ ```
203
203
 
204
- `checkInterval` (default 10 min) only applies to automatic checks in
205
- `next-launch` and `next-resume`. Manual `check()` / `download()` calls are
206
- always live, and `immediate` mode ignores the interval entirely.
204
+ That keeps the manual convenience path atomic inside the native plugin instead
205
+ of splitting it into separate `download()` and `apply()` calls.
207
206
 
208
- ## Retention and deletion
207
+ `apply()` and successful `update()` are terminal operations:
209
208
 
210
- - the builtin, current, fallback, and staged bundles are protected
211
- - superseded staged bundles are deleted automatically
212
- - failed trial bundles are deleted during rollback
213
- - `debug.deleteBundle()` only works for downloaded bundles outside runtime state
209
+ - on success they reload the WebView
210
+ - they do not resolve back into the old JS context
211
+ - call `notifyAppReady()` from normal startup after the reloaded app boots
214
212
 
215
- ## Source areas
213
+ There is no listener/event API in this refactor. If an app later needs a
214
+ smaller reactive surface, that can be added intentionally.
216
215
 
217
- - `src/definitions.ts`: public types and methods
218
- - `src/index.ts`: Capacitor registration and JS wrapper
219
- - `src/web.ts`: web fallback implementation
220
- - `ios/Sources/UpdaterPlugin/*`: iOS implementation
221
- - `android/src/main/java/com/updatekit/updater/*`: Android implementation
216
+ ## Example manual flow
222
217
 
223
- ## Build locally
218
+ ```ts
219
+ const check = await OtaKit.check();
224
220
 
225
- ```bash
226
- pnpm --filter @otakit/capacitor-updater build
227
- pnpm --filter @otakit/capacitor-updater typecheck
221
+ if (check.kind === 'update_available') {
222
+ const result = await OtaKit.download();
223
+ if (result.kind === 'staged') {
224
+ await OtaKit.apply();
225
+ }
226
+ }
228
227
  ```
229
228
 
230
- Repo-level verification:
229
+ Or use the one-shot helper:
231
230
 
232
- ```bash
233
- npm run build
231
+ ```ts
232
+ await OtaKit.update();
233
+ ```
234
+
235
+ After the app reloads and starts again, call:
236
+
237
+ ```ts
238
+ await OtaKit.notifyAppReady();
234
239
  ```
240
+
241
+ ## Compatibility lanes
242
+
243
+ - `channel` answers "who should get this rollout?"
244
+ - `runtimeVersion` answers "which native app shell can safely run this bundle?"
245
+
246
+ Use channels for rollout tracks such as `beta`, `staging`, or `production`.
247
+
248
+ Use `runtimeVersion` when a new store build creates a new compatibility
249
+ boundary and you do not want devices on that native shell to keep receiving
250
+ older OTA bundles.
251
+
252
+ ## Trust model
253
+
254
+ The plugin does not just download arbitrary zips from a URL.
255
+
256
+ 1. it fetches the latest manifest for the current app + channel + runtime lane
257
+ 2. it verifies the manifest signature when keys are configured
258
+ 3. it compares the manifest with current, staged, and last-failed local state
259
+ 4. it downloads only when a newer usable bundle exists
260
+ 5. it verifies the zip against the manifest `sha256`
261
+ 6. it stages and later applies the bundle
262
+
263
+ ## Source areas
264
+
265
+ - `src/definitions.ts`: public types and config
266
+ - `src/index.ts`: Capacitor registration and JS wrapper
267
+ - `src/web.ts`: web fallback implementation
268
+ - `ios/Sources/UpdaterPlugin/*`: iOS implementation
269
+ - `android/src/main/java/com/otakit/updater/*`: Android implementation
@@ -17,7 +17,7 @@ buildscript {
17
17
  apply plugin: 'com.android.library'
18
18
 
19
19
  android {
20
- namespace "com.updatekit.updater"
20
+ namespace "com.otakit.updater"
21
21
  compileSdk project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 34
22
22
  defaultConfig {
23
23
  minSdk project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 23
@@ -1,4 +1,4 @@
1
- package com.updatekit.updater;
1
+ package com.otakit.updater;
2
2
 
3
3
  import com.getcapacitor.JSObject;
4
4
  import org.json.JSONException;
@@ -1,4 +1,4 @@
1
- package com.updatekit.updater;
1
+ package com.otakit.updater;
2
2
 
3
3
  enum BundleStatus {
4
4
  BUILTIN("builtin"),
@@ -1,4 +1,4 @@
1
- package com.updatekit.updater;
1
+ package com.otakit.updater;
2
2
 
3
3
  import android.content.Context;
4
4
  import android.content.SharedPreferences;
@@ -20,7 +20,8 @@ final class BundleStore {
20
20
  private static final String KEY_CURRENT = "current_bundle_id";
21
21
  private static final String KEY_FALLBACK = "fallback_bundle_id";
22
22
  private static final String KEY_STAGED = "staged_bundle_id";
23
- private static final String KEY_FAILED_INFO = "failed_bundle_info";
23
+ private static final String KEY_LAST_FAILED_BUNDLE_INFO = "last_failed_bundle_info";
24
+ private static final String KEY_LAST_RESOLVED_RUNTIME_KEY = "last_resolved_runtime_key";
24
25
 
25
26
  private final Context context;
26
27
  private final SharedPreferences prefs;
@@ -29,7 +30,12 @@ final class BundleStore {
29
30
  private final String nativeBuild;
30
31
  private final String appRuntimeVersion;
31
32
 
32
- BundleStore(Context context, String builtinVersion, String nativeBuild, String appRuntimeVersion) {
33
+ BundleStore(
34
+ Context context,
35
+ String builtinVersion,
36
+ String nativeBuild,
37
+ String appRuntimeVersion
38
+ ) {
33
39
  this.context = context.getApplicationContext();
34
40
  this.prefs = this.context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
35
41
  this.builtinVersion = builtinVersion;
@@ -134,6 +140,10 @@ final class BundleStore {
134
140
  return current != null ? current : builtinBundle();
135
141
  }
136
142
 
143
+ synchronized String getCurrentBundleId() {
144
+ return prefs.getString(KEY_CURRENT, null);
145
+ }
146
+
137
147
  synchronized void setCurrentBundleId(String id) {
138
148
  SharedPreferences.Editor editor = prefs.edit();
139
149
  if (id == null) {
@@ -153,6 +163,10 @@ final class BundleStore {
153
163
  return fallback != null ? fallback : builtinBundle();
154
164
  }
155
165
 
166
+ synchronized String getFallbackBundleId() {
167
+ return prefs.getString(KEY_FALLBACK, null);
168
+ }
169
+
156
170
  synchronized void setFallbackBundleId(String id) {
157
171
  SharedPreferences.Editor editor = prefs.edit();
158
172
  if (id == null) {
@@ -177,22 +191,22 @@ final class BundleStore {
177
191
  editor.commit();
178
192
  }
179
193
 
180
- synchronized void setFailedBundle(BundleInfo bundle) {
194
+ synchronized void setLastFailedBundle(BundleInfo bundle) {
181
195
  SharedPreferences.Editor editor = prefs.edit();
182
196
  if (bundle == null) {
183
- editor.remove(KEY_FAILED_INFO);
197
+ editor.remove(KEY_LAST_FAILED_BUNDLE_INFO);
184
198
  } else {
185
199
  try {
186
- editor.putString(KEY_FAILED_INFO, bundle.toJSONObject().toString());
200
+ editor.putString(KEY_LAST_FAILED_BUNDLE_INFO, bundle.toJSONObject().toString());
187
201
  } catch (JSONException ignored) {
188
- editor.remove(KEY_FAILED_INFO);
202
+ editor.remove(KEY_LAST_FAILED_BUNDLE_INFO);
189
203
  }
190
204
  }
191
205
  editor.apply();
192
206
  }
193
207
 
194
- synchronized BundleInfo getFailedBundle() {
195
- String json = prefs.getString(KEY_FAILED_INFO, null);
208
+ synchronized BundleInfo getLastFailedBundle() {
209
+ String json = prefs.getString(KEY_LAST_FAILED_BUNDLE_INFO, null);
196
210
  if (json == null) {
197
211
  return null;
198
212
  }
@@ -203,6 +217,21 @@ final class BundleStore {
203
217
  }
204
218
  }
205
219
 
220
+ synchronized String getLastResolvedRuntimeKey() {
221
+ return prefs.getString(KEY_LAST_RESOLVED_RUNTIME_KEY, null);
222
+ }
223
+
224
+ synchronized void setLastResolvedRuntimeKey(String runtimeKey) {
225
+ SharedPreferences.Editor editor = prefs.edit();
226
+ if (runtimeKey == null) {
227
+ // null clears the key so the next cold start is treated as unresolved again
228
+ editor.remove(KEY_LAST_RESOLVED_RUNTIME_KEY);
229
+ } else {
230
+ editor.putString(KEY_LAST_RESOLVED_RUNTIME_KEY, runtimeKey);
231
+ }
232
+ editor.commit();
233
+ }
234
+
206
235
  synchronized void markStatus(String bundleId, BundleStatus status) {
207
236
  BundleInfo existing = getBundle(bundleId);
208
237
  if (existing == null) {
@@ -1,4 +1,4 @@
1
- package com.updatekit.updater;
1
+ package com.otakit.updater;
2
2
 
3
3
  import java.text.SimpleDateFormat;
4
4
  import java.util.Date;
@@ -1,4 +1,4 @@
1
- package com.updatekit.updater;
1
+ package com.otakit.updater;
2
2
 
3
3
  import java.io.OutputStream;
4
4
  import java.net.HttpURLConnection;
@@ -8,9 +8,9 @@ import java.text.SimpleDateFormat;
8
8
  import java.util.Date;
9
9
  import java.util.Locale;
10
10
  import java.util.TimeZone;
11
+ import java.util.UUID;
11
12
  import java.util.concurrent.ExecutorService;
12
13
  import java.util.concurrent.Executors;
13
- import java.util.UUID;
14
14
  import org.json.JSONObject;
15
15
 
16
16
  final class DeviceEventClient {
@@ -52,8 +52,7 @@ final class DeviceEventClient {
52
52
  payload.put("releaseId", releaseId);
53
53
  payload.put("nativeBuild", nativeBuild);
54
54
  if (detail != null) {
55
- String truncated =
56
- detail.length() > 500 ? detail.substring(0, 500) : detail;
55
+ String truncated = detail.length() > 500 ? detail.substring(0, 500) : detail;
57
56
  payload.put("detail", truncated);
58
57
  }
59
58
 
@@ -1,4 +1,4 @@
1
- package com.updatekit.updater;
1
+ package com.otakit.updater;
2
2
 
3
3
  import java.io.File;
4
4
  import java.io.FileInputStream;
@@ -1,4 +1,4 @@
1
- package com.updatekit.updater;
1
+ package com.otakit.updater;
2
2
 
3
3
  import android.util.Base64;
4
4
  import java.util.ArrayList;
@@ -1,5 +1,6 @@
1
- package com.updatekit.updater;
1
+ package com.otakit.updater;
2
2
 
3
+ import android.net.Uri;
3
4
  import java.io.ByteArrayOutputStream;
4
5
  import java.io.InputStream;
5
6
  import java.net.HttpURLConnection;
@@ -74,14 +75,25 @@ final class ManifestClient {
74
75
  java.util.List<ManifestVerifier.KeyEntry> manifestKeys
75
76
  ) throws Exception {
76
77
  String base = cdnUrl.replaceAll("/+$", "");
77
- String channelKey = channel != null && !channel.trim().isEmpty() ? channel.trim() : BASE_CHANNEL_KEY;
78
+ String channelKey =
79
+ channel != null && !channel.trim().isEmpty() ? channel.trim() : BASE_CHANNEL_KEY;
78
80
  String runtimeKey =
79
81
  runtimeVersion != null && !runtimeVersion.trim().isEmpty()
80
82
  ? runtimeVersion.trim()
81
83
  : DEFAULT_RUNTIME_KEY;
82
- URL url = new URL(
83
- base + "/manifests/" + appId + "/" + channelKey + "/" + runtimeKey + "/manifest.json"
84
- );
84
+ Uri baseUri = Uri.parse(base);
85
+ if (baseUri.getScheme() == null || baseUri.getHost() == null) {
86
+ throw new IllegalStateException("Invalid CDN URL");
87
+ }
88
+ Uri urlUri = baseUri
89
+ .buildUpon()
90
+ .appendPath("manifests")
91
+ .appendPath(appId)
92
+ .appendPath(channelKey)
93
+ .appendPath(runtimeKey)
94
+ .appendPath("manifest.json")
95
+ .build();
96
+ URL url = new URL(urlUri.toString());
85
97
 
86
98
  requireHTTPS(url, allowInsecureUrls);
87
99
 
@@ -112,9 +124,10 @@ final class ManifestClient {
112
124
  String sha256 = json.getString("sha256");
113
125
  int size = json.getInt("size");
114
126
 
115
- String responseRuntimeVersion = json.has("runtimeVersion") && !json.isNull("runtimeVersion")
116
- ? json.getString("runtimeVersion").trim()
117
- : null;
127
+ String responseRuntimeVersion =
128
+ json.has("runtimeVersion") && !json.isNull("runtimeVersion")
129
+ ? json.getString("runtimeVersion").trim()
130
+ : null;
118
131
  if (responseRuntimeVersion != null && responseRuntimeVersion.isEmpty()) {
119
132
  responseRuntimeVersion = null;
120
133
  }
@@ -136,7 +149,7 @@ final class ManifestClient {
136
149
 
137
150
  if (manifestKeys == null || manifestKeys.isEmpty()) {
138
151
  android.util.Log.w(
139
- "UpdateKit",
152
+ "OtaKit",
140
153
  "No manifest signing keys configured — signature verification is disabled for this request."
141
154
  );
142
155
  }
@@ -194,11 +207,15 @@ final class ManifestClient {
194
207
  if (!sigObj.has("kid") || !sigObj.has("sig") || !sigObj.has("iat") || !sigObj.has("exp")) {
195
208
  return null;
196
209
  }
197
- return new ManifestSignature(
198
- sigObj.getString("kid"),
199
- sigObj.getString("sig"),
200
- sigObj.getInt("iat"),
201
- sigObj.getInt("exp")
202
- );
210
+ try {
211
+ return new ManifestSignature(
212
+ sigObj.getString("kid"),
213
+ sigObj.getString("sig"),
214
+ sigObj.getInt("iat"),
215
+ sigObj.getInt("exp")
216
+ );
217
+ } catch (org.json.JSONException e) {
218
+ return null;
219
+ }
203
220
  }
204
221
  }
@@ -1,4 +1,4 @@
1
- package com.updatekit.updater;
1
+ package com.otakit.updater;
2
2
 
3
3
  import android.util.Base64;
4
4
  import java.security.KeyFactory;