@absolutejs/absolute 0.20.0-beta.85 → 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.
- package/dist/angular/components/core/streamingSlotRegistrar.js +1 -1
- package/dist/angular/components/core/streamingSlotRegistry.js +2 -2
- package/dist/build.js +656 -18
- package/dist/build.js.map +5 -5
- package/dist/cli/{compile-d7g2fvqe.js → compile-5hchbcbd.js} +3 -3
- package/dist/cli/{config-naw5fer4.js → config-vy04mqce.js} +1 -1
- package/dist/cli/{dev-skb8dz33.js → dev-6shm8qsn.js} +2 -2
- package/dist/cli/{expoProject-3mmwtemn.js → expoProject-dac8fqhq.js} +1 -1
- package/dist/cli/{index-q78x1k9q.js → index-39j0dhq7.js} +2 -2
- package/dist/cli/{index-8gksmws3.js → index-jqdt7841.js} +2 -2
- package/dist/cli/{index-czw3jd8f.js → index-rd2hbttb.js} +67 -1
- package/dist/cli/{index-p8ezgd5e.js → index-tame6e2c.js} +65 -3
- package/dist/cli/index.js +5 -5
- package/dist/cli/{mobile-pdck6a35.js → mobile-c94a5y9k.js} +205 -10
- package/dist/cli/{start-jbgfw27x.js → start-7jg0csqy.js} +4 -4
- package/dist/index.js +656 -18
- package/dist/index.js.map +5 -5
- package/dist/mobile/index.js +840 -25
- package/dist/mobile/index.js.map +8 -8
- package/dist/mobile/remoteMacAgentEntry.js +270 -208
- package/dist/mobile/shellUpdate.js +138 -23
- package/dist/src/mobile/config.d.ts +14 -0
- package/dist/src/mobile/updateClient.d.ts +13 -0
- package/dist/src/mobile/updatePublisher.d.ts +78 -1
- package/dist/src/mobile/updateServer.d.ts +28 -0
- package/dist/types/build.d.ts +25 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -10636,7 +10636,7 @@ __export(exports_config, {
|
|
|
10636
10636
|
import { readFileSync as readFileSync11 } from "fs";
|
|
10637
10637
|
import { resolve as resolve12 } from "path";
|
|
10638
10638
|
import { createHash as createHash4, createPublicKey, X509Certificate } from "crypto";
|
|
10639
|
-
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, MINIMUM_UPDATE_BOOT_TIMEOUT_MS = 5000, MAXIMUM_UPDATE_BOOT_TIMEOUT_MS = 120000, HOSTNAME_PATTERN, EXPO_RESERVED_ROUTE_PREFIXES, resolveProjectPath = (projectRoot, value, field) => {
|
|
10639
|
+
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, field) => {
|
|
10640
10640
|
const root = resolve12(projectRoot);
|
|
10641
10641
|
const path = resolve12(root, value);
|
|
10642
10642
|
if (path !== root && !path.startsWith(`${root}/`)) {
|
|
@@ -10830,6 +10830,56 @@ var APP_ID_PATTERN, SCHEME_PATTERN, APPLE_APP_ID_PREFIX_PATTERN, CERTIFICATE_FIN
|
|
|
10830
10830
|
if (!ENVIRONMENT_NAME_PATTERN.test(expoPrivateKeyEnv))
|
|
10831
10831
|
throw new TypeError("mobile.updates.server.expoPrivateKeyEnv must be a valid environment variable name.");
|
|
10832
10832
|
const autoMount = config.updates.server?.autoMount ?? true;
|
|
10833
|
+
const configuredHealth = config.updates.server?.health;
|
|
10834
|
+
let health;
|
|
10835
|
+
if (configuredHealth !== false) {
|
|
10836
|
+
const failureRate = configuredHealth?.failureRate ?? DEFAULT_UPDATE_HEALTH_FAILURE_RATE;
|
|
10837
|
+
const minimumReports = configuredHealth?.minimumReports ?? DEFAULT_UPDATE_HEALTH_MINIMUM_REPORTS;
|
|
10838
|
+
const secretEnv = requireText(configuredHealth?.secretEnv ?? "ABSOLUTE_MOBILE_UPDATE_HEALTH_SECRET", "mobile.updates.server.health.secretEnv");
|
|
10839
|
+
if (!Number.isFinite(failureRate) || failureRate <= 0 || failureRate > 1)
|
|
10840
|
+
throw new TypeError("mobile.updates.server.health.failureRate must be greater than 0 and at most 1.");
|
|
10841
|
+
if (!Number.isSafeInteger(minimumReports) || minimumReports < 1)
|
|
10842
|
+
throw new TypeError("mobile.updates.server.health.minimumReports must be a positive integer.");
|
|
10843
|
+
if (!ENVIRONMENT_NAME_PATTERN.test(secretEnv))
|
|
10844
|
+
throw new TypeError("mobile.updates.server.health.secretEnv must be a valid environment variable name.");
|
|
10845
|
+
health = { failureRate, minimumReports, secretEnv };
|
|
10846
|
+
}
|
|
10847
|
+
const configuredRollout = config.updates.server?.rollout;
|
|
10848
|
+
let rollout;
|
|
10849
|
+
if (configuredRollout !== undefined && configuredRollout !== false) {
|
|
10850
|
+
if (!health)
|
|
10851
|
+
throw new TypeError("mobile.updates.server.rollout requires fleet health to be enabled.");
|
|
10852
|
+
if (configuredRollout.automatic !== undefined && typeof configuredRollout.automatic !== "boolean")
|
|
10853
|
+
throw new TypeError("mobile.updates.server.rollout.automatic must be boolean.");
|
|
10854
|
+
const configuredStages = configuredRollout.stages ?? DEFAULT_UPDATE_ROLLOUT_STAGES;
|
|
10855
|
+
const stages = configuredStages.map((stage, index) => {
|
|
10856
|
+
const previousStage = configuredStages[index - 1];
|
|
10857
|
+
const maximumFailureRate = stage.maximumFailureRate ?? DEFAULT_UPDATE_ROLLOUT_FAILURE_RATE;
|
|
10858
|
+
const minimumReports = stage.minimumReports ?? DEFAULT_UPDATE_HEALTH_MINIMUM_REPORTS;
|
|
10859
|
+
const observationMinutes = stage.observationMinutes ?? DEFAULT_UPDATE_ROLLOUT_OBSERVATION_MINUTES;
|
|
10860
|
+
const observationMs = observationMinutes * MINUTE_MS;
|
|
10861
|
+
if (!Number.isFinite(stage.rollout) || stage.rollout <= 0 || stage.rollout > 1 || previousStage !== undefined && stage.rollout <= previousStage.rollout)
|
|
10862
|
+
throw new TypeError("mobile.updates.server.rollout stages must be strictly increasing fractions greater than 0 and at most 1.");
|
|
10863
|
+
if (!Number.isFinite(maximumFailureRate) || maximumFailureRate < 0 || maximumFailureRate >= health.failureRate)
|
|
10864
|
+
throw new TypeError("mobile.updates.server.rollout maximumFailureRate must be non-negative and lower than the fleet-health pause rate.");
|
|
10865
|
+
if (!Number.isSafeInteger(minimumReports) || minimumReports < health.minimumReports)
|
|
10866
|
+
throw new TypeError("mobile.updates.server.rollout minimumReports must be an integer at least as large as the fleet-health minimumReports.");
|
|
10867
|
+
if (!Number.isFinite(observationMinutes) || observationMinutes < 0 || !Number.isSafeInteger(observationMs))
|
|
10868
|
+
throw new TypeError("mobile.updates.server.rollout observationMinutes must produce a non-negative whole number of milliseconds.");
|
|
10869
|
+
return {
|
|
10870
|
+
maximumFailureRate,
|
|
10871
|
+
minimumReports,
|
|
10872
|
+
observationMs,
|
|
10873
|
+
rollout: stage.rollout
|
|
10874
|
+
};
|
|
10875
|
+
});
|
|
10876
|
+
if (stages.length === 0 || stages.at(-1)?.rollout !== 1)
|
|
10877
|
+
throw new TypeError("mobile.updates.server.rollout stages must end at rollout 1.");
|
|
10878
|
+
rollout = {
|
|
10879
|
+
automatic: configuredRollout.automatic ?? false,
|
|
10880
|
+
stages
|
|
10881
|
+
};
|
|
10882
|
+
}
|
|
10833
10883
|
if (autoMount) {
|
|
10834
10884
|
const manifest = new URL(updates.manifestUrl);
|
|
10835
10885
|
if (manifest.origin !== productionOrigin)
|
|
@@ -10862,7 +10912,13 @@ var APP_ID_PATTERN, SCHEME_PATTERN, APPLE_APP_ID_PREFIX_PATTERN, CERTIFICATE_FIN
|
|
|
10862
10912
|
throw new TypeError(`mobile.updates.server.expoCodeSigningKeys.${keyId} certificate is not currently valid.`);
|
|
10863
10913
|
expoCodeSigningKeys[keyId] = { certificatePem, privateKeyEnv };
|
|
10864
10914
|
}
|
|
10865
|
-
return {
|
|
10915
|
+
return {
|
|
10916
|
+
autoMount,
|
|
10917
|
+
expoCodeSigningKeys,
|
|
10918
|
+
...health ? { health } : {},
|
|
10919
|
+
...rollout ? { rollout } : {},
|
|
10920
|
+
registryModule
|
|
10921
|
+
};
|
|
10866
10922
|
}, validateExpoNativeRouteSegment = (path, segment, index, count, parameters) => {
|
|
10867
10923
|
if (segment === "*" && (index !== count - 1 || count === 1)) {
|
|
10868
10924
|
throw new TypeError(`mobile.routes.native route ${path} must use * once, as the final segment after a static or parameterized prefix.`);
|
|
@@ -10954,6 +11010,12 @@ var init_config = __esm(() => {
|
|
|
10954
11010
|
UPDATE_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u;
|
|
10955
11011
|
UPDATE_PUBLIC_KEY_PATTERN = /^[A-Za-z0-9+/]+={0,2}$/u;
|
|
10956
11012
|
ENVIRONMENT_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/u;
|
|
11013
|
+
MINUTE_MS = 60 * 1000;
|
|
11014
|
+
DEFAULT_UPDATE_ROLLOUT_STAGES = [
|
|
11015
|
+
{ minimumReports: 20, observationMinutes: 60, rollout: 0.05 },
|
|
11016
|
+
{ minimumReports: 100, observationMinutes: 360, rollout: 0.25 },
|
|
11017
|
+
{ minimumReports: 100, observationMinutes: 0, rollout: 1 }
|
|
11018
|
+
];
|
|
10957
11019
|
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])?))*$/;
|
|
10958
11020
|
EXPO_RESERVED_ROUTE_PREFIXES = new Set([
|
|
10959
11021
|
"_expo",
|
|
@@ -12280,15 +12342,18 @@ __export(exports_mobileUpdate, {
|
|
|
12280
12342
|
});
|
|
12281
12343
|
import {
|
|
12282
12344
|
createHash as createHash5,
|
|
12345
|
+
createHmac,
|
|
12283
12346
|
createPrivateKey,
|
|
12284
12347
|
createPublicKey as createPublicKey2,
|
|
12348
|
+
randomUUID,
|
|
12285
12349
|
sign,
|
|
12350
|
+
timingSafeEqual,
|
|
12286
12351
|
verify,
|
|
12287
12352
|
X509Certificate as X509Certificate2
|
|
12288
12353
|
} from "crypto";
|
|
12289
12354
|
import { readFile as readFile4, stat as stat2 } from "fs/promises";
|
|
12290
12355
|
import path from "path";
|
|
12291
|
-
var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updates", MAX_FILE_BYTES, MAX_TOTAL_BYTES, DAY_MS, DEFAULT_MIN_AGE_MS, DEFAULT_GRACE_PERIOD_MS, DEFAULT_RETAIN_RECENT = 5, HASH, RELEASE, APP_ID, NAME, EXPO_DESCRIPTOR = "_absolute/expo-update.json", EXPO_CODE_SIGNING_ALGORITHM = "rsa-v1_5-sha256", MobileUpdateRegistryError, object3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), text2 = (value, field) => {
|
|
12356
|
+
var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updates", MAX_FILE_BYTES, MAX_TOTAL_BYTES, DAY_MS, DEFAULT_MIN_AGE_MS, DEFAULT_GRACE_PERIOD_MS, DEFAULT_RETAIN_RECENT = 5, HASH, RELEASE, APP_ID, NAME, EXPO_DESCRIPTOR = "_absolute/expo-update.json", EXPO_CODE_SIGNING_ALGORITHM = "rsa-v1_5-sha256", HEALTH_TOKEN_VERSION = 1, HEALTH_KINDS, FAILURE_HEALTH_KINDS, MobileUpdateRegistryError, object3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), text2 = (value, field) => {
|
|
12292
12357
|
if (typeof value !== "string" || value.length === 0)
|
|
12293
12358
|
throw new MobileUpdateRegistryError(`Mobile update ${field} is invalid`);
|
|
12294
12359
|
return value;
|
|
@@ -12417,7 +12482,7 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
12417
12482
|
throw new MobileUpdateRegistryError("Mobile update channel is invalid");
|
|
12418
12483
|
const appId = text2(value.appId, "channel appId");
|
|
12419
12484
|
const channel = text2(value.channel, "channel");
|
|
12420
|
-
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))
|
|
12485
|
+
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))
|
|
12421
12486
|
throw new MobileUpdateRegistryError("Mobile update channel is invalid");
|
|
12422
12487
|
return {
|
|
12423
12488
|
...value.activationId ? { activationId: value.activationId } : {},
|
|
@@ -12426,6 +12491,7 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
12426
12491
|
channel,
|
|
12427
12492
|
...value.fallbackReleaseId ? { fallbackReleaseId: value.fallbackReleaseId } : {},
|
|
12428
12493
|
format: MOBILE_UPDATE_REGISTRY_FORMAT,
|
|
12494
|
+
...value.promotionId ? { promotionId: value.promotionId } : {},
|
|
12429
12495
|
promotedAt: value.promotedAt,
|
|
12430
12496
|
...value.releaseId ? { releaseId: value.releaseId } : {},
|
|
12431
12497
|
rollout: value.rollout
|
|
@@ -12448,15 +12514,50 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
12448
12514
|
return true;
|
|
12449
12515
|
const value = createHash5("sha256").update(`${input.appId}\x00${input.channel}\x00${input.releaseId}\x00${input.installationId}`).digest().readUInt32BE(0);
|
|
12450
12516
|
return value / 4294967296 < input.rollout;
|
|
12517
|
+
}, 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, field) => {
|
|
12518
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0)
|
|
12519
|
+
throw new MobileUpdateRegistryError(`Mobile update health ${field} is invalid`);
|
|
12520
|
+
return value;
|
|
12521
|
+
}, parseHealthTransfer = (value) => {
|
|
12522
|
+
if (value === undefined)
|
|
12523
|
+
return;
|
|
12524
|
+
if (!object3(value))
|
|
12525
|
+
throw new MobileUpdateRegistryError("Mobile update health transfer is invalid");
|
|
12526
|
+
return {
|
|
12527
|
+
avoidedBytes: finiteMetric(value.avoidedBytes, "avoidedBytes"),
|
|
12528
|
+
downloadedBytes: finiteMetric(value.downloadedBytes, "downloadedBytes"),
|
|
12529
|
+
durationMs: finiteMetric(value.durationMs, "durationMs"),
|
|
12530
|
+
resumedBytes: finiteMetric(value.resumedBytes, "resumedBytes"),
|
|
12531
|
+
reusedBytes: finiteMetric(value.reusedBytes, "reusedBytes"),
|
|
12532
|
+
throughputBytesPerSecond: finiteMetric(value.throughputBytesPerSecond, "throughputBytesPerSecond")
|
|
12533
|
+
};
|
|
12451
12534
|
}, createMobileUpdateRegistry = (options) => {
|
|
12452
12535
|
const prefix = normalizedPrefix(options.prefix ?? DEFAULT_PREFIX);
|
|
12453
12536
|
const clock = options.clock ?? (() => new Date);
|
|
12537
|
+
const health = options.health;
|
|
12538
|
+
const rollout = options.rollout;
|
|
12539
|
+
if (health && health.secret.length < 32)
|
|
12540
|
+
throw new MobileUpdateRegistryError("Mobile update health secret must contain at least 32 characters");
|
|
12541
|
+
if (health && !options.store.list)
|
|
12542
|
+
throw new MobileUpdateRegistryError("Mobile update health requires storage lifecycle listing");
|
|
12543
|
+
if (rollout && (!health || !options.store.list))
|
|
12544
|
+
throw new MobileUpdateRegistryError("Mobile update rollout orchestration requires fleet health and storage lifecycle listing");
|
|
12545
|
+
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)))
|
|
12546
|
+
throw new MobileUpdateRegistryError("Mobile update rollout stages are invalid");
|
|
12547
|
+
const minimumReports = health?.autoPause?.minimumReports ?? 20;
|
|
12548
|
+
const failureThreshold = health?.autoPause?.failureRate ?? 0.2;
|
|
12549
|
+
if (health && (!Number.isSafeInteger(minimumReports) || minimumReports < 1 || failureThreshold <= 0 || failureThreshold > 1))
|
|
12550
|
+
throw new MobileUpdateRegistryError("Mobile update health auto-pause policy is invalid");
|
|
12454
12551
|
const root = (appId) => `${prefix}/${appHash(appId)}`;
|
|
12455
12552
|
const releaseRoot = (manifest) => `${root(manifest.appId)}/releases/${manifest.releaseId}`;
|
|
12456
12553
|
const manifestKey = (manifest) => `${releaseRoot(manifest)}/update.json`;
|
|
12457
12554
|
const fileKey = (manifest, file2) => `${releaseRoot(manifest)}/files/${file2.path}`;
|
|
12458
12555
|
const contentBlobKey = (appId, sha256) => `${root(appId)}/blobs/${sha256}`;
|
|
12459
12556
|
const tombstoneKey = (appId, releaseId) => `${root(appId)}/gc/${releaseId}.json`;
|
|
12557
|
+
const healthRoot = (appId, promotionId, releaseId) => `${root(appId)}/health/${promotionId}/${releaseId}`;
|
|
12558
|
+
const pauseKey = (appId, promotionId, releaseId) => `${healthRoot(appId, promotionId, releaseId)}/paused.json`;
|
|
12559
|
+
const rolloutRoot = (appId, promotionId, releaseId) => `${healthRoot(appId, promotionId, releaseId)}/rollout`;
|
|
12560
|
+
const rolloutPlanKey = (appId, promotionId, releaseId) => `${rolloutRoot(appId, promotionId, releaseId)}/plan.json`;
|
|
12460
12561
|
const channelKey = (appId, channel) => {
|
|
12461
12562
|
if (!APP_ID.test(appId) || !NAME.test(channel))
|
|
12462
12563
|
throw new MobileUpdateRegistryError("Mobile update channel identity is invalid");
|
|
@@ -12485,18 +12586,128 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
12485
12586
|
throw new MobileUpdateRegistryError("Stored mobile update channel identity changed");
|
|
12486
12587
|
return value;
|
|
12487
12588
|
};
|
|
12589
|
+
const listPrefix = async (value) => {
|
|
12590
|
+
const objects = [];
|
|
12591
|
+
const cursors = new Set;
|
|
12592
|
+
let cursor;
|
|
12593
|
+
do {
|
|
12594
|
+
const page = await options.store.list({
|
|
12595
|
+
...cursor ? { cursor } : {},
|
|
12596
|
+
prefix: value
|
|
12597
|
+
});
|
|
12598
|
+
objects.push(...page.objects);
|
|
12599
|
+
if (!page.truncated)
|
|
12600
|
+
break;
|
|
12601
|
+
if (!page.cursor || cursors.has(page.cursor))
|
|
12602
|
+
throw new MobileUpdateRegistryError("Mobile update storage returned an invalid cursor");
|
|
12603
|
+
cursors.add(page.cursor);
|
|
12604
|
+
cursor = page.cursor;
|
|
12605
|
+
} while (true);
|
|
12606
|
+
return objects;
|
|
12607
|
+
};
|
|
12608
|
+
const readVerifiedObject = async (key, label) => {
|
|
12609
|
+
const bytes = await options.store.get(key);
|
|
12610
|
+
if (!bytes)
|
|
12611
|
+
return null;
|
|
12612
|
+
const head = await options.store.head(key);
|
|
12613
|
+
if (!head || head.size !== bytes.byteLength || head.metadata?.sha256 !== digest(bytes))
|
|
12614
|
+
throw new MobileUpdateRegistryError(`Stored mobile update ${label} integrity failed`);
|
|
12615
|
+
return decode(bytes);
|
|
12616
|
+
};
|
|
12617
|
+
const parseRolloutPlan = (value, promotionId, releaseId) => {
|
|
12618
|
+
if (!object3(value) || value.format !== 1 || value.promotionId !== promotionId || value.releaseId !== releaseId || typeof value.automatic !== "boolean" || !iso(value.createdAt) || !Array.isArray(value.stages))
|
|
12619
|
+
throw new MobileUpdateRegistryError("Stored mobile update rollout plan is invalid");
|
|
12620
|
+
const stages = value.stages.map((stage) => {
|
|
12621
|
+
if (!object3(stage) || typeof stage.rollout !== "number" || typeof stage.maximumFailureRate !== "number" || !Number.isSafeInteger(stage.minimumReports) || !Number.isSafeInteger(stage.observationMs))
|
|
12622
|
+
throw new MobileUpdateRegistryError("Stored mobile update rollout plan is invalid");
|
|
12623
|
+
return {
|
|
12624
|
+
maximumFailureRate: stage.maximumFailureRate,
|
|
12625
|
+
minimumReports: stage.minimumReports,
|
|
12626
|
+
observationMs: stage.observationMs,
|
|
12627
|
+
rollout: stage.rollout
|
|
12628
|
+
};
|
|
12629
|
+
});
|
|
12630
|
+
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))
|
|
12631
|
+
throw new MobileUpdateRegistryError("Stored mobile update rollout plan is invalid");
|
|
12632
|
+
return {
|
|
12633
|
+
automatic: value.automatic,
|
|
12634
|
+
createdAt: value.createdAt,
|
|
12635
|
+
format: 1,
|
|
12636
|
+
promotionId,
|
|
12637
|
+
releaseId,
|
|
12638
|
+
stages
|
|
12639
|
+
};
|
|
12640
|
+
};
|
|
12641
|
+
const initializeRollout = async (channel, signal) => {
|
|
12642
|
+
if (!rollout || !channel.releaseId)
|
|
12643
|
+
return;
|
|
12644
|
+
if (!rollout.stages.some((stage) => stage.rollout === channel.rollout))
|
|
12645
|
+
throw new MobileUpdateRegistryError("Mobile update promotion rollout must match a configured rollout stage");
|
|
12646
|
+
const promotionId = healthPromotionId(channel);
|
|
12647
|
+
const plan = {
|
|
12648
|
+
automatic: rollout.automatic ?? false,
|
|
12649
|
+
createdAt: channel.promotedAt,
|
|
12650
|
+
format: 1,
|
|
12651
|
+
promotionId,
|
|
12652
|
+
releaseId: channel.releaseId,
|
|
12653
|
+
stages: rollout.stages.map((stage) => ({ ...stage }))
|
|
12654
|
+
};
|
|
12655
|
+
const bytes = json(plan);
|
|
12656
|
+
await options.store.put(rolloutPlanKey(channel.appId, promotionId, channel.releaseId), bytes, {
|
|
12657
|
+
cacheControl: "no-store",
|
|
12658
|
+
contentType: "application/json",
|
|
12659
|
+
maxBytes: bytes.byteLength,
|
|
12660
|
+
metadata: { releaseid: channel.releaseId, sha256: digest(bytes) },
|
|
12661
|
+
signal
|
|
12662
|
+
});
|
|
12663
|
+
};
|
|
12664
|
+
const signHealthToken = (payload) => {
|
|
12665
|
+
if (!health)
|
|
12666
|
+
return null;
|
|
12667
|
+
const encoded = base64Url(JSON.stringify(payload));
|
|
12668
|
+
const signature = createHmac("sha256", health.secret).update(encoded).digest("base64url");
|
|
12669
|
+
return `${encoded}.${signature}`;
|
|
12670
|
+
};
|
|
12671
|
+
const verifyHealthToken = (token) => {
|
|
12672
|
+
if (!health)
|
|
12673
|
+
throw new MobileUpdateRegistryError("Mobile update health reporting is not configured");
|
|
12674
|
+
const [encoded, provided, extra] = token.split(".");
|
|
12675
|
+
if (!encoded || !provided || extra)
|
|
12676
|
+
throw new MobileUpdateRegistryError("Mobile update health token is invalid");
|
|
12677
|
+
const expected = createHmac("sha256", health.secret).update(encoded).digest();
|
|
12678
|
+
let actual;
|
|
12679
|
+
try {
|
|
12680
|
+
actual = Buffer.from(provided, "base64url");
|
|
12681
|
+
} catch {
|
|
12682
|
+
actual = Buffer.alloc(0);
|
|
12683
|
+
}
|
|
12684
|
+
if (actual.byteLength !== expected.byteLength || !timingSafeEqual(actual, expected))
|
|
12685
|
+
throw new MobileUpdateRegistryError("Mobile update health token is invalid");
|
|
12686
|
+
let value;
|
|
12687
|
+
try {
|
|
12688
|
+
value = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
|
|
12689
|
+
} catch {
|
|
12690
|
+
throw new MobileUpdateRegistryError("Mobile update health token is invalid");
|
|
12691
|
+
}
|
|
12692
|
+
if (!object3(value) || value.format !== HEALTH_TOKEN_VERSION || typeof value.appId !== "string" || typeof value.channel !== "string" || typeof value.installationId !== "string" || typeof value.promotionId !== "string" || typeof value.releaseId !== "string" || typeof value.runtimeFingerprint !== "string")
|
|
12693
|
+
throw new MobileUpdateRegistryError("Mobile update health token is invalid");
|
|
12694
|
+
return value;
|
|
12695
|
+
};
|
|
12488
12696
|
const assertNotMarked = async (appId, releaseId) => {
|
|
12489
12697
|
if (!APP_ID.test(appId) || !RELEASE.test(releaseId))
|
|
12490
12698
|
throw new MobileUpdateRegistryError("Mobile update release identity is invalid");
|
|
12491
12699
|
if (await options.store.head(tombstoneKey(appId, releaseId)))
|
|
12492
12700
|
throw new MobileUpdateRegistryError("Mobile update release is marked for collection. Increase retention and apply garbage collection to restore it before promotion");
|
|
12493
12701
|
};
|
|
12494
|
-
const writeChannel = async (input, signal) => {
|
|
12702
|
+
const writeChannel = async (input, signal, beforeWrite) => {
|
|
12703
|
+
const promotedAt = clock().toISOString();
|
|
12495
12704
|
const value = {
|
|
12496
12705
|
...input,
|
|
12497
12706
|
format: MOBILE_UPDATE_REGISTRY_FORMAT,
|
|
12498
|
-
promotedAt
|
|
12707
|
+
promotedAt,
|
|
12708
|
+
promotionId: digest(new TextEncoder().encode(`${input.appId}\x00${input.channel}\x00${input.releaseId ?? "embedded"}\x00${promotedAt}\x00${randomUUID()}`))
|
|
12499
12709
|
};
|
|
12710
|
+
await beforeWrite?.(value);
|
|
12500
12711
|
const bytes = json(value);
|
|
12501
12712
|
await options.store.put(channelKey(value.appId, value.channel), bytes, {
|
|
12502
12713
|
cacheControl: "no-cache",
|
|
@@ -12511,10 +12722,92 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
12511
12722
|
});
|
|
12512
12723
|
return value;
|
|
12513
12724
|
};
|
|
12725
|
+
const rolloutContext = async (channel) => {
|
|
12726
|
+
if (!options.store.list || !channel.releaseId)
|
|
12727
|
+
return null;
|
|
12728
|
+
const promotionId = healthPromotionId(channel);
|
|
12729
|
+
const storedPlan = await readVerifiedObject(rolloutPlanKey(channel.appId, promotionId, channel.releaseId), "rollout plan");
|
|
12730
|
+
if (storedPlan === null)
|
|
12731
|
+
return null;
|
|
12732
|
+
const plan = parseRolloutPlan(storedPlan, promotionId, channel.releaseId);
|
|
12733
|
+
const initialStage = plan.stages.findIndex((stage) => stage.rollout === channel.rollout);
|
|
12734
|
+
if (initialStage < 0)
|
|
12735
|
+
throw new MobileUpdateRegistryError("Stored mobile update rollout does not match its plan");
|
|
12736
|
+
let currentStage = initialStage;
|
|
12737
|
+
let enteredAt = plan.createdAt;
|
|
12738
|
+
const advances = await listPrefix(`${rolloutRoot(channel.appId, promotionId, channel.releaseId)}/advances/`);
|
|
12739
|
+
for (const item of advances) {
|
|
12740
|
+
const value = await readVerifiedObject(item.key, "rollout advancement");
|
|
12741
|
+
if (!object3(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))
|
|
12742
|
+
throw new MobileUpdateRegistryError("Stored mobile update rollout advancement is invalid");
|
|
12743
|
+
if (Number(value.stage) >= currentStage) {
|
|
12744
|
+
currentStage = Number(value.stage);
|
|
12745
|
+
enteredAt = value.createdAt;
|
|
12746
|
+
}
|
|
12747
|
+
}
|
|
12748
|
+
const controls = await listPrefix(`${rolloutRoot(channel.appId, promotionId, channel.releaseId)}/controls/`);
|
|
12749
|
+
const parsedControls = [];
|
|
12750
|
+
for (const item of controls) {
|
|
12751
|
+
const value = await readVerifiedObject(item.key, "rollout control");
|
|
12752
|
+
if (!object3(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")))
|
|
12753
|
+
throw new MobileUpdateRegistryError("Stored mobile update rollout control is invalid");
|
|
12754
|
+
parsedControls.push(value);
|
|
12755
|
+
}
|
|
12756
|
+
const cancelled = parsedControls.some(({ action }) => action === "cancel");
|
|
12757
|
+
const resumedPauseIds = new Set(parsedControls.flatMap((control) => control.resumedPauseIds ?? []));
|
|
12758
|
+
const activePauseIds = parsedControls.filter(({ action, id }) => action === "pause" && !resumedPauseIds.has(id)).map(({ id }) => id);
|
|
12759
|
+
const operatorPaused = activePauseIds.length > 0;
|
|
12760
|
+
const fleetPaused = Boolean(await options.store.head(pauseKey(channel.appId, promotionId, channel.releaseId)));
|
|
12761
|
+
return {
|
|
12762
|
+
cancelled,
|
|
12763
|
+
activePauseIds,
|
|
12764
|
+
channel,
|
|
12765
|
+
currentStage,
|
|
12766
|
+
enteredAt,
|
|
12767
|
+
fleetPaused,
|
|
12768
|
+
operatorPaused,
|
|
12769
|
+
plan,
|
|
12770
|
+
promotionId,
|
|
12771
|
+
rollout: plan.stages[currentStage].rollout
|
|
12772
|
+
};
|
|
12773
|
+
};
|
|
12774
|
+
const writeRolloutControl = async (channel, action, signal) => {
|
|
12775
|
+
const context = await rolloutContext(channel);
|
|
12776
|
+
if (!context)
|
|
12777
|
+
throw new MobileUpdateRegistryError("Mobile update rollout orchestration is not configured");
|
|
12778
|
+
if (context.cancelled)
|
|
12779
|
+
throw new MobileUpdateRegistryError("Mobile update rollout was already cancelled");
|
|
12780
|
+
if (action === "resume" && context.fleetPaused)
|
|
12781
|
+
throw new MobileUpdateRegistryError("A fleet-health pause requires an explicit re-promotion");
|
|
12782
|
+
const releaseId = channel.releaseId;
|
|
12783
|
+
if (!releaseId)
|
|
12784
|
+
throw new MobileUpdateRegistryError("Mobile update channel does not have an active release");
|
|
12785
|
+
const createdAt = clock().toISOString();
|
|
12786
|
+
const id = randomUUID();
|
|
12787
|
+
const event = {
|
|
12788
|
+
action,
|
|
12789
|
+
createdAt,
|
|
12790
|
+
format: 1,
|
|
12791
|
+
id,
|
|
12792
|
+
promotionId: context.promotionId,
|
|
12793
|
+
releaseId,
|
|
12794
|
+
...action === "resume" ? { resumedPauseIds: context.activePauseIds } : {}
|
|
12795
|
+
};
|
|
12796
|
+
const bytes = json(event);
|
|
12797
|
+
await options.store.put(`${rolloutRoot(channel.appId, context.promotionId, releaseId)}/controls/${createdAt}-${id}-${action}.json`, bytes, {
|
|
12798
|
+
cacheControl: "no-store",
|
|
12799
|
+
contentType: "application/json",
|
|
12800
|
+
maxBytes: bytes.byteLength,
|
|
12801
|
+
metadata: { action, sha256: digest(bytes) },
|
|
12802
|
+
signal
|
|
12803
|
+
});
|
|
12804
|
+
};
|
|
12514
12805
|
const promoteUpdate = async (input) => {
|
|
12515
12806
|
input.signal?.throwIfAborted();
|
|
12516
12807
|
if (input.rollout <= 0 || input.rollout > 1)
|
|
12517
12808
|
throw new MobileUpdateRegistryError("Mobile update rollout is invalid");
|
|
12809
|
+
if (rollout && !rollout.stages.some((stage) => stage.rollout === input.rollout))
|
|
12810
|
+
throw new MobileUpdateRegistryError("Mobile update promotion rollout must match a configured rollout stage");
|
|
12518
12811
|
await assertNotMarked(input.appId, input.releaseId);
|
|
12519
12812
|
const release = await readManifest(input.appId, input.releaseId);
|
|
12520
12813
|
if (!release || release.manifest.channel !== input.channel)
|
|
@@ -12526,7 +12819,7 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
12526
12819
|
...existing?.releaseId && existing.releaseId !== input.releaseId ? { fallbackReleaseId: existing.releaseId } : existing?.fallbackReleaseId ? { fallbackReleaseId: existing.fallbackReleaseId } : {},
|
|
12527
12820
|
releaseId: input.releaseId,
|
|
12528
12821
|
rollout: input.rollout
|
|
12529
|
-
}, input.signal);
|
|
12822
|
+
}, input.signal, (channel) => initializeRollout(channel, input.signal));
|
|
12530
12823
|
return {
|
|
12531
12824
|
appId: input.appId,
|
|
12532
12825
|
channel: input.channel,
|
|
@@ -12539,13 +12832,16 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
12539
12832
|
const channel = await readChannel(input.appId, input.channel);
|
|
12540
12833
|
if (!channel?.releaseId)
|
|
12541
12834
|
return { status: "empty" };
|
|
12542
|
-
const
|
|
12835
|
+
const rolloutState = await rolloutContext(channel);
|
|
12836
|
+
let selected = rolloutMember({
|
|
12543
12837
|
appId: input.appId,
|
|
12544
12838
|
channel: input.channel,
|
|
12545
12839
|
installationId: input.installationId,
|
|
12546
12840
|
releaseId: channel.releaseId,
|
|
12547
|
-
rollout: channel.rollout
|
|
12841
|
+
rollout: rolloutState?.rollout ?? channel.rollout
|
|
12548
12842
|
}) ? channel.releaseId : channel.fallbackReleaseId;
|
|
12843
|
+
if (selected === channel.releaseId && (rolloutState?.cancelled || rolloutState?.fleetPaused || rolloutState?.operatorPaused || health && await options.store.head(pauseKey(input.appId, healthPromotionId(channel), channel.releaseId))))
|
|
12844
|
+
selected = channel.fallbackReleaseId;
|
|
12549
12845
|
if (!selected)
|
|
12550
12846
|
return { status: "empty" };
|
|
12551
12847
|
const release = await readManifest(input.appId, selected);
|
|
@@ -12561,6 +12857,243 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
12561
12857
|
status: "selected"
|
|
12562
12858
|
};
|
|
12563
12859
|
};
|
|
12860
|
+
const issueUpdateHealthToken = async (input) => {
|
|
12861
|
+
if (!health)
|
|
12862
|
+
return null;
|
|
12863
|
+
const channel = await readChannel(input.appId, input.channel);
|
|
12864
|
+
if (!channel?.releaseId || channel.releaseId !== input.releaseId)
|
|
12865
|
+
return null;
|
|
12866
|
+
const resolution = await resolveUpdateState(input);
|
|
12867
|
+
if (resolution.status !== "selected" || resolution.manifest.releaseId !== input.releaseId)
|
|
12868
|
+
return null;
|
|
12869
|
+
return signHealthToken({
|
|
12870
|
+
appId: input.appId,
|
|
12871
|
+
channel: input.channel,
|
|
12872
|
+
format: HEALTH_TOKEN_VERSION,
|
|
12873
|
+
installationId: input.installationId,
|
|
12874
|
+
promotionId: healthPromotionId(channel),
|
|
12875
|
+
releaseId: input.releaseId,
|
|
12876
|
+
runtimeFingerprint: input.runtimeFingerprint
|
|
12877
|
+
});
|
|
12878
|
+
};
|
|
12879
|
+
const inspectUpdateHealth = async (input) => {
|
|
12880
|
+
if (!health)
|
|
12881
|
+
throw new MobileUpdateRegistryError("Mobile update health reporting is not configured");
|
|
12882
|
+
const channel = await readChannel(input.appId, input.channel);
|
|
12883
|
+
const releaseId = input.releaseId ?? channel?.releaseId;
|
|
12884
|
+
if (!channel || !releaseId || channel.releaseId !== releaseId)
|
|
12885
|
+
return null;
|
|
12886
|
+
const promotionId = healthPromotionId(channel);
|
|
12887
|
+
const objects = await listPrefix(`${healthRoot(input.appId, promotionId, releaseId)}/events/`);
|
|
12888
|
+
const installations = new Set;
|
|
12889
|
+
const byKind = new Map([...HEALTH_KINDS].map((kind) => [
|
|
12890
|
+
kind,
|
|
12891
|
+
new Set
|
|
12892
|
+
]));
|
|
12893
|
+
const transfer = {
|
|
12894
|
+
avoidedBytes: 0,
|
|
12895
|
+
downloadedBytes: 0,
|
|
12896
|
+
durationMs: 0,
|
|
12897
|
+
resumedBytes: 0,
|
|
12898
|
+
reusedBytes: 0,
|
|
12899
|
+
throughputBytesPerSecond: 0
|
|
12900
|
+
};
|
|
12901
|
+
for (const item of objects) {
|
|
12902
|
+
const bytes = await options.store.get(item.key);
|
|
12903
|
+
if (!bytes)
|
|
12904
|
+
continue;
|
|
12905
|
+
const head = await options.store.head(item.key);
|
|
12906
|
+
if (!head || head.size !== bytes.byteLength || head.metadata?.sha256 !== digest(bytes))
|
|
12907
|
+
throw new MobileUpdateRegistryError("Stored mobile update health evidence integrity failed");
|
|
12908
|
+
const value = decode(bytes);
|
|
12909
|
+
if (!object3(value) || typeof value.installationHash !== "string" || !HEALTH_KINDS.has(String(value.kind)))
|
|
12910
|
+
throw new MobileUpdateRegistryError("Stored mobile update health evidence is invalid");
|
|
12911
|
+
const kind = value.kind;
|
|
12912
|
+
installations.add(value.installationHash);
|
|
12913
|
+
byKind.get(kind).add(value.installationHash);
|
|
12914
|
+
if (kind === "downloaded" && object3(value.transfer)) {
|
|
12915
|
+
const parsed = parseHealthTransfer(value.transfer);
|
|
12916
|
+
for (const key of Object.keys(transfer))
|
|
12917
|
+
transfer[key] += parsed[key];
|
|
12918
|
+
}
|
|
12919
|
+
}
|
|
12920
|
+
const failures = new Set([
|
|
12921
|
+
...byKind.get("quarantined"),
|
|
12922
|
+
...byKind.get("rolled-back")
|
|
12923
|
+
]);
|
|
12924
|
+
const terminals = new Set([...byKind.get("activated"), ...failures]);
|
|
12925
|
+
const failureRate = terminals.size === 0 ? 0 : failures.size / terminals.size;
|
|
12926
|
+
const rolloutState = await rolloutContext(channel);
|
|
12927
|
+
return {
|
|
12928
|
+
activated: byKind.get("activated").size,
|
|
12929
|
+
appId: input.appId,
|
|
12930
|
+
channel: input.channel,
|
|
12931
|
+
downloaded: byKind.get("downloaded").size,
|
|
12932
|
+
downloadFailed: byKind.get("download-failed").size,
|
|
12933
|
+
failureRate,
|
|
12934
|
+
failures: failures.size,
|
|
12935
|
+
paused: Boolean(rolloutState?.cancelled || rolloutState?.fleetPaused || rolloutState?.operatorPaused || await options.store.head(pauseKey(input.appId, promotionId, releaseId))),
|
|
12936
|
+
promotionId,
|
|
12937
|
+
quarantined: byKind.get("quarantined").size,
|
|
12938
|
+
releaseId,
|
|
12939
|
+
reportedInstallations: installations.size,
|
|
12940
|
+
rolledBack: byKind.get("rolled-back").size,
|
|
12941
|
+
rollout: rolloutState?.rollout ?? channel.rollout,
|
|
12942
|
+
terminalReports: terminals.size,
|
|
12943
|
+
transfer
|
|
12944
|
+
};
|
|
12945
|
+
};
|
|
12946
|
+
const inspectUpdateRollout = async (input) => {
|
|
12947
|
+
const channel = await readChannel(input.appId, input.channel);
|
|
12948
|
+
if (!channel?.releaseId)
|
|
12949
|
+
return null;
|
|
12950
|
+
const context = await rolloutContext(channel);
|
|
12951
|
+
if (!context)
|
|
12952
|
+
throw new MobileUpdateRegistryError("Mobile update rollout orchestration is not configured");
|
|
12953
|
+
const healthReport = await inspectUpdateHealth(input);
|
|
12954
|
+
if (!healthReport)
|
|
12955
|
+
return null;
|
|
12956
|
+
const paused = context.fleetPaused || context.operatorPaused;
|
|
12957
|
+
const complete = context.currentStage === context.plan.stages.length - 1;
|
|
12958
|
+
return {
|
|
12959
|
+
...healthReport,
|
|
12960
|
+
automatic: context.plan.automatic,
|
|
12961
|
+
currentStage: context.currentStage,
|
|
12962
|
+
enteredAt: context.enteredAt,
|
|
12963
|
+
...!complete ? { nextStage: context.plan.stages[context.currentStage + 1] } : {},
|
|
12964
|
+
...context.fleetPaused ? { pausedBy: "fleet-health" } : context.operatorPaused ? { pausedBy: "operator" } : {},
|
|
12965
|
+
status: context.cancelled ? "cancelled" : paused ? "paused" : complete ? "complete" : "active"
|
|
12966
|
+
};
|
|
12967
|
+
};
|
|
12968
|
+
const advanceRollout = async (input, strict) => {
|
|
12969
|
+
input.signal?.throwIfAborted();
|
|
12970
|
+
const channel = await readChannel(input.appId, input.channel);
|
|
12971
|
+
if (!channel?.releaseId)
|
|
12972
|
+
throw new MobileUpdateRegistryError("Mobile update channel does not have an active release");
|
|
12973
|
+
const context = await rolloutContext(channel);
|
|
12974
|
+
if (!context)
|
|
12975
|
+
throw new MobileUpdateRegistryError("Mobile update rollout orchestration is not configured");
|
|
12976
|
+
const report = await inspectUpdateRollout(input);
|
|
12977
|
+
if (!report)
|
|
12978
|
+
throw new MobileUpdateRegistryError("Mobile update rollout report is unavailable");
|
|
12979
|
+
const nextStage = context.plan.stages[context.currentStage + 1];
|
|
12980
|
+
if (!nextStage)
|
|
12981
|
+
return report;
|
|
12982
|
+
if (input.rollout !== undefined && input.rollout !== nextStage.rollout)
|
|
12983
|
+
throw new MobileUpdateRegistryError("Mobile update rollout can advance only to the next configured stage");
|
|
12984
|
+
if (report.status !== "active") {
|
|
12985
|
+
if (strict)
|
|
12986
|
+
throw new MobileUpdateRegistryError(`Mobile update rollout cannot advance while ${report.status}`);
|
|
12987
|
+
return report;
|
|
12988
|
+
}
|
|
12989
|
+
const gate = context.plan.stages[context.currentStage];
|
|
12990
|
+
const observedMs = clock().getTime() - Date.parse(context.enteredAt);
|
|
12991
|
+
const blocked = report.terminalReports < gate.minimumReports || report.failureRate > gate.maximumFailureRate || observedMs < gate.observationMs;
|
|
12992
|
+
if (blocked) {
|
|
12993
|
+
if (strict)
|
|
12994
|
+
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`);
|
|
12995
|
+
return report;
|
|
12996
|
+
}
|
|
12997
|
+
const stage = context.currentStage + 1;
|
|
12998
|
+
const event = {
|
|
12999
|
+
createdAt: clock().toISOString(),
|
|
13000
|
+
failureRate: report.failureRate,
|
|
13001
|
+
format: 1,
|
|
13002
|
+
promotionId: context.promotionId,
|
|
13003
|
+
releaseId: channel.releaseId,
|
|
13004
|
+
rollout: nextStage.rollout,
|
|
13005
|
+
stage,
|
|
13006
|
+
terminalReports: report.terminalReports
|
|
13007
|
+
};
|
|
13008
|
+
const bytes = json(event);
|
|
13009
|
+
await options.store.put(`${rolloutRoot(input.appId, context.promotionId, channel.releaseId)}/advances/${String(stage).padStart(4, "0")}.json`, bytes, {
|
|
13010
|
+
cacheControl: "no-store",
|
|
13011
|
+
contentType: "application/json",
|
|
13012
|
+
maxBytes: bytes.byteLength,
|
|
13013
|
+
metadata: { releaseid: channel.releaseId, sha256: digest(bytes) },
|
|
13014
|
+
signal: input.signal
|
|
13015
|
+
});
|
|
13016
|
+
return await inspectUpdateRollout(input);
|
|
13017
|
+
};
|
|
13018
|
+
const advanceUpdateRollout = (input) => advanceRollout(input, true);
|
|
13019
|
+
const reconcileUpdateRollout = async (input) => {
|
|
13020
|
+
const report = await inspectUpdateRollout(input);
|
|
13021
|
+
if (!report || !report.automatic)
|
|
13022
|
+
return report;
|
|
13023
|
+
return advanceRollout(input, false);
|
|
13024
|
+
};
|
|
13025
|
+
const rolloutControl = (action) => async (input) => {
|
|
13026
|
+
input.signal?.throwIfAborted();
|
|
13027
|
+
const channel = await readChannel(input.appId, input.channel);
|
|
13028
|
+
if (!channel?.releaseId)
|
|
13029
|
+
throw new MobileUpdateRegistryError("Mobile update channel does not have an active release");
|
|
13030
|
+
await writeRolloutControl(channel, action, input.signal);
|
|
13031
|
+
return await inspectUpdateRollout(input);
|
|
13032
|
+
};
|
|
13033
|
+
const pauseUpdateRollout = rolloutControl("pause");
|
|
13034
|
+
const resumeUpdateRollout = rolloutControl("resume");
|
|
13035
|
+
const cancelUpdateRollout = rolloutControl("cancel");
|
|
13036
|
+
const recordUpdateHealth = async (input) => {
|
|
13037
|
+
const payload = verifyHealthToken(input.token);
|
|
13038
|
+
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")
|
|
13039
|
+
throw new MobileUpdateRegistryError("Mobile update health evidence does not match its token");
|
|
13040
|
+
const release = await readManifest(input.appId, input.releaseId);
|
|
13041
|
+
if (!release || release.manifest.runtimeFingerprint !== input.runtimeFingerprint)
|
|
13042
|
+
throw new MobileUpdateRegistryError("Mobile update health release is invalid");
|
|
13043
|
+
const activeChannel = await readChannel(input.appId, input.channel);
|
|
13044
|
+
if (!activeChannel || activeChannel.releaseId !== input.releaseId || healthPromotionId(activeChannel) !== payload.promotionId)
|
|
13045
|
+
throw new MobileUpdateRegistryError("Mobile update health promotion is no longer active");
|
|
13046
|
+
const transfer = parseHealthTransfer(input.transfer);
|
|
13047
|
+
const installationHash = createHmac("sha256", health.secret).update(input.installationId).digest("hex");
|
|
13048
|
+
const evidence = {
|
|
13049
|
+
format: 1,
|
|
13050
|
+
installationHash,
|
|
13051
|
+
kind: input.kind,
|
|
13052
|
+
observedAt: clock().toISOString(),
|
|
13053
|
+
...input.reason ? { reason: input.reason } : {},
|
|
13054
|
+
...transfer ? { transfer } : {}
|
|
13055
|
+
};
|
|
13056
|
+
const bytes = json(evidence);
|
|
13057
|
+
await options.store.put(`${healthRoot(input.appId, payload.promotionId, input.releaseId)}/events/${installationHash}/${input.kind}.json`, bytes, {
|
|
13058
|
+
cacheControl: "no-store",
|
|
13059
|
+
contentType: "application/json",
|
|
13060
|
+
maxBytes: bytes.byteLength,
|
|
13061
|
+
metadata: { kind: input.kind, sha256: digest(bytes) }
|
|
13062
|
+
});
|
|
13063
|
+
let report = await inspectUpdateHealth({
|
|
13064
|
+
appId: input.appId,
|
|
13065
|
+
channel: input.channel,
|
|
13066
|
+
releaseId: input.releaseId
|
|
13067
|
+
});
|
|
13068
|
+
if (!report)
|
|
13069
|
+
throw new MobileUpdateRegistryError("Mobile update health promotion is no longer active");
|
|
13070
|
+
if (FAILURE_HEALTH_KINDS.has(input.kind) && report.terminalReports >= minimumReports && report.failureRate >= failureThreshold && !report.paused) {
|
|
13071
|
+
const marker = json({
|
|
13072
|
+
appId: input.appId,
|
|
13073
|
+
channel: input.channel,
|
|
13074
|
+
failureRate: report.failureRate,
|
|
13075
|
+
failures: report.failures,
|
|
13076
|
+
format: 1,
|
|
13077
|
+
pausedAt: clock().toISOString(),
|
|
13078
|
+
promotionId: payload.promotionId,
|
|
13079
|
+
releaseId: input.releaseId,
|
|
13080
|
+
reports: report.terminalReports
|
|
13081
|
+
});
|
|
13082
|
+
await options.store.put(pauseKey(input.appId, payload.promotionId, input.releaseId), marker, {
|
|
13083
|
+
cacheControl: "no-store",
|
|
13084
|
+
contentType: "application/json",
|
|
13085
|
+
maxBytes: marker.byteLength,
|
|
13086
|
+
metadata: { releaseid: input.releaseId, sha256: digest(marker) }
|
|
13087
|
+
});
|
|
13088
|
+
report = { ...report, paused: true };
|
|
13089
|
+
}
|
|
13090
|
+
if (rollout && (input.kind === "activated" || FAILURE_HEALTH_KINDS.has(input.kind)))
|
|
13091
|
+
return await reconcileUpdateRollout({
|
|
13092
|
+
appId: input.appId,
|
|
13093
|
+
channel: input.channel
|
|
13094
|
+
}) ?? report;
|
|
13095
|
+
return report;
|
|
13096
|
+
};
|
|
12564
13097
|
const retentionValues = (input) => {
|
|
12565
13098
|
if (!APP_ID.test(input.appId))
|
|
12566
13099
|
throw new MobileUpdateRegistryError("Mobile update appId is invalid");
|
|
@@ -12803,6 +13336,15 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
12803
13336
|
return result;
|
|
12804
13337
|
};
|
|
12805
13338
|
return {
|
|
13339
|
+
...health ? { inspectUpdateHealth, issueUpdateHealthToken, recordUpdateHealth } : {},
|
|
13340
|
+
...rollout ? {
|
|
13341
|
+
advanceUpdateRollout,
|
|
13342
|
+
cancelUpdateRollout,
|
|
13343
|
+
inspectUpdateRollout,
|
|
13344
|
+
pauseUpdateRollout,
|
|
13345
|
+
reconcileUpdateRollout,
|
|
13346
|
+
resumeUpdateRollout
|
|
13347
|
+
} : {},
|
|
12806
13348
|
inspectUpdateStorage: async (input) => (await inventory(input)).report,
|
|
12807
13349
|
pruneUpdates,
|
|
12808
13350
|
publishUpdate: async (input) => {
|
|
@@ -13080,7 +13622,7 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
13080
13622
|
const origin = request.headers.get("origin");
|
|
13081
13623
|
const cors = origin && allowedOrigins.has(origin) ? {
|
|
13082
13624
|
"access-control-allow-origin": origin,
|
|
13083
|
-
"access-control-expose-headers": "content-range,etag",
|
|
13625
|
+
"access-control-expose-headers": "content-range,etag,x-absolute-mobile-health-token",
|
|
13084
13626
|
vary: "Origin"
|
|
13085
13627
|
} : {};
|
|
13086
13628
|
if (request.method === "OPTIONS") {
|
|
@@ -13089,17 +13631,58 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
13089
13631
|
return new Response(null, {
|
|
13090
13632
|
headers: {
|
|
13091
13633
|
...cors,
|
|
13092
|
-
"access-control-allow-headers": "if-range,range,x-absolute-mobile-app,x-absolute-mobile-channel,x-absolute-mobile-installation,x-absolute-mobile-release,x-absolute-mobile-runtime",
|
|
13093
|
-
"access-control-allow-methods": "GET,OPTIONS",
|
|
13634
|
+
"access-control-allow-headers": "content-type,if-range,range,x-absolute-mobile-app,x-absolute-mobile-channel,x-absolute-mobile-health-token,x-absolute-mobile-installation,x-absolute-mobile-release,x-absolute-mobile-runtime",
|
|
13635
|
+
"access-control-allow-methods": "GET,POST,OPTIONS",
|
|
13094
13636
|
"access-control-max-age": "600"
|
|
13095
13637
|
},
|
|
13096
13638
|
status: 204
|
|
13097
13639
|
});
|
|
13098
13640
|
}
|
|
13099
|
-
if (request.method !== "GET")
|
|
13100
|
-
return new Response(null, { status: 405 });
|
|
13101
13641
|
const pathname = new URL(request.url).pathname.replace(/^\/+/, "");
|
|
13102
13642
|
const relative7 = pathname.startsWith(`${route}/`) ? pathname.slice(route.length + 1) : "";
|
|
13643
|
+
if (request.method === "POST" && relative7 === "health") {
|
|
13644
|
+
if (!options.registry.recordUpdateHealth)
|
|
13645
|
+
return new Response(null, { status: 404 });
|
|
13646
|
+
const appId = request.headers.get("x-absolute-mobile-app");
|
|
13647
|
+
const channel = request.headers.get("x-absolute-mobile-channel");
|
|
13648
|
+
const installationId = request.headers.get("x-absolute-mobile-installation");
|
|
13649
|
+
const runtimeFingerprint = request.headers.get("x-absolute-mobile-runtime");
|
|
13650
|
+
const token = request.headers.get("x-absolute-mobile-health-token");
|
|
13651
|
+
const declared = Number(request.headers.get("content-length"));
|
|
13652
|
+
if (appId !== options.appId || channel !== options.channel || !installationId || !runtimeFingerprint || !token || Number.isFinite(declared) && declared > 4096)
|
|
13653
|
+
return new Response(null, { status: 400 });
|
|
13654
|
+
const bodyBytes = new Uint8Array(await request.arrayBuffer());
|
|
13655
|
+
if (bodyBytes.byteLength > 4096)
|
|
13656
|
+
return new Response(null, { status: 413 });
|
|
13657
|
+
let body;
|
|
13658
|
+
try {
|
|
13659
|
+
body = JSON.parse(new TextDecoder().decode(bodyBytes));
|
|
13660
|
+
} catch {
|
|
13661
|
+
return new Response(null, { status: 400 });
|
|
13662
|
+
}
|
|
13663
|
+
if (!object3(body) || typeof body.releaseId !== "string" || typeof body.kind !== "string")
|
|
13664
|
+
return new Response(null, { status: 400 });
|
|
13665
|
+
try {
|
|
13666
|
+
const report = await options.registry.recordUpdateHealth({
|
|
13667
|
+
appId,
|
|
13668
|
+
channel,
|
|
13669
|
+
installationId,
|
|
13670
|
+
kind: body.kind,
|
|
13671
|
+
...body.reason === "boot-interrupted" || body.reason === "boot-timeout" ? { reason: body.reason } : {},
|
|
13672
|
+
releaseId: body.releaseId,
|
|
13673
|
+
runtimeFingerprint,
|
|
13674
|
+
token,
|
|
13675
|
+
...object3(body.transfer) ? { transfer: body.transfer } : {}
|
|
13676
|
+
});
|
|
13677
|
+
return Response.json({ paused: report.paused }, { headers: { ...cors, "cache-control": "no-store" }, status: 202 });
|
|
13678
|
+
} catch (error) {
|
|
13679
|
+
if (error instanceof MobileUpdateRegistryError)
|
|
13680
|
+
return new Response(null, { status: 403 });
|
|
13681
|
+
throw error;
|
|
13682
|
+
}
|
|
13683
|
+
}
|
|
13684
|
+
if (request.method !== "GET")
|
|
13685
|
+
return new Response(null, { status: 405 });
|
|
13103
13686
|
if (relative7 === "update.json") {
|
|
13104
13687
|
const expoProtocolVersion = request.headers.get("expo-protocol-version");
|
|
13105
13688
|
const expoProtocol = expoProtocolVersion !== null;
|
|
@@ -13125,6 +13708,13 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
13125
13708
|
installationId,
|
|
13126
13709
|
runtimeFingerprint
|
|
13127
13710
|
});
|
|
13711
|
+
const healthToken = selected && options.registry.issueUpdateHealthToken ? await options.registry.issueUpdateHealthToken({
|
|
13712
|
+
appId,
|
|
13713
|
+
channel,
|
|
13714
|
+
installationId,
|
|
13715
|
+
releaseId: selected.manifest.releaseId,
|
|
13716
|
+
runtimeFingerprint
|
|
13717
|
+
}) : null;
|
|
13128
13718
|
if (expoProtocol) {
|
|
13129
13719
|
let requestedCodeSigning;
|
|
13130
13720
|
try {
|
|
@@ -13176,6 +13766,7 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
13176
13766
|
extra: {
|
|
13177
13767
|
absolutejs: {
|
|
13178
13768
|
channel: selected.manifest.channel,
|
|
13769
|
+
...healthToken ? { healthToken } : {},
|
|
13179
13770
|
releaseId: selected.manifest.releaseId
|
|
13180
13771
|
},
|
|
13181
13772
|
expoClient: descriptor.expoConfig
|
|
@@ -13203,7 +13794,8 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
13203
13794
|
headers: {
|
|
13204
13795
|
...cors,
|
|
13205
13796
|
"cache-control": "no-store",
|
|
13206
|
-
etag: `"${selected.manifest.releaseId}"
|
|
13797
|
+
etag: `"${selected.manifest.releaseId}"`,
|
|
13798
|
+
...healthToken ? { "x-absolute-mobile-health-token": healthToken } : {}
|
|
13207
13799
|
}
|
|
13208
13800
|
});
|
|
13209
13801
|
}
|
|
@@ -13266,6 +13858,14 @@ var init_mobileUpdate = __esm(() => {
|
|
|
13266
13858
|
RELEASE = /^amu_[a-f0-9]{64}$/;
|
|
13267
13859
|
APP_ID = /^[A-Za-z][\w]*(?:\.[A-Za-z][\w]*)+$/;
|
|
13268
13860
|
NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
13861
|
+
HEALTH_KINDS = new Set([
|
|
13862
|
+
"activated",
|
|
13863
|
+
"downloaded",
|
|
13864
|
+
"download-failed",
|
|
13865
|
+
"quarantined",
|
|
13866
|
+
"rolled-back"
|
|
13867
|
+
]);
|
|
13868
|
+
FAILURE_HEALTH_KINDS = new Set(["quarantined", "rolled-back"]);
|
|
13269
13869
|
MobileUpdateRegistryError = class MobileUpdateRegistryError extends Error {
|
|
13270
13870
|
};
|
|
13271
13871
|
expoProtocolHeaders = {
|
|
@@ -13353,6 +13953,16 @@ var ABSOLUTE_MOBILE_UPDATE_SERVER_FORMAT = 1, DEFAULT_MOBILE_UPDATE_REGISTRY_MOD
|
|
|
13353
13953
|
} catch (error) {
|
|
13354
13954
|
throw new TypeError(`Durable mobile update storage verification failed for ${module.metadata.provider}. Check the bucket, endpoint, credentials, and read/write/delete permissions.`, { cause: error });
|
|
13355
13955
|
}
|
|
13956
|
+
}, verifyHealthModule = (config, module) => {
|
|
13957
|
+
if (!config.updateServer?.health)
|
|
13958
|
+
return;
|
|
13959
|
+
if (typeof module.registry.inspectUpdateHealth !== "function" || typeof module.registry.issueUpdateHealthToken !== "function" || typeof module.registry.recordUpdateHealth !== "function")
|
|
13960
|
+
throw new TypeError("Mobile update fleet health is enabled but the registry is not provisioned for it. Run `absolute mobile update provision --force`.");
|
|
13961
|
+
}, verifyRolloutModule = (config, module) => {
|
|
13962
|
+
if (!config.updateServer?.rollout)
|
|
13963
|
+
return;
|
|
13964
|
+
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")
|
|
13965
|
+
throw new TypeError("Mobile update rollout orchestration is enabled but the registry is not provisioned for it. Run `absolute mobile update provision --force`.");
|
|
13356
13966
|
}, expoSigningOptions = (config) => {
|
|
13357
13967
|
if (!config.updates?.expoCodeSigning)
|
|
13358
13968
|
return;
|
|
@@ -13384,8 +13994,11 @@ var ABSOLUTE_MOBILE_UPDATE_SERVER_FORMAT = 1, DEFAULT_MOBILE_UPDATE_REGISTRY_MOD
|
|
|
13384
13994
|
if (!updates || !server?.autoMount)
|
|
13385
13995
|
return new Elysia4({ name: "absolutejs-mobile-updates-disabled" });
|
|
13386
13996
|
const module = await loadAbsoluteMobileUpdateServerModule(projectRoot, server.registryModule);
|
|
13387
|
-
if (options.production)
|
|
13997
|
+
if (options.production) {
|
|
13388
13998
|
await verifyDurableModule(module);
|
|
13999
|
+
verifyHealthModule(config, module);
|
|
14000
|
+
verifyRolloutModule(config, module);
|
|
14001
|
+
}
|
|
13389
14002
|
const manifest = new URL(updates.manifestUrl);
|
|
13390
14003
|
if (!manifest.pathname.endsWith("/update.json"))
|
|
13391
14004
|
throw new TypeError("Auto-mounted mobile update manifests must end in /update.json.");
|
|
@@ -13404,10 +14017,12 @@ var ABSOLUTE_MOBILE_UPDATE_SERVER_FORMAT = 1, DEFAULT_MOBILE_UPDATE_REGISTRY_MOD
|
|
|
13404
14017
|
return;
|
|
13405
14018
|
const module = await loadAbsoluteMobileUpdateServerModule(projectRoot, config.updateServer?.registryModule);
|
|
13406
14019
|
await verifyDurableModule(module);
|
|
14020
|
+
verifyHealthModule(config, module);
|
|
14021
|
+
verifyRolloutModule(config, module);
|
|
13407
14022
|
if (config.engine === "expo")
|
|
13408
14023
|
expoSigningOptions(config);
|
|
13409
14024
|
return module.metadata;
|
|
13410
|
-
}, publicKeysSource = (publicKeys) => JSON.stringify(publicKeys, null, "\t"),
|
|
14025
|
+
}, publicKeysSource = (publicKeys) => JSON.stringify(publicKeys, null, "\t"), renderAbsoluteMobileUpdateRegistryBase = (options) => {
|
|
13411
14026
|
const metadata2 = `export const absoluteMobileUpdateServer = {
|
|
13412
14027
|
format: 1,
|
|
13413
14028
|
provider: '${options.storage}',
|
|
@@ -13473,6 +14088,27 @@ export default createMobileUpdateRegistry({
|
|
|
13473
14088
|
store
|
|
13474
14089
|
});
|
|
13475
14090
|
`;
|
|
14091
|
+
}, renderAbsoluteMobileUpdateRegistry = (options) => {
|
|
14092
|
+
const source = renderAbsoluteMobileUpdateRegistryBase(options);
|
|
14093
|
+
let generated = "";
|
|
14094
|
+
if (options.health) {
|
|
14095
|
+
const secret = options.storage === "local" ? `process.env.${options.health.secretEnv} ?? 'absolutejs-local-health-secret-not-for-production'` : `required('${options.health.secretEnv}')`;
|
|
14096
|
+
generated += ` health: {
|
|
14097
|
+
autoPause: { failureRate: ${options.health.failureRate}, minimumReports: ${options.health.minimumReports} },
|
|
14098
|
+
secret: ${secret}
|
|
14099
|
+
},
|
|
14100
|
+
`;
|
|
14101
|
+
}
|
|
14102
|
+
if (options.rollout)
|
|
14103
|
+
generated += ` rollout: ${JSON.stringify(options.rollout, null, "\t").replaceAll(`
|
|
14104
|
+
`, `
|
|
14105
|
+
`)},
|
|
14106
|
+
`;
|
|
14107
|
+
if (!generated)
|
|
14108
|
+
return source;
|
|
14109
|
+
return source.replace(`export default createMobileUpdateRegistry({
|
|
14110
|
+
`, `export default createMobileUpdateRegistry({
|
|
14111
|
+
${generated}`);
|
|
13476
14112
|
}, writeAbsoluteMobileUpdateRegistry = async (options) => {
|
|
13477
14113
|
const path2 = projectPath(options.projectRoot, options.modulePath ?? DEFAULT_MOBILE_UPDATE_REGISTRY_MODULE);
|
|
13478
14114
|
if (!options.force) {
|
|
@@ -13484,6 +14120,8 @@ export default createMobileUpdateRegistry({
|
|
|
13484
14120
|
}
|
|
13485
14121
|
await mkdir6(dirname12(path2), { recursive: true });
|
|
13486
14122
|
await Bun.write(path2, renderAbsoluteMobileUpdateRegistry({
|
|
14123
|
+
...options.health ? { health: options.health } : {},
|
|
14124
|
+
...options.rollout ? { rollout: options.rollout } : {},
|
|
13487
14125
|
publicKeys: options.publicKeys,
|
|
13488
14126
|
storage: options.storage
|
|
13489
14127
|
}));
|
|
@@ -44896,5 +45534,5 @@ export {
|
|
|
44896
45534
|
wrapPageHandlerWithStreamingSlots
|
|
44897
45535
|
};
|
|
44898
45536
|
|
|
44899
|
-
//# debugId=
|
|
45537
|
+
//# debugId=95E12D596BC2404464756E2164756E21
|
|
44900
45538
|
//# sourceMappingURL=index.js.map
|