@absolutejs/absolute 0.20.0-beta.86 → 0.20.0-beta.87

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.
@@ -604,7 +604,7 @@ __export(exports_config, {
604
604
  import { readFileSync as readFileSync2 } from "fs";
605
605
  import { resolve as resolve6 } from "path";
606
606
  import { createHash as createHash8, createPublicKey, X509Certificate as X509Certificate2 } from "crypto";
607
- var APP_ID_PATTERN, SCHEME_PATTERN, APPLE_APP_ID_PREFIX_PATTERN, CERTIFICATE_FINGERPRINT_PATTERN, UPDATE_NAME_PATTERN, UPDATE_PUBLIC_KEY_PATTERN, ENVIRONMENT_NAME_PATTERN, DEFAULT_UPDATE_BOOT_TIMEOUT_MS = 20000, DEFAULT_UPDATE_HEALTH_FAILURE_RATE = 0.2, DEFAULT_UPDATE_HEALTH_MINIMUM_REPORTS = 20, MINIMUM_UPDATE_BOOT_TIMEOUT_MS = 5000, MAXIMUM_UPDATE_BOOT_TIMEOUT_MS = 120000, HOSTNAME_PATTERN, EXPO_RESERVED_ROUTE_PREFIXES, resolveProjectPath = (projectRoot, value, field2) => {
607
+ var APP_ID_PATTERN, SCHEME_PATTERN, APPLE_APP_ID_PREFIX_PATTERN, CERTIFICATE_FINGERPRINT_PATTERN, UPDATE_NAME_PATTERN, UPDATE_PUBLIC_KEY_PATTERN, ENVIRONMENT_NAME_PATTERN, DEFAULT_UPDATE_BOOT_TIMEOUT_MS = 20000, DEFAULT_UPDATE_HEALTH_FAILURE_RATE = 0.2, DEFAULT_UPDATE_HEALTH_MINIMUM_REPORTS = 20, DEFAULT_UPDATE_ROLLOUT_FAILURE_RATE = 0.05, DEFAULT_UPDATE_ROLLOUT_OBSERVATION_MINUTES = 60, MINUTE_MS, DEFAULT_UPDATE_ROLLOUT_STAGES, MINIMUM_UPDATE_BOOT_TIMEOUT_MS = 5000, MAXIMUM_UPDATE_BOOT_TIMEOUT_MS = 120000, HOSTNAME_PATTERN, EXPO_RESERVED_ROUTE_PREFIXES, resolveProjectPath = (projectRoot, value, field2) => {
608
608
  const root = resolve6(projectRoot);
609
609
  const path = resolve6(root, value);
610
610
  if (path !== root && !path.startsWith(`${root}/`)) {
@@ -812,6 +812,42 @@ var APP_ID_PATTERN, SCHEME_PATTERN, APPLE_APP_ID_PREFIX_PATTERN, CERTIFICATE_FIN
812
812
  throw new TypeError("mobile.updates.server.health.secretEnv must be a valid environment variable name.");
813
813
  health = { failureRate, minimumReports, secretEnv };
814
814
  }
815
+ const configuredRollout = config.updates.server?.rollout;
816
+ let rollout;
817
+ if (configuredRollout !== undefined && configuredRollout !== false) {
818
+ if (!health)
819
+ throw new TypeError("mobile.updates.server.rollout requires fleet health to be enabled.");
820
+ if (configuredRollout.automatic !== undefined && typeof configuredRollout.automatic !== "boolean")
821
+ throw new TypeError("mobile.updates.server.rollout.automatic must be boolean.");
822
+ const configuredStages = configuredRollout.stages ?? DEFAULT_UPDATE_ROLLOUT_STAGES;
823
+ const stages = configuredStages.map((stage, index) => {
824
+ const previousStage = configuredStages[index - 1];
825
+ const maximumFailureRate = stage.maximumFailureRate ?? DEFAULT_UPDATE_ROLLOUT_FAILURE_RATE;
826
+ const minimumReports = stage.minimumReports ?? DEFAULT_UPDATE_HEALTH_MINIMUM_REPORTS;
827
+ const observationMinutes = stage.observationMinutes ?? DEFAULT_UPDATE_ROLLOUT_OBSERVATION_MINUTES;
828
+ const observationMs = observationMinutes * MINUTE_MS;
829
+ if (!Number.isFinite(stage.rollout) || stage.rollout <= 0 || stage.rollout > 1 || previousStage !== undefined && stage.rollout <= previousStage.rollout)
830
+ throw new TypeError("mobile.updates.server.rollout stages must be strictly increasing fractions greater than 0 and at most 1.");
831
+ if (!Number.isFinite(maximumFailureRate) || maximumFailureRate < 0 || maximumFailureRate >= health.failureRate)
832
+ throw new TypeError("mobile.updates.server.rollout maximumFailureRate must be non-negative and lower than the fleet-health pause rate.");
833
+ if (!Number.isSafeInteger(minimumReports) || minimumReports < health.minimumReports)
834
+ throw new TypeError("mobile.updates.server.rollout minimumReports must be an integer at least as large as the fleet-health minimumReports.");
835
+ if (!Number.isFinite(observationMinutes) || observationMinutes < 0 || !Number.isSafeInteger(observationMs))
836
+ throw new TypeError("mobile.updates.server.rollout observationMinutes must produce a non-negative whole number of milliseconds.");
837
+ return {
838
+ maximumFailureRate,
839
+ minimumReports,
840
+ observationMs,
841
+ rollout: stage.rollout
842
+ };
843
+ });
844
+ if (stages.length === 0 || stages.at(-1)?.rollout !== 1)
845
+ throw new TypeError("mobile.updates.server.rollout stages must end at rollout 1.");
846
+ rollout = {
847
+ automatic: configuredRollout.automatic ?? false,
848
+ stages
849
+ };
850
+ }
815
851
  if (autoMount) {
816
852
  const manifest = new URL(updates.manifestUrl);
817
853
  if (manifest.origin !== productionOrigin)
@@ -848,6 +884,7 @@ var APP_ID_PATTERN, SCHEME_PATTERN, APPLE_APP_ID_PREFIX_PATTERN, CERTIFICATE_FIN
848
884
  autoMount,
849
885
  expoCodeSigningKeys,
850
886
  ...health ? { health } : {},
887
+ ...rollout ? { rollout } : {},
851
888
  registryModule
852
889
  };
853
890
  }, validateExpoNativeRouteSegment = (path, segment, index, count, parameters) => {
@@ -941,6 +978,12 @@ var init_config = __esm(() => {
941
978
  UPDATE_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u;
942
979
  UPDATE_PUBLIC_KEY_PATTERN = /^[A-Za-z0-9+/]+={0,2}$/u;
943
980
  ENVIRONMENT_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/u;
981
+ MINUTE_MS = 60 * 1000;
982
+ DEFAULT_UPDATE_ROLLOUT_STAGES = [
983
+ { minimumReports: 20, observationMinutes: 60, rollout: 0.05 },
984
+ { minimumReports: 100, observationMinutes: 360, rollout: 0.25 },
985
+ { minimumReports: 100, observationMinutes: 0, rollout: 1 }
986
+ ];
944
987
  HOSTNAME_PATTERN = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(?:\.(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?))*$/;
945
988
  EXPO_RESERVED_ROUTE_PREFIXES = new Set([
946
989
  "_expo",
@@ -19984,6 +20027,7 @@ import {
19984
20027
  createHmac,
19985
20028
  createPrivateKey,
19986
20029
  createPublicKey as createPublicKey2,
20030
+ randomUUID as randomUUID5,
19987
20031
  sign as sign2,
19988
20032
  timingSafeEqual,
19989
20033
  verify as verify2,
@@ -20120,7 +20164,7 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
20120
20164
  throw new MobileUpdateRegistryError("Mobile update channel is invalid");
20121
20165
  const appId = text2(value.appId, "channel appId");
20122
20166
  const channel = text2(value.channel, "channel");
20123
- if (!APP_ID.test(appId) || !NAME.test(channel) || !iso(value.promotedAt) || typeof value.rollout !== "number" || value.rollout < 0 || value.rollout > 1 || value.releaseId !== undefined && (typeof value.releaseId !== "string" || !RELEASE.test(value.releaseId)) || value.fallbackReleaseId !== undefined && (typeof value.fallbackReleaseId !== "string" || !RELEASE.test(value.fallbackReleaseId)) || value.activationId !== undefined && (typeof value.activationId !== "string" || !HASH.test(value.activationId)) || value.activatedAt !== undefined && !iso(value.activatedAt) || value.activationId === undefined !== (value.activatedAt === undefined))
20167
+ if (!APP_ID.test(appId) || !NAME.test(channel) || !iso(value.promotedAt) || typeof value.rollout !== "number" || value.rollout < 0 || value.rollout > 1 || value.releaseId !== undefined && (typeof value.releaseId !== "string" || !RELEASE.test(value.releaseId)) || value.fallbackReleaseId !== undefined && (typeof value.fallbackReleaseId !== "string" || !RELEASE.test(value.fallbackReleaseId)) || value.promotionId !== undefined && (typeof value.promotionId !== "string" || !HASH.test(value.promotionId)) || value.activationId !== undefined && (typeof value.activationId !== "string" || !HASH.test(value.activationId)) || value.activatedAt !== undefined && !iso(value.activatedAt) || value.activationId === undefined !== (value.activatedAt === undefined))
20124
20168
  throw new MobileUpdateRegistryError("Mobile update channel is invalid");
20125
20169
  return {
20126
20170
  ...value.activationId ? { activationId: value.activationId } : {},
@@ -20129,6 +20173,7 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
20129
20173
  channel,
20130
20174
  ...value.fallbackReleaseId ? { fallbackReleaseId: value.fallbackReleaseId } : {},
20131
20175
  format: MOBILE_UPDATE_REGISTRY_FORMAT,
20176
+ ...value.promotionId ? { promotionId: value.promotionId } : {},
20132
20177
  promotedAt: value.promotedAt,
20133
20178
  ...value.releaseId ? { releaseId: value.releaseId } : {},
20134
20179
  rollout: value.rollout
@@ -20151,7 +20196,7 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
20151
20196
  return true;
20152
20197
  const value = createHash15("sha256").update(`${input.appId}\x00${input.channel}\x00${input.releaseId}\x00${input.installationId}`).digest().readUInt32BE(0);
20153
20198
  return value / 4294967296 < input.rollout;
20154
- }, base64Url = (value) => Buffer.from(value).toString("base64url"), healthPromotionId = (channel) => digest(new TextEncoder().encode(`${channel.appId}\x00${channel.channel}\x00${channel.releaseId ?? "embedded"}\x00${channel.promotedAt}`)), finiteMetric = (value, field2) => {
20199
+ }, base64Url = (value) => Buffer.from(value).toString("base64url"), healthPromotionId = (channel) => channel.promotionId ?? digest(new TextEncoder().encode(`${channel.appId}\x00${channel.channel}\x00${channel.releaseId ?? "embedded"}\x00${channel.promotedAt}`)), finiteMetric = (value, field2) => {
20155
20200
  if (typeof value !== "number" || !Number.isFinite(value) || value < 0)
20156
20201
  throw new MobileUpdateRegistryError(`Mobile update health ${field2} is invalid`);
20157
20202
  return value;
@@ -20172,10 +20217,15 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
20172
20217
  const prefix = normalizedPrefix(options.prefix ?? DEFAULT_PREFIX);
20173
20218
  const clock = options.clock ?? (() => new Date);
20174
20219
  const health = options.health;
20220
+ const rollout = options.rollout;
20175
20221
  if (health && health.secret.length < 32)
20176
20222
  throw new MobileUpdateRegistryError("Mobile update health secret must contain at least 32 characters");
20177
20223
  if (health && !options.store.list)
20178
20224
  throw new MobileUpdateRegistryError("Mobile update health requires storage lifecycle listing");
20225
+ if (rollout && (!health || !options.store.list))
20226
+ throw new MobileUpdateRegistryError("Mobile update rollout orchestration requires fleet health and storage lifecycle listing");
20227
+ if (rollout && (rollout.stages.length === 0 || rollout.stages.some((stage, index) => stage.rollout <= 0 || stage.rollout > 1 || !Number.isSafeInteger(stage.minimumReports) || stage.minimumReports < 1 || !Number.isSafeInteger(stage.observationMs) || stage.observationMs < 0 || stage.maximumFailureRate < 0 || stage.maximumFailureRate >= 1 || index > 0 && stage.rollout <= rollout.stages[index - 1].rollout)))
20228
+ throw new MobileUpdateRegistryError("Mobile update rollout stages are invalid");
20179
20229
  const minimumReports = health?.autoPause?.minimumReports ?? 20;
20180
20230
  const failureThreshold = health?.autoPause?.failureRate ?? 0.2;
20181
20231
  if (health && (!Number.isSafeInteger(minimumReports) || minimumReports < 1 || failureThreshold <= 0 || failureThreshold > 1))
@@ -20188,6 +20238,8 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
20188
20238
  const tombstoneKey = (appId, releaseId) => `${root(appId)}/gc/${releaseId}.json`;
20189
20239
  const healthRoot = (appId, promotionId, releaseId) => `${root(appId)}/health/${promotionId}/${releaseId}`;
20190
20240
  const pauseKey = (appId, promotionId, releaseId) => `${healthRoot(appId, promotionId, releaseId)}/paused.json`;
20241
+ const rolloutRoot = (appId, promotionId, releaseId) => `${healthRoot(appId, promotionId, releaseId)}/rollout`;
20242
+ const rolloutPlanKey = (appId, promotionId, releaseId) => `${rolloutRoot(appId, promotionId, releaseId)}/plan.json`;
20191
20243
  const channelKey = (appId, channel) => {
20192
20244
  if (!APP_ID.test(appId) || !NAME.test(channel))
20193
20245
  throw new MobileUpdateRegistryError("Mobile update channel identity is invalid");
@@ -20216,6 +20268,81 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
20216
20268
  throw new MobileUpdateRegistryError("Stored mobile update channel identity changed");
20217
20269
  return value;
20218
20270
  };
20271
+ const listPrefix = async (value) => {
20272
+ const objects = [];
20273
+ const cursors = new Set;
20274
+ let cursor;
20275
+ do {
20276
+ const page = await options.store.list({
20277
+ ...cursor ? { cursor } : {},
20278
+ prefix: value
20279
+ });
20280
+ objects.push(...page.objects);
20281
+ if (!page.truncated)
20282
+ break;
20283
+ if (!page.cursor || cursors.has(page.cursor))
20284
+ throw new MobileUpdateRegistryError("Mobile update storage returned an invalid cursor");
20285
+ cursors.add(page.cursor);
20286
+ cursor = page.cursor;
20287
+ } while (true);
20288
+ return objects;
20289
+ };
20290
+ const readVerifiedObject = async (key, label) => {
20291
+ const bytes = await options.store.get(key);
20292
+ if (!bytes)
20293
+ return null;
20294
+ const head = await options.store.head(key);
20295
+ if (!head || head.size !== bytes.byteLength || head.metadata?.sha256 !== digest(bytes))
20296
+ throw new MobileUpdateRegistryError(`Stored mobile update ${label} integrity failed`);
20297
+ return decode2(bytes);
20298
+ };
20299
+ const parseRolloutPlan = (value, promotionId, releaseId) => {
20300
+ if (!object6(value) || value.format !== 1 || value.promotionId !== promotionId || value.releaseId !== releaseId || typeof value.automatic !== "boolean" || !iso(value.createdAt) || !Array.isArray(value.stages))
20301
+ throw new MobileUpdateRegistryError("Stored mobile update rollout plan is invalid");
20302
+ const stages = value.stages.map((stage) => {
20303
+ if (!object6(stage) || typeof stage.rollout !== "number" || typeof stage.maximumFailureRate !== "number" || !Number.isSafeInteger(stage.minimumReports) || !Number.isSafeInteger(stage.observationMs))
20304
+ throw new MobileUpdateRegistryError("Stored mobile update rollout plan is invalid");
20305
+ return {
20306
+ maximumFailureRate: stage.maximumFailureRate,
20307
+ minimumReports: stage.minimumReports,
20308
+ observationMs: stage.observationMs,
20309
+ rollout: stage.rollout
20310
+ };
20311
+ });
20312
+ if (stages.length === 0 || stages.some((stage, index) => stage.rollout <= 0 || stage.rollout > 1 || stage.minimumReports < 1 || stage.observationMs < 0 || stage.maximumFailureRate < 0 || stage.maximumFailureRate >= 1 || index > 0 && stage.rollout <= stages[index - 1].rollout))
20313
+ throw new MobileUpdateRegistryError("Stored mobile update rollout plan is invalid");
20314
+ return {
20315
+ automatic: value.automatic,
20316
+ createdAt: value.createdAt,
20317
+ format: 1,
20318
+ promotionId,
20319
+ releaseId,
20320
+ stages
20321
+ };
20322
+ };
20323
+ const initializeRollout = async (channel, signal) => {
20324
+ if (!rollout || !channel.releaseId)
20325
+ return;
20326
+ if (!rollout.stages.some((stage) => stage.rollout === channel.rollout))
20327
+ throw new MobileUpdateRegistryError("Mobile update promotion rollout must match a configured rollout stage");
20328
+ const promotionId = healthPromotionId(channel);
20329
+ const plan = {
20330
+ automatic: rollout.automatic ?? false,
20331
+ createdAt: channel.promotedAt,
20332
+ format: 1,
20333
+ promotionId,
20334
+ releaseId: channel.releaseId,
20335
+ stages: rollout.stages.map((stage) => ({ ...stage }))
20336
+ };
20337
+ const bytes = json(plan);
20338
+ await options.store.put(rolloutPlanKey(channel.appId, promotionId, channel.releaseId), bytes, {
20339
+ cacheControl: "no-store",
20340
+ contentType: "application/json",
20341
+ maxBytes: bytes.byteLength,
20342
+ metadata: { releaseid: channel.releaseId, sha256: digest(bytes) },
20343
+ signal
20344
+ });
20345
+ };
20219
20346
  const signHealthToken = (payload) => {
20220
20347
  if (!health)
20221
20348
  return null;
@@ -20254,12 +20381,15 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
20254
20381
  if (await options.store.head(tombstoneKey(appId, releaseId)))
20255
20382
  throw new MobileUpdateRegistryError("Mobile update release is marked for collection. Increase retention and apply garbage collection to restore it before promotion");
20256
20383
  };
20257
- const writeChannel = async (input, signal) => {
20384
+ const writeChannel = async (input, signal, beforeWrite) => {
20385
+ const promotedAt = clock().toISOString();
20258
20386
  const value = {
20259
20387
  ...input,
20260
20388
  format: MOBILE_UPDATE_REGISTRY_FORMAT,
20261
- promotedAt: clock().toISOString()
20389
+ promotedAt,
20390
+ promotionId: digest(new TextEncoder().encode(`${input.appId}\x00${input.channel}\x00${input.releaseId ?? "embedded"}\x00${promotedAt}\x00${randomUUID5()}`))
20262
20391
  };
20392
+ await beforeWrite?.(value);
20263
20393
  const bytes = json(value);
20264
20394
  await options.store.put(channelKey(value.appId, value.channel), bytes, {
20265
20395
  cacheControl: "no-cache",
@@ -20274,10 +20404,92 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
20274
20404
  });
20275
20405
  return value;
20276
20406
  };
20407
+ const rolloutContext = async (channel) => {
20408
+ if (!options.store.list || !channel.releaseId)
20409
+ return null;
20410
+ const promotionId = healthPromotionId(channel);
20411
+ const storedPlan = await readVerifiedObject(rolloutPlanKey(channel.appId, promotionId, channel.releaseId), "rollout plan");
20412
+ if (storedPlan === null)
20413
+ return null;
20414
+ const plan = parseRolloutPlan(storedPlan, promotionId, channel.releaseId);
20415
+ const initialStage = plan.stages.findIndex((stage) => stage.rollout === channel.rollout);
20416
+ if (initialStage < 0)
20417
+ throw new MobileUpdateRegistryError("Stored mobile update rollout does not match its plan");
20418
+ let currentStage = initialStage;
20419
+ let enteredAt = plan.createdAt;
20420
+ const advances = await listPrefix(`${rolloutRoot(channel.appId, promotionId, channel.releaseId)}/advances/`);
20421
+ for (const item of advances) {
20422
+ const value = await readVerifiedObject(item.key, "rollout advancement");
20423
+ if (!object6(value) || value.format !== 1 || value.promotionId !== promotionId || value.releaseId !== channel.releaseId || !Number.isSafeInteger(value.stage) || Number(value.stage) < initialStage || Number(value.stage) >= plan.stages.length || value.rollout !== plan.stages[Number(value.stage)].rollout || !iso(value.createdAt) || typeof value.failureRate !== "number" || !Number.isSafeInteger(value.terminalReports))
20424
+ throw new MobileUpdateRegistryError("Stored mobile update rollout advancement is invalid");
20425
+ if (Number(value.stage) >= currentStage) {
20426
+ currentStage = Number(value.stage);
20427
+ enteredAt = value.createdAt;
20428
+ }
20429
+ }
20430
+ const controls = await listPrefix(`${rolloutRoot(channel.appId, promotionId, channel.releaseId)}/controls/`);
20431
+ const parsedControls = [];
20432
+ for (const item of controls) {
20433
+ const value = await readVerifiedObject(item.key, "rollout control");
20434
+ if (!object6(value) || value.format !== 1 || value.promotionId !== promotionId || value.releaseId !== channel.releaseId || typeof value.id !== "string" || value.action !== "pause" && value.action !== "resume" && value.action !== "cancel" || !iso(value.createdAt) || value.resumedPauseIds !== undefined && (!Array.isArray(value.resumedPauseIds) || value.resumedPauseIds.some((id) => typeof id !== "string")))
20435
+ throw new MobileUpdateRegistryError("Stored mobile update rollout control is invalid");
20436
+ parsedControls.push(value);
20437
+ }
20438
+ const cancelled = parsedControls.some(({ action }) => action === "cancel");
20439
+ const resumedPauseIds = new Set(parsedControls.flatMap((control) => control.resumedPauseIds ?? []));
20440
+ const activePauseIds = parsedControls.filter(({ action, id }) => action === "pause" && !resumedPauseIds.has(id)).map(({ id }) => id);
20441
+ const operatorPaused = activePauseIds.length > 0;
20442
+ const fleetPaused = Boolean(await options.store.head(pauseKey(channel.appId, promotionId, channel.releaseId)));
20443
+ return {
20444
+ cancelled,
20445
+ activePauseIds,
20446
+ channel,
20447
+ currentStage,
20448
+ enteredAt,
20449
+ fleetPaused,
20450
+ operatorPaused,
20451
+ plan,
20452
+ promotionId,
20453
+ rollout: plan.stages[currentStage].rollout
20454
+ };
20455
+ };
20456
+ const writeRolloutControl = async (channel, action, signal) => {
20457
+ const context = await rolloutContext(channel);
20458
+ if (!context)
20459
+ throw new MobileUpdateRegistryError("Mobile update rollout orchestration is not configured");
20460
+ if (context.cancelled)
20461
+ throw new MobileUpdateRegistryError("Mobile update rollout was already cancelled");
20462
+ if (action === "resume" && context.fleetPaused)
20463
+ throw new MobileUpdateRegistryError("A fleet-health pause requires an explicit re-promotion");
20464
+ const releaseId = channel.releaseId;
20465
+ if (!releaseId)
20466
+ throw new MobileUpdateRegistryError("Mobile update channel does not have an active release");
20467
+ const createdAt = clock().toISOString();
20468
+ const id = randomUUID5();
20469
+ const event = {
20470
+ action,
20471
+ createdAt,
20472
+ format: 1,
20473
+ id,
20474
+ promotionId: context.promotionId,
20475
+ releaseId,
20476
+ ...action === "resume" ? { resumedPauseIds: context.activePauseIds } : {}
20477
+ };
20478
+ const bytes = json(event);
20479
+ await options.store.put(`${rolloutRoot(channel.appId, context.promotionId, releaseId)}/controls/${createdAt}-${id}-${action}.json`, bytes, {
20480
+ cacheControl: "no-store",
20481
+ contentType: "application/json",
20482
+ maxBytes: bytes.byteLength,
20483
+ metadata: { action, sha256: digest(bytes) },
20484
+ signal
20485
+ });
20486
+ };
20277
20487
  const promoteUpdate = async (input) => {
20278
20488
  input.signal?.throwIfAborted();
20279
20489
  if (input.rollout <= 0 || input.rollout > 1)
20280
20490
  throw new MobileUpdateRegistryError("Mobile update rollout is invalid");
20491
+ if (rollout && !rollout.stages.some((stage) => stage.rollout === input.rollout))
20492
+ throw new MobileUpdateRegistryError("Mobile update promotion rollout must match a configured rollout stage");
20281
20493
  await assertNotMarked(input.appId, input.releaseId);
20282
20494
  const release = await readManifest(input.appId, input.releaseId);
20283
20495
  if (!release || release.manifest.channel !== input.channel)
@@ -20289,7 +20501,7 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
20289
20501
  ...existing?.releaseId && existing.releaseId !== input.releaseId ? { fallbackReleaseId: existing.releaseId } : existing?.fallbackReleaseId ? { fallbackReleaseId: existing.fallbackReleaseId } : {},
20290
20502
  releaseId: input.releaseId,
20291
20503
  rollout: input.rollout
20292
- }, input.signal);
20504
+ }, input.signal, (channel) => initializeRollout(channel, input.signal));
20293
20505
  return {
20294
20506
  appId: input.appId,
20295
20507
  channel: input.channel,
@@ -20302,14 +20514,15 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
20302
20514
  const channel = await readChannel(input.appId, input.channel);
20303
20515
  if (!channel?.releaseId)
20304
20516
  return { status: "empty" };
20517
+ const rolloutState = await rolloutContext(channel);
20305
20518
  let selected = rolloutMember({
20306
20519
  appId: input.appId,
20307
20520
  channel: input.channel,
20308
20521
  installationId: input.installationId,
20309
20522
  releaseId: channel.releaseId,
20310
- rollout: channel.rollout
20523
+ rollout: rolloutState?.rollout ?? channel.rollout
20311
20524
  }) ? channel.releaseId : channel.fallbackReleaseId;
20312
- if (health && selected === channel.releaseId && await options.store.head(pauseKey(input.appId, healthPromotionId(channel), channel.releaseId)))
20525
+ if (selected === channel.releaseId && (rolloutState?.cancelled || rolloutState?.fleetPaused || rolloutState?.operatorPaused || health && await options.store.head(pauseKey(input.appId, healthPromotionId(channel), channel.releaseId))))
20313
20526
  selected = channel.fallbackReleaseId;
20314
20527
  if (!selected)
20315
20528
  return { status: "empty" };
@@ -20353,23 +20566,7 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
20353
20566
  if (!channel || !releaseId || channel.releaseId !== releaseId)
20354
20567
  return null;
20355
20568
  const promotionId = healthPromotionId(channel);
20356
- const prefix2 = `${healthRoot(input.appId, promotionId, releaseId)}/events/`;
20357
- const objects = [];
20358
- const cursors = new Set;
20359
- let cursor;
20360
- do {
20361
- const page = await options.store.list({
20362
- ...cursor ? { cursor } : {},
20363
- prefix: prefix2
20364
- });
20365
- objects.push(...page.objects);
20366
- if (!page.truncated)
20367
- break;
20368
- if (!page.cursor || cursors.has(page.cursor))
20369
- throw new MobileUpdateRegistryError("Mobile update health storage returned an invalid cursor");
20370
- cursors.add(page.cursor);
20371
- cursor = page.cursor;
20372
- } while (true);
20569
+ const objects = await listPrefix(`${healthRoot(input.appId, promotionId, releaseId)}/events/`);
20373
20570
  const installations = new Set;
20374
20571
  const byKind = new Map([...HEALTH_KINDS].map((kind) => [
20375
20572
  kind,
@@ -20408,6 +20605,7 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
20408
20605
  ]);
20409
20606
  const terminals = new Set([...byKind.get("activated"), ...failures]);
20410
20607
  const failureRate = terminals.size === 0 ? 0 : failures.size / terminals.size;
20608
+ const rolloutState = await rolloutContext(channel);
20411
20609
  return {
20412
20610
  activated: byKind.get("activated").size,
20413
20611
  appId: input.appId,
@@ -20416,17 +20614,107 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
20416
20614
  downloadFailed: byKind.get("download-failed").size,
20417
20615
  failureRate,
20418
20616
  failures: failures.size,
20419
- paused: Boolean(await options.store.head(pauseKey(input.appId, promotionId, releaseId))),
20617
+ paused: Boolean(rolloutState?.cancelled || rolloutState?.fleetPaused || rolloutState?.operatorPaused || await options.store.head(pauseKey(input.appId, promotionId, releaseId))),
20420
20618
  promotionId,
20421
20619
  quarantined: byKind.get("quarantined").size,
20422
20620
  releaseId,
20423
20621
  reportedInstallations: installations.size,
20424
20622
  rolledBack: byKind.get("rolled-back").size,
20425
- rollout: channel.rollout,
20623
+ rollout: rolloutState?.rollout ?? channel.rollout,
20426
20624
  terminalReports: terminals.size,
20427
20625
  transfer
20428
20626
  };
20429
20627
  };
20628
+ const inspectUpdateRollout = async (input) => {
20629
+ const channel = await readChannel(input.appId, input.channel);
20630
+ if (!channel?.releaseId)
20631
+ return null;
20632
+ const context = await rolloutContext(channel);
20633
+ if (!context)
20634
+ throw new MobileUpdateRegistryError("Mobile update rollout orchestration is not configured");
20635
+ const healthReport = await inspectUpdateHealth(input);
20636
+ if (!healthReport)
20637
+ return null;
20638
+ const paused = context.fleetPaused || context.operatorPaused;
20639
+ const complete = context.currentStage === context.plan.stages.length - 1;
20640
+ return {
20641
+ ...healthReport,
20642
+ automatic: context.plan.automatic,
20643
+ currentStage: context.currentStage,
20644
+ enteredAt: context.enteredAt,
20645
+ ...!complete ? { nextStage: context.plan.stages[context.currentStage + 1] } : {},
20646
+ ...context.fleetPaused ? { pausedBy: "fleet-health" } : context.operatorPaused ? { pausedBy: "operator" } : {},
20647
+ status: context.cancelled ? "cancelled" : paused ? "paused" : complete ? "complete" : "active"
20648
+ };
20649
+ };
20650
+ const advanceRollout = async (input, strict) => {
20651
+ input.signal?.throwIfAborted();
20652
+ const channel = await readChannel(input.appId, input.channel);
20653
+ if (!channel?.releaseId)
20654
+ throw new MobileUpdateRegistryError("Mobile update channel does not have an active release");
20655
+ const context = await rolloutContext(channel);
20656
+ if (!context)
20657
+ throw new MobileUpdateRegistryError("Mobile update rollout orchestration is not configured");
20658
+ const report = await inspectUpdateRollout(input);
20659
+ if (!report)
20660
+ throw new MobileUpdateRegistryError("Mobile update rollout report is unavailable");
20661
+ const nextStage = context.plan.stages[context.currentStage + 1];
20662
+ if (!nextStage)
20663
+ return report;
20664
+ if (input.rollout !== undefined && input.rollout !== nextStage.rollout)
20665
+ throw new MobileUpdateRegistryError("Mobile update rollout can advance only to the next configured stage");
20666
+ if (report.status !== "active") {
20667
+ if (strict)
20668
+ throw new MobileUpdateRegistryError(`Mobile update rollout cannot advance while ${report.status}`);
20669
+ return report;
20670
+ }
20671
+ const gate = context.plan.stages[context.currentStage];
20672
+ const observedMs = clock().getTime() - Date.parse(context.enteredAt);
20673
+ const blocked = report.terminalReports < gate.minimumReports || report.failureRate > gate.maximumFailureRate || observedMs < gate.observationMs;
20674
+ if (blocked) {
20675
+ if (strict)
20676
+ throw new MobileUpdateRegistryError(`Mobile update rollout needs ${gate.minimumReports} terminal reports, at most ${(gate.maximumFailureRate * 100).toFixed(1)}% failures, and ${gate.observationMs}ms observation at the current stage`);
20677
+ return report;
20678
+ }
20679
+ const stage = context.currentStage + 1;
20680
+ const event = {
20681
+ createdAt: clock().toISOString(),
20682
+ failureRate: report.failureRate,
20683
+ format: 1,
20684
+ promotionId: context.promotionId,
20685
+ releaseId: channel.releaseId,
20686
+ rollout: nextStage.rollout,
20687
+ stage,
20688
+ terminalReports: report.terminalReports
20689
+ };
20690
+ const bytes = json(event);
20691
+ await options.store.put(`${rolloutRoot(input.appId, context.promotionId, channel.releaseId)}/advances/${String(stage).padStart(4, "0")}.json`, bytes, {
20692
+ cacheControl: "no-store",
20693
+ contentType: "application/json",
20694
+ maxBytes: bytes.byteLength,
20695
+ metadata: { releaseid: channel.releaseId, sha256: digest(bytes) },
20696
+ signal: input.signal
20697
+ });
20698
+ return await inspectUpdateRollout(input);
20699
+ };
20700
+ const advanceUpdateRollout = (input) => advanceRollout(input, true);
20701
+ const reconcileUpdateRollout = async (input) => {
20702
+ const report = await inspectUpdateRollout(input);
20703
+ if (!report || !report.automatic)
20704
+ return report;
20705
+ return advanceRollout(input, false);
20706
+ };
20707
+ const rolloutControl = (action) => async (input) => {
20708
+ input.signal?.throwIfAborted();
20709
+ const channel = await readChannel(input.appId, input.channel);
20710
+ if (!channel?.releaseId)
20711
+ throw new MobileUpdateRegistryError("Mobile update channel does not have an active release");
20712
+ await writeRolloutControl(channel, action, input.signal);
20713
+ return await inspectUpdateRollout(input);
20714
+ };
20715
+ const pauseUpdateRollout = rolloutControl("pause");
20716
+ const resumeUpdateRollout = rolloutControl("resume");
20717
+ const cancelUpdateRollout = rolloutControl("cancel");
20430
20718
  const recordUpdateHealth = async (input) => {
20431
20719
  const payload = verifyHealthToken(input.token);
20432
20720
  if (payload.appId !== input.appId || payload.channel !== input.channel || payload.installationId !== input.installationId || payload.releaseId !== input.releaseId || payload.runtimeFingerprint !== input.runtimeFingerprint || !HEALTH_KINDS.has(input.kind) || input.reason !== undefined && input.reason !== "boot-interrupted" && input.reason !== "boot-timeout")
@@ -20481,6 +20769,11 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
20481
20769
  });
20482
20770
  report = { ...report, paused: true };
20483
20771
  }
20772
+ if (rollout && (input.kind === "activated" || FAILURE_HEALTH_KINDS.has(input.kind)))
20773
+ return await reconcileUpdateRollout({
20774
+ appId: input.appId,
20775
+ channel: input.channel
20776
+ }) ?? report;
20484
20777
  return report;
20485
20778
  };
20486
20779
  const retentionValues = (input) => {
@@ -20726,6 +21019,14 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
20726
21019
  };
20727
21020
  return {
20728
21021
  ...health ? { inspectUpdateHealth, issueUpdateHealthToken, recordUpdateHealth } : {},
21022
+ ...rollout ? {
21023
+ advanceUpdateRollout,
21024
+ cancelUpdateRollout,
21025
+ inspectUpdateRollout,
21026
+ pauseUpdateRollout,
21027
+ reconcileUpdateRollout,
21028
+ resumeUpdateRollout
21029
+ } : {},
20729
21030
  inspectUpdateStorage: async (input) => (await inventory(input)).report,
20730
21031
  pruneUpdates,
20731
21032
  publishUpdate: async (input) => {
@@ -21339,6 +21640,11 @@ var ABSOLUTE_MOBILE_UPDATE_SERVER_FORMAT = 1, DEFAULT_MOBILE_UPDATE_REGISTRY_MOD
21339
21640
  return;
21340
21641
  if (typeof module.registry.inspectUpdateHealth !== "function" || typeof module.registry.issueUpdateHealthToken !== "function" || typeof module.registry.recordUpdateHealth !== "function")
21341
21642
  throw new TypeError("Mobile update fleet health is enabled but the registry is not provisioned for it. Run `absolute mobile update provision --force`.");
21643
+ }, verifyRolloutModule = (config, module) => {
21644
+ if (!config.updateServer?.rollout)
21645
+ return;
21646
+ if (typeof module.registry.advanceUpdateRollout !== "function" || typeof module.registry.cancelUpdateRollout !== "function" || typeof module.registry.inspectUpdateRollout !== "function" || typeof module.registry.pauseUpdateRollout !== "function" || typeof module.registry.reconcileUpdateRollout !== "function" || typeof module.registry.resumeUpdateRollout !== "function")
21647
+ throw new TypeError("Mobile update rollout orchestration is enabled but the registry is not provisioned for it. Run `absolute mobile update provision --force`.");
21342
21648
  }, expoSigningOptions = (config) => {
21343
21649
  if (!config.updates?.expoCodeSigning)
21344
21650
  return;
@@ -21373,6 +21679,7 @@ var ABSOLUTE_MOBILE_UPDATE_SERVER_FORMAT = 1, DEFAULT_MOBILE_UPDATE_REGISTRY_MOD
21373
21679
  if (options.production) {
21374
21680
  await verifyDurableModule(module);
21375
21681
  verifyHealthModule(config, module);
21682
+ verifyRolloutModule(config, module);
21376
21683
  }
21377
21684
  const manifest = new URL(updates.manifestUrl);
21378
21685
  if (!manifest.pathname.endsWith("/update.json"))
@@ -21393,6 +21700,7 @@ var ABSOLUTE_MOBILE_UPDATE_SERVER_FORMAT = 1, DEFAULT_MOBILE_UPDATE_REGISTRY_MOD
21393
21700
  const module = await loadAbsoluteMobileUpdateServerModule(projectRoot, config.updateServer?.registryModule);
21394
21701
  await verifyDurableModule(module);
21395
21702
  verifyHealthModule(config, module);
21703
+ verifyRolloutModule(config, module);
21396
21704
  if (config.engine === "expo")
21397
21705
  expoSigningOptions(config);
21398
21706
  return module.metadata;
@@ -21464,17 +21772,25 @@ export default createMobileUpdateRegistry({
21464
21772
  `;
21465
21773
  }, renderAbsoluteMobileUpdateRegistry = (options) => {
21466
21774
  const source = renderAbsoluteMobileUpdateRegistryBase(options);
21467
- if (!options.health)
21468
- return source;
21469
- const secret = options.storage === "local" ? `process.env.${options.health.secretEnv} ?? 'absolutejs-local-health-secret-not-for-production'` : `required('${options.health.secretEnv}')`;
21470
- const health = ` health: {
21775
+ let generated = "";
21776
+ if (options.health) {
21777
+ const secret = options.storage === "local" ? `process.env.${options.health.secretEnv} ?? 'absolutejs-local-health-secret-not-for-production'` : `required('${options.health.secretEnv}')`;
21778
+ generated += ` health: {
21471
21779
  autoPause: { failureRate: ${options.health.failureRate}, minimumReports: ${options.health.minimumReports} },
21472
21780
  secret: ${secret}
21473
21781
  },
21474
21782
  `;
21783
+ }
21784
+ if (options.rollout)
21785
+ generated += ` rollout: ${JSON.stringify(options.rollout, null, "\t").replaceAll(`
21786
+ `, `
21787
+ `)},
21788
+ `;
21789
+ if (!generated)
21790
+ return source;
21475
21791
  return source.replace(`export default createMobileUpdateRegistry({
21476
21792
  `, `export default createMobileUpdateRegistry({
21477
- ${health}`);
21793
+ ${generated}`);
21478
21794
  }, writeAbsoluteMobileUpdateRegistry = async (options) => {
21479
21795
  const path2 = projectPath3(options.projectRoot, options.modulePath ?? DEFAULT_MOBILE_UPDATE_REGISTRY_MODULE);
21480
21796
  if (!options.force) {
@@ -21487,6 +21803,7 @@ ${health}`);
21487
21803
  await mkdir18(dirname19(path2), { recursive: true });
21488
21804
  await Bun.write(path2, renderAbsoluteMobileUpdateRegistry({
21489
21805
  ...options.health ? { health: options.health } : {},
21806
+ ...options.rollout ? { rollout: options.rollout } : {},
21490
21807
  publicKeys: options.publicKeys,
21491
21808
  storage: options.storage
21492
21809
  }));
@@ -39554,14 +39871,7 @@ var lifecycleMethod = (publisher, name) => {
39554
39871
  throw new TypeError(`Mobile update registry does not support ${name}. Re-run \`absolute mobile update provision --force\` after upgrading @absolutejs/deploy.`);
39555
39872
  return method;
39556
39873
  };
39557
- var inspectAbsoluteMobileUpdateHealth = async (options) => {
39558
- const report = await lifecycleMethod(options.publisher, "inspectUpdateHealth")({
39559
- appId: options.appId,
39560
- channel: options.channel,
39561
- ...options.releaseId ? { releaseId: options.releaseId } : {}
39562
- });
39563
- if (report === null)
39564
- return null;
39874
+ var validateHealthReport = (report, options) => {
39565
39875
  if (!object5(report) || report.appId !== options.appId || report.channel !== options.channel || options.releaseId !== undefined && report.releaseId !== options.releaseId || typeof report.paused !== "boolean" || ![
39566
39876
  report.activated,
39567
39877
  report.downloaded,
@@ -39582,6 +39892,45 @@ var inspectAbsoluteMobileUpdateHealth = async (options) => {
39582
39892
  throw new TypeError("Mobile update registry returned an invalid health report.");
39583
39893
  return report;
39584
39894
  };
39895
+ var inspectAbsoluteMobileUpdateHealth = async (options) => {
39896
+ const report = await lifecycleMethod(options.publisher, "inspectUpdateHealth")({
39897
+ appId: options.appId,
39898
+ channel: options.channel,
39899
+ ...options.releaseId ? { releaseId: options.releaseId } : {}
39900
+ });
39901
+ if (report === null)
39902
+ return null;
39903
+ return validateHealthReport(report, options);
39904
+ };
39905
+ var validateRolloutReport = (report, options) => {
39906
+ validateHealthReport(report, options);
39907
+ const validStage = (stage) => stage === undefined || object5(stage) && Number.isFinite(stage.rollout) && stage.rollout > 0 && stage.rollout <= 1 && Number.isSafeInteger(stage.minimumReports) && stage.minimumReports > 0 && Number.isSafeInteger(stage.observationMs) && stage.observationMs >= 0 && Number.isFinite(stage.maximumFailureRate) && stage.maximumFailureRate >= 0 && stage.maximumFailureRate < 1;
39908
+ if (typeof report.automatic !== "boolean" || !Number.isSafeInteger(report.currentStage) || report.currentStage < 0 || !Number.isFinite(Date.parse(report.enteredAt)) || !["active", "cancelled", "complete", "paused"].includes(report.status) || report.pausedBy !== undefined && report.pausedBy !== "fleet-health" && report.pausedBy !== "operator" || !validStage(report.nextStage) || report.paused !== (report.status === "paused" || report.status === "cancelled"))
39909
+ throw new TypeError("Mobile update registry returned an invalid rollout report.");
39910
+ return report;
39911
+ };
39912
+ var inspectAbsoluteMobileUpdateRollout = async (options) => {
39913
+ const report = await lifecycleMethod(options.publisher, "inspectUpdateRollout")({ appId: options.appId, channel: options.channel });
39914
+ return report === null ? null : validateRolloutReport(report, options);
39915
+ };
39916
+ var mutateAbsoluteMobileUpdateRollout = async (method, options) => validateRolloutReport(await lifecycleMethod(options.publisher, method)({
39917
+ appId: options.appId,
39918
+ channel: options.channel,
39919
+ ...method === "advanceUpdateRollout" && options.rollout !== undefined ? { rollout: options.rollout } : {},
39920
+ ...options.signal ? { signal: options.signal } : {}
39921
+ }), options);
39922
+ var advanceAbsoluteMobileUpdateRollout = (options) => mutateAbsoluteMobileUpdateRollout("advanceUpdateRollout", options);
39923
+ var cancelAbsoluteMobileUpdateRollout = (options) => mutateAbsoluteMobileUpdateRollout("cancelUpdateRollout", options);
39924
+ var pauseAbsoluteMobileUpdateRollout = (options) => mutateAbsoluteMobileUpdateRollout("pauseUpdateRollout", options);
39925
+ var reconcileAbsoluteMobileUpdateRollout = async (options) => {
39926
+ const report = await lifecycleMethod(options.publisher, "reconcileUpdateRollout")({
39927
+ appId: options.appId,
39928
+ channel: options.channel,
39929
+ ...options.signal ? { signal: options.signal } : {}
39930
+ });
39931
+ return report === null ? null : validateRolloutReport(report, options);
39932
+ };
39933
+ var resumeAbsoluteMobileUpdateRollout = (options) => mutateAbsoluteMobileUpdateRollout("resumeUpdateRollout", options);
39585
39934
  var validOptionalCounters = (values) => values.every((value) => value === undefined) || values.every((value) => Number.isSafeInteger(value) && (value ?? -1) >= 0);
39586
39935
  var validateStorageIdentity = (result, appId) => {
39587
39936
  if (!object5(result) || result.appId !== appId || !Array.isArray(result.releases) || ![
@@ -39782,6 +40131,7 @@ export {
39782
40131
  acceptsAbsoluteRouteData,
39783
40132
  acquireAbsoluteRemoteMacReleaseLease,
39784
40133
  activateAbsoluteMobilePage,
40134
+ advanceAbsoluteMobileUpdateRollout,
39785
40135
  applyAbsoluteNativeDeepLinks,
39786
40136
  applyAbsoluteNativeDeviceCapabilities,
39787
40137
  applyAbsoluteNativeObservability,
@@ -39793,6 +40143,7 @@ export {
39793
40143
  buildAbsoluteMobileCompatibilityRelease,
39794
40144
  buildAbsoluteMobileUpdate,
39795
40145
  buildAbsoluteRemoteIosRelease,
40146
+ cancelAbsoluteMobileUpdateRollout,
39796
40147
  canonicalizeAbsoluteMobileUpdate,
39797
40148
  captureAbsoluteMobileRouteGraph,
39798
40149
  captureAbsoluteRemoteMacCommand,
@@ -39844,6 +40195,7 @@ export {
39844
40195
  inspectAbsoluteAndroidInstalledApp,
39845
40196
  inspectAbsoluteMobileRouteMetadata,
39846
40197
  inspectAbsoluteMobileUpdateHealth,
40198
+ inspectAbsoluteMobileUpdateRollout,
39847
40199
  inspectAbsoluteMobileUpdateServer,
39848
40200
  inspectAbsoluteMobileUpdateStorage,
39849
40201
  inspectAbsoluteRemoteMac,
@@ -39895,6 +40247,7 @@ export {
39895
40247
  parseIosDeviceTypes,
39896
40248
  parseIosRuntimes,
39897
40249
  parseIosSimulators,
40250
+ pauseAbsoluteMobileUpdateRollout,
39898
40251
  planAbsoluteExpoDevSession,
39899
40252
  prepareAbsoluteAndroidRelease,
39900
40253
  prepareAbsoluteIosDevProject,
@@ -39910,6 +40263,7 @@ export {
39910
40263
  readAbsoluteMobileLinkIntent,
39911
40264
  readAbsoluteMobileMaterializedReleases,
39912
40265
  readAbsoluteMobileUpdate,
40266
+ reconcileAbsoluteMobileUpdateRollout,
39913
40267
  redactAbsoluteIosLog,
39914
40268
  removeAbsoluteRemoteMacProfile,
39915
40269
  renderAbsoluteMobileUpdateRegistry,
@@ -39925,6 +40279,7 @@ export {
39925
40279
  resolveAbsoluteMobileNavigation,
39926
40280
  resolveAbsoluteMobileRoute,
39927
40281
  resolveAbsoluteMobileUpdateRuntime,
40282
+ resumeAbsoluteMobileUpdateRollout,
39928
40283
  retainAbsoluteMobileCompatibilityArtifacts,
39929
40284
  rollbackAbsoluteMobileUpdate,
39930
40285
  runAbsoluteAndroidUpgradeConformance,
@@ -39953,5 +40308,5 @@ export {
39953
40308
  writeAbsoluteMobileUpdateRegistry
39954
40309
  };
39955
40310
 
39956
- //# debugId=01F0CE93B9ED6BCC64756E2164756E21
40311
+ //# debugId=DB548297ABB3C7C964756E2164756E21
39957
40312
  //# sourceMappingURL=index.js.map