react-native-update 10.49.0 → 10.51.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.
Files changed (61) hide show
  1. package/BUNDLEHASH_MIGRATION.md +22 -2
  2. package/NATIVE_CHECKUPDATE_DESIGN.md +251 -21
  3. package/NATIVE_CHECK_FOLLOWUPS.md +365 -0
  4. package/README-CN.md +14 -1
  5. package/README.md +14 -1
  6. package/android/jni/Android.mk +5 -1
  7. package/android/lib/arm64-v8a/librnupdate.so +0 -0
  8. package/android/lib/armeabi-v7a/librnupdate.so +0 -0
  9. package/android/lib/x86/librnupdate.so +0 -0
  10. package/android/lib/x86_64/librnupdate.so +0 -0
  11. package/android/src/main/java/cn/reactnative/modules/update/BundledResourceCopier.java +42 -9
  12. package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +98 -22
  13. package/android/src/main/java/cn/reactnative/modules/update/DownloadTaskParams.java +3 -0
  14. package/android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java +455 -0
  15. package/android/src/main/java/cn/reactnative/modules/update/NativeUpdateFlow.java +23 -0
  16. package/android/src/main/java/cn/reactnative/modules/update/PatchFailedException.java +16 -0
  17. package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +196 -42
  18. package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +56 -3
  19. package/android/src/newarch/cn/reactnative/modules/update/UpdateModule.java +10 -0
  20. package/android/src/oldarch/cn/reactnative/modules/update/UpdateModule.java +10 -0
  21. package/cpp/patch_core/digest.cpp +53 -0
  22. package/cpp/patch_core/digest.h +18 -0
  23. package/cpp/patch_core/patch_core.cpp +17 -1
  24. package/cpp/patch_core/patch_core.h +8 -0
  25. package/cpp/patch_core/tests/patch_core_test.cpp +99 -0
  26. package/cpp/update_flow_core/flow_json.cpp +522 -0
  27. package/cpp/update_flow_core/flow_json.h +103 -0
  28. package/cpp/update_flow_core/tests/flow_vectors.json +1401 -0
  29. package/cpp/update_flow_core/tests/update_flow_core_test.cpp +317 -0
  30. package/cpp/update_flow_core/update_flow_core.cpp +301 -0
  31. package/cpp/update_flow_core/update_flow_core.h +77 -0
  32. package/cpp/update_flow_core/update_flow_jni.cpp +121 -0
  33. package/harmony/pushy/src/main/cpp/CMakeLists.txt +7 -0
  34. package/harmony/pushy/src/main/cpp/pushy.cpp +150 -1
  35. package/harmony/pushy/src/main/ets/DownloadTask.ts +133 -9
  36. package/harmony/pushy/src/main/ets/DownloadTaskParams.ts +3 -0
  37. package/harmony/pushy/src/main/ets/MonotonicClock.ts +13 -0
  38. package/harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts +500 -0
  39. package/harmony/pushy/src/main/ets/NativePatchCore.ts +16 -0
  40. package/harmony/pushy/src/main/ets/PathUtils.ts +21 -0
  41. package/harmony/pushy/src/main/ets/PushyTurboModule.ts +22 -0
  42. package/harmony/pushy/src/main/ets/UpdateContext.ts +152 -52
  43. package/harmony/pushy.har +0 -0
  44. package/ios/RCTPushy/RCTPushy.mm +840 -68
  45. package/ios/RCTPushy/RCTPushyDownloader.h +1 -0
  46. package/ios/RCTPushy/RCTPushyDownloader.mm +4 -3
  47. package/package.json +3 -1
  48. package/react-native-update.podspec +2 -0
  49. package/src/NativePushy.ts +14 -0
  50. package/src/client.ts +439 -122
  51. package/src/endpoint.ts +105 -80
  52. package/src/error.ts +4 -0
  53. package/src/locales/en.ts +4 -0
  54. package/src/locales/zh.ts +4 -0
  55. package/src/provider.tsx +74 -9
  56. package/src/telemetry.ts +8 -0
  57. package/src/type.ts +31 -0
  58. package/src/updateFlowCore.ts +320 -0
  59. package/src/utils.ts +14 -12
  60. package/src/isInRollout.ts +0 -86
  61. package/src/resolveCheckResult.ts +0 -30
package/src/endpoint.ts CHANGED
@@ -1,4 +1,7 @@
1
1
  import { UpdateError } from './error';
2
+ import { dedupeEndpoints, orderEndpointCandidates } from './updateFlowCore';
3
+
4
+ export { dedupeEndpoints };
2
5
 
3
6
  export interface EndpointAttemptSuccess<T> {
4
7
  endpoint: string;
@@ -14,9 +17,10 @@ export interface EndpointAttemptFailure {
14
17
  export interface ExecuteEndpointFallbackOptions<T> {
15
18
  configuredEndpoints: string[];
16
19
  getRemoteEndpoints?: () => Promise<string[]>;
17
- tryEndpoint: (endpoint: string) => Promise<T>;
20
+ tryEndpoint: (endpoint: string, signal?: AbortSignal) => Promise<T>;
18
21
  random?: () => number;
19
22
  now?: () => number;
23
+ hedgeDelayMs?: number;
20
24
  onFirstFailure?: (failure: EndpointAttemptFailure) => void | Promise<void>;
21
25
  }
22
26
 
@@ -27,87 +31,104 @@ const normalizeError = (error: unknown) => {
27
31
  return new Error(String(error));
28
32
  };
29
33
 
30
- export const dedupeEndpoints = (
31
- endpoints: Array<string | null | undefined>
32
- ): string[] => {
33
- const result: string[] = [];
34
- const visited = new Set<string>();
35
-
36
- for (const endpoint of endpoints) {
37
- if (!endpoint || visited.has(endpoint)) {
38
- continue;
39
- }
40
- visited.add(endpoint);
41
- result.push(endpoint);
42
- }
43
-
44
- return result;
45
- };
46
-
47
- export const pickRandomEndpoint = (
34
+ export const DEFAULT_HEDGE_DELAY_MS = 250;
35
+
36
+ /**
37
+ * Hedged race over the candidate endpoints: the first candidate starts
38
+ * immediately, each following one is only started after `hedgeDelayMs` of
39
+ * silence (or immediately when a previous attempt failed). The first
40
+ * successful response wins and every other in-flight request is aborted, so
41
+ * one slow endpoint never gates a fast one, and healthy rounds send a single
42
+ * request instead of blasting every candidate at once.
43
+ */
44
+ export function selectFastestSuccessfulEndpoint<T>(
48
45
  endpoints: string[],
49
- random: () => number = Math.random
50
- ) => {
51
- if (!endpoints.length) {
52
- throw new UpdateError('No endpoints configured', 'NO_ENDPOINTS');
53
- }
54
- return endpoints[Math.floor(random() * endpoints.length)];
55
- };
56
-
57
- export async function selectFastestSuccessfulEndpoint<T>(
58
- endpoints: string[],
59
- tryEndpoint: (endpoint: string) => Promise<T>,
60
- now: () => number = Date.now
46
+ tryEndpoint: (endpoint: string, signal?: AbortSignal) => Promise<T>,
47
+ now: () => number = Date.now,
48
+ hedgeDelayMs: number = DEFAULT_HEDGE_DELAY_MS
61
49
  ): Promise<{
62
- successes: EndpointAttemptSuccess<T>[];
50
+ success?: EndpointAttemptSuccess<T>;
63
51
  failures: EndpointAttemptFailure[];
64
52
  }> {
65
- const attempts = await Promise.all(
66
- endpoints.map(async (endpoint) => {
67
- const start = now();
68
- try {
69
- const value = await tryEndpoint(endpoint);
70
- return {
71
- ok: true as const,
72
- endpoint,
73
- value,
74
- duration: now() - start,
75
- };
76
- } catch (error) {
77
- return {
78
- ok: false as const,
79
- endpoint,
80
- error: normalizeError(error),
81
- };
82
- }
83
- })
84
- );
85
-
86
- const successes: EndpointAttemptSuccess<T>[] = [];
87
- const failures: EndpointAttemptFailure[] = [];
88
-
89
- for (const attempt of attempts) {
90
- if (attempt.ok) {
91
- successes.push({
92
- endpoint: attempt.endpoint,
93
- value: attempt.value,
94
- duration: attempt.duration,
95
- });
96
- continue;
53
+ return new Promise((resolve) => {
54
+ if (!endpoints.length) {
55
+ resolve({ failures: [] });
56
+ return;
97
57
  }
98
58
 
99
- failures.push({
100
- endpoint: attempt.endpoint,
101
- error: attempt.error,
102
- });
103
- }
59
+ const failures: EndpointAttemptFailure[] = [];
60
+ const controllers: AbortController[] = [];
61
+ let nextIndex = 0;
62
+ let pending = 0;
63
+ let settled = false;
64
+ let hedgeTimer: ReturnType<typeof setTimeout> | undefined;
65
+
66
+ const finish = (success?: EndpointAttemptSuccess<T>) => {
67
+ if (settled) {
68
+ return;
69
+ }
70
+ settled = true;
71
+ if (hedgeTimer) {
72
+ clearTimeout(hedgeTimer);
73
+ }
74
+ for (const controller of controllers) {
75
+ controller.abort();
76
+ }
77
+ resolve({ success, failures });
78
+ };
104
79
 
105
- successes.sort((left, right) => left.duration - right.duration);
80
+ const launchNext = () => {
81
+ if (hedgeTimer) {
82
+ clearTimeout(hedgeTimer);
83
+ hedgeTimer = undefined;
84
+ }
85
+ if (settled || nextIndex >= endpoints.length) {
86
+ return;
87
+ }
88
+ const endpoint = endpoints[nextIndex++];
89
+ const controller =
90
+ typeof AbortController === 'undefined'
91
+ ? undefined
92
+ : new AbortController();
93
+ if (controller) {
94
+ controllers.push(controller);
95
+ }
96
+ pending++;
97
+ const start = now();
98
+ tryEndpoint(endpoint, controller?.signal)
99
+ .then((value) => {
100
+ if (controller) {
101
+ // Only the losing requests get aborted, not the winner's own
102
+ // (already settled) one.
103
+ const index = controllers.indexOf(controller);
104
+ if (index >= 0) {
105
+ controllers.splice(index, 1);
106
+ }
107
+ }
108
+ finish({ endpoint, value, duration: now() - start });
109
+ })
110
+ .catch((error) => {
111
+ pending--;
112
+ if (settled) {
113
+ // Losers cancelled after a win must not count as failures.
114
+ return;
115
+ }
116
+ failures.push({ endpoint, error: normalizeError(error) });
117
+ if (nextIndex < endpoints.length) {
118
+ // A failure frees its slot: hedge the next candidate right away
119
+ // instead of waiting out the stagger.
120
+ launchNext();
121
+ } else if (pending === 0) {
122
+ finish(undefined);
123
+ }
124
+ });
125
+ if (!settled && nextIndex < endpoints.length) {
126
+ hedgeTimer = setTimeout(launchNext, hedgeDelayMs);
127
+ }
128
+ };
106
129
 
107
- return {
108
- successes,
109
- failures,
110
- };
130
+ launchNext();
131
+ });
111
132
  }
112
133
 
113
134
  export async function executeEndpointFallback<T>({
@@ -116,16 +137,19 @@ export async function executeEndpointFallback<T>({
116
137
  tryEndpoint,
117
138
  random = Math.random,
118
139
  now = Date.now,
140
+ hedgeDelayMs = DEFAULT_HEDGE_DELAY_MS,
119
141
  onFirstFailure,
120
142
  }: ExecuteEndpointFallbackOptions<T>): Promise<EndpointAttemptSuccess<T>> {
121
143
  const excludedEndpoints = new Set<string>();
122
- let candidates = dedupeEndpoints(configuredEndpoints);
144
+ // The candidate ordering (random first pick, configured order as fallback)
145
+ // is pure policy; this side only executes it.
146
+ let candidates = orderEndpointCandidates(configuredEndpoints, random());
123
147
 
124
148
  if (!candidates.length) {
125
149
  throw new UpdateError('No endpoints configured', 'NO_ENDPOINTS');
126
150
  }
127
151
 
128
- const firstEndpoint = pickRandomEndpoint(candidates, random);
152
+ const firstEndpoint = candidates[0];
129
153
 
130
154
  try {
131
155
  return {
@@ -154,14 +178,15 @@ export async function executeEndpointFallback<T>({
154
178
  throw lastError;
155
179
  }
156
180
 
157
- const { successes, failures } = await selectFastestSuccessfulEndpoint(
181
+ const { success, failures } = await selectFastestSuccessfulEndpoint(
158
182
  candidates,
159
183
  tryEndpoint,
160
- now
184
+ now,
185
+ hedgeDelayMs
161
186
  );
162
187
 
163
- if (successes.length) {
164
- return successes[0];
188
+ if (success) {
189
+ return success;
165
190
  }
166
191
 
167
192
  for (const failure of failures) {
package/src/error.ts CHANGED
@@ -24,6 +24,9 @@ export type UpdateErrorCode =
24
24
  // A throw from a user-provided hook (e.g. beforeReload) — not an update
25
25
  // pipeline failure, and excluded from server-side patch-health telemetry.
26
26
  | 'USER_HOOK_ERROR'
27
+ // A second client/provider in the same process — a hard integration error;
28
+ // the SDK is a process-level singleton.
29
+ | 'SINGLETON_VIOLATION'
27
30
  // Native codes (see cpp/patch_core/error_codes.h)
28
31
  | 'INVALID_OPTIONS'
29
32
  | 'PATCH_FAILED'
@@ -47,6 +50,7 @@ const KNOWN_CODES = new Set<string>([
47
50
  'STORAGE_PERMISSION_ERROR',
48
51
  'APK_DOWNLOAD_FAILED',
49
52
  'USER_HOOK_ERROR',
53
+ 'SINGLETON_VIOLATION',
50
54
  'INVALID_OPTIONS',
51
55
  'PATCH_FAILED',
52
56
  'FILE_OPERATION_FAILED',
package/src/locales/en.ts CHANGED
@@ -43,6 +43,10 @@ export default {
43
43
 
44
44
  // Error messages
45
45
  error_appkey_required: 'appKey is required',
46
+ error_client_singleton:
47
+ 'A second update client was created: the SDK is a process-level singleton — create one client and reuse it everywhere (use client.setOptions to change options at runtime)',
48
+ error_provider_singleton:
49
+ 'A second UpdateProvider was mounted: make sure the app renders exactly one UpdateProvider',
46
50
  error_parse_version_info: 'Failed to parse currentVersionInfo: {{info}}',
47
51
  warn_unknown_bundle:
48
52
  'The bundle embedded in this binary is not registered on the update platform; incremental updates are degraded to full downloads. Upload the package with pushy uploadIpa / uploadApk / uploadApp.',
package/src/locales/zh.ts CHANGED
@@ -41,6 +41,10 @@ export default {
41
41
 
42
42
  // Error messages
43
43
  error_appkey_required: '需要提供 appKey',
44
+ error_client_singleton:
45
+ '检测到重复创建热更新 client:SDK 是进程级单例,请只创建一个 client 并在全局复用(运行期改配置请调用 client.setOptions)',
46
+ error_provider_singleton:
47
+ '检测到同时挂载多个 UpdateProvider:请确保应用中只有一个 UpdateProvider',
44
48
  error_parse_version_info: '解析 currentVersionInfo 失败: {{info}}',
45
49
  warn_unknown_bundle:
46
50
  '当前二进制内嵌 bundle 未在更新平台注册,增量更新已降级为全量。请使用 pushy uploadIpa / uploadApk / uploadApp 上传对应安装包。',
package/src/provider.tsx CHANGED
@@ -18,13 +18,15 @@ import { URL } from 'react-native-url-polyfill';
18
18
  import { type Cresc, type Pushy, sharedState } from './client';
19
19
  import { ProgressContext, UpdateContext } from './context';
20
20
  import {
21
+ cInfo,
21
22
  currentVersion,
22
23
  currentVersionInfo,
23
24
  getCurrentVersionInfo,
24
25
  packageVersion,
26
+ rolledBackVersion,
25
27
  } from './core';
26
- import { resolveCheckResult } from './resolveCheckResult';
27
28
  import type { CheckResult, ProgressData, UpdateTestPayload } from './type';
29
+ import { decideDownload, resolveCheckResult } from './updateFlowCore';
28
30
  import { assertWeb, log, noop } from './utils';
29
31
 
30
32
  export const UpdateProvider = ({
@@ -37,6 +39,26 @@ export const UpdateProvider = ({
37
39
  client = useRef(client).current;
38
40
  const { options } = client;
39
41
 
42
+ // A second concurrently mounted provider is a hard integration error (the
43
+ // client is a process-level singleton); the client throws on the second
44
+ // claim. Also releases the claim on unmount.
45
+ useEffect(() => client.claimProviderMount?.(), [client]);
46
+
47
+ // options is mutated in place by client.setOptions (its identity never
48
+ // changes), so effects keyed on it would never re-run. The client bumps a
49
+ // version on every setOptions; mirroring it into state re-renders this
50
+ // provider and re-runs the effects that list it as a dependency.
51
+ const [optionsVersion, setOptionsVersion] = useState(
52
+ client.optionsVersion ?? 0
53
+ );
54
+ useEffect(
55
+ () =>
56
+ client.onOptionsChange?.(() => {
57
+ setOptionsVersion((version) => version + 1);
58
+ }),
59
+ [client]
60
+ );
61
+
40
62
  const stateListener = useRef<NativeEventSubscription>(undefined);
41
63
  const [updateInfo, setUpdateInfo] = useState<CheckResult>();
42
64
  const updateInfoRef = useRef(updateInfo);
@@ -202,7 +224,41 @@ export const UpdateProvider = ({
202
224
  // known updateInfo instead of overwriting it with an empty object.
203
225
  return;
204
226
  }
205
- const info = resolveCheckResult(rootInfo);
227
+ let info = resolveCheckResult(
228
+ rootInfo,
229
+ {
230
+ packageVersion: client.getEffectivePackageVersion(),
231
+ currentVersion,
232
+ uuid: cInfo.uuid,
233
+ },
234
+ log
235
+ );
236
+ if (
237
+ !info.expired &&
238
+ info.update &&
239
+ (typeof info.hash !== 'string' || info.hash.length === 0)
240
+ ) {
241
+ // A malformed rollout/root entry must not produce an alert whose
242
+ // confirm button can never download anything. Surface it to the
243
+ // developer telemetry/logger and present it to the app as no update.
244
+ client.reportInvalidUpdateOnce('missingHash');
245
+ info = { upToDate: true };
246
+ }
247
+ if (info.update && !info.expired) {
248
+ const decision = decideDownload(
249
+ info,
250
+ { currentVersion, rolledBackVersion },
251
+ __DEV__
252
+ );
253
+ if (decision.action === 'none') {
254
+ if (decision.reason === 'noArtifact') {
255
+ // Invalid server data is worth reporting; local rollout guards are
256
+ // expected no-ops and stay silent.
257
+ client.reportInvalidUpdateOnce('noArtifact', info.hash || '');
258
+ }
259
+ info = { upToDate: true };
260
+ }
261
+ }
206
262
  if (info.update) {
207
263
  info.description = info.description ?? '';
208
264
  }
@@ -279,6 +335,7 @@ export const UpdateProvider = ({
279
335
 
280
336
  const markSuccess = client.markSuccess;
281
337
 
338
+ // biome-ignore lint/correctness/useExhaustiveDependencies: optionsVersion re-runs this when setOptions mutates options in place
282
339
  useEffect(() => {
283
340
  if (!client.assertDebug('checkUpdate()')) {
284
341
  return;
@@ -313,8 +370,11 @@ export const UpdateProvider = ({
313
370
  }
314
371
  stateListener.current?.remove();
315
372
  };
316
- }, [checkUpdate, options, markSuccess, client]);
373
+ // optionsVersion: checkStrategy/autoMarkSuccess are read from the
374
+ // in-place-mutated options object, so option changes must re-run this.
375
+ }, [checkUpdate, options, markSuccess, client, optionsVersion]);
317
376
 
377
+ // biome-ignore lint/correctness/useExhaustiveDependencies: optionsVersion re-runs this when setOptions mutates options in place
318
378
  useEffect(() => {
319
379
  const { dismissErrorAfter } = options;
320
380
  if (
@@ -329,17 +389,22 @@ export const UpdateProvider = ({
329
389
  clearTimeout(dismissErrorTimer);
330
390
  };
331
391
  }
332
- }, [lastError, options, dismissError]);
392
+ // optionsVersion: dismissErrorAfter changes must reschedule a running
393
+ // timer, not only apply to the next error.
394
+ }, [lastError, options, dismissError, optionsVersion]);
333
395
 
334
396
  const parseTestPayload = useCallback(
335
397
  (payload: UpdateTestPayload) => {
336
398
  if (payload?.type?.startsWith('__rnPushy')) {
337
- const logger = options.logger || (() => {});
338
- options.logger = ({ type, data }) => {
339
- logger({ type, data });
340
- Alert.alert(type, JSON.stringify(data));
341
- };
342
399
  if (payload.type === '__rnPushyVersionHash') {
400
+ // Wrap the logger only for the duration of this test check; wrapping
401
+ // on any other __rnPushy* payload would never be undone (the finally
402
+ // below is the only restore point).
403
+ const logger = options.logger || (() => {});
404
+ options.logger = ({ type, data }) => {
405
+ logger({ type, data });
406
+ Alert.alert(type, JSON.stringify(data));
407
+ };
343
408
  const toHash = payload.data;
344
409
  sharedState.toHash = toHash;
345
410
  checkUpdate({ extra: { toHash } })
package/src/telemetry.ts CHANGED
@@ -53,6 +53,14 @@ export const resolveServerEventType = (
53
53
  return 'mark_success';
54
54
  case 'rollback':
55
55
  return 'rollback';
56
+ case 'downloadFallback':
57
+ // An incremental strategy failed but a later one rescued the download.
58
+ // When the incremental patch itself failed to apply (e.g. pdiff
59
+ // copiesCrc mismatch on a rebuilt binary), that is still a patch-health
60
+ // signal even though the end user got the update via full. Plain
61
+ // download errors that full rescued stay local-only — they are network
62
+ // noise, not patch health.
63
+ return code === 'PATCH_FAILED' ? 'patch_fail' : undefined;
56
64
  default:
57
65
  return undefined;
58
66
  }
package/src/type.ts CHANGED
@@ -9,6 +9,14 @@ export interface VersionInfo {
9
9
  rollout: {
10
10
  [packageVersion: string]: number;
11
11
  };
12
+ /**
13
+ * Server-set per-version override: the native cold-start check activates
14
+ * this version for the next launch regardless of the client's
15
+ * updateStrategy (the brick-rescue directive). Native-only — the JS
16
+ * interactive flow ignores it. The device-local rolledBack guard still
17
+ * wins, and first_time crash protection still applies.
18
+ */
19
+ forceBoot?: boolean;
12
20
  [key: string]: any;
13
21
  };
14
22
  pdiff?: string;
@@ -83,6 +91,11 @@ export type EventType =
83
91
  // registered on the update platform (developer-facing only, local logger;
84
92
  // not a server telemetry event).
85
93
  | 'bundleMismatch'
94
+ // An incremental strategy (diff/pdiff) failed but a later strategy saved the
95
+ // download. Carries which strategy failed and why — the signal that watches
96
+ // pdiff copiesCrc verification failures (BUNDLEHASH_MIGRATION §4.2.1) in
97
+ // the field.
98
+ | 'downloadFallback'
86
99
  | 'errorRestart'
87
100
  | 'errorSwitchVersion'
88
101
  | 'downloadingApk'
@@ -164,6 +177,24 @@ export interface ClientOptions {
164
177
  * the version health view in the console. Default: false (enabled).
165
178
  */
166
179
  disableTelemetry?: boolean;
180
+ /**
181
+ * Disable the native cold-start update check: the background check that runs
182
+ * a few seconds after every launch, independent of JS
183
+ * (NATIVE_CHECKUPDATE_DESIGN §10). Default: false (enabled).
184
+ *
185
+ * That check is what rescues a device bricked by a bad update — its JS never
186
+ * runs, so nothing else can pull the fix. Turning it off gives up that
187
+ * recovery path (and the response cache the JS check reuses) in exchange for
188
+ * one fewer background request per cold start; choose it only when the extra
189
+ * request is itself the problem (traffic/battery budgets, privacy manifests,
190
+ * consent-gated networking).
191
+ *
192
+ * Orthogonal to `checkStrategy`, which governs activation authority rather
193
+ * than whether the check runs: with `checkStrategy: null` the native check
194
+ * still downloads but never activates on its own — only the server's
195
+ * per-version forceBoot directive may.
196
+ */
197
+ disableNativeCheck?: boolean;
167
198
  }
168
199
 
169
200
  export interface UpdateTestPayload {