@capgo/capacitor-updater 8.51.24 → 8.52.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.
@@ -15,7 +15,6 @@ import java.io.*;
15
15
  import java.io.FileInputStream;
16
16
  import java.net.HttpURLConnection;
17
17
  import java.net.MalformedURLException;
18
- import java.net.URL;
19
18
  import java.nio.channels.FileChannel;
20
19
  import java.security.MessageDigest;
21
20
  import java.util.ArrayList;
@@ -35,7 +34,9 @@ import java.util.function.BooleanSupplier;
35
34
  import java.util.function.LongConsumer;
36
35
  import okhttp3.Call;
37
36
  import okhttp3.Callback;
37
+ import okhttp3.CookieJar;
38
38
  import okhttp3.Dispatcher;
39
+ import okhttp3.HttpUrl;
39
40
  import okhttp3.Interceptor;
40
41
  import okhttp3.MediaType;
41
42
  import okhttp3.OkHttpClient;
@@ -50,6 +51,14 @@ import org.json.JSONObject;
50
51
 
51
52
  public class DownloadService extends Worker {
52
53
 
54
+ /** A refused HTTPS to HTTP redirect. Permanent for the given URL, so downloads must not retry it. */
55
+ static final class BlockedRedirectException extends IOException {
56
+
57
+ BlockedRedirectException() {
58
+ super("Blocked HTTPS to HTTP redirect; set allowHttpsToHttpRedirect to true to allow it");
59
+ }
60
+ }
61
+
53
62
  static final class DownloadRetryException extends RuntimeException {
54
63
 
55
64
  DownloadRetryException(String message) {
@@ -126,6 +135,8 @@ public class DownloadService extends Worker {
126
135
  // Match CapgoUpdater.timeout / responseTimeout default (20s). OkHttp's 10s
127
136
  // defaults were unused by the plugin config and aborted slow manifest GETs.
128
137
  private static volatile int httpTimeoutMs = 20_000;
138
+ // Off by default: a redirect must never downgrade updater traffic from HTTPS to plain HTTP.
139
+ private static volatile boolean allowHttpsToHttpRedirect = false;
129
140
  private static String currentAppId = "unknown";
130
141
  private static String currentPluginVersion = "unknown";
131
142
  private static String currentVersionOs = "unknown";
@@ -138,6 +149,9 @@ public class DownloadService extends Worker {
138
149
  sharedClient = new OkHttpClient.Builder()
139
150
  .dispatcher(dispatcher)
140
151
  .protocols(Arrays.asList(Protocol.HTTP_2, Protocol.HTTP_1_1))
152
+ // Never share cookies with Capgo endpoints: the app's java.net.CookieHandler default is
153
+ // Capacitor's WebView cookie manager, and it must not see (or be fed by) plugin traffic.
154
+ .cookieJar(CookieJar.NO_COOKIES)
141
155
  .connectTimeout(httpTimeoutMs, TimeUnit.MILLISECONDS)
142
156
  .readTimeout(httpTimeoutMs, TimeUnit.MILLISECONDS)
143
157
  .writeTimeout(httpTimeoutMs, TimeUnit.MILLISECONDS)
@@ -147,9 +161,33 @@ public class DownloadService extends Worker {
147
161
  Request requestWithUserAgent = originalRequest.newBuilder().header("User-Agent", userAgent).build();
148
162
  return chain.proceed(requestWithUserAgent);
149
163
  })
164
+ .addNetworkInterceptor((chain) -> {
165
+ Response response = chain.proceed(chain.request());
166
+ if (!allowHttpsToHttpRedirect && isHttpsToHttpRedirect(response)) {
167
+ response.close();
168
+ throw new BlockedRedirectException();
169
+ }
170
+ return response;
171
+ })
150
172
  .build();
151
173
  }
152
174
 
175
+ static void setAllowHttpsToHttpRedirect(boolean allow) {
176
+ allowHttpsToHttpRedirect = allow;
177
+ }
178
+
179
+ static boolean isHttpsToHttpRedirect(Response response) {
180
+ if (!response.isRedirect() || !response.request().isHttps()) {
181
+ return false;
182
+ }
183
+ String location = response.header("Location");
184
+ if (location == null) {
185
+ return false;
186
+ }
187
+ HttpUrl target = response.request().url().resolve(location);
188
+ return target != null && !target.isHttps();
189
+ }
190
+
153
191
  static int httpTimeoutMs() {
154
192
  return httpTimeoutMs;
155
193
  }
@@ -768,20 +806,16 @@ public class DownloadService extends Worker {
768
806
  throw new RuntimeException("insufficient_disk_space");
769
807
  }
770
808
 
771
- HttpURLConnection httpConn = null;
809
+ Response response = null;
772
810
  InputStream inputStream = null;
773
811
  BufferedReader reader = null;
774
812
  BufferedWriter writer = null;
775
813
 
776
814
  try {
777
- URL u = new URL(url);
778
- httpConn = (HttpURLConnection) u.openConnection();
779
-
780
- // Zip can stall longer than a JSON API call; keep a floor so
781
- // responseTimeout cannot shrink large-bundle downloads.
782
- int zipTimeoutMs = Math.max(httpTimeoutMs, 60_000);
783
- httpConn.setConnectTimeout(zipTimeoutMs);
784
- httpConn.setReadTimeout(zipTimeoutMs);
815
+ HttpUrl zipUrl = url != null ? HttpUrl.parse(url) : null;
816
+ if (zipUrl == null) {
817
+ throw new MalformedURLException("Expected an http or https URL");
818
+ }
785
819
 
786
820
  // Reading progress file (if exist)
787
821
  long downloadedBytes = 0;
@@ -810,15 +844,16 @@ public class DownloadService extends Worker {
810
844
  throw new DownloadRetryException("download_stopped");
811
845
  }
812
846
 
813
- if (downloadedBytes > 0) {
814
- httpConn.setRequestProperty("Range", "bytes=" + downloadedBytes + "-");
815
- }
816
-
817
- int responseCode = httpConn.getResponseCode();
847
+ response = executeZipRequest(zipUrl, downloadedBytes);
848
+ int responseCode = response.code();
818
849
 
819
850
  if (responseCode == HttpURLConnection.HTTP_OK || responseCode == HttpURLConnection.HTTP_PARTIAL) {
820
- ZipWritePlan writePlan = planZipResumeWrite(responseCode, downloadedBytes, httpConn.getHeaderField("Content-Range"));
821
- long responseBodyLength = httpConn.getContentLength();
851
+ ResponseBody responseBody = response.body();
852
+ if (responseBody == null) {
853
+ throw new IOException("Response body is null");
854
+ }
855
+ ZipWritePlan writePlan = planZipResumeWrite(responseCode, downloadedBytes, response.header("Content-Range"));
856
+ long responseBodyLength = responseBody.contentLength();
822
857
  long contentLength = shouldAppendHttpBody(writePlan.statusCode, writePlan.writeOffset)
823
858
  ? (responseBodyLength >= 0 ? responseBodyLength + writePlan.writeOffset : -1)
824
859
  : responseBodyLength;
@@ -829,7 +864,7 @@ public class DownloadService extends Worker {
829
864
  }
830
865
 
831
866
  try {
832
- inputStream = httpConn.getInputStream();
867
+ inputStream = responseBody.byteStream();
833
868
 
834
869
  if (writePlan.writeOffset == 0 && downloadedBytes == 0) {
835
870
  writer = new BufferedWriter(new FileWriter(infoFile));
@@ -840,7 +875,7 @@ public class DownloadService extends Worker {
840
875
 
841
876
  final int[] lastNotifiedPercent = { 0 };
842
877
  final long totalContentLength = contentLength;
843
- String contentRangeHeader = httpConn.getHeaderField("Content-Range");
878
+ String contentRangeHeader = response.header("Content-Range");
844
879
  writeHttpBody(tempFile, inputStream, writePlan.statusCode, writePlan.writeOffset, this::isStopped, (written) -> {
845
880
  int percent = calcTotalPercent(written, totalContentLength);
846
881
  if (percent >= lastNotifiedPercent[0] + 10) {
@@ -908,6 +943,9 @@ public class DownloadService extends Worker {
908
943
  } catch (MalformedURLException e) {
909
944
  logger.error("Invalid download URL: " + e.getMessage());
910
945
  throw new RuntimeException("invalid_url: " + e.getMessage());
946
+ } catch (BlockedRedirectException e) {
947
+ logger.error("Download error: " + e.getMessage());
948
+ throw new RuntimeException("blocked_redirect: " + e.getMessage());
911
949
  } catch (IOException e) {
912
950
  logger.error("Download error: " + e.getMessage());
913
951
  throw new DownloadRetryException(e.getMessage(), e);
@@ -916,14 +954,35 @@ public class DownloadService extends Worker {
916
954
  throw new RuntimeException(e.getMessage());
917
955
  } finally {
918
956
  // Ensure connection is closed
919
- if (httpConn != null) {
957
+ if (response != null) {
920
958
  try {
921
- httpConn.disconnect();
959
+ response.close();
922
960
  } catch (Exception ignored) {}
923
961
  }
924
962
  }
925
963
  }
926
964
 
965
+ /**
966
+ * Zip bundle request on the shared client (no cookies, same User-Agent), resuming from
967
+ * {@code downloadedBytes} with a Range header. The caller closes the response.
968
+ */
969
+ static Response executeZipRequest(HttpUrl url, long downloadedBytes) throws IOException {
970
+ // Zip can stall longer than a JSON API call; keep a floor so
971
+ // responseTimeout cannot shrink large-bundle downloads.
972
+ int zipTimeoutMs = Math.max(httpTimeoutMs, 60_000);
973
+ OkHttpClient zipClient = sharedClient
974
+ .newBuilder()
975
+ .connectTimeout(zipTimeoutMs, TimeUnit.MILLISECONDS)
976
+ .readTimeout(zipTimeoutMs, TimeUnit.MILLISECONDS)
977
+ .writeTimeout(zipTimeoutMs, TimeUnit.MILLISECONDS)
978
+ .build();
979
+ Request.Builder builder = new Request.Builder().url(url);
980
+ if (downloadedBytes > 0) {
981
+ builder.header("Range", "bytes=" + downloadedBytes + "-");
982
+ }
983
+ return zipClient.newCall(builder.build()).execute();
984
+ }
985
+
927
986
  static boolean isRetryableHttpStatus(int responseCode) {
928
987
  return responseCode >= 500 || responseCode == 408 || responseCode == 429;
929
988
  }
@@ -0,0 +1,306 @@
1
+ /*
2
+ * This Source Code Form is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Parsing and comparison logic ported from version-compare 1.5.0
7
+ * (https://github.com/G00fY2/version-compare), Copyright G00fY2,
8
+ * licensed under the Apache License, Version 2.0
9
+ * (https://www.apache.org/licenses/LICENSE-2.0).
10
+ */
11
+
12
+ package ee.forgr.capacitor_updater;
13
+
14
+ import androidx.annotation.NonNull;
15
+ import androidx.annotation.Nullable;
16
+ import java.util.ArrayList;
17
+ import java.util.List;
18
+ import java.util.Locale;
19
+
20
+ /**
21
+ * Lenient version parser and comparator.
22
+ *
23
+ * <p>Behavior-compatible port of {@code io.github.g00fy2:versioncompare:1.5.0} (Apache-2.0), kept in-repo so the
24
+ * plugin does not ship an extra third-party dependency. Parsing never throws for non-null input that starts with a
25
+ * digit; unparsable input (null or not starting with a digit) yields an empty version equal to {@code 0}.
26
+ *
27
+ * <p>Pre-release suffix ordering: {@code snapshot < pre-alpha < alpha < beta < rc < stable}, with an optional number
28
+ * right after the qualifier ({@code 1.0.0-rc2 > 1.0.0-rc1}).
29
+ */
30
+ public class Version implements Comparable<Version> {
31
+
32
+ enum ReleaseType {
33
+ SNAPSHOT,
34
+ PRE_ALPHA,
35
+ ALPHA,
36
+ BETA,
37
+ RC,
38
+ STABLE
39
+ }
40
+
41
+ private static final String SNAPSHOT_STRING = "snapshot";
42
+ private static final String PRE_STRING = "pre";
43
+ private static final String ALPHA_STRING = "alpha";
44
+ private static final String BETA_STRING = "beta";
45
+ private static final String RC_STRING = "rc";
46
+
47
+ @Nullable
48
+ private final String originalString;
49
+
50
+ private final List<Long> subversionNumbers = new ArrayList<>();
51
+ private final List<Long> trimmedSubversionNumbers = new ArrayList<>();
52
+ private final String suffix;
53
+ private final ReleaseType releaseType;
54
+ private final long preReleaseVersion;
55
+
56
+ public Version(@Nullable String versionString) {
57
+ originalString = versionString;
58
+ if (originalString != null && startsNumeric(originalString)) {
59
+ String[] versionTokens = originalString.replaceAll("\\s", "").split("\\.");
60
+ boolean suffixFound = false;
61
+ StringBuilder suffixSb = null;
62
+
63
+ for (String versionToken : versionTokens) {
64
+ if (suffixFound) {
65
+ suffixSb.append(".");
66
+ suffixSb.append(versionToken);
67
+ } else if (isNumeric(versionToken)) {
68
+ subversionNumbers.add(safeParseLong(versionToken));
69
+ } else {
70
+ for (int i = 0; i < versionToken.length(); i++) {
71
+ if (!Character.isDigit(versionToken.charAt(i))) {
72
+ suffixSb = new StringBuilder();
73
+ if (i > 0) {
74
+ subversionNumbers.add(safeParseLong(versionToken.substring(0, i)));
75
+ suffixSb.append(versionToken.substring(i));
76
+ } else {
77
+ suffixSb.append(versionToken);
78
+ }
79
+ suffixFound = true;
80
+ break;
81
+ }
82
+ }
83
+ }
84
+ }
85
+ suffix = (suffixSb != null) ? suffixSb.toString() : "";
86
+ trimmedSubversionNumbers.addAll(subversionNumbers);
87
+ while (!trimmedSubversionNumbers.isEmpty() && trimmedSubversionNumbers.get(trimmedSubversionNumbers.size() - 1) == 0L) {
88
+ trimmedSubversionNumbers.remove(trimmedSubversionNumbers.size() - 1);
89
+ }
90
+ } else {
91
+ suffix = "";
92
+ }
93
+ releaseType = qualifierToReleaseType(suffix);
94
+ preReleaseVersion = preReleaseVersion(suffix, releaseType);
95
+ }
96
+
97
+ public long getMajor() {
98
+ return trimmedSubversionNumbers.size() > 0 ? trimmedSubversionNumbers.get(0) : 0L;
99
+ }
100
+
101
+ public long getMinor() {
102
+ return trimmedSubversionNumbers.size() > 1 ? trimmedSubversionNumbers.get(1) : 0L;
103
+ }
104
+
105
+ public long getPatch() {
106
+ return trimmedSubversionNumbers.size() > 2 ? trimmedSubversionNumbers.get(2) : 0L;
107
+ }
108
+
109
+ @NonNull
110
+ public List<Long> getSubversionNumbers() {
111
+ return subversionNumbers;
112
+ }
113
+
114
+ @NonNull
115
+ public String getSuffix() {
116
+ return suffix;
117
+ }
118
+
119
+ @Nullable
120
+ public String getOriginalString() {
121
+ return originalString;
122
+ }
123
+
124
+ public boolean isHigherThan(String otherVersion) {
125
+ return isHigherThan(new Version(otherVersion));
126
+ }
127
+
128
+ public boolean isHigherThan(Version otherVersion) {
129
+ return compareTo(otherVersion) > 0;
130
+ }
131
+
132
+ public boolean isLowerThan(String otherVersion) {
133
+ return isLowerThan(new Version(otherVersion));
134
+ }
135
+
136
+ public boolean isLowerThan(Version otherVersion) {
137
+ return compareTo(otherVersion) < 0;
138
+ }
139
+
140
+ public boolean isEqual(String otherVersion) {
141
+ return isEqual(new Version(otherVersion));
142
+ }
143
+
144
+ public boolean isEqual(Version otherVersion) {
145
+ return compareTo(otherVersion) == 0;
146
+ }
147
+
148
+ public boolean isAtLeast(String otherVersion) {
149
+ return isAtLeast(new Version(otherVersion));
150
+ }
151
+
152
+ public boolean isAtLeast(Version otherVersion) {
153
+ return compareTo(otherVersion) >= 0;
154
+ }
155
+
156
+ public boolean isAtLeast(String otherVersion, boolean ignoreSuffix) {
157
+ return isAtLeast(new Version(otherVersion), ignoreSuffix);
158
+ }
159
+
160
+ public boolean isAtLeast(Version otherVersion, boolean ignoreSuffix) {
161
+ return compareTo(otherVersion, ignoreSuffix) >= 0;
162
+ }
163
+
164
+ @Override
165
+ public final int compareTo(@NonNull Version version) {
166
+ return compareTo(version, false);
167
+ }
168
+
169
+ private int compareTo(@NonNull Version version, boolean ignoreSuffix) {
170
+ int versionNumberResult = compareSubversionNumbers(trimmedSubversionNumbers, version.trimmedSubversionNumbers);
171
+ if (versionNumberResult != 0 || ignoreSuffix) {
172
+ return versionNumberResult;
173
+ }
174
+ int releaseTypeResult = releaseType.compareTo(version.releaseType);
175
+ if (releaseTypeResult != 0) {
176
+ return releaseTypeResult;
177
+ }
178
+ return Long.compare(preReleaseVersion, version.preReleaseVersion);
179
+ }
180
+
181
+ @Override
182
+ public final boolean equals(Object o) {
183
+ return o instanceof Version && isEqual((Version) o);
184
+ }
185
+
186
+ @Override
187
+ public final int hashCode() {
188
+ int result = trimmedSubversionNumbers.hashCode();
189
+ result = 31 * result + releaseType.hashCode();
190
+ result = 31 * result + (int) (preReleaseVersion ^ (preReleaseVersion >>> 32));
191
+ return result;
192
+ }
193
+
194
+ @NonNull
195
+ @Override
196
+ public String toString() {
197
+ return String.valueOf(originalString);
198
+ }
199
+
200
+ // helpers
201
+
202
+ private static int compareSubversionNumbers(@NonNull final List<Long> versionNumbersA, @NonNull final List<Long> versionNumbersB) {
203
+ final int numbersSizeA = versionNumbersA.size();
204
+ final int numbersSizeB = versionNumbersB.size();
205
+ final int maxSize = Math.max(numbersSizeA, numbersSizeB);
206
+
207
+ for (int i = 0; i < maxSize; i++) {
208
+ final long a = i < numbersSizeA ? versionNumbersA.get(i) : 0L;
209
+ final long b = i < numbersSizeB ? versionNumbersB.get(i) : 0L;
210
+ if (a > b) {
211
+ return 1;
212
+ } else if (a < b) {
213
+ return -1;
214
+ }
215
+ }
216
+ return 0;
217
+ }
218
+
219
+ static ReleaseType qualifierToReleaseType(@NonNull String suffix) {
220
+ if (suffix.length() > 0) {
221
+ suffix = suffix.toLowerCase(Locale.ROOT);
222
+ if (suffix.contains(RC_STRING)) return ReleaseType.RC;
223
+ if (suffix.contains(BETA_STRING)) return ReleaseType.BETA;
224
+ if (suffix.contains(ALPHA_STRING)) {
225
+ if (suffix.substring(0, suffix.indexOf(ALPHA_STRING)).contains(PRE_STRING)) {
226
+ return ReleaseType.PRE_ALPHA;
227
+ }
228
+ return ReleaseType.ALPHA;
229
+ }
230
+ if (suffix.contains(SNAPSHOT_STRING)) return ReleaseType.SNAPSHOT;
231
+ }
232
+ return ReleaseType.STABLE;
233
+ }
234
+
235
+ private static long preReleaseVersion(@NonNull final String suffix, final ReleaseType releaseType) {
236
+ if (releaseType == ReleaseType.STABLE || releaseType == ReleaseType.SNAPSHOT) return 0;
237
+
238
+ final int startIndex = indexOfQualifier(suffix, releaseType);
239
+ if (startIndex < suffix.length()) {
240
+ final int maxStartIndex = Math.min(startIndex + 2, suffix.length());
241
+ if (containsNumeric(suffix.substring(startIndex, maxStartIndex))) {
242
+ final StringBuilder versionNumber = new StringBuilder();
243
+ for (int i = startIndex; i < suffix.length(); i++) {
244
+ final char c = suffix.charAt(i);
245
+ if (Character.isDigit(c)) {
246
+ versionNumber.append(c);
247
+ } else if (i != startIndex) {
248
+ break;
249
+ }
250
+ }
251
+ return safeParseLong(versionNumber.toString());
252
+ }
253
+ }
254
+ return 0;
255
+ }
256
+
257
+ private static int indexOfQualifier(@NonNull String suffix, final ReleaseType releaseType) {
258
+ suffix = suffix.toLowerCase(Locale.ROOT);
259
+ switch (releaseType) {
260
+ case RC:
261
+ return suffix.indexOf(RC_STRING) + RC_STRING.length();
262
+ case BETA:
263
+ return suffix.indexOf(BETA_STRING) + BETA_STRING.length();
264
+ case ALPHA:
265
+ case PRE_ALPHA:
266
+ return suffix.indexOf(ALPHA_STRING) + ALPHA_STRING.length();
267
+ default:
268
+ return 0;
269
+ }
270
+ }
271
+
272
+ private static boolean startsNumeric(@NonNull String str) {
273
+ str = str.trim();
274
+ return str.length() > 0 && Character.isDigit(str.charAt(0));
275
+ }
276
+
277
+ private static long safeParseLong(@NonNull String numbers) {
278
+ if (numbers.length() > 19) {
279
+ numbers = numbers.substring(0, 19);
280
+ }
281
+ return Long.parseLong(numbers);
282
+ }
283
+
284
+ private static boolean isNumeric(@NonNull final CharSequence cs) {
285
+ final int sz = cs.length();
286
+ if (sz > 0) {
287
+ for (int i = 0; i < sz; i++) {
288
+ if (!Character.isDigit(cs.charAt(i))) {
289
+ return false;
290
+ }
291
+ }
292
+ return true;
293
+ }
294
+ return false;
295
+ }
296
+
297
+ private static boolean containsNumeric(@NonNull final CharSequence cs) {
298
+ final int sz = cs.length();
299
+ for (int i = 0; i < sz; i++) {
300
+ if (Character.isDigit(cs.charAt(i))) {
301
+ return true;
302
+ }
303
+ }
304
+ return false;
305
+ }
306
+ }
package/dist/docs.json CHANGED
@@ -4947,6 +4947,22 @@
4947
4947
  "complexTypes": [],
4948
4948
  "type": "boolean | undefined"
4949
4949
  },
4950
+ {
4951
+ "name": "allowHttpsToHttpRedirect",
4952
+ "tags": [
4953
+ {
4954
+ "text": "false",
4955
+ "name": "default"
4956
+ },
4957
+ {
4958
+ "text": "8.52.0",
4959
+ "name": "since"
4960
+ }
4961
+ ],
4962
+ "docs": "Allow the plugin to follow redirects from HTTPS to plain HTTP for its own network requests\n(update checks, stats, channel calls, and bundle/manifest downloads).\n\nBlocked by default so a redirect can never downgrade updater traffic to an unencrypted connection.\nOnly enable this if your self-hosted update server or CDN must redirect to an HTTP URL.\nDirect HTTP URLs (for example `localApi` during development) are not affected.\n\nOnly available for Android and iOS.",
4963
+ "complexTypes": [],
4964
+ "type": "boolean | undefined"
4965
+ },
4950
4966
  {
4951
4967
  "name": "allowModifyAppId",
4952
4968
  "tags": [
@@ -264,6 +264,20 @@ declare module '@capacitor/cli' {
264
264
  * @since 5.4.0
265
265
  */
266
266
  allowModifyUrl?: boolean;
267
+ /**
268
+ * Allow the plugin to follow redirects from HTTPS to plain HTTP for its own network requests
269
+ * (update checks, stats, channel calls, and bundle/manifest downloads).
270
+ *
271
+ * Blocked by default so a redirect can never downgrade updater traffic to an unencrypted connection.
272
+ * Only enable this if your self-hosted update server or CDN must redirect to an HTTP URL.
273
+ * Direct HTTP URLs (for example `localApi` during development) are not affected.
274
+ *
275
+ * Only available for Android and iOS.
276
+ *
277
+ * @default false
278
+ * @since 8.52.0
279
+ */
280
+ allowHttpsToHttpRedirect?: boolean;
267
281
  /**
268
282
  * Allow the plugin to modify the appId dynamically from the JavaScript side.
269
283
  *