@otakit/capacitor-updater 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,6 +10,7 @@ Capacitor OTA updater plugin for OtaKit.
10
10
  - activates them on the next launch, next resume, or immediately
11
11
  - checks for updates on cold start and app resume (configurable interval)
12
12
  - also supports fully manual update prompts when the app wants control
13
+ - supports optional `runtimeVersion` lanes for native compatibility boundaries
13
14
  - requires `notifyAppReady()` as the success handshake
14
15
  - rolls back automatically if the new bundle does not prove healthy
15
16
 
@@ -46,6 +47,7 @@ plugins: {
46
47
  appReadyTimeout: 10000,
47
48
  // Optional:
48
49
  // channel: "staging",
50
+ // runtimeVersion: "2026.04",
49
51
  // updateMode: "next-resume",
50
52
  // updateMode: "manual",
51
53
  // updateMode: "immediate",
@@ -62,6 +64,22 @@ Advanced overrides for self-hosting or custom trust only:
62
64
  Hosted OtaKit already points at `https://otakit.app/api/v1` and already trusts
63
65
  the managed manifest signing keys.
64
66
 
67
+ ## Channels vs runtimeVersion
68
+
69
+ - `channel` answers "who should get this rollout?"
70
+ - `runtimeVersion` answers "which native app shell can safely run this bundle?"
71
+
72
+ Use channels for rollout tracks such as `beta`, `staging`, or `production`.
73
+
74
+ Use `runtimeVersion` when a new store build creates a new compatibility boundary and you do not
75
+ want devices on that new native shell to keep receiving older OTA bundles.
76
+
77
+ When `runtimeVersion` is set:
78
+
79
+ - the plugin requests bundle updates only for that runtime lane
80
+ - bundle uploads inherit the same runtime value automatically through the CLI
81
+ - releases stay simple: publish the bundle, and it naturally stays inside its own runtime lane
82
+
65
83
  ## Trust model
66
84
 
67
85
  The plugin does not just download from a URL and trust the result.
@@ -8,6 +8,7 @@ class BundleInfo {
8
8
 
9
9
  final String id;
10
10
  final String version;
11
+ final String runtimeVersion;
11
12
  final BundleStatus status;
12
13
  final Long downloadedAt;
13
14
  final String sha256;
@@ -18,6 +19,7 @@ class BundleInfo {
18
19
  BundleInfo(
19
20
  String id,
20
21
  String version,
22
+ String runtimeVersion,
21
23
  BundleStatus status,
22
24
  Long downloadedAt,
23
25
  String sha256,
@@ -27,6 +29,7 @@ class BundleInfo {
27
29
  ) {
28
30
  this.id = id;
29
31
  this.version = version;
32
+ this.runtimeVersion = runtimeVersion;
30
33
  this.status = status;
31
34
  this.downloadedAt = downloadedAt;
32
35
  this.sha256 = sha256;
@@ -43,6 +46,9 @@ class BundleInfo {
43
46
  JSObject object = new JSObject();
44
47
  object.put("id", id);
45
48
  object.put("version", version);
49
+ if (runtimeVersion != null) {
50
+ object.put("runtimeVersion", runtimeVersion);
51
+ }
46
52
  object.put("status", status.value());
47
53
  if (downloadedAt != null) {
48
54
  object.put("downloadedAt", DateUtils.toIsoString(downloadedAt));
@@ -63,6 +69,9 @@ class BundleInfo {
63
69
  JSONObject object = new JSONObject();
64
70
  object.put("id", id);
65
71
  object.put("version", version);
72
+ if (runtimeVersion != null) {
73
+ object.put("runtimeVersion", runtimeVersion);
74
+ }
66
75
  object.put("status", status.value());
67
76
  if (downloadedAt != null) {
68
77
  object.put("downloadedAt", downloadedAt);
@@ -85,16 +94,39 @@ class BundleInfo {
85
94
  static BundleInfo fromJSONObject(JSONObject object) {
86
95
  String id = object.optString("id", "builtin");
87
96
  String version = object.optString("version", "0.0.0");
97
+ String runtimeVersion = object.has("runtimeVersion")
98
+ ? object.optString("runtimeVersion", null)
99
+ : null;
88
100
  BundleStatus status = BundleStatus.from(object.optString("status", "pending"));
89
101
  Long downloadedAt = object.has("downloadedAt") ? object.optLong("downloadedAt") : null;
90
102
  String sha256 = object.has("sha256") ? object.optString("sha256", null) : null;
91
103
  String channel = object.has("channel") ? object.optString("channel", null) : null;
92
104
  String releaseId = object.has("releaseId") ? object.optString("releaseId", null) : null;
93
105
  String path = object.has("path") ? object.optString("path", null) : null;
94
- return new BundleInfo(id, version, status, downloadedAt, sha256, path, channel, releaseId);
106
+ return new BundleInfo(
107
+ id,
108
+ version,
109
+ runtimeVersion,
110
+ status,
111
+ downloadedAt,
112
+ sha256,
113
+ path,
114
+ channel,
115
+ releaseId
116
+ );
95
117
  }
96
118
 
97
119
  BundleInfo withStatus(BundleStatus nextStatus) {
98
- return new BundleInfo(id, version, nextStatus, downloadedAt, sha256, path, channel, releaseId);
120
+ return new BundleInfo(
121
+ id,
122
+ version,
123
+ runtimeVersion,
124
+ nextStatus,
125
+ downloadedAt,
126
+ sha256,
127
+ path,
128
+ channel,
129
+ releaseId
130
+ );
99
131
  }
100
132
  }
@@ -27,12 +27,14 @@ final class BundleStore {
27
27
  private final File bundlesDirectory;
28
28
  private final String builtinVersion;
29
29
  private final String nativeBuild;
30
+ private final String appRuntimeVersion;
30
31
 
31
- BundleStore(Context context, String builtinVersion, String nativeBuild) {
32
+ BundleStore(Context context, String builtinVersion, String nativeBuild, String appRuntimeVersion) {
32
33
  this.context = context.getApplicationContext();
33
34
  this.prefs = this.context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
34
35
  this.builtinVersion = builtinVersion;
35
36
  this.nativeBuild = nativeBuild;
37
+ this.appRuntimeVersion = appRuntimeVersion;
36
38
  this.bundlesDirectory = new File(this.context.getFilesDir(), "otakit_bundles");
37
39
  if (!bundlesDirectory.exists()) {
38
40
  //noinspection ResultOfMethodCallIgnored
@@ -56,6 +58,7 @@ final class BundleStore {
56
58
  return new BundleInfo(
57
59
  "builtin",
58
60
  builtinVersion,
61
+ appRuntimeVersion,
59
62
  BundleStatus.BUILTIN,
60
63
  null,
61
64
  null,
@@ -229,7 +232,7 @@ final class BundleStore {
229
232
  }
230
233
  }
231
234
 
232
- synchronized JSArray listDownloadedBundles() {
235
+ synchronized List<BundleInfo> listDownloadedBundleInfos() {
233
236
  List<BundleInfo> result = new ArrayList<>();
234
237
  File[] entries = bundlesDirectory.listFiles();
235
238
  if (entries != null) {
@@ -241,7 +244,11 @@ final class BundleStore {
241
244
  }
242
245
  }
243
246
  }
247
+ return result;
248
+ }
244
249
 
250
+ synchronized JSArray listDownloadedBundles() {
251
+ List<BundleInfo> result = listDownloadedBundleInfos();
245
252
  JSArray array = new JSArray();
246
253
  for (BundleInfo bundle : result) {
247
254
  array.put(bundle.toJSObject());
@@ -30,26 +30,23 @@ final class ManifestClient {
30
30
  final String url;
31
31
  final String sha256;
32
32
  final int size;
33
- final Integer minNativeBuild;
33
+ final String runtimeVersion;
34
34
  final String releaseId;
35
- final ManifestSignature signature;
36
35
 
37
36
  LatestManifest(
38
37
  String version,
39
38
  String url,
40
39
  String sha256,
41
40
  int size,
42
- Integer minNativeBuild,
43
- String releaseId,
44
- ManifestSignature signature
41
+ String runtimeVersion,
42
+ String releaseId
45
43
  ) {
46
44
  this.version = version;
47
45
  this.url = url;
48
46
  this.sha256 = sha256;
49
47
  this.size = size;
50
- this.minNativeBuild = minNativeBuild;
48
+ this.runtimeVersion = runtimeVersion;
51
49
  this.releaseId = releaseId;
52
- this.signature = signature;
53
50
  }
54
51
  }
55
52
 
@@ -71,7 +68,7 @@ final class ManifestClient {
71
68
  String channel,
72
69
  String currentVersion,
73
70
  String currentReleaseId,
74
- String nativeBuild,
71
+ String runtimeVersion,
75
72
  String platform,
76
73
  boolean allowInsecureUrls,
77
74
  java.util.List<ManifestVerifier.KeyEntry> manifestKeys
@@ -93,7 +90,9 @@ final class ManifestClient {
93
90
  if (currentReleaseId != null && !currentReleaseId.trim().isEmpty()) {
94
91
  connection.setRequestProperty("X-Release-Id", currentReleaseId);
95
92
  }
96
- connection.setRequestProperty("X-Native-Build", nativeBuild);
93
+ if (runtimeVersion != null && !runtimeVersion.trim().isEmpty()) {
94
+ connection.setRequestProperty("X-Runtime-Version", runtimeVersion);
95
+ }
97
96
  connection.setConnectTimeout(15_000);
98
97
  connection.setReadTimeout(30_000);
99
98
 
@@ -118,31 +117,15 @@ final class ManifestClient {
118
117
  String sha256 = json.getString("sha256");
119
118
  int size = json.getInt("size");
120
119
 
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
- }
120
+ String responseRuntimeVersion = json.has("runtimeVersion") && !json.isNull("runtimeVersion")
121
+ ? json.getString("runtimeVersion").trim()
122
+ : null;
123
+ if (responseRuntimeVersion != null && responseRuntimeVersion.isEmpty()) {
124
+ responseRuntimeVersion = null;
132
125
  }
133
126
 
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
- }
127
+ ManifestSignature signature = parseSignature(json.optJSONObject("signature"));
128
+ ManifestSignature signatureV2 = parseSignature(json.optJSONObject("signatureV2"));
146
129
 
147
130
  String releaseId = null;
148
131
  if (json.has("releaseId") && !json.isNull("releaseId")) {
@@ -161,22 +144,34 @@ final class ManifestClient {
161
144
 
162
145
  // Verify manifest signature if signing keys are configured
163
146
  if (manifestKeys != null && !manifestKeys.isEmpty()) {
164
- if (signature == null) {
147
+ if (signatureV2 != null) {
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 {
165
171
  throw new IllegalStateException(
166
172
  "Manifest signature missing but signing keys are configured"
167
173
  );
168
174
  }
169
- ManifestVerifier.verify(
170
- appId,
171
- channel,
172
- platform,
173
- version,
174
- sha256,
175
- size,
176
- minNativeBuild,
177
- signature,
178
- manifestKeys
179
- );
180
175
  }
181
176
 
182
177
  return new LatestManifest(
@@ -184,9 +179,8 @@ final class ManifestClient {
184
179
  downloadUrl,
185
180
  sha256,
186
181
  size,
187
- minNativeBuild,
188
- releaseId,
189
- signature
182
+ responseRuntimeVersion,
183
+ releaseId
190
184
  );
191
185
  } finally {
192
186
  connection.disconnect();
@@ -206,4 +200,19 @@ final class ManifestClient {
206
200
  return new String(out.toByteArray(), StandardCharsets.UTF_8);
207
201
  }
208
202
  }
203
+
204
+ private static ManifestSignature parseSignature(JSONObject sigObj) {
205
+ if (sigObj == null) {
206
+ return null;
207
+ }
208
+ if (!sigObj.has("kid") || !sigObj.has("sig") || !sigObj.has("iat") || !sigObj.has("exp")) {
209
+ return null;
210
+ }
211
+ return new ManifestSignature(
212
+ sigObj.getString("kid"),
213
+ sigObj.getString("sig"),
214
+ sigObj.getInt("iat"),
215
+ sigObj.getInt("exp")
216
+ );
217
+ }
209
218
  }
@@ -34,7 +34,51 @@ final class ManifestVerifier {
34
34
  String version,
35
35
  String sha256,
36
36
  int size,
37
- Integer minNativeBuild,
37
+ String runtimeVersion,
38
+ ManifestClient.ManifestSignature signature,
39
+ List<KeyEntry> trustedKeys
40
+ ) throws Exception {
41
+ String payload = buildCanonicalPayload(
42
+ appId,
43
+ channel,
44
+ platform,
45
+ version,
46
+ sha256,
47
+ size,
48
+ runtimeVersion,
49
+ signature.kid,
50
+ signature.iat,
51
+ signature.exp
52
+ );
53
+ verifyPayload(payload, signature, trustedKeys);
54
+ }
55
+
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
+ private static void verifyPayload(
81
+ String payload,
38
82
  ManifestClient.ManifestSignature signature,
39
83
  List<KeyEntry> trustedKeys
40
84
  ) throws Exception {
@@ -56,20 +100,6 @@ final class ManifestVerifier {
56
100
  throw new IllegalStateException("Unknown signing key ID: " + signature.kid);
57
101
  }
58
102
 
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
103
  // Decode base64url signature
74
104
  byte[] sigBytes = base64UrlDecode(signature.sig);
75
105
 
@@ -94,12 +124,56 @@ final class ManifestVerifier {
94
124
  String version,
95
125
  String sha256,
96
126
  int size,
97
- Integer minNativeBuild,
127
+ String runtimeVersion,
128
+ String kid,
129
+ int iat,
130
+ int exp
131
+ ) {
132
+ return (
133
+ "MANIFEST_V2\n" +
134
+ "appId:" +
135
+ appId +
136
+ "\n" +
137
+ "channel:" +
138
+ (channel != null ? channel : "null") +
139
+ "\n" +
140
+ "platform:" +
141
+ platform +
142
+ "\n" +
143
+ "version:" +
144
+ version +
145
+ "\n" +
146
+ "sha256:" +
147
+ sha256 +
148
+ "\n" +
149
+ "size:" +
150
+ size +
151
+ "\n" +
152
+ "runtimeVersion:" +
153
+ (runtimeVersion != null ? runtimeVersion : "null") +
154
+ "\n" +
155
+ "kid:" +
156
+ kid +
157
+ "\n" +
158
+ "iat:" +
159
+ iat +
160
+ "\n" +
161
+ "exp:" +
162
+ exp
163
+ );
164
+ }
165
+
166
+ private static String buildLegacyCanonicalPayload(
167
+ String appId,
168
+ String channel,
169
+ String platform,
170
+ String version,
171
+ String sha256,
172
+ int size,
98
173
  String kid,
99
174
  int iat,
100
175
  int exp
101
176
  ) {
102
- String minBuildStr = minNativeBuild != null ? String.valueOf(minNativeBuild) : "null";
103
177
  return (
104
178
  "MANIFEST_V1\n" +
105
179
  "appId:" +
@@ -120,8 +194,7 @@ final class ManifestVerifier {
120
194
  "size:" +
121
195
  size +
122
196
  "\n" +
123
- "minNativeBuild:" +
124
- minBuildStr +
197
+ "minNativeBuild:null" +
125
198
  "\n" +
126
199
  "kid:" +
127
200
  kid +
@@ -39,6 +39,7 @@ public class UpdaterPlugin extends Plugin {
39
39
  private String updateUrl;
40
40
  private String appId;
41
41
  private String channel;
42
+ private String runtimeVersion;
42
43
  private java.util.List<ManifestVerifier.KeyEntry> manifestKeys = new java.util.ArrayList<>();
43
44
  private long checkIntervalMs = 600_000;
44
45
  private final AtomicBoolean checkInProgress = new AtomicBoolean(false);
@@ -71,13 +72,14 @@ public class UpdaterPlugin extends Plugin {
71
72
  }
72
73
  } catch (PackageManager.NameNotFoundException ignored) {}
73
74
 
74
- this.store = new BundleStore(getContext(), builtinVersion, nativeBuild);
75
75
  this.updateUrl = resolveUpdateUrl(
76
76
  getConfig().getString("serverUrl"),
77
77
  System.getenv("OTAKIT_SERVER_URL")
78
78
  );
79
79
  this.appId = getConfig().getString("appId");
80
80
  this.channel = trimToNull(getConfig().getString("channel"));
81
+ this.runtimeVersion = trimToNull(getConfig().getString("runtimeVersion"));
82
+ this.store = new BundleStore(getContext(), builtinVersion, nativeBuild, this.runtimeVersion);
81
83
  this.allowInsecureUrls = getConfig().getBoolean("allowInsecureUrls", false);
82
84
  String configuredUpdateMode = getConfig().getString("updateMode", UPDATE_MODE_NEXT_LAUNCH);
83
85
  this.updateMode = resolveUpdateMode(configuredUpdateMode);
@@ -121,6 +123,8 @@ public class UpdaterPlugin extends Plugin {
121
123
  this.appReadyTimeoutMs = Math.max(1000, getConfig().getInt("appReadyTimeout", 10_000));
122
124
  this.checkIntervalMs = Math.max(600_000, getConfig().getInt("checkInterval", 600_000));
123
125
 
126
+ pruneIncompatibleBundles();
127
+
124
128
  BundleInfo current = store.getCurrentBundle();
125
129
  if (current.status == BundleStatus.TRIAL) {
126
130
  rollbackCurrentBundle("app_restarted_before_notify");
@@ -290,6 +294,9 @@ public class UpdaterPlugin extends Plugin {
290
294
  result.put("sha256", staged.sha256 != null ? staged.sha256 : "");
291
295
  result.put("size", 0);
292
296
  result.put("downloaded", true);
297
+ if (staged.runtimeVersion != null) {
298
+ result.put("runtimeVersion", staged.runtimeVersion);
299
+ }
293
300
  if (staged.releaseId != null) {
294
301
  result.put("releaseId", staged.releaseId);
295
302
  }
@@ -513,7 +520,7 @@ public class UpdaterPlugin extends Plugin {
513
520
  channel,
514
521
  current.version,
515
522
  current.releaseId,
516
- store.getNativeBuild(),
523
+ runtimeVersion,
517
524
  "android",
518
525
  allowInsecureUrls,
519
526
  manifestKeys
@@ -530,6 +537,12 @@ public class UpdaterPlugin extends Plugin {
530
537
  return null;
531
538
  }
532
539
 
540
+ if (!isCompatibleRuntime(latest.runtimeVersion)) {
541
+ throw new IllegalStateException(
542
+ "Manifest runtimeVersion does not match the installed app runtime"
543
+ );
544
+ }
545
+
533
546
  BundleInfo staged = findMatchingStagedBundle(latest, targetChannel);
534
547
  if (emitEvents) {
535
548
  notifyListeners("updateAvailable", manifestToJSObject(latest, staged != null));
@@ -545,6 +558,7 @@ public class UpdaterPlugin extends Plugin {
545
558
  latest.version,
546
559
  latest.sha256,
547
560
  latest.size,
561
+ latest.runtimeVersion,
548
562
  targetChannel,
549
563
  latest.releaseId
550
564
  );
@@ -558,6 +572,7 @@ public class UpdaterPlugin extends Plugin {
558
572
  refreshed.version,
559
573
  refreshed.sha256,
560
574
  refreshed.size,
575
+ refreshed.runtimeVersion,
561
576
  targetChannel,
562
577
  refreshed.releaseId
563
578
  );
@@ -580,7 +595,7 @@ public class UpdaterPlugin extends Plugin {
580
595
 
581
596
  private BundleInfo downloadAndStage(URL url, String version, String expectedSha256)
582
597
  throws Exception {
583
- return downloadAndStage(url, version, expectedSha256, 0, null, null);
598
+ return downloadAndStage(url, version, expectedSha256, 0, null, null, null);
584
599
  }
585
600
 
586
601
  private BundleInfo downloadAndStage(
@@ -588,6 +603,7 @@ public class UpdaterPlugin extends Plugin {
588
603
  String version,
589
604
  String expectedSha256,
590
605
  int expectedSize,
606
+ String runtimeVersion,
591
607
  String channel,
592
608
  String releaseId
593
609
  ) throws Exception {
@@ -635,6 +651,7 @@ public class UpdaterPlugin extends Plugin {
635
651
  BundleInfo info = new BundleInfo(
636
652
  bundleId,
637
653
  version,
654
+ runtimeVersion,
638
655
  BundleStatus.PENDING,
639
656
  System.currentTimeMillis(),
640
657
  expectedSha256,
@@ -730,6 +747,12 @@ public class UpdaterPlugin extends Plugin {
730
747
  store.setStagedBundleId(null);
731
748
  return store.getCurrentBundle();
732
749
  }
750
+ if (!isCompatibleRuntime(staged)) {
751
+ try {
752
+ store.deleteBundle(staged.id);
753
+ } catch (Exception ignored) {}
754
+ return store.getCurrentBundle();
755
+ }
733
756
 
734
757
  store.setCurrentBundleId(staged.id);
735
758
  store.setStagedBundleId(null);
@@ -747,6 +770,12 @@ public class UpdaterPlugin extends Plugin {
747
770
  store.setStagedBundleId(null);
748
771
  return;
749
772
  }
773
+ if (!isCompatibleRuntime(staged)) {
774
+ try {
775
+ store.deleteBundle(staged.id);
776
+ } catch (Exception ignored) {}
777
+ return;
778
+ }
750
779
 
751
780
  store.setCurrentBundleId(staged.id);
752
781
  store.setStagedBundleId(null);
@@ -874,12 +903,12 @@ public class UpdaterPlugin extends Plugin {
874
903
  object.put("sha256", latest.sha256);
875
904
  object.put("size", latest.size);
876
905
  object.put("downloaded", downloaded);
906
+ if (latest.runtimeVersion != null) {
907
+ object.put("runtimeVersion", latest.runtimeVersion);
908
+ }
877
909
  if (latest.releaseId != null) {
878
910
  object.put("releaseId", latest.releaseId);
879
911
  }
880
- if (latest.minNativeBuild != null) {
881
- object.put("minNativeBuild", latest.minNativeBuild);
882
- }
883
912
  return object;
884
913
  }
885
914
 
@@ -902,6 +931,10 @@ public class UpdaterPlugin extends Plugin {
902
931
  return null;
903
932
  }
904
933
 
934
+ if (!java.util.Objects.equals(trimToNull(staged.runtimeVersion), trimToNull(latest.runtimeVersion))) {
935
+ return null;
936
+ }
937
+
905
938
  if (
906
939
  latest.releaseId != null &&
907
940
  staged.releaseId != null &&
@@ -1061,6 +1094,29 @@ public class UpdaterPlugin extends Plugin {
1061
1094
  );
1062
1095
  }
1063
1096
 
1097
+ private void pruneIncompatibleBundles() {
1098
+ for (BundleInfo bundle : store.listDownloadedBundleInfos()) {
1099
+ if (!isCompatibleRuntime(bundle)) {
1100
+ try {
1101
+ store.deleteBundle(bundle.id);
1102
+ } catch (Exception ignored) {}
1103
+ }
1104
+ }
1105
+
1106
+ BundleInfo failed = store.getFailedBundle();
1107
+ if (failed != null && !isCompatibleRuntime(failed)) {
1108
+ store.setFailedBundle(null);
1109
+ }
1110
+ }
1111
+
1112
+ private boolean isCompatibleRuntime(String bundleRuntimeVersion) {
1113
+ return java.util.Objects.equals(trimToNull(bundleRuntimeVersion), runtimeVersion);
1114
+ }
1115
+
1116
+ private boolean isCompatibleRuntime(BundleInfo bundle) {
1117
+ return isCompatibleRuntime(bundle.runtimeVersion);
1118
+ }
1119
+
1064
1120
  private long getFreeDiskSpace() {
1065
1121
  try {
1066
1122
  android.os.StatFs statFs = new android.os.StatFs(
@@ -19,6 +19,8 @@ export interface BundleInfo {
19
19
  id: string;
20
20
  /** Semantic version string */
21
21
  version: string;
22
+ /** Native compatibility lane for this bundle. */
23
+ runtimeVersion?: string;
22
24
  /** Current status of the bundle */
23
25
  status: BundleStatus;
24
26
  /** ISO timestamp when bundle was downloaded */
@@ -33,6 +35,8 @@ export interface BundleInfo {
33
35
  export interface LatestVersion {
34
36
  /** Version string */
35
37
  version: string;
38
+ /** Native compatibility lane for this update. */
39
+ runtimeVersion?: string;
36
40
  /** Download URL */
37
41
  url: string;
38
42
  /** SHA-256 checksum */
@@ -43,8 +47,6 @@ export interface LatestVersion {
43
47
  downloaded?: boolean;
44
48
  /** Release history ID associated with this manifest */
45
49
  releaseId?: string;
46
- /** Minimum native build required */
47
- minNativeBuild?: number;
48
50
  }
49
51
  export interface BundleListResult {
50
52
  bundles: BundleInfo[];
@@ -70,6 +72,8 @@ export interface OtaKitConfig {
70
72
  appId: string;
71
73
  /** Optional named release track. Omit to use the base channel. */
72
74
  channel?: string;
75
+ /** Optional native compatibility lane. Set this when a new store build should start a new OTA line. */
76
+ runtimeVersion?: string;
73
77
  /** Overall update behavior. Defaults to next-launch. */
74
78
  updateMode?: OtaKitUpdateMode;
75
79
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"definitions.d.ts","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"definitions.d.ts","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"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"}
@@ -3,6 +3,7 @@ import Foundation
3
3
  struct BundleInfo: Codable {
4
4
  let id: String
5
5
  let version: String
6
+ let runtimeVersion: String?
6
7
  let status: BundleStatus
7
8
  let downloadedAt: Date?
8
9
  let sha256: String?
@@ -21,6 +22,10 @@ struct BundleInfo: Codable {
21
22
  "status": status.rawValue,
22
23
  ]
23
24
 
25
+ if let runtimeVersion {
26
+ result["runtimeVersion"] = runtimeVersion
27
+ }
28
+
24
29
  if let downloadedAt {
25
30
  result["downloadedAt"] = ISO8601DateFormatter().string(from: downloadedAt)
26
31
  }
@@ -10,6 +10,7 @@ final class BundleStore {
10
10
 
11
11
  private let defaults = UserDefaults.standard
12
12
  private let fileManager = FileManager.default
13
+ var appRuntimeVersion: String?
13
14
 
14
15
  private lazy var decoder: JSONDecoder = {
15
16
  let decoder = JSONDecoder()
@@ -51,6 +52,7 @@ final class BundleStore {
51
52
  BundleInfo(
52
53
  id: "builtin",
53
54
  version: builtinVersion,
55
+ runtimeVersion: appRuntimeVersion,
54
56
  status: .builtin,
55
57
  downloadedAt: nil,
56
58
  sha256: nil,
@@ -202,6 +204,7 @@ final class BundleStore {
202
204
  bundle = BundleInfo(
203
205
  id: bundle.id,
204
206
  version: bundle.version,
207
+ runtimeVersion: bundle.runtimeVersion,
205
208
  status: status,
206
209
  downloadedAt: bundle.downloadedAt,
207
210
  sha256: bundle.sha256,
@@ -5,9 +5,8 @@ struct LatestManifest {
5
5
  let url: String
6
6
  let sha256: String
7
7
  let size: Int
8
- let minNativeBuild: Int?
8
+ let runtimeVersion: String?
9
9
  let releaseId: String?
10
- let signature: ManifestSignature?
11
10
  }
12
11
 
13
12
  struct ManifestSignature {
@@ -44,7 +43,7 @@ enum ManifestClient {
44
43
  channel: String?,
45
44
  currentVersion: String,
46
45
  currentReleaseId: String?,
47
- nativeBuild: String,
46
+ runtimeVersion: String?,
48
47
  platform: String,
49
48
  allowInsecureUrls: Bool = false,
50
49
  manifestKeys: [ManifestKey] = []
@@ -73,7 +72,9 @@ enum ManifestClient {
73
72
  if let currentReleaseId, !currentReleaseId.isEmpty {
74
73
  request.setValue(currentReleaseId, forHTTPHeaderField: "X-Release-Id")
75
74
  }
76
- request.setValue(nativeBuild, forHTTPHeaderField: "X-Native-Build")
75
+ if let runtimeVersion, !runtimeVersion.isEmpty {
76
+ request.setValue(runtimeVersion, forHTTPHeaderField: "X-Runtime-Version")
77
+ }
77
78
  request.timeoutInterval = 30
78
79
 
79
80
  let (data, response) = try await URLSession.shared.data(for: request)
@@ -102,21 +103,12 @@ enum ManifestClient {
102
103
  throw ManifestClientError.invalidResponse
103
104
  }
104
105
 
105
- var minNativeBuild: Int?
106
- if let numeric = object["minNativeBuild"] as? NSNumber {
107
- minNativeBuild = numeric.intValue
108
- } else if let stringValue = object["minNativeBuild"] as? String {
109
- minNativeBuild = Int(stringValue)
110
- }
106
+ let runtimeVersion = (object["runtimeVersion"] as? String)?
107
+ .trimmingCharacters(in: .whitespacesAndNewlines)
108
+ .nilIfEmpty
111
109
 
112
- var signature: ManifestSignature?
113
- if let sigObj = object["signature"] as? [String: Any],
114
- let kid = sigObj["kid"] as? String,
115
- let sig = sigObj["sig"] as? String,
116
- let iat = sigObj["iat"] as? Int,
117
- let exp = sigObj["exp"] as? Int {
118
- signature = ManifestSignature(kid: kid, sig: sig, iat: iat, exp: exp)
119
- }
110
+ let signature = parseSignature(object["signature"])
111
+ let signatureV2 = parseSignature(object["signatureV2"])
120
112
 
121
113
  let releaseId = object["releaseId"] as? String
122
114
 
@@ -130,22 +122,36 @@ enum ManifestClient {
130
122
  print("[UpdateKit] WARNING: No manifest signing keys configured — signature verification is disabled for this request.")
131
123
  }
132
124
 
133
- // Verify manifest signature if signing keys are configured
125
+ // Verify manifest signature if signing keys are configured.
126
+ // New manifests carry signatureV2 with runtimeVersion in the signed payload.
127
+ // Older servers only return the legacy signature field.
134
128
  if !manifestKeys.isEmpty {
135
- guard let sig = signature else {
129
+ if let sigV2 = signatureV2 {
130
+ try ManifestVerifier.verify(
131
+ appId: appId,
132
+ channel: channel,
133
+ platform: platform,
134
+ version: version,
135
+ sha256: sha256,
136
+ size: size,
137
+ runtimeVersion: runtimeVersion,
138
+ signature: sigV2,
139
+ trustedKeys: manifestKeys
140
+ )
141
+ } else if let sig = signature {
142
+ try ManifestVerifier.verifyLegacy(
143
+ appId: appId,
144
+ channel: channel,
145
+ platform: platform,
146
+ version: version,
147
+ sha256: sha256,
148
+ size: size,
149
+ signature: sig,
150
+ trustedKeys: manifestKeys
151
+ )
152
+ } else {
136
153
  throw ManifestVerifierError.missingSignature
137
154
  }
138
- try ManifestVerifier.verify(
139
- appId: appId,
140
- channel: channel,
141
- platform: platform,
142
- version: version,
143
- sha256: sha256,
144
- size: size,
145
- minNativeBuild: minNativeBuild,
146
- signature: sig,
147
- trustedKeys: manifestKeys
148
- )
149
155
  }
150
156
 
151
157
  return LatestManifest(
@@ -153,9 +159,26 @@ enum ManifestClient {
153
159
  url: downloadUrl,
154
160
  sha256: sha256,
155
161
  size: size,
156
- minNativeBuild: minNativeBuild,
157
- releaseId: releaseId,
158
- signature: signature
162
+ runtimeVersion: runtimeVersion,
163
+ releaseId: releaseId
159
164
  )
160
165
  }
166
+
167
+ private static func parseSignature(_ rawValue: Any?) -> ManifestSignature? {
168
+ guard let sigObj = rawValue as? [String: Any],
169
+ let kid = sigObj["kid"] as? String,
170
+ let sig = sigObj["sig"] as? String,
171
+ let iat = sigObj["iat"] as? Int,
172
+ let exp = sigObj["exp"] as? Int else {
173
+ return nil
174
+ }
175
+
176
+ return ManifestSignature(kid: kid, sig: sig, iat: iat, exp: exp)
177
+ }
178
+ }
179
+
180
+ private extension String {
181
+ var nilIfEmpty: String? {
182
+ isEmpty ? nil : self
183
+ }
161
184
  }
@@ -19,7 +19,7 @@ enum ManifestVerifier {
19
19
  ///
20
20
  /// - Parameters:
21
21
  /// - appId, channel, platform: Request context (known by plugin).
22
- /// - version, sha256, size, minNativeBuild: Response fields.
22
+ /// - version, sha256, size, runtimeVersion: Response fields.
23
23
  /// - signature: The signature object from the manifest response.
24
24
  /// - trustedKeys: Array of verification keys configured in the plugin.
25
25
  ///
@@ -31,22 +31,10 @@ enum ManifestVerifier {
31
31
  version: String,
32
32
  sha256: String,
33
33
  size: Int,
34
- minNativeBuild: Int?,
34
+ runtimeVersion: String?,
35
35
  signature: ManifestSignature,
36
36
  trustedKeys: [ManifestKey]
37
37
  ) throws {
38
- // Check expiry
39
- let now = Int(Date().timeIntervalSince1970)
40
- guard signature.exp > now else {
41
- throw ManifestVerifierError.expired
42
- }
43
-
44
- // Find matching key
45
- guard let keyEntry = trustedKeys.first(where: { $0.kid == signature.kid }) else {
46
- throw ManifestVerifierError.unknownKid(signature.kid)
47
- }
48
-
49
- // Build canonical payload (must match server exactly)
50
38
  let payload = buildCanonicalPayload(
51
39
  appId: appId,
52
40
  channel: channel,
@@ -54,11 +42,53 @@ enum ManifestVerifier {
54
42
  version: version,
55
43
  sha256: sha256,
56
44
  size: size,
57
- minNativeBuild: minNativeBuild,
45
+ runtimeVersion: runtimeVersion,
58
46
  kid: signature.kid,
59
47
  iat: signature.iat,
60
48
  exp: signature.exp
61
49
  )
50
+ try verifyPayload(payload, signature: signature, trustedKeys: trustedKeys)
51
+ }
52
+
53
+ static func verifyLegacy(
54
+ appId: String,
55
+ channel: String?,
56
+ platform: String,
57
+ version: String,
58
+ sha256: String,
59
+ size: Int,
60
+ signature: ManifestSignature,
61
+ trustedKeys: [ManifestKey]
62
+ ) throws {
63
+ let payload = buildLegacyCanonicalPayload(
64
+ appId: appId,
65
+ channel: channel,
66
+ platform: platform,
67
+ version: version,
68
+ sha256: sha256,
69
+ size: size,
70
+ kid: signature.kid,
71
+ iat: signature.iat,
72
+ exp: signature.exp
73
+ )
74
+ try verifyPayload(payload, signature: signature, trustedKeys: trustedKeys)
75
+ }
76
+
77
+ private static func verifyPayload(
78
+ _ payload: String,
79
+ signature: ManifestSignature,
80
+ trustedKeys: [ManifestKey]
81
+ ) throws {
82
+ // Check expiry
83
+ let now = Int(Date().timeIntervalSince1970)
84
+ guard signature.exp > now else {
85
+ throw ManifestVerifierError.expired
86
+ }
87
+
88
+ // Find matching key
89
+ guard let keyEntry = trustedKeys.first(where: { $0.kid == signature.kid }) else {
90
+ throw ManifestVerifierError.unknownKid(signature.kid)
91
+ }
62
92
 
63
93
  // Decode base64url signature
64
94
  guard let sigData = base64UrlDecode(signature.sig) else {
@@ -81,12 +111,37 @@ enum ManifestVerifier {
81
111
  version: String,
82
112
  sha256: String,
83
113
  size: Int,
84
- minNativeBuild: Int?,
114
+ runtimeVersion: String?,
115
+ kid: String,
116
+ iat: Int,
117
+ exp: Int
118
+ ) -> String {
119
+ return [
120
+ "MANIFEST_V2",
121
+ "appId:\(appId)",
122
+ "channel:\(channel ?? "null")",
123
+ "platform:\(platform)",
124
+ "version:\(version)",
125
+ "sha256:\(sha256)",
126
+ "size:\(size)",
127
+ "runtimeVersion:\(runtimeVersion ?? "null")",
128
+ "kid:\(kid)",
129
+ "iat:\(iat)",
130
+ "exp:\(exp)",
131
+ ].joined(separator: "\n")
132
+ }
133
+
134
+ private static func buildLegacyCanonicalPayload(
135
+ appId: String,
136
+ channel: String?,
137
+ platform: String,
138
+ version: String,
139
+ sha256: String,
140
+ size: Int,
85
141
  kid: String,
86
142
  iat: Int,
87
143
  exp: Int
88
144
  ) -> String {
89
- let minBuildStr = minNativeBuild.map { String($0) } ?? "null"
90
145
  return [
91
146
  "MANIFEST_V1",
92
147
  "appId:\(appId)",
@@ -95,7 +150,7 @@ enum ManifestVerifier {
95
150
  "version:\(version)",
96
151
  "sha256:\(sha256)",
97
152
  "size:\(size)",
98
- "minNativeBuild:\(minBuildStr)",
153
+ "minNativeBuild:null",
99
154
  "kid:\(kid)",
100
155
  "iat:\(iat)",
101
156
  "exp:\(exp)",
@@ -43,6 +43,7 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
43
43
  private var updateUrl = UpdaterPlugin.defaultUpdateURL
44
44
  private var appId: String?
45
45
  private var channel: String?
46
+ private var runtimeVersion: String?
46
47
  private var manifestKeys: [(kid: String, key: Data)] = []
47
48
  private var trialTimeoutWorkItem: DispatchWorkItem?
48
49
  private var checkIntervalMs: Int = 600_000
@@ -61,6 +62,8 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
61
62
  )
62
63
  appId = getConfig().getString("appId")
63
64
  channel = trimToNil(getConfig().getString("channel"))
65
+ runtimeVersion = trimToNil(getConfig().getString("runtimeVersion"))
66
+ store.appRuntimeVersion = runtimeVersion
64
67
  allowInsecureUrls = getConfig().getBoolean("allowInsecureUrls", false)
65
68
  let updateModeRaw = getConfig().getString("updateMode", UpdateMode.nextLaunch.rawValue)
66
69
  updateMode = resolveUpdateMode(configured: updateModeRaw)
@@ -89,6 +92,8 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
89
92
  manifestKeys = HostedManifestKeys.defaults
90
93
  }
91
94
 
95
+ pruneIncompatibleBundles()
96
+
92
97
  var current = store.getCurrentBundle()
93
98
  if current.status == .trial {
94
99
  rollbackCurrentBundle(reason: "app_restarted_before_notify")
@@ -307,6 +312,9 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
307
312
  "size": 0,
308
313
  "downloaded": true,
309
314
  ]
315
+ if let runtimeVersion = staged.runtimeVersion {
316
+ payload["runtimeVersion"] = runtimeVersion
317
+ }
310
318
  if let releaseId = staged.releaseId {
311
319
  payload["releaseId"] = releaseId
312
320
  }
@@ -503,7 +511,7 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
503
511
  channel: channel,
504
512
  currentVersion: current.version,
505
513
  currentReleaseId: current.releaseId,
506
- nativeBuild: store.nativeBuild,
514
+ runtimeVersion: runtimeVersion,
507
515
  platform: "ios",
508
516
  allowInsecureUrls: allowInsecureUrls,
509
517
  manifestKeys: manifestKeys.map {
@@ -526,6 +534,14 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
526
534
  return nil
527
535
  }
528
536
 
537
+ guard isCompatibleRuntime(manifest.runtimeVersion) else {
538
+ throw NSError(
539
+ domain: "OtaKit",
540
+ code: 1,
541
+ userInfo: [NSLocalizedDescriptionKey: "Manifest runtimeVersion does not match the installed app runtime"]
542
+ )
543
+ }
544
+
529
545
  let staged = findMatchingStagedBundle(latest: manifest, targetChannel: targetChannel)
530
546
  if emitEvents {
531
547
  notifyListeners("updateAvailable", data: manifestToDictionary(manifest, downloaded: staged != nil))
@@ -549,6 +565,7 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
549
565
  version: manifest.version,
550
566
  expectedSha256: manifest.sha256,
551
567
  expectedSize: manifest.size,
568
+ runtimeVersion: manifest.runtimeVersion,
552
569
  channel: targetChannel,
553
570
  releaseId: manifest.releaseId
554
571
  )
@@ -568,6 +585,7 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
568
585
  version: manifest.version,
569
586
  expectedSha256: manifest.sha256,
570
587
  expectedSize: manifest.size,
588
+ runtimeVersion: manifest.runtimeVersion,
571
589
  channel: targetChannel,
572
590
  releaseId: manifest.releaseId
573
591
  )
@@ -588,6 +606,7 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
588
606
  version: String,
589
607
  expectedSha256: String,
590
608
  expectedSize: Int? = nil,
609
+ runtimeVersion: String? = nil,
591
610
  channel: String? = nil,
592
611
  releaseId: String? = nil
593
612
  ) async throws -> BundleInfo {
@@ -652,6 +671,7 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
652
671
  let info = BundleInfo(
653
672
  id: bundleId,
654
673
  version: version,
674
+ runtimeVersion: runtimeVersion,
655
675
  status: .pending,
656
676
  downloadedAt: Date(),
657
677
  sha256: expectedSha256,
@@ -723,6 +743,10 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
723
743
  store.setStagedBundleId(nil)
724
744
  return store.getCurrentBundle()
725
745
  }
746
+ guard isCompatibleRuntime(staged) else {
747
+ try? store.deleteBundle(id: staged.id)
748
+ return store.getCurrentBundle()
749
+ }
726
750
 
727
751
  store.setCurrentBundleId(staged.id)
728
752
  store.setStagedBundleId(nil)
@@ -737,6 +761,10 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
737
761
  store.setStagedBundleId(nil)
738
762
  return
739
763
  }
764
+ guard isCompatibleRuntime(staged) else {
765
+ try? store.deleteBundle(id: staged.id)
766
+ return
767
+ }
740
768
 
741
769
  store.setCurrentBundleId(staged.id)
742
770
  store.setStagedBundleId(nil)
@@ -795,6 +823,7 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
795
823
  let failed = BundleInfo(
796
824
  id: current.id,
797
825
  version: current.version,
826
+ runtimeVersion: current.runtimeVersion,
798
827
  status: .error,
799
828
  downloadedAt: current.downloadedAt,
800
829
  sha256: current.sha256,
@@ -901,12 +930,12 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
901
930
  "size": latest.size,
902
931
  "downloaded": downloaded,
903
932
  ]
933
+ if let runtimeVersion = latest.runtimeVersion {
934
+ payload["runtimeVersion"] = runtimeVersion
935
+ }
904
936
  if let releaseId = latest.releaseId {
905
937
  payload["releaseId"] = releaseId
906
938
  }
907
- if let minNativeBuild = latest.minNativeBuild {
908
- payload["minNativeBuild"] = minNativeBuild
909
- }
910
939
  return payload
911
940
  }
912
941
 
@@ -926,6 +955,10 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
926
955
  return nil
927
956
  }
928
957
 
958
+ if trimToNil(staged.runtimeVersion) != trimToNil(latest.runtimeVersion) {
959
+ return nil
960
+ }
961
+
929
962
  if let latestReleaseId = latest.releaseId,
930
963
  let stagedReleaseId = staged.releaseId,
931
964
  latestReleaseId == stagedReleaseId {
@@ -940,6 +973,24 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
940
973
  return nil
941
974
  }
942
975
 
976
+ private func pruneIncompatibleBundles() {
977
+ for bundle in store.listDownloadedBundles() where !isCompatibleRuntime(bundle) {
978
+ try? store.deleteBundle(id: bundle.id)
979
+ }
980
+
981
+ if let failed = store.getFailedBundle(), !isCompatibleRuntime(failed) {
982
+ store.setFailedBundle(nil)
983
+ }
984
+ }
985
+
986
+ private func isCompatibleRuntime(_ runtimeVersion: String?) -> Bool {
987
+ trimToNil(runtimeVersion) == self.runtimeVersion
988
+ }
989
+
990
+ private func isCompatibleRuntime(_ bundle: BundleInfo) -> Bool {
991
+ isCompatibleRuntime(bundle.runtimeVersion)
992
+ }
993
+
943
994
  private func buildBundleId(from version: String) -> String {
944
995
  let trimmed = version.trimmingCharacters(in: .whitespacesAndNewlines)
945
996
  let allowed = CharacterSet(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@otakit/capacitor-updater",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Capacitor plugin for OTA updates",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",