@capgo/capacitor-updater 7.9.1 → 7.11.6

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.
@@ -16,6 +16,7 @@ import java.net.HttpURLConnection;
16
16
  import java.net.URL;
17
17
  import java.nio.channels.FileChannel;
18
18
  import java.nio.file.Files;
19
+ import java.nio.file.StandardCopyOption;
19
20
  import java.security.MessageDigest;
20
21
  import java.util.ArrayList;
21
22
  import java.util.Arrays;
@@ -33,6 +34,11 @@ import okhttp3.Protocol;
33
34
  import okhttp3.Request;
34
35
  import okhttp3.Response;
35
36
  import okhttp3.ResponseBody;
37
+ import okio.Buffer;
38
+ import okio.BufferedSink;
39
+ import okio.BufferedSource;
40
+ import okio.Okio;
41
+ import okio.Source;
36
42
  import org.brotli.dec.BrotliInputStream;
37
43
  import org.json.JSONArray;
38
44
  import org.json.JSONObject;
@@ -60,29 +66,44 @@ public class DownloadService extends Worker {
60
66
  public static final String PLUGIN_VERSION = "plugin_version";
61
67
  private static final String UPDATE_FILE = "update.dat";
62
68
 
63
- private final OkHttpClient client;
69
+ // Shared OkHttpClient to prevent resource leaks
70
+ private static OkHttpClient sharedClient;
71
+ private static String currentAppId = "unknown";
72
+ private static String currentPluginVersion = "unknown";
64
73
 
65
- public DownloadService(@NonNull Context context, @NonNull WorkerParameters params) {
66
- super(context, params);
67
- // Get appId and plugin version from input data
68
- String appId = getInputData().getString(APP_ID);
69
- String pluginVersion = getInputData().getString(PLUGIN_VERSION);
70
-
71
- // Build user agent with appId and plugin version
72
- String userAgent =
73
- "CapacitorUpdater/" + (pluginVersion != null ? pluginVersion : "unknown") + " (" + (appId != null ? appId : "unknown") + ")";
74
-
75
- // Create OkHttpClient with custom user agent
76
- this.client = new OkHttpClient.Builder()
74
+ // Initialize shared client with User-Agent interceptor
75
+ static {
76
+ sharedClient = new OkHttpClient.Builder()
77
77
  .protocols(Arrays.asList(Protocol.HTTP_2, Protocol.HTTP_1_1))
78
78
  .addInterceptor(chain -> {
79
79
  Request originalRequest = chain.request();
80
+ String userAgent =
81
+ "CapacitorUpdater/" +
82
+ (currentPluginVersion != null ? currentPluginVersion : "unknown") +
83
+ " (" +
84
+ (currentAppId != null ? currentAppId : "unknown") +
85
+ ")";
80
86
  Request requestWithUserAgent = originalRequest.newBuilder().header("User-Agent", userAgent).build();
81
87
  return chain.proceed(requestWithUserAgent);
82
88
  })
83
89
  .build();
84
90
  }
85
91
 
92
+ // Method to update User-Agent values
93
+ public static void updateUserAgent(String appId, String pluginVersion) {
94
+ currentAppId = appId != null ? appId : "unknown";
95
+ currentPluginVersion = pluginVersion != null ? pluginVersion : "unknown";
96
+ logger.debug("Updated User-Agent: CapacitorUpdater/" + currentPluginVersion + " (" + currentAppId + ")");
97
+ }
98
+
99
+ public DownloadService(@NonNull Context context, @NonNull WorkerParameters params) {
100
+ super(context, params);
101
+ // Use shared client - no need to create new instances
102
+
103
+ // Clean up old temporary files on service initialization
104
+ cleanupOldTempFiles(getApplicationContext().getCacheDir());
105
+ }
106
+
86
107
  private void setProgress(int percent) {
87
108
  Data progress = new Data.Builder().putInt(PERCENT, percent).build();
88
109
  setProgressAsync(progress);
@@ -272,93 +293,156 @@ public class DownloadService extends Worker {
272
293
  String checksum
273
294
  ) {
274
295
  File target = new File(documentsDir, dest);
275
- File infoFile = new File(documentsDir, UPDATE_FILE); // The file where the download progress (how much byte
276
- // downloaded) is stored
277
- File tempFile = new File(documentsDir, "temp" + ".tmp"); // Temp file, where the downloaded data is stored
296
+ File infoFile = new File(documentsDir, UPDATE_FILE);
297
+ File tempFile = new File(documentsDir, "temp" + ".tmp");
298
+
299
+ // Check available disk space before starting
300
+ long availableSpace = target.getParentFile().getUsableSpace();
301
+ long estimatedSize = 50 * 1024 * 1024; // 50MB default estimate
302
+ if (availableSpace < estimatedSize * 2) {
303
+ throw new RuntimeException("insufficient_disk_space");
304
+ }
305
+
306
+ HttpURLConnection httpConn = null;
307
+ InputStream inputStream = null;
308
+ FileOutputStream outputStream = null;
309
+ BufferedReader reader = null;
310
+ BufferedWriter writer = null;
311
+
278
312
  try {
279
313
  URL u = new URL(url);
280
- HttpURLConnection httpConn = null;
281
- try {
282
- httpConn = (HttpURLConnection) u.openConnection();
314
+ httpConn = (HttpURLConnection) u.openConnection();
283
315
 
284
- // Reading progress file (if exist)
285
- long downloadedBytes = 0;
316
+ // Set reasonable timeouts
317
+ httpConn.setConnectTimeout(30000); // 30 seconds
318
+ httpConn.setReadTimeout(60000); // 60 seconds
286
319
 
287
- if (infoFile.exists() && tempFile.exists()) {
288
- try (BufferedReader reader = new BufferedReader(new FileReader(infoFile))) {
289
- String updateVersion = reader.readLine();
290
- if (updateVersion != null && !updateVersion.equals(version)) {
291
- clearDownloadData(documentsDir);
292
- } else {
293
- downloadedBytes = tempFile.length();
294
- }
320
+ // Reading progress file (if exist)
321
+ long downloadedBytes = 0;
322
+
323
+ if (infoFile.exists() && tempFile.exists()) {
324
+ try {
325
+ reader = new BufferedReader(new FileReader(infoFile));
326
+ String updateVersion = reader.readLine();
327
+ if (updateVersion != null && !updateVersion.equals(version)) {
328
+ clearDownloadData(documentsDir);
329
+ } else {
330
+ downloadedBytes = tempFile.length();
331
+ }
332
+ } finally {
333
+ if (reader != null) {
334
+ try {
335
+ reader.close();
336
+ } catch (Exception ignored) {}
295
337
  }
296
- } else {
297
- clearDownloadData(documentsDir);
298
338
  }
339
+ } else {
340
+ clearDownloadData(documentsDir);
341
+ }
342
+
343
+ if (downloadedBytes > 0) {
344
+ httpConn.setRequestProperty("Range", "bytes=" + downloadedBytes + "-");
345
+ }
299
346
 
300
- if (downloadedBytes > 0) {
301
- httpConn.setRequestProperty("Range", "bytes=" + downloadedBytes + "-");
347
+ int responseCode = httpConn.getResponseCode();
348
+
349
+ if (responseCode == HttpURLConnection.HTTP_OK || responseCode == HttpURLConnection.HTTP_PARTIAL) {
350
+ long contentLength = httpConn.getContentLength() + downloadedBytes;
351
+
352
+ // Check if we have enough space for the actual file
353
+ if (contentLength > 0 && availableSpace < contentLength * 2) {
354
+ throw new RuntimeException("insufficient_disk_space");
302
355
  }
303
356
 
304
- int responseCode = httpConn.getResponseCode();
357
+ try {
358
+ inputStream = httpConn.getInputStream();
359
+ outputStream = new FileOutputStream(tempFile, downloadedBytes > 0);
360
+
361
+ if (downloadedBytes == 0) {
362
+ writer = new BufferedWriter(new FileWriter(infoFile));
363
+ writer.write(String.valueOf(version));
364
+ writer.close();
365
+ writer = null;
366
+ }
305
367
 
306
- if (responseCode == HttpURLConnection.HTTP_OK || responseCode == HttpURLConnection.HTTP_PARTIAL) {
307
- long contentLength = httpConn.getContentLength() + downloadedBytes;
368
+ byte[] buffer = new byte[8192]; // Larger buffer for better performance
369
+ int lastNotifiedPercent = 0;
370
+ int bytesRead;
308
371
 
309
- try (
310
- InputStream inputStream = httpConn.getInputStream();
311
- FileOutputStream outputStream = new FileOutputStream(tempFile, downloadedBytes > 0)
312
- ) {
313
- if (downloadedBytes == 0) {
314
- try (BufferedWriter writer = new BufferedWriter(new FileWriter(infoFile))) {
315
- writer.write(String.valueOf(version));
316
- }
317
- }
318
- // Updating the info file
319
- try (BufferedWriter writer = new BufferedWriter(new FileWriter(infoFile))) {
320
- writer.write(String.valueOf(version));
372
+ while ((bytesRead = inputStream.read(buffer)) != -1) {
373
+ outputStream.write(buffer, 0, bytesRead);
374
+ downloadedBytes += bytesRead;
375
+
376
+ // Flush every 1MB to ensure progress is saved
377
+ if (downloadedBytes % (1024 * 1024) == 0) {
378
+ outputStream.flush();
321
379
  }
322
380
 
323
- int bytesRead = -1;
324
- byte[] buffer = new byte[4096];
325
- int lastNotifiedPercent = 0;
326
- while ((bytesRead = inputStream.read(buffer)) != -1) {
327
- outputStream.write(buffer, 0, bytesRead);
328
- downloadedBytes += bytesRead;
329
- // Saving progress (flushing every 100 Ko)
330
- if (downloadedBytes % 102400 == 0) {
331
- outputStream.flush();
332
- }
333
- // Computing percentage
334
- int percent = calcTotalPercent(downloadedBytes, contentLength);
335
- while (lastNotifiedPercent + 10 <= percent) {
336
- lastNotifiedPercent += 10;
337
- // Artificial delay using CPU-bound calculation to take ~5 seconds
338
- double result = 0;
339
- setProgress(lastNotifiedPercent);
340
- }
381
+ // Computing percentage
382
+ int percent = calcTotalPercent(downloadedBytes, contentLength);
383
+ if (percent >= lastNotifiedPercent + 10) {
384
+ lastNotifiedPercent = (percent / 10) * 10;
385
+ setProgress(lastNotifiedPercent);
341
386
  }
387
+ }
342
388
 
343
- outputStream.close();
344
- inputStream.close();
389
+ // Final flush
390
+ outputStream.flush();
391
+ outputStream.close();
392
+ outputStream = null;
345
393
 
346
- // Rename the temp file with the final name (dest)
347
- tempFile.renameTo(new File(documentsDir, dest));
348
- infoFile.delete();
394
+ inputStream.close();
395
+ inputStream = null;
396
+
397
+ // Rename the temp file with the final name (dest)
398
+ if (!tempFile.renameTo(new File(documentsDir, dest))) {
399
+ throw new RuntimeException("Failed to rename temp file to final destination");
349
400
  }
350
- } else {
351
401
  infoFile.delete();
402
+ } catch (OutOfMemoryError e) {
403
+ logger.error("Out of memory during download: " + e.getMessage());
404
+ // Try to free some memory
405
+ System.gc();
406
+ throw new RuntimeException("low_mem_fail");
407
+ } finally {
408
+ // Ensure all resources are closed
409
+ if (outputStream != null) {
410
+ try {
411
+ outputStream.close();
412
+ } catch (Exception ignored) {}
413
+ }
414
+ if (inputStream != null) {
415
+ try {
416
+ inputStream.close();
417
+ } catch (Exception ignored) {}
418
+ }
419
+ if (writer != null) {
420
+ try {
421
+ writer.close();
422
+ } catch (Exception ignored) {}
423
+ }
352
424
  }
353
- } finally {
354
- if (httpConn != null) {
355
- httpConn.disconnect();
356
- }
425
+ } else {
426
+ infoFile.delete();
427
+ throw new RuntimeException("HTTP error: " + responseCode);
357
428
  }
358
429
  } catch (OutOfMemoryError e) {
430
+ logger.error("Critical memory error: " + e.getMessage());
431
+ System.gc(); // Suggest garbage collection
359
432
  throw new RuntimeException("low_mem_fail");
433
+ } catch (SecurityException e) {
434
+ logger.error("Security error during download: " + e.getMessage());
435
+ throw new RuntimeException("security_error: " + e.getMessage());
360
436
  } catch (Exception e) {
361
- throw new RuntimeException(e.getLocalizedMessage());
437
+ logger.error("Download error: " + e.getMessage());
438
+ throw new RuntimeException(e.getMessage());
439
+ } finally {
440
+ // Ensure connection is closed
441
+ if (httpConn != null) {
442
+ try {
443
+ httpConn.disconnect();
444
+ } catch (Exception ignored) {}
445
+ }
362
446
  }
363
447
  }
364
448
 
@@ -412,26 +496,20 @@ public class DownloadService extends Worker {
412
496
  // Create a temporary file for the compressed data
413
497
  File compressedFile = new File(getApplicationContext().getCacheDir(), "temp_" + targetFile.getName() + ".tmp");
414
498
 
415
- try (Response response = client.newCall(request).execute()) {
499
+ try (Response response = sharedClient.newCall(request).execute()) {
416
500
  if (!response.isSuccessful()) {
417
501
  throw new IOException("Unexpected response code: " + response.code());
418
502
  }
419
503
 
420
- // Download compressed file
421
- try (ResponseBody responseBody = response.body(); FileOutputStream compressedFos = new FileOutputStream(compressedFile)) {
422
- if (responseBody == null) {
423
- throw new IOException("Response body is null");
424
- }
425
-
426
- byte[] buffer = new byte[8192];
427
- int bytesRead;
428
- try (InputStream inputStream = responseBody.byteStream()) {
429
- while ((bytesRead = inputStream.read(buffer)) != -1) {
430
- compressedFos.write(buffer, 0, bytesRead);
431
- }
432
- }
504
+ // Download compressed file atomically
505
+ ResponseBody responseBody = response.body();
506
+ if (responseBody == null) {
507
+ throw new IOException("Response body is null");
433
508
  }
434
509
 
510
+ // Use OkIO for atomic write
511
+ writeFileAtomic(compressedFile, responseBody.byteStream(), null);
512
+
435
513
  if (!publicKey.isEmpty() && sessionKey != null && !sessionKey.isEmpty()) {
436
514
  logger.debug("Decrypting file " + targetFile.getName());
437
515
  CryptoCipherV2.decryptFile(compressedFile, publicKey, sessionKey);
@@ -439,13 +517,22 @@ public class DownloadService extends Worker {
439
517
 
440
518
  // Only decompress if file has .br extension
441
519
  if (isBrotli) {
442
- // Use new decompression method
443
- byte[] compressedData = Files.readAllBytes(compressedFile.toPath());
444
- byte[] decompressedData = decompressBrotli(compressedData, targetFile.getName());
445
- Files.write(finalTargetFile.toPath(), decompressedData);
520
+ // Use new decompression method with atomic write
521
+ try (FileInputStream fis = new FileInputStream(compressedFile)) {
522
+ byte[] compressedData = new byte[(int) compressedFile.length()];
523
+ fis.read(compressedData);
524
+ byte[] decompressedData = decompressBrotli(compressedData, targetFile.getName());
525
+
526
+ // Write decompressed data atomically
527
+ try (java.io.ByteArrayInputStream bais = new java.io.ByteArrayInputStream(decompressedData)) {
528
+ writeFileAtomic(finalTargetFile, bais, null);
529
+ }
530
+ }
446
531
  } else {
447
- // Just copy the file without decompression
448
- Files.copy(compressedFile.toPath(), finalTargetFile.toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING);
532
+ // Just copy the file without decompression using atomic operation
533
+ try (FileInputStream fis = new FileInputStream(compressedFile)) {
534
+ writeFileAtomic(finalTargetFile, fis, null);
535
+ }
449
536
  }
450
537
 
451
538
  // Delete the compressed file
@@ -454,8 +541,10 @@ public class DownloadService extends Worker {
454
541
 
455
542
  // Verify checksum
456
543
  if (calculatedHash.equals(expectedHash)) {
457
- // Only cache if checksum is correct
458
- copyFile(finalTargetFile, cacheFile);
544
+ // Only cache if checksum is correct - use atomic copy
545
+ try (FileInputStream fis = new FileInputStream(finalTargetFile)) {
546
+ writeFileAtomic(cacheFile, fis, expectedHash);
547
+ }
459
548
  } else {
460
549
  finalTargetFile.delete();
461
550
  throw new IOException(
@@ -561,4 +650,75 @@ public class DownloadService extends Worker {
561
650
  throw e;
562
651
  }
563
652
  }
653
+
654
+ /**
655
+ * Atomically write data to a file using OkIO
656
+ */
657
+ private void writeFileAtomic(File targetFile, InputStream inputStream, String expectedChecksum) throws IOException {
658
+ File tempFile = new File(targetFile.getParent(), targetFile.getName() + ".tmp");
659
+
660
+ try {
661
+ // Write to temp file first using OkIO
662
+ try (BufferedSink sink = Okio.buffer(Okio.sink(tempFile)); BufferedSource source = Okio.buffer(Okio.source(inputStream))) {
663
+ sink.writeAll(source);
664
+ }
665
+
666
+ // Verify checksum if provided
667
+ if (expectedChecksum != null && !expectedChecksum.isEmpty()) {
668
+ String actualChecksum = calculateFileChecksum(tempFile);
669
+ if (!expectedChecksum.equalsIgnoreCase(actualChecksum)) {
670
+ tempFile.delete();
671
+ throw new IOException("Checksum verification failed");
672
+ }
673
+ }
674
+
675
+ // Atomic rename (on same filesystem)
676
+ Files.move(tempFile.toPath(), targetFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
677
+ } catch (Exception e) {
678
+ // Clean up temp file on error
679
+ if (tempFile.exists()) {
680
+ tempFile.delete();
681
+ }
682
+ throw new IOException("Failed to write file atomically: " + e.getMessage(), e);
683
+ }
684
+ }
685
+
686
+ /**
687
+ * Calculate MD5 checksum of a file
688
+ */
689
+ private String calculateFileChecksum(File file) throws IOException {
690
+ try (FileInputStream fis = new FileInputStream(file)) {
691
+ MessageDigest md = MessageDigest.getInstance("MD5");
692
+ byte[] buffer = new byte[8192];
693
+ int bytesRead;
694
+ while ((bytesRead = fis.read(buffer)) != -1) {
695
+ md.update(buffer, 0, bytesRead);
696
+ }
697
+ byte[] digest = md.digest();
698
+ StringBuilder sb = new StringBuilder();
699
+ for (byte b : digest) {
700
+ sb.append(String.format("%02x", b));
701
+ }
702
+ return sb.toString();
703
+ } catch (Exception e) {
704
+ throw new IOException("Failed to calculate checksum: " + e.getMessage(), e);
705
+ }
706
+ }
707
+
708
+ /**
709
+ * Clean up old temporary files
710
+ */
711
+ private void cleanupOldTempFiles(File directory) {
712
+ if (directory == null || !directory.exists()) return;
713
+
714
+ File[] tempFiles = directory.listFiles((dir, name) -> name.endsWith(".tmp"));
715
+ if (tempFiles != null) {
716
+ long oneHourAgo = System.currentTimeMillis() - 3600000;
717
+ for (File tempFile : tempFiles) {
718
+ if (tempFile.lastModified() < oneHourAgo) {
719
+ tempFile.delete();
720
+ }
721
+ }
722
+ }
723
+ }
564
724
  }
@@ -5,12 +5,11 @@ import androidx.work.BackoffPolicy;
5
5
  import androidx.work.Configuration;
6
6
  import androidx.work.Constraints;
7
7
  import androidx.work.Data;
8
+ import androidx.work.ExistingWorkPolicy;
8
9
  import androidx.work.NetworkType;
9
10
  import androidx.work.OneTimeWorkRequest;
10
11
  import androidx.work.WorkManager;
11
12
  import androidx.work.WorkRequest;
12
- import java.util.HashSet;
13
- import java.util.Set;
14
13
  import java.util.concurrent.TimeUnit;
15
14
 
16
15
  public class DownloadWorkerManager {
@@ -22,8 +21,6 @@ public class DownloadWorkerManager {
22
21
  }
23
22
 
24
23
  private static volatile boolean isInitialized = false;
25
- private static final Set<String> activeVersions = new HashSet<>();
26
- private static final Object activeVersionsLock = new Object();
27
24
 
28
25
  private static synchronized void initializeIfNeeded(Context context) {
29
26
  if (!isInitialized) {
@@ -37,9 +34,17 @@ public class DownloadWorkerManager {
37
34
  }
38
35
  }
39
36
 
40
- public static boolean isVersionDownloading(String version) {
41
- synchronized (activeVersionsLock) {
42
- return activeVersions.contains(version);
37
+ public static boolean isVersionDownloading(Context context, String version) {
38
+ initializeIfNeeded(context.getApplicationContext());
39
+ try {
40
+ return WorkManager.getInstance(context)
41
+ .getWorkInfosByTag(version)
42
+ .get()
43
+ .stream()
44
+ .anyMatch(workInfo -> !workInfo.getState().isFinished());
45
+ } catch (Exception e) {
46
+ logger.error("Error checking download status: " + e.getMessage());
47
+ return false;
43
48
  }
44
49
  }
45
50
 
@@ -60,14 +65,8 @@ public class DownloadWorkerManager {
60
65
  ) {
61
66
  initializeIfNeeded(context.getApplicationContext());
62
67
 
63
- // If version is already downloading, don't start another one
64
- synchronized (activeVersionsLock) {
65
- if (activeVersions.contains(version)) {
66
- logger.info("Version " + version + " is already downloading");
67
- return;
68
- }
69
- activeVersions.add(version);
70
- }
68
+ // Use unique work name for this bundle to prevent duplicates
69
+ String uniqueWorkName = "bundle_" + id + "_" + version;
71
70
 
72
71
  // Create input data
73
72
  Data inputData = new Data.Builder()
@@ -100,7 +99,7 @@ public class DownloadWorkerManager {
100
99
  .setConstraints(constraints)
101
100
  .setInputData(inputData)
102
101
  .addTag(id)
103
- .addTag(version) // Add version tag for tracking
102
+ .addTag(version)
104
103
  .addTag("capacitor_updater_download");
105
104
 
106
105
  // More aggressive retry policy for emulators
@@ -112,23 +111,29 @@ public class DownloadWorkerManager {
112
111
 
113
112
  OneTimeWorkRequest workRequest = workRequestBuilder.build();
114
113
 
115
- // Enqueue work
116
- WorkManager.getInstance(context).enqueue(workRequest);
114
+ // Use beginUniqueWork to prevent duplicate downloads
115
+ WorkManager.getInstance(context)
116
+ .beginUniqueWork(
117
+ uniqueWorkName,
118
+ ExistingWorkPolicy.KEEP, // Don't start if already running
119
+ workRequest
120
+ )
121
+ .enqueue();
117
122
  }
118
123
 
119
124
  public static void cancelVersionDownload(Context context, String version) {
120
125
  initializeIfNeeded(context.getApplicationContext());
121
126
  WorkManager.getInstance(context).cancelAllWorkByTag(version);
122
- synchronized (activeVersionsLock) {
123
- activeVersions.remove(version);
124
- }
127
+ }
128
+
129
+ public static void cancelBundleDownload(Context context, String id, String version) {
130
+ String uniqueWorkName = "bundle_" + id + "_" + version;
131
+ initializeIfNeeded(context.getApplicationContext());
132
+ WorkManager.getInstance(context).cancelUniqueWork(uniqueWorkName);
125
133
  }
126
134
 
127
135
  public static void cancelAllDownloads(Context context) {
128
136
  initializeIfNeeded(context.getApplicationContext());
129
137
  WorkManager.getInstance(context).cancelAllWorkByTag("capacitor_updater_download");
130
- synchronized (activeVersionsLock) {
131
- activeVersions.clear();
132
- }
133
138
  }
134
139
  }
package/dist/docs.json CHANGED
@@ -2512,7 +2512,7 @@
2512
2512
  "name": "since"
2513
2513
  }
2514
2514
  ],
2515
- "docs": "Set the default channel for the app in the config. Case sensitive.\nThis will setting will override the default channel set in the cloud, but will still respect overrides made in the cloud.",
2515
+ "docs": "Set the default channel for the app in the config. Case sensitive.\nThis will setting will override the default channel set in the cloud, but will still respect overrides made in the cloud.\nThis requires the channel to allow devices to self dissociate/associate in the channel settings. https://capgo.app/docs/public-api/channels/#channel-configuration-options",
2516
2516
  "complexTypes": [],
2517
2517
  "type": "string | undefined"
2518
2518
  },
@@ -209,6 +209,7 @@ declare module '@capacitor/cli' {
209
209
  /**
210
210
  * Set the default channel for the app in the config. Case sensitive.
211
211
  * This will setting will override the default channel set in the cloud, but will still respect overrides made in the cloud.
212
+ * This requires the channel to allow devices to self dissociate/associate in the channel settings. https://capgo.app/docs/public-api/channels/#channel-configuration-options
212
213
  *
213
214
  *
214
215
  * @default undefined