@capgo/capacitor-updater 6.30.0 → 6.34.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.
@@ -10,8 +10,6 @@ package ee.forgr.capacitor_updater;
10
10
  * Created by Awesometic
11
11
  * It's encrypt returns Base64 encoded, and also decrypt for Base64 encoded cipher
12
12
  * references: http://stackoverflow.com/questions/12471999/rsa-encryption-decryption-in-android
13
- *
14
- * V2 Encryption - uses publicKey (modern encryption from main branch)
15
13
  */
16
14
  import android.util.Base64;
17
15
  import java.io.BufferedInputStream;
@@ -37,7 +35,7 @@ import javax.crypto.SecretKey;
37
35
  import javax.crypto.spec.IvParameterSpec;
38
36
  import javax.crypto.spec.SecretKeySpec;
39
37
 
40
- public class CryptoCipherV2 {
38
+ public class CryptoCipher {
41
39
 
42
40
  private static Logger logger;
43
41
 
@@ -155,10 +153,10 @@ public class CryptoCipherV2 {
155
153
  String sessionKeyB64 = ivSessionKey.split(":")[1];
156
154
  byte[] iv = Base64.decode(ivB64.getBytes(), Base64.DEFAULT);
157
155
  byte[] sessionKey = Base64.decode(sessionKeyB64.getBytes(), Base64.DEFAULT);
158
- PublicKey pKey = CryptoCipherV2.stringToPublicKey(publicKey);
159
- byte[] decryptedSessionKey = CryptoCipherV2.decryptRSA(sessionKey, pKey);
156
+ PublicKey pKey = CryptoCipher.stringToPublicKey(publicKey);
157
+ byte[] decryptedSessionKey = CryptoCipher.decryptRSA(sessionKey, pKey);
160
158
 
161
- SecretKey sKey = CryptoCipherV2.byteToSessionKey(decryptedSessionKey);
159
+ SecretKey sKey = CryptoCipher.byteToSessionKey(decryptedSessionKey);
162
160
  byte[] content = new byte[(int) file.length()];
163
161
 
164
162
  try (
@@ -168,7 +166,7 @@ public class CryptoCipherV2 {
168
166
  ) {
169
167
  dis.readFully(content);
170
168
  dis.close();
171
- byte[] decrypted = CryptoCipherV2.decryptAES(content, sKey, iv);
169
+ byte[] decrypted = CryptoCipher.decryptAES(content, sKey, iv);
172
170
  // write the decrypted string to the file
173
171
  try (final FileOutputStream fos = new FileOutputStream(file.getAbsolutePath())) {
174
172
  fos.write(decrypted);
@@ -200,14 +198,26 @@ public class CryptoCipherV2 {
200
198
  // Determine if input is hex or base64 encoded
201
199
  // Hex strings only contain 0-9 and a-f, while base64 contains other characters
202
200
  byte[] checksumBytes;
201
+ String detectedFormat;
203
202
  if (checksum.matches("^[0-9a-fA-F]+$")) {
204
203
  // Hex encoded (new format from CLI for plugin versions >= 5.30.0, 6.30.0, 7.30.0)
205
204
  checksumBytes = hexStringToByteArray(checksum);
205
+ detectedFormat = "hex";
206
206
  } else {
207
207
  // TODO: remove backwards compatibility
208
208
  // Base64 encoded (old format for backwards compatibility)
209
209
  checksumBytes = Base64.decode(checksum, Base64.DEFAULT);
210
+ detectedFormat = "base64";
210
211
  }
212
+ logger.debug(
213
+ "Received encrypted checksum format: " +
214
+ detectedFormat +
215
+ " (length: " +
216
+ checksum.length() +
217
+ " chars, " +
218
+ checksumBytes.length +
219
+ " bytes)"
220
+ );
211
221
  PublicKey pKey = CryptoCipher.stringToPublicKey(publicKey);
212
222
  byte[] decryptedChecksum = CryptoCipher.decryptRSA(checksumBytes, pKey);
213
223
  // Return as hex string to match calcChecksum output format
@@ -217,13 +227,75 @@ public class CryptoCipherV2 {
217
227
  if (hex.length() == 1) hexString.append('0');
218
228
  hexString.append(hex);
219
229
  }
220
- return hexString.toString();
230
+ String result = hexString.toString();
231
+
232
+ // Detect checksum algorithm based on length
233
+ String detectedAlgorithm;
234
+ if (decryptedChecksum.length == 32) {
235
+ detectedAlgorithm = "SHA-256";
236
+ } else if (decryptedChecksum.length == 4) {
237
+ detectedAlgorithm = "CRC32 (deprecated)";
238
+ logger.error(
239
+ "CRC32 checksum detected. This algorithm is deprecated and no longer supported. Please update your CLI to use SHA-256 checksums."
240
+ );
241
+ } else {
242
+ detectedAlgorithm = "unknown (" + decryptedChecksum.length + " bytes)";
243
+ logger.error(
244
+ "Unknown checksum algorithm detected with " + decryptedChecksum.length + " bytes. Expected SHA-256 (32 bytes)."
245
+ );
246
+ }
247
+ logger.debug(
248
+ "Decrypted checksum: " +
249
+ detectedAlgorithm +
250
+ " hex format (length: " +
251
+ result.length() +
252
+ " chars, " +
253
+ decryptedChecksum.length +
254
+ " bytes)"
255
+ );
256
+ return result;
221
257
  } catch (GeneralSecurityException e) {
222
258
  logger.error("decryptChecksum fail: " + e.getMessage());
223
259
  throw new IOException("Decryption failed: " + e.getMessage());
224
260
  }
225
261
  }
226
262
 
263
+ /**
264
+ * Detect checksum algorithm based on hex string length.
265
+ * SHA-256 = 64 hex chars (32 bytes)
266
+ * CRC32 = 8 hex chars (4 bytes)
267
+ */
268
+ public static String detectChecksumAlgorithm(String hexChecksum) {
269
+ if (hexChecksum == null || hexChecksum.isEmpty()) {
270
+ return "empty";
271
+ }
272
+ int len = hexChecksum.length();
273
+ if (len == 64) {
274
+ return "SHA-256";
275
+ } else if (len == 8) {
276
+ return "CRC32 (deprecated)";
277
+ } else {
278
+ return "unknown (" + len + " hex chars)";
279
+ }
280
+ }
281
+
282
+ /**
283
+ * Log checksum info and warn if deprecated algorithm detected.
284
+ */
285
+ public static void logChecksumInfo(String label, String hexChecksum) {
286
+ String algorithm = detectChecksumAlgorithm(hexChecksum);
287
+ logger.debug(label + ": " + algorithm + " hex format (length: " + hexChecksum.length() + " chars)");
288
+ if (algorithm.contains("CRC32")) {
289
+ logger.error(
290
+ "CRC32 checksum detected. This algorithm is deprecated and no longer supported. Please update your CLI to use SHA-256 checksums."
291
+ );
292
+ } else if (algorithm.contains("unknown")) {
293
+ logger.error(
294
+ "Unknown checksum algorithm detected. Expected SHA-256 (64 hex chars) but got " + hexChecksum.length() + " chars."
295
+ );
296
+ }
297
+ }
298
+
227
299
  public static String calcChecksum(File file) {
228
300
  final int BUFFER_SIZE = 1024 * 1024 * 5; // 5 MB buffer size
229
301
  MessageDigest digest;
@@ -293,7 +293,7 @@ public class DownloadService extends Worker {
293
293
 
294
294
  if (!publicKey.isEmpty() && sessionKey != null && !sessionKey.isEmpty()) {
295
295
  try {
296
- fileHash = CryptoCipherV2.decryptChecksum(fileHash, publicKey);
296
+ fileHash = CryptoCipher.decryptChecksum(fileHash, publicKey);
297
297
  } catch (Exception e) {
298
298
  logger.error("Error decrypting checksum for " + fileName + "fileHash: " + fileHash);
299
299
  hasError.set(true);
@@ -606,7 +606,7 @@ public class DownloadService extends Worker {
606
606
 
607
607
  if (!publicKey.isEmpty() && sessionKey != null && !sessionKey.isEmpty()) {
608
608
  logger.debug("Decrypting file " + targetFile.getName());
609
- CryptoCipherV2.decryptFile(compressedFile, publicKey, sessionKey);
609
+ CryptoCipher.decryptFile(compressedFile, publicKey, sessionKey);
610
610
  }
611
611
 
612
612
  // Only decompress if file has .br extension
@@ -619,6 +619,8 @@ public class DownloadService extends Worker {
619
619
  try {
620
620
  decompressedData = decompressBrotli(compressedData, targetFile.getName());
621
621
  } catch (IOException e) {
622
+ // Delete the compressed file before throwing error
623
+ compressedFile.delete();
622
624
  sendStatsAsync(
623
625
  "download_manifest_brotli_fail",
624
626
  getInputData().getString(VERSION) + ":" + finalTargetFile.getName()
@@ -640,7 +642,9 @@ public class DownloadService extends Worker {
640
642
 
641
643
  // Delete the compressed file
642
644
  compressedFile.delete();
643
- String calculatedHash = CryptoCipherV2.calcChecksum(finalTargetFile);
645
+ String calculatedHash = CryptoCipher.calcChecksum(finalTargetFile);
646
+ CryptoCipher.logChecksumInfo("Calculated checksum", calculatedHash);
647
+ CryptoCipher.logChecksumInfo("Expected checksum", expectedHash);
644
648
 
645
649
  // Verify checksum
646
650
  if (calculatedHash.equals(expectedHash)) {
@@ -769,7 +773,7 @@ public class DownloadService extends Worker {
769
773
 
770
774
  // Verify checksum if provided
771
775
  if (expectedChecksum != null && !expectedChecksum.isEmpty()) {
772
- String actualChecksum = CryptoCipherV2.calcChecksum(tempFile);
776
+ String actualChecksum = CryptoCipher.calcChecksum(tempFile);
773
777
  if (!expectedChecksum.equalsIgnoreCase(actualChecksum)) {
774
778
  tempFile.delete();
775
779
  throw new IOException("Checksum verification failed");
package/dist/docs.json CHANGED
@@ -492,14 +492,14 @@
492
492
  },
493
493
  {
494
494
  "name": "throws",
495
- "text": "{Error} If the request fails or the server returns an error."
495
+ "text": "{Error} Always throws when no new version is available (`error: \"no_new_version_available\"`), or when the request fails."
496
496
  },
497
497
  {
498
498
  "name": "since",
499
499
  "text": "4.0.0"
500
500
  }
501
501
  ],
502
- "docs": "Check the update server for the latest available bundle version.\n\nThis queries your configured update URL (or Capgo backend) to see if a newer bundle\nis available for download. It does NOT download the bundle automatically.\n\nThe response includes:\n- `version`: The latest available version identifier\n- `url`: Download URL for the bundle (if available)\n- `breaking`: Whether this update is marked as incompatible (requires native app update)\n- `message`: Optional message from the server\n- `manifest`: File list for partial updates (if using multi-file downloads)\n\nAfter receiving the latest version info, you can:\n1. Compare it with your current version\n2. Download it using {@link download}\n3. Apply it using {@link next} or {@link set}",
502
+ "docs": "Check the update server for the latest available bundle version.\n\nThis queries your configured update URL (or Capgo backend) to see if a newer bundle\nis available for download. It does NOT download the bundle automatically.\n\nThe response includes:\n- `version`: The latest available version identifier\n- `url`: Download URL for the bundle (if available)\n- `breaking`: Whether this update is marked as incompatible (requires native app update)\n- `message`: Optional message from the server\n- `manifest`: File list for partial updates (if using multi-file downloads)\n\nAfter receiving the latest version info, you can:\n1. Compare it with your current version\n2. Download it using {@link download}\n3. Apply it using {@link next} or {@link set}\n\n**Important: Error handling for \"no new version available\"**\n\nWhen the device's current version matches the latest version on the server (i.e., the device is already\nup-to-date), the server returns a 200 response with `error: \"no_new_version_available\"` and\n`message: \"No new version available\"`. **This causes `getLatest()` to throw an error**, even though\nthis is a normal, expected condition.\n\nYou should catch this specific error to handle it gracefully:\n\n```typescript\ntry {\n const latest = await CapacitorUpdater.getLatest();\n // New version is available, proceed with download\n} catch (error) {\n if (error.message === 'No new version available') {\n // Device is already on the latest version - this is normal\n console.log('Already up to date');\n } else {\n // Actual error occurred\n console.error('Failed to check for updates:', error);\n }\n}\n```\n\nIn this scenario, the server:\n- Logs the request with a \"No new version available\" message\n- Sends a \"noNew\" stat action to track that the device checked for updates but was already current (done on the backend)",
503
503
  "complexTypes": [
504
504
  "LatestVersion",
505
505
  "GetLatestOptions"
@@ -535,7 +535,7 @@
535
535
  "text": "4.7.0"
536
536
  }
537
537
  ],
538
- "docs": "Assign this device to a specific update channel at runtime.\n\nChannels allow you to distribute different bundle versions to different groups of users\n(e.g., \"production\", \"beta\", \"staging\"). This method switches the device to a new channel.\n\n**Requirements:**\n- The target channel must allow self-assignment (configured in your Capgo dashboard or backend)\n- The backend may accept or reject the request based on channel settings\n\n**When to use:**\n- After the app is ready and the user has interacted (e.g., opted into beta program)\n- To implement in-app channel switching (beta toggle, tester access, etc.)\n- For user-driven channel changes\n\n**When NOT to use:**\n- At app boot/initialization - use {@link PluginsConfig.CapacitorUpdater.defaultChannel} config instead\n- Before user interaction\n\nThis sends a request to the Capgo backend linking your device ID to the specified channel.",
538
+ "docs": "Assign this device to a specific update channel at runtime.\n\nChannels allow you to distribute different bundle versions to different groups of users\n(e.g., \"production\", \"beta\", \"staging\"). This method switches the device to a new channel.\n\n**Requirements:**\n- The target channel must allow self-assignment (configured in your Capgo dashboard or backend)\n- The backend may accept or reject the request based on channel settings\n\n**When to use:**\n- After the app is ready and the user has interacted (e.g., opted into beta program)\n- To implement in-app channel switching (beta toggle, tester access, etc.)\n- For user-driven channel changes\n\n**When NOT to use:**\n- At app boot/initialization - use {@link PluginsConfig.CapacitorUpdater.defaultChannel} config instead\n- Before user interaction\n\n**Important: Listen for the `channelPrivate` event**\n\nWhen a user attempts to set a channel that doesn't allow device self-assignment, the method will\nthrow an error AND fire a {@link addListener}('channelPrivate') event. You should listen to this event\nto provide appropriate feedback to users:\n\n```typescript\nCapacitorUpdater.addListener('channelPrivate', (data) => {\n console.warn(`Cannot access channel \"${data.channel}\": ${data.message}`);\n // Show user-friendly message\n});\n```\n\nThis sends a request to the Capgo backend linking your device ID to the specified channel.",
539
539
  "complexTypes": [
540
540
  "ChannelRes",
541
541
  "SetChannelOptions"
@@ -1062,6 +1062,35 @@
1062
1062
  ],
1063
1063
  "slug": "addlistenerappready-"
1064
1064
  },
1065
+ {
1066
+ "name": "addListener",
1067
+ "signature": "(eventName: 'channelPrivate', listenerFunc: (state: ChannelPrivateEvent) => void) => Promise<PluginListenerHandle>",
1068
+ "parameters": [
1069
+ {
1070
+ "name": "eventName",
1071
+ "docs": "",
1072
+ "type": "'channelPrivate'"
1073
+ },
1074
+ {
1075
+ "name": "listenerFunc",
1076
+ "docs": "",
1077
+ "type": "(state: ChannelPrivateEvent) => void"
1078
+ }
1079
+ ],
1080
+ "returns": "Promise<PluginListenerHandle>",
1081
+ "tags": [
1082
+ {
1083
+ "name": "since",
1084
+ "text": "7.34.0"
1085
+ }
1086
+ ],
1087
+ "docs": "Listen for channel private event, fired when attempting to set a channel that doesn't allow device self-assignment.\n\nThis event is useful for:\n- Informing users they don't have permission to switch to a specific channel\n- Implementing custom error handling for channel restrictions\n- Logging unauthorized channel access attempts",
1088
+ "complexTypes": [
1089
+ "PluginListenerHandle",
1090
+ "ChannelPrivateEvent"
1091
+ ],
1092
+ "slug": "addlistenerchannelprivate-"
1093
+ },
1065
1094
  {
1066
1095
  "name": "isAutoUpdateAvailable",
1067
1096
  "signature": "() => Promise<AutoUpdateAvailable>",
@@ -1690,7 +1719,7 @@
1690
1719
  {
1691
1720
  "name": "message",
1692
1721
  "tags": [],
1693
- "docs": "",
1722
+ "docs": "Optional message from the server.\nWhen no new version is available, this will be \"No new version available\".",
1694
1723
  "complexTypes": [],
1695
1724
  "type": "string | undefined"
1696
1725
  },
@@ -1704,21 +1733,21 @@
1704
1733
  {
1705
1734
  "name": "error",
1706
1735
  "tags": [],
1707
- "docs": "",
1736
+ "docs": "Error code from the server, if any.\nCommon values:\n- `\"no_new_version_available\"`: Device is already on the latest version (not a failure)\n- Other error codes indicate actual failures in the update process",
1708
1737
  "complexTypes": [],
1709
1738
  "type": "string | undefined"
1710
1739
  },
1711
1740
  {
1712
1741
  "name": "old",
1713
1742
  "tags": [],
1714
- "docs": "",
1743
+ "docs": "The previous/current version name (provided for reference).",
1715
1744
  "complexTypes": [],
1716
1745
  "type": "string | undefined"
1717
1746
  },
1718
1747
  {
1719
1748
  "name": "url",
1720
1749
  "tags": [],
1721
- "docs": "",
1750
+ "docs": "Download URL for the bundle (when a new version is available).",
1722
1751
  "complexTypes": [],
1723
1752
  "type": "string | undefined"
1724
1753
  },
@@ -1730,7 +1759,7 @@
1730
1759
  "name": "since"
1731
1760
  }
1732
1761
  ],
1733
- "docs": "",
1762
+ "docs": "File list for partial updates (when using multi-file downloads).",
1734
1763
  "complexTypes": [
1735
1764
  "ManifestEntry"
1736
1765
  ],
@@ -2260,6 +2289,34 @@
2260
2289
  }
2261
2290
  ]
2262
2291
  },
2292
+ {
2293
+ "name": "ChannelPrivateEvent",
2294
+ "slug": "channelprivateevent",
2295
+ "docs": "",
2296
+ "tags": [],
2297
+ "methods": [],
2298
+ "properties": [
2299
+ {
2300
+ "name": "channel",
2301
+ "tags": [
2302
+ {
2303
+ "text": "7.34.0",
2304
+ "name": "since"
2305
+ }
2306
+ ],
2307
+ "docs": "Emitted when attempting to set a channel that doesn't allow device self-assignment.",
2308
+ "complexTypes": [],
2309
+ "type": "string"
2310
+ },
2311
+ {
2312
+ "name": "message",
2313
+ "tags": [],
2314
+ "docs": "",
2315
+ "complexTypes": [],
2316
+ "type": "string"
2317
+ }
2318
+ ]
2319
+ },
2263
2320
  {
2264
2321
  "name": "AutoUpdateAvailable",
2265
2322
  "slug": "autoupdateavailable",
@@ -2876,6 +2933,22 @@
2876
2933
  "complexTypes": [],
2877
2934
  "type": "boolean | undefined"
2878
2935
  },
2936
+ {
2937
+ "name": "allowSetDefaultChannel",
2938
+ "tags": [
2939
+ {
2940
+ "text": "true",
2941
+ "name": "default"
2942
+ },
2943
+ {
2944
+ "text": "7.34.0",
2945
+ "name": "since"
2946
+ }
2947
+ ],
2948
+ "docs": "Allow or disallow the {@link CapacitorUpdaterPlugin.setChannel} method to modify the defaultChannel.\nWhen set to `false`, calling `setChannel()` will return an error with code `disabled_by_config`.",
2949
+ "complexTypes": [],
2950
+ "type": "boolean | undefined"
2951
+ },
2879
2952
  {
2880
2953
  "name": "defaultChannel",
2881
2954
  "tags": [
@@ -278,6 +278,14 @@ declare module '@capacitor/cli' {
278
278
  * @since 7.20.0
279
279
  */
280
280
  persistModifyUrl?: boolean;
281
+ /**
282
+ * Allow or disallow the {@link CapacitorUpdaterPlugin.setChannel} method to modify the defaultChannel.
283
+ * When set to `false`, calling `setChannel()` will return an error with code `disabled_by_config`.
284
+ *
285
+ * @default true
286
+ * @since 7.34.0
287
+ */
288
+ allowSetDefaultChannel?: boolean;
281
289
  /**
282
290
  * Set the default channel for the app in the config. Case sensitive.
283
291
  * This will setting will override the default channel set in the cloud, but will still respect overrides made in the cloud.
@@ -665,9 +673,37 @@ export interface CapacitorUpdaterPlugin {
665
673
  * 2. Download it using {@link download}
666
674
  * 3. Apply it using {@link next} or {@link set}
667
675
  *
676
+ * **Important: Error handling for "no new version available"**
677
+ *
678
+ * When the device's current version matches the latest version on the server (i.e., the device is already
679
+ * up-to-date), the server returns a 200 response with `error: "no_new_version_available"` and
680
+ * `message: "No new version available"`. **This causes `getLatest()` to throw an error**, even though
681
+ * this is a normal, expected condition.
682
+ *
683
+ * You should catch this specific error to handle it gracefully:
684
+ *
685
+ * ```typescript
686
+ * try {
687
+ * const latest = await CapacitorUpdater.getLatest();
688
+ * // New version is available, proceed with download
689
+ * } catch (error) {
690
+ * if (error.message === 'No new version available') {
691
+ * // Device is already on the latest version - this is normal
692
+ * console.log('Already up to date');
693
+ * } else {
694
+ * // Actual error occurred
695
+ * console.error('Failed to check for updates:', error);
696
+ * }
697
+ * }
698
+ * ```
699
+ *
700
+ * In this scenario, the server:
701
+ * - Logs the request with a "No new version available" message
702
+ * - Sends a "noNew" stat action to track that the device checked for updates but was already current (done on the backend)
703
+ *
668
704
  * @param options Optional {@link GetLatestOptions} to specify which channel to check.
669
705
  * @returns {Promise<LatestVersion>} Information about the latest available bundle version.
670
- * @throws {Error} If the request fails or the server returns an error.
706
+ * @throws {Error} Always throws when no new version is available (`error: "no_new_version_available"`), or when the request fails.
671
707
  * @since 4.0.0
672
708
  */
673
709
  getLatest(options?: GetLatestOptions): Promise<LatestVersion>;
@@ -690,6 +726,19 @@ export interface CapacitorUpdaterPlugin {
690
726
  * - At app boot/initialization - use {@link PluginsConfig.CapacitorUpdater.defaultChannel} config instead
691
727
  * - Before user interaction
692
728
  *
729
+ * **Important: Listen for the `channelPrivate` event**
730
+ *
731
+ * When a user attempts to set a channel that doesn't allow device self-assignment, the method will
732
+ * throw an error AND fire a {@link addListener}('channelPrivate') event. You should listen to this event
733
+ * to provide appropriate feedback to users:
734
+ *
735
+ * ```typescript
736
+ * CapacitorUpdater.addListener('channelPrivate', (data) => {
737
+ * console.warn(`Cannot access channel "${data.channel}": ${data.message}`);
738
+ * // Show user-friendly message
739
+ * });
740
+ * ```
741
+ *
693
742
  * This sends a request to the Capgo backend linking your device ID to the specified channel.
694
743
  *
695
744
  * @param options The {@link SetChannelOptions} containing the channel name and optional auto-update trigger.
@@ -948,6 +997,17 @@ export interface CapacitorUpdaterPlugin {
948
997
  * @since 5.1.0
949
998
  */
950
999
  addListener(eventName: 'appReady', listenerFunc: (state: AppReadyEvent) => void): Promise<PluginListenerHandle>;
1000
+ /**
1001
+ * Listen for channel private event, fired when attempting to set a channel that doesn't allow device self-assignment.
1002
+ *
1003
+ * This event is useful for:
1004
+ * - Informing users they don't have permission to switch to a specific channel
1005
+ * - Implementing custom error handling for channel restrictions
1006
+ * - Logging unauthorized channel access attempts
1007
+ *
1008
+ * @since 7.34.0
1009
+ */
1010
+ addListener(eventName: 'channelPrivate', listenerFunc: (state: ChannelPrivateEvent) => void): Promise<PluginListenerHandle>;
951
1011
  /**
952
1012
  * Check if the auto-update feature is available (not disabled by custom server configuration).
953
1013
  *
@@ -1234,6 +1294,15 @@ export interface AppReadyEvent {
1234
1294
  bundle: BundleInfo;
1235
1295
  status: string;
1236
1296
  }
1297
+ export interface ChannelPrivateEvent {
1298
+ /**
1299
+ * Emitted when attempting to set a channel that doesn't allow device self-assignment.
1300
+ *
1301
+ * @since 7.34.0
1302
+ */
1303
+ channel: string;
1304
+ message: string;
1305
+ }
1237
1306
  export interface ManifestEntry {
1238
1307
  file_name: string | null;
1239
1308
  file_hash: string | null;
@@ -1260,12 +1329,29 @@ export interface LatestVersion {
1260
1329
  * @deprecated Use {@link LatestVersion.breaking} instead.
1261
1330
  */
1262
1331
  major?: boolean;
1332
+ /**
1333
+ * Optional message from the server.
1334
+ * When no new version is available, this will be "No new version available".
1335
+ */
1263
1336
  message?: string;
1264
1337
  sessionKey?: string;
1338
+ /**
1339
+ * Error code from the server, if any.
1340
+ * Common values:
1341
+ * - `"no_new_version_available"`: Device is already on the latest version (not a failure)
1342
+ * - Other error codes indicate actual failures in the update process
1343
+ */
1265
1344
  error?: string;
1345
+ /**
1346
+ * The previous/current version name (provided for reference).
1347
+ */
1266
1348
  old?: string;
1349
+ /**
1350
+ * Download URL for the bundle (when a new version is available).
1351
+ */
1267
1352
  url?: string;
1268
1353
  /**
1354
+ * File list for partial updates (when using multi-file downloads).
1269
1355
  * @since 6.1
1270
1356
  */
1271
1357
  manifest?: ManifestEntry[];