@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/build.js
CHANGED
|
@@ -27625,7 +27625,7 @@ __export(exports_config, {
|
|
|
27625
27625
|
import { readFileSync as readFileSync35 } from "fs";
|
|
27626
27626
|
import { resolve as resolve44 } from "path";
|
|
27627
27627
|
import { createHash as createHash5, createPublicKey, X509Certificate } from "crypto";
|
|
27628
|
-
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) => {
|
|
27628
|
+
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) => {
|
|
27629
27629
|
const root = resolve44(projectRoot);
|
|
27630
27630
|
const path = resolve44(root, value);
|
|
27631
27631
|
if (path !== root && !path.startsWith(`${root}/`)) {
|
|
@@ -27819,6 +27819,56 @@ var APP_ID_PATTERN, SCHEME_PATTERN, APPLE_APP_ID_PREFIX_PATTERN, CERTIFICATE_FIN
|
|
|
27819
27819
|
if (!ENVIRONMENT_NAME_PATTERN.test(expoPrivateKeyEnv))
|
|
27820
27820
|
throw new TypeError("mobile.updates.server.expoPrivateKeyEnv must be a valid environment variable name.");
|
|
27821
27821
|
const autoMount = config.updates.server?.autoMount ?? true;
|
|
27822
|
+
const configuredHealth = config.updates.server?.health;
|
|
27823
|
+
let health;
|
|
27824
|
+
if (configuredHealth !== false) {
|
|
27825
|
+
const failureRate = configuredHealth?.failureRate ?? DEFAULT_UPDATE_HEALTH_FAILURE_RATE;
|
|
27826
|
+
const minimumReports = configuredHealth?.minimumReports ?? DEFAULT_UPDATE_HEALTH_MINIMUM_REPORTS;
|
|
27827
|
+
const secretEnv = requireText(configuredHealth?.secretEnv ?? "ABSOLUTE_MOBILE_UPDATE_HEALTH_SECRET", "mobile.updates.server.health.secretEnv");
|
|
27828
|
+
if (!Number.isFinite(failureRate) || failureRate <= 0 || failureRate > 1)
|
|
27829
|
+
throw new TypeError("mobile.updates.server.health.failureRate must be greater than 0 and at most 1.");
|
|
27830
|
+
if (!Number.isSafeInteger(minimumReports) || minimumReports < 1)
|
|
27831
|
+
throw new TypeError("mobile.updates.server.health.minimumReports must be a positive integer.");
|
|
27832
|
+
if (!ENVIRONMENT_NAME_PATTERN.test(secretEnv))
|
|
27833
|
+
throw new TypeError("mobile.updates.server.health.secretEnv must be a valid environment variable name.");
|
|
27834
|
+
health = { failureRate, minimumReports, secretEnv };
|
|
27835
|
+
}
|
|
27836
|
+
const configuredRollout = config.updates.server?.rollout;
|
|
27837
|
+
let rollout;
|
|
27838
|
+
if (configuredRollout !== undefined && configuredRollout !== false) {
|
|
27839
|
+
if (!health)
|
|
27840
|
+
throw new TypeError("mobile.updates.server.rollout requires fleet health to be enabled.");
|
|
27841
|
+
if (configuredRollout.automatic !== undefined && typeof configuredRollout.automatic !== "boolean")
|
|
27842
|
+
throw new TypeError("mobile.updates.server.rollout.automatic must be boolean.");
|
|
27843
|
+
const configuredStages = configuredRollout.stages ?? DEFAULT_UPDATE_ROLLOUT_STAGES;
|
|
27844
|
+
const stages = configuredStages.map((stage, index) => {
|
|
27845
|
+
const previousStage = configuredStages[index - 1];
|
|
27846
|
+
const maximumFailureRate = stage.maximumFailureRate ?? DEFAULT_UPDATE_ROLLOUT_FAILURE_RATE;
|
|
27847
|
+
const minimumReports = stage.minimumReports ?? DEFAULT_UPDATE_HEALTH_MINIMUM_REPORTS;
|
|
27848
|
+
const observationMinutes = stage.observationMinutes ?? DEFAULT_UPDATE_ROLLOUT_OBSERVATION_MINUTES;
|
|
27849
|
+
const observationMs = observationMinutes * MINUTE_MS;
|
|
27850
|
+
if (!Number.isFinite(stage.rollout) || stage.rollout <= 0 || stage.rollout > 1 || previousStage !== undefined && stage.rollout <= previousStage.rollout)
|
|
27851
|
+
throw new TypeError("mobile.updates.server.rollout stages must be strictly increasing fractions greater than 0 and at most 1.");
|
|
27852
|
+
if (!Number.isFinite(maximumFailureRate) || maximumFailureRate < 0 || maximumFailureRate >= health.failureRate)
|
|
27853
|
+
throw new TypeError("mobile.updates.server.rollout maximumFailureRate must be non-negative and lower than the fleet-health pause rate.");
|
|
27854
|
+
if (!Number.isSafeInteger(minimumReports) || minimumReports < health.minimumReports)
|
|
27855
|
+
throw new TypeError("mobile.updates.server.rollout minimumReports must be an integer at least as large as the fleet-health minimumReports.");
|
|
27856
|
+
if (!Number.isFinite(observationMinutes) || observationMinutes < 0 || !Number.isSafeInteger(observationMs))
|
|
27857
|
+
throw new TypeError("mobile.updates.server.rollout observationMinutes must produce a non-negative whole number of milliseconds.");
|
|
27858
|
+
return {
|
|
27859
|
+
maximumFailureRate,
|
|
27860
|
+
minimumReports,
|
|
27861
|
+
observationMs,
|
|
27862
|
+
rollout: stage.rollout
|
|
27863
|
+
};
|
|
27864
|
+
});
|
|
27865
|
+
if (stages.length === 0 || stages.at(-1)?.rollout !== 1)
|
|
27866
|
+
throw new TypeError("mobile.updates.server.rollout stages must end at rollout 1.");
|
|
27867
|
+
rollout = {
|
|
27868
|
+
automatic: configuredRollout.automatic ?? false,
|
|
27869
|
+
stages
|
|
27870
|
+
};
|
|
27871
|
+
}
|
|
27822
27872
|
if (autoMount) {
|
|
27823
27873
|
const manifest = new URL(updates.manifestUrl);
|
|
27824
27874
|
if (manifest.origin !== productionOrigin)
|
|
@@ -27851,7 +27901,13 @@ var APP_ID_PATTERN, SCHEME_PATTERN, APPLE_APP_ID_PREFIX_PATTERN, CERTIFICATE_FIN
|
|
|
27851
27901
|
throw new TypeError(`mobile.updates.server.expoCodeSigningKeys.${keyId} certificate is not currently valid.`);
|
|
27852
27902
|
expoCodeSigningKeys[keyId] = { certificatePem, privateKeyEnv };
|
|
27853
27903
|
}
|
|
27854
|
-
return {
|
|
27904
|
+
return {
|
|
27905
|
+
autoMount,
|
|
27906
|
+
expoCodeSigningKeys,
|
|
27907
|
+
...health ? { health } : {},
|
|
27908
|
+
...rollout ? { rollout } : {},
|
|
27909
|
+
registryModule
|
|
27910
|
+
};
|
|
27855
27911
|
}, validateExpoNativeRouteSegment = (path, segment, index, count, parameters) => {
|
|
27856
27912
|
if (segment === "*" && (index !== count - 1 || count === 1)) {
|
|
27857
27913
|
throw new TypeError(`mobile.routes.native route ${path} must use * once, as the final segment after a static or parameterized prefix.`);
|
|
@@ -27943,6 +27999,12 @@ var init_config = __esm(() => {
|
|
|
27943
27999
|
UPDATE_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u;
|
|
27944
28000
|
UPDATE_PUBLIC_KEY_PATTERN = /^[A-Za-z0-9+/]+={0,2}$/u;
|
|
27945
28001
|
ENVIRONMENT_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/u;
|
|
28002
|
+
MINUTE_MS = 60 * 1000;
|
|
28003
|
+
DEFAULT_UPDATE_ROLLOUT_STAGES = [
|
|
28004
|
+
{ minimumReports: 20, observationMinutes: 60, rollout: 0.05 },
|
|
28005
|
+
{ minimumReports: 100, observationMinutes: 360, rollout: 0.25 },
|
|
28006
|
+
{ minimumReports: 100, observationMinutes: 0, rollout: 1 }
|
|
28007
|
+
];
|
|
27946
28008
|
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])?))*$/;
|
|
27947
28009
|
EXPO_RESERVED_ROUTE_PREFIXES = new Set([
|
|
27948
28010
|
"_expo",
|
|
@@ -28472,15 +28534,18 @@ __export(exports_mobileUpdate, {
|
|
|
28472
28534
|
});
|
|
28473
28535
|
import {
|
|
28474
28536
|
createHash as createHash6,
|
|
28537
|
+
createHmac,
|
|
28475
28538
|
createPrivateKey,
|
|
28476
28539
|
createPublicKey as createPublicKey2,
|
|
28540
|
+
randomUUID,
|
|
28477
28541
|
sign,
|
|
28542
|
+
timingSafeEqual,
|
|
28478
28543
|
verify,
|
|
28479
28544
|
X509Certificate as X509Certificate2
|
|
28480
28545
|
} from "crypto";
|
|
28481
28546
|
import { readFile as readFile9, stat as stat3 } from "fs/promises";
|
|
28482
28547
|
import path from "path";
|
|
28483
|
-
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) => {
|
|
28548
|
+
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) => {
|
|
28484
28549
|
if (typeof value !== "string" || value.length === 0)
|
|
28485
28550
|
throw new MobileUpdateRegistryError(`Mobile update ${field} is invalid`);
|
|
28486
28551
|
return value;
|
|
@@ -28609,7 +28674,7 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
28609
28674
|
throw new MobileUpdateRegistryError("Mobile update channel is invalid");
|
|
28610
28675
|
const appId = text2(value.appId, "channel appId");
|
|
28611
28676
|
const channel = text2(value.channel, "channel");
|
|
28612
|
-
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))
|
|
28677
|
+
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))
|
|
28613
28678
|
throw new MobileUpdateRegistryError("Mobile update channel is invalid");
|
|
28614
28679
|
return {
|
|
28615
28680
|
...value.activationId ? { activationId: value.activationId } : {},
|
|
@@ -28618,6 +28683,7 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
28618
28683
|
channel,
|
|
28619
28684
|
...value.fallbackReleaseId ? { fallbackReleaseId: value.fallbackReleaseId } : {},
|
|
28620
28685
|
format: MOBILE_UPDATE_REGISTRY_FORMAT,
|
|
28686
|
+
...value.promotionId ? { promotionId: value.promotionId } : {},
|
|
28621
28687
|
promotedAt: value.promotedAt,
|
|
28622
28688
|
...value.releaseId ? { releaseId: value.releaseId } : {},
|
|
28623
28689
|
rollout: value.rollout
|
|
@@ -28640,15 +28706,50 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
28640
28706
|
return true;
|
|
28641
28707
|
const value = createHash6("sha256").update(`${input.appId}\x00${input.channel}\x00${input.releaseId}\x00${input.installationId}`).digest().readUInt32BE(0);
|
|
28642
28708
|
return value / 4294967296 < input.rollout;
|
|
28709
|
+
}, 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) => {
|
|
28710
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0)
|
|
28711
|
+
throw new MobileUpdateRegistryError(`Mobile update health ${field} is invalid`);
|
|
28712
|
+
return value;
|
|
28713
|
+
}, parseHealthTransfer = (value) => {
|
|
28714
|
+
if (value === undefined)
|
|
28715
|
+
return;
|
|
28716
|
+
if (!object3(value))
|
|
28717
|
+
throw new MobileUpdateRegistryError("Mobile update health transfer is invalid");
|
|
28718
|
+
return {
|
|
28719
|
+
avoidedBytes: finiteMetric(value.avoidedBytes, "avoidedBytes"),
|
|
28720
|
+
downloadedBytes: finiteMetric(value.downloadedBytes, "downloadedBytes"),
|
|
28721
|
+
durationMs: finiteMetric(value.durationMs, "durationMs"),
|
|
28722
|
+
resumedBytes: finiteMetric(value.resumedBytes, "resumedBytes"),
|
|
28723
|
+
reusedBytes: finiteMetric(value.reusedBytes, "reusedBytes"),
|
|
28724
|
+
throughputBytesPerSecond: finiteMetric(value.throughputBytesPerSecond, "throughputBytesPerSecond")
|
|
28725
|
+
};
|
|
28643
28726
|
}, createMobileUpdateRegistry = (options) => {
|
|
28644
28727
|
const prefix = normalizedPrefix(options.prefix ?? DEFAULT_PREFIX);
|
|
28645
28728
|
const clock = options.clock ?? (() => new Date);
|
|
28729
|
+
const health = options.health;
|
|
28730
|
+
const rollout = options.rollout;
|
|
28731
|
+
if (health && health.secret.length < 32)
|
|
28732
|
+
throw new MobileUpdateRegistryError("Mobile update health secret must contain at least 32 characters");
|
|
28733
|
+
if (health && !options.store.list)
|
|
28734
|
+
throw new MobileUpdateRegistryError("Mobile update health requires storage lifecycle listing");
|
|
28735
|
+
if (rollout && (!health || !options.store.list))
|
|
28736
|
+
throw new MobileUpdateRegistryError("Mobile update rollout orchestration requires fleet health and storage lifecycle listing");
|
|
28737
|
+
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)))
|
|
28738
|
+
throw new MobileUpdateRegistryError("Mobile update rollout stages are invalid");
|
|
28739
|
+
const minimumReports = health?.autoPause?.minimumReports ?? 20;
|
|
28740
|
+
const failureThreshold = health?.autoPause?.failureRate ?? 0.2;
|
|
28741
|
+
if (health && (!Number.isSafeInteger(minimumReports) || minimumReports < 1 || failureThreshold <= 0 || failureThreshold > 1))
|
|
28742
|
+
throw new MobileUpdateRegistryError("Mobile update health auto-pause policy is invalid");
|
|
28646
28743
|
const root = (appId) => `${prefix}/${appHash(appId)}`;
|
|
28647
28744
|
const releaseRoot = (manifest) => `${root(manifest.appId)}/releases/${manifest.releaseId}`;
|
|
28648
28745
|
const manifestKey = (manifest) => `${releaseRoot(manifest)}/update.json`;
|
|
28649
28746
|
const fileKey = (manifest, file5) => `${releaseRoot(manifest)}/files/${file5.path}`;
|
|
28650
28747
|
const contentBlobKey = (appId, sha256) => `${root(appId)}/blobs/${sha256}`;
|
|
28651
28748
|
const tombstoneKey = (appId, releaseId) => `${root(appId)}/gc/${releaseId}.json`;
|
|
28749
|
+
const healthRoot = (appId, promotionId, releaseId) => `${root(appId)}/health/${promotionId}/${releaseId}`;
|
|
28750
|
+
const pauseKey = (appId, promotionId, releaseId) => `${healthRoot(appId, promotionId, releaseId)}/paused.json`;
|
|
28751
|
+
const rolloutRoot = (appId, promotionId, releaseId) => `${healthRoot(appId, promotionId, releaseId)}/rollout`;
|
|
28752
|
+
const rolloutPlanKey = (appId, promotionId, releaseId) => `${rolloutRoot(appId, promotionId, releaseId)}/plan.json`;
|
|
28652
28753
|
const channelKey = (appId, channel) => {
|
|
28653
28754
|
if (!APP_ID.test(appId) || !NAME.test(channel))
|
|
28654
28755
|
throw new MobileUpdateRegistryError("Mobile update channel identity is invalid");
|
|
@@ -28677,18 +28778,128 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
28677
28778
|
throw new MobileUpdateRegistryError("Stored mobile update channel identity changed");
|
|
28678
28779
|
return value;
|
|
28679
28780
|
};
|
|
28781
|
+
const listPrefix = async (value) => {
|
|
28782
|
+
const objects = [];
|
|
28783
|
+
const cursors = new Set;
|
|
28784
|
+
let cursor;
|
|
28785
|
+
do {
|
|
28786
|
+
const page = await options.store.list({
|
|
28787
|
+
...cursor ? { cursor } : {},
|
|
28788
|
+
prefix: value
|
|
28789
|
+
});
|
|
28790
|
+
objects.push(...page.objects);
|
|
28791
|
+
if (!page.truncated)
|
|
28792
|
+
break;
|
|
28793
|
+
if (!page.cursor || cursors.has(page.cursor))
|
|
28794
|
+
throw new MobileUpdateRegistryError("Mobile update storage returned an invalid cursor");
|
|
28795
|
+
cursors.add(page.cursor);
|
|
28796
|
+
cursor = page.cursor;
|
|
28797
|
+
} while (true);
|
|
28798
|
+
return objects;
|
|
28799
|
+
};
|
|
28800
|
+
const readVerifiedObject = async (key, label) => {
|
|
28801
|
+
const bytes = await options.store.get(key);
|
|
28802
|
+
if (!bytes)
|
|
28803
|
+
return null;
|
|
28804
|
+
const head = await options.store.head(key);
|
|
28805
|
+
if (!head || head.size !== bytes.byteLength || head.metadata?.sha256 !== digest(bytes))
|
|
28806
|
+
throw new MobileUpdateRegistryError(`Stored mobile update ${label} integrity failed`);
|
|
28807
|
+
return decode(bytes);
|
|
28808
|
+
};
|
|
28809
|
+
const parseRolloutPlan = (value, promotionId, releaseId) => {
|
|
28810
|
+
if (!object3(value) || value.format !== 1 || value.promotionId !== promotionId || value.releaseId !== releaseId || typeof value.automatic !== "boolean" || !iso(value.createdAt) || !Array.isArray(value.stages))
|
|
28811
|
+
throw new MobileUpdateRegistryError("Stored mobile update rollout plan is invalid");
|
|
28812
|
+
const stages = value.stages.map((stage) => {
|
|
28813
|
+
if (!object3(stage) || typeof stage.rollout !== "number" || typeof stage.maximumFailureRate !== "number" || !Number.isSafeInteger(stage.minimumReports) || !Number.isSafeInteger(stage.observationMs))
|
|
28814
|
+
throw new MobileUpdateRegistryError("Stored mobile update rollout plan is invalid");
|
|
28815
|
+
return {
|
|
28816
|
+
maximumFailureRate: stage.maximumFailureRate,
|
|
28817
|
+
minimumReports: stage.minimumReports,
|
|
28818
|
+
observationMs: stage.observationMs,
|
|
28819
|
+
rollout: stage.rollout
|
|
28820
|
+
};
|
|
28821
|
+
});
|
|
28822
|
+
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))
|
|
28823
|
+
throw new MobileUpdateRegistryError("Stored mobile update rollout plan is invalid");
|
|
28824
|
+
return {
|
|
28825
|
+
automatic: value.automatic,
|
|
28826
|
+
createdAt: value.createdAt,
|
|
28827
|
+
format: 1,
|
|
28828
|
+
promotionId,
|
|
28829
|
+
releaseId,
|
|
28830
|
+
stages
|
|
28831
|
+
};
|
|
28832
|
+
};
|
|
28833
|
+
const initializeRollout = async (channel, signal) => {
|
|
28834
|
+
if (!rollout || !channel.releaseId)
|
|
28835
|
+
return;
|
|
28836
|
+
if (!rollout.stages.some((stage) => stage.rollout === channel.rollout))
|
|
28837
|
+
throw new MobileUpdateRegistryError("Mobile update promotion rollout must match a configured rollout stage");
|
|
28838
|
+
const promotionId = healthPromotionId(channel);
|
|
28839
|
+
const plan = {
|
|
28840
|
+
automatic: rollout.automatic ?? false,
|
|
28841
|
+
createdAt: channel.promotedAt,
|
|
28842
|
+
format: 1,
|
|
28843
|
+
promotionId,
|
|
28844
|
+
releaseId: channel.releaseId,
|
|
28845
|
+
stages: rollout.stages.map((stage) => ({ ...stage }))
|
|
28846
|
+
};
|
|
28847
|
+
const bytes = json(plan);
|
|
28848
|
+
await options.store.put(rolloutPlanKey(channel.appId, promotionId, channel.releaseId), bytes, {
|
|
28849
|
+
cacheControl: "no-store",
|
|
28850
|
+
contentType: "application/json",
|
|
28851
|
+
maxBytes: bytes.byteLength,
|
|
28852
|
+
metadata: { releaseid: channel.releaseId, sha256: digest(bytes) },
|
|
28853
|
+
signal
|
|
28854
|
+
});
|
|
28855
|
+
};
|
|
28856
|
+
const signHealthToken = (payload) => {
|
|
28857
|
+
if (!health)
|
|
28858
|
+
return null;
|
|
28859
|
+
const encoded = base64Url(JSON.stringify(payload));
|
|
28860
|
+
const signature = createHmac("sha256", health.secret).update(encoded).digest("base64url");
|
|
28861
|
+
return `${encoded}.${signature}`;
|
|
28862
|
+
};
|
|
28863
|
+
const verifyHealthToken = (token) => {
|
|
28864
|
+
if (!health)
|
|
28865
|
+
throw new MobileUpdateRegistryError("Mobile update health reporting is not configured");
|
|
28866
|
+
const [encoded, provided, extra] = token.split(".");
|
|
28867
|
+
if (!encoded || !provided || extra)
|
|
28868
|
+
throw new MobileUpdateRegistryError("Mobile update health token is invalid");
|
|
28869
|
+
const expected = createHmac("sha256", health.secret).update(encoded).digest();
|
|
28870
|
+
let actual;
|
|
28871
|
+
try {
|
|
28872
|
+
actual = Buffer.from(provided, "base64url");
|
|
28873
|
+
} catch {
|
|
28874
|
+
actual = Buffer.alloc(0);
|
|
28875
|
+
}
|
|
28876
|
+
if (actual.byteLength !== expected.byteLength || !timingSafeEqual(actual, expected))
|
|
28877
|
+
throw new MobileUpdateRegistryError("Mobile update health token is invalid");
|
|
28878
|
+
let value;
|
|
28879
|
+
try {
|
|
28880
|
+
value = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
|
|
28881
|
+
} catch {
|
|
28882
|
+
throw new MobileUpdateRegistryError("Mobile update health token is invalid");
|
|
28883
|
+
}
|
|
28884
|
+
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")
|
|
28885
|
+
throw new MobileUpdateRegistryError("Mobile update health token is invalid");
|
|
28886
|
+
return value;
|
|
28887
|
+
};
|
|
28680
28888
|
const assertNotMarked = async (appId, releaseId) => {
|
|
28681
28889
|
if (!APP_ID.test(appId) || !RELEASE.test(releaseId))
|
|
28682
28890
|
throw new MobileUpdateRegistryError("Mobile update release identity is invalid");
|
|
28683
28891
|
if (await options.store.head(tombstoneKey(appId, releaseId)))
|
|
28684
28892
|
throw new MobileUpdateRegistryError("Mobile update release is marked for collection. Increase retention and apply garbage collection to restore it before promotion");
|
|
28685
28893
|
};
|
|
28686
|
-
const writeChannel = async (input, signal) => {
|
|
28894
|
+
const writeChannel = async (input, signal, beforeWrite) => {
|
|
28895
|
+
const promotedAt = clock().toISOString();
|
|
28687
28896
|
const value = {
|
|
28688
28897
|
...input,
|
|
28689
28898
|
format: MOBILE_UPDATE_REGISTRY_FORMAT,
|
|
28690
|
-
promotedAt
|
|
28899
|
+
promotedAt,
|
|
28900
|
+
promotionId: digest(new TextEncoder().encode(`${input.appId}\x00${input.channel}\x00${input.releaseId ?? "embedded"}\x00${promotedAt}\x00${randomUUID()}`))
|
|
28691
28901
|
};
|
|
28902
|
+
await beforeWrite?.(value);
|
|
28692
28903
|
const bytes = json(value);
|
|
28693
28904
|
await options.store.put(channelKey(value.appId, value.channel), bytes, {
|
|
28694
28905
|
cacheControl: "no-cache",
|
|
@@ -28703,10 +28914,92 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
28703
28914
|
});
|
|
28704
28915
|
return value;
|
|
28705
28916
|
};
|
|
28917
|
+
const rolloutContext = async (channel) => {
|
|
28918
|
+
if (!options.store.list || !channel.releaseId)
|
|
28919
|
+
return null;
|
|
28920
|
+
const promotionId = healthPromotionId(channel);
|
|
28921
|
+
const storedPlan = await readVerifiedObject(rolloutPlanKey(channel.appId, promotionId, channel.releaseId), "rollout plan");
|
|
28922
|
+
if (storedPlan === null)
|
|
28923
|
+
return null;
|
|
28924
|
+
const plan = parseRolloutPlan(storedPlan, promotionId, channel.releaseId);
|
|
28925
|
+
const initialStage = plan.stages.findIndex((stage) => stage.rollout === channel.rollout);
|
|
28926
|
+
if (initialStage < 0)
|
|
28927
|
+
throw new MobileUpdateRegistryError("Stored mobile update rollout does not match its plan");
|
|
28928
|
+
let currentStage = initialStage;
|
|
28929
|
+
let enteredAt = plan.createdAt;
|
|
28930
|
+
const advances = await listPrefix(`${rolloutRoot(channel.appId, promotionId, channel.releaseId)}/advances/`);
|
|
28931
|
+
for (const item of advances) {
|
|
28932
|
+
const value = await readVerifiedObject(item.key, "rollout advancement");
|
|
28933
|
+
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))
|
|
28934
|
+
throw new MobileUpdateRegistryError("Stored mobile update rollout advancement is invalid");
|
|
28935
|
+
if (Number(value.stage) >= currentStage) {
|
|
28936
|
+
currentStage = Number(value.stage);
|
|
28937
|
+
enteredAt = value.createdAt;
|
|
28938
|
+
}
|
|
28939
|
+
}
|
|
28940
|
+
const controls = await listPrefix(`${rolloutRoot(channel.appId, promotionId, channel.releaseId)}/controls/`);
|
|
28941
|
+
const parsedControls = [];
|
|
28942
|
+
for (const item of controls) {
|
|
28943
|
+
const value = await readVerifiedObject(item.key, "rollout control");
|
|
28944
|
+
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")))
|
|
28945
|
+
throw new MobileUpdateRegistryError("Stored mobile update rollout control is invalid");
|
|
28946
|
+
parsedControls.push(value);
|
|
28947
|
+
}
|
|
28948
|
+
const cancelled = parsedControls.some(({ action }) => action === "cancel");
|
|
28949
|
+
const resumedPauseIds = new Set(parsedControls.flatMap((control) => control.resumedPauseIds ?? []));
|
|
28950
|
+
const activePauseIds = parsedControls.filter(({ action, id }) => action === "pause" && !resumedPauseIds.has(id)).map(({ id }) => id);
|
|
28951
|
+
const operatorPaused = activePauseIds.length > 0;
|
|
28952
|
+
const fleetPaused = Boolean(await options.store.head(pauseKey(channel.appId, promotionId, channel.releaseId)));
|
|
28953
|
+
return {
|
|
28954
|
+
cancelled,
|
|
28955
|
+
activePauseIds,
|
|
28956
|
+
channel,
|
|
28957
|
+
currentStage,
|
|
28958
|
+
enteredAt,
|
|
28959
|
+
fleetPaused,
|
|
28960
|
+
operatorPaused,
|
|
28961
|
+
plan,
|
|
28962
|
+
promotionId,
|
|
28963
|
+
rollout: plan.stages[currentStage].rollout
|
|
28964
|
+
};
|
|
28965
|
+
};
|
|
28966
|
+
const writeRolloutControl = async (channel, action, signal) => {
|
|
28967
|
+
const context = await rolloutContext(channel);
|
|
28968
|
+
if (!context)
|
|
28969
|
+
throw new MobileUpdateRegistryError("Mobile update rollout orchestration is not configured");
|
|
28970
|
+
if (context.cancelled)
|
|
28971
|
+
throw new MobileUpdateRegistryError("Mobile update rollout was already cancelled");
|
|
28972
|
+
if (action === "resume" && context.fleetPaused)
|
|
28973
|
+
throw new MobileUpdateRegistryError("A fleet-health pause requires an explicit re-promotion");
|
|
28974
|
+
const releaseId = channel.releaseId;
|
|
28975
|
+
if (!releaseId)
|
|
28976
|
+
throw new MobileUpdateRegistryError("Mobile update channel does not have an active release");
|
|
28977
|
+
const createdAt = clock().toISOString();
|
|
28978
|
+
const id = randomUUID();
|
|
28979
|
+
const event = {
|
|
28980
|
+
action,
|
|
28981
|
+
createdAt,
|
|
28982
|
+
format: 1,
|
|
28983
|
+
id,
|
|
28984
|
+
promotionId: context.promotionId,
|
|
28985
|
+
releaseId,
|
|
28986
|
+
...action === "resume" ? { resumedPauseIds: context.activePauseIds } : {}
|
|
28987
|
+
};
|
|
28988
|
+
const bytes = json(event);
|
|
28989
|
+
await options.store.put(`${rolloutRoot(channel.appId, context.promotionId, releaseId)}/controls/${createdAt}-${id}-${action}.json`, bytes, {
|
|
28990
|
+
cacheControl: "no-store",
|
|
28991
|
+
contentType: "application/json",
|
|
28992
|
+
maxBytes: bytes.byteLength,
|
|
28993
|
+
metadata: { action, sha256: digest(bytes) },
|
|
28994
|
+
signal
|
|
28995
|
+
});
|
|
28996
|
+
};
|
|
28706
28997
|
const promoteUpdate = async (input) => {
|
|
28707
28998
|
input.signal?.throwIfAborted();
|
|
28708
28999
|
if (input.rollout <= 0 || input.rollout > 1)
|
|
28709
29000
|
throw new MobileUpdateRegistryError("Mobile update rollout is invalid");
|
|
29001
|
+
if (rollout && !rollout.stages.some((stage) => stage.rollout === input.rollout))
|
|
29002
|
+
throw new MobileUpdateRegistryError("Mobile update promotion rollout must match a configured rollout stage");
|
|
28710
29003
|
await assertNotMarked(input.appId, input.releaseId);
|
|
28711
29004
|
const release = await readManifest2(input.appId, input.releaseId);
|
|
28712
29005
|
if (!release || release.manifest.channel !== input.channel)
|
|
@@ -28718,7 +29011,7 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
28718
29011
|
...existing?.releaseId && existing.releaseId !== input.releaseId ? { fallbackReleaseId: existing.releaseId } : existing?.fallbackReleaseId ? { fallbackReleaseId: existing.fallbackReleaseId } : {},
|
|
28719
29012
|
releaseId: input.releaseId,
|
|
28720
29013
|
rollout: input.rollout
|
|
28721
|
-
}, input.signal);
|
|
29014
|
+
}, input.signal, (channel) => initializeRollout(channel, input.signal));
|
|
28722
29015
|
return {
|
|
28723
29016
|
appId: input.appId,
|
|
28724
29017
|
channel: input.channel,
|
|
@@ -28731,13 +29024,16 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
28731
29024
|
const channel = await readChannel(input.appId, input.channel);
|
|
28732
29025
|
if (!channel?.releaseId)
|
|
28733
29026
|
return { status: "empty" };
|
|
28734
|
-
const
|
|
29027
|
+
const rolloutState = await rolloutContext(channel);
|
|
29028
|
+
let selected = rolloutMember({
|
|
28735
29029
|
appId: input.appId,
|
|
28736
29030
|
channel: input.channel,
|
|
28737
29031
|
installationId: input.installationId,
|
|
28738
29032
|
releaseId: channel.releaseId,
|
|
28739
|
-
rollout: channel.rollout
|
|
29033
|
+
rollout: rolloutState?.rollout ?? channel.rollout
|
|
28740
29034
|
}) ? channel.releaseId : channel.fallbackReleaseId;
|
|
29035
|
+
if (selected === channel.releaseId && (rolloutState?.cancelled || rolloutState?.fleetPaused || rolloutState?.operatorPaused || health && await options.store.head(pauseKey(input.appId, healthPromotionId(channel), channel.releaseId))))
|
|
29036
|
+
selected = channel.fallbackReleaseId;
|
|
28741
29037
|
if (!selected)
|
|
28742
29038
|
return { status: "empty" };
|
|
28743
29039
|
const release = await readManifest2(input.appId, selected);
|
|
@@ -28753,6 +29049,243 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
28753
29049
|
status: "selected"
|
|
28754
29050
|
};
|
|
28755
29051
|
};
|
|
29052
|
+
const issueUpdateHealthToken = async (input) => {
|
|
29053
|
+
if (!health)
|
|
29054
|
+
return null;
|
|
29055
|
+
const channel = await readChannel(input.appId, input.channel);
|
|
29056
|
+
if (!channel?.releaseId || channel.releaseId !== input.releaseId)
|
|
29057
|
+
return null;
|
|
29058
|
+
const resolution = await resolveUpdateState(input);
|
|
29059
|
+
if (resolution.status !== "selected" || resolution.manifest.releaseId !== input.releaseId)
|
|
29060
|
+
return null;
|
|
29061
|
+
return signHealthToken({
|
|
29062
|
+
appId: input.appId,
|
|
29063
|
+
channel: input.channel,
|
|
29064
|
+
format: HEALTH_TOKEN_VERSION,
|
|
29065
|
+
installationId: input.installationId,
|
|
29066
|
+
promotionId: healthPromotionId(channel),
|
|
29067
|
+
releaseId: input.releaseId,
|
|
29068
|
+
runtimeFingerprint: input.runtimeFingerprint
|
|
29069
|
+
});
|
|
29070
|
+
};
|
|
29071
|
+
const inspectUpdateHealth = async (input) => {
|
|
29072
|
+
if (!health)
|
|
29073
|
+
throw new MobileUpdateRegistryError("Mobile update health reporting is not configured");
|
|
29074
|
+
const channel = await readChannel(input.appId, input.channel);
|
|
29075
|
+
const releaseId = input.releaseId ?? channel?.releaseId;
|
|
29076
|
+
if (!channel || !releaseId || channel.releaseId !== releaseId)
|
|
29077
|
+
return null;
|
|
29078
|
+
const promotionId = healthPromotionId(channel);
|
|
29079
|
+
const objects = await listPrefix(`${healthRoot(input.appId, promotionId, releaseId)}/events/`);
|
|
29080
|
+
const installations = new Set;
|
|
29081
|
+
const byKind = new Map([...HEALTH_KINDS].map((kind) => [
|
|
29082
|
+
kind,
|
|
29083
|
+
new Set
|
|
29084
|
+
]));
|
|
29085
|
+
const transfer = {
|
|
29086
|
+
avoidedBytes: 0,
|
|
29087
|
+
downloadedBytes: 0,
|
|
29088
|
+
durationMs: 0,
|
|
29089
|
+
resumedBytes: 0,
|
|
29090
|
+
reusedBytes: 0,
|
|
29091
|
+
throughputBytesPerSecond: 0
|
|
29092
|
+
};
|
|
29093
|
+
for (const item of objects) {
|
|
29094
|
+
const bytes = await options.store.get(item.key);
|
|
29095
|
+
if (!bytes)
|
|
29096
|
+
continue;
|
|
29097
|
+
const head = await options.store.head(item.key);
|
|
29098
|
+
if (!head || head.size !== bytes.byteLength || head.metadata?.sha256 !== digest(bytes))
|
|
29099
|
+
throw new MobileUpdateRegistryError("Stored mobile update health evidence integrity failed");
|
|
29100
|
+
const value = decode(bytes);
|
|
29101
|
+
if (!object3(value) || typeof value.installationHash !== "string" || !HEALTH_KINDS.has(String(value.kind)))
|
|
29102
|
+
throw new MobileUpdateRegistryError("Stored mobile update health evidence is invalid");
|
|
29103
|
+
const kind = value.kind;
|
|
29104
|
+
installations.add(value.installationHash);
|
|
29105
|
+
byKind.get(kind).add(value.installationHash);
|
|
29106
|
+
if (kind === "downloaded" && object3(value.transfer)) {
|
|
29107
|
+
const parsed = parseHealthTransfer(value.transfer);
|
|
29108
|
+
for (const key of Object.keys(transfer))
|
|
29109
|
+
transfer[key] += parsed[key];
|
|
29110
|
+
}
|
|
29111
|
+
}
|
|
29112
|
+
const failures = new Set([
|
|
29113
|
+
...byKind.get("quarantined"),
|
|
29114
|
+
...byKind.get("rolled-back")
|
|
29115
|
+
]);
|
|
29116
|
+
const terminals = new Set([...byKind.get("activated"), ...failures]);
|
|
29117
|
+
const failureRate = terminals.size === 0 ? 0 : failures.size / terminals.size;
|
|
29118
|
+
const rolloutState = await rolloutContext(channel);
|
|
29119
|
+
return {
|
|
29120
|
+
activated: byKind.get("activated").size,
|
|
29121
|
+
appId: input.appId,
|
|
29122
|
+
channel: input.channel,
|
|
29123
|
+
downloaded: byKind.get("downloaded").size,
|
|
29124
|
+
downloadFailed: byKind.get("download-failed").size,
|
|
29125
|
+
failureRate,
|
|
29126
|
+
failures: failures.size,
|
|
29127
|
+
paused: Boolean(rolloutState?.cancelled || rolloutState?.fleetPaused || rolloutState?.operatorPaused || await options.store.head(pauseKey(input.appId, promotionId, releaseId))),
|
|
29128
|
+
promotionId,
|
|
29129
|
+
quarantined: byKind.get("quarantined").size,
|
|
29130
|
+
releaseId,
|
|
29131
|
+
reportedInstallations: installations.size,
|
|
29132
|
+
rolledBack: byKind.get("rolled-back").size,
|
|
29133
|
+
rollout: rolloutState?.rollout ?? channel.rollout,
|
|
29134
|
+
terminalReports: terminals.size,
|
|
29135
|
+
transfer
|
|
29136
|
+
};
|
|
29137
|
+
};
|
|
29138
|
+
const inspectUpdateRollout = async (input) => {
|
|
29139
|
+
const channel = await readChannel(input.appId, input.channel);
|
|
29140
|
+
if (!channel?.releaseId)
|
|
29141
|
+
return null;
|
|
29142
|
+
const context = await rolloutContext(channel);
|
|
29143
|
+
if (!context)
|
|
29144
|
+
throw new MobileUpdateRegistryError("Mobile update rollout orchestration is not configured");
|
|
29145
|
+
const healthReport = await inspectUpdateHealth(input);
|
|
29146
|
+
if (!healthReport)
|
|
29147
|
+
return null;
|
|
29148
|
+
const paused = context.fleetPaused || context.operatorPaused;
|
|
29149
|
+
const complete = context.currentStage === context.plan.stages.length - 1;
|
|
29150
|
+
return {
|
|
29151
|
+
...healthReport,
|
|
29152
|
+
automatic: context.plan.automatic,
|
|
29153
|
+
currentStage: context.currentStage,
|
|
29154
|
+
enteredAt: context.enteredAt,
|
|
29155
|
+
...!complete ? { nextStage: context.plan.stages[context.currentStage + 1] } : {},
|
|
29156
|
+
...context.fleetPaused ? { pausedBy: "fleet-health" } : context.operatorPaused ? { pausedBy: "operator" } : {},
|
|
29157
|
+
status: context.cancelled ? "cancelled" : paused ? "paused" : complete ? "complete" : "active"
|
|
29158
|
+
};
|
|
29159
|
+
};
|
|
29160
|
+
const advanceRollout = async (input, strict) => {
|
|
29161
|
+
input.signal?.throwIfAborted();
|
|
29162
|
+
const channel = await readChannel(input.appId, input.channel);
|
|
29163
|
+
if (!channel?.releaseId)
|
|
29164
|
+
throw new MobileUpdateRegistryError("Mobile update channel does not have an active release");
|
|
29165
|
+
const context = await rolloutContext(channel);
|
|
29166
|
+
if (!context)
|
|
29167
|
+
throw new MobileUpdateRegistryError("Mobile update rollout orchestration is not configured");
|
|
29168
|
+
const report = await inspectUpdateRollout(input);
|
|
29169
|
+
if (!report)
|
|
29170
|
+
throw new MobileUpdateRegistryError("Mobile update rollout report is unavailable");
|
|
29171
|
+
const nextStage = context.plan.stages[context.currentStage + 1];
|
|
29172
|
+
if (!nextStage)
|
|
29173
|
+
return report;
|
|
29174
|
+
if (input.rollout !== undefined && input.rollout !== nextStage.rollout)
|
|
29175
|
+
throw new MobileUpdateRegistryError("Mobile update rollout can advance only to the next configured stage");
|
|
29176
|
+
if (report.status !== "active") {
|
|
29177
|
+
if (strict)
|
|
29178
|
+
throw new MobileUpdateRegistryError(`Mobile update rollout cannot advance while ${report.status}`);
|
|
29179
|
+
return report;
|
|
29180
|
+
}
|
|
29181
|
+
const gate = context.plan.stages[context.currentStage];
|
|
29182
|
+
const observedMs = clock().getTime() - Date.parse(context.enteredAt);
|
|
29183
|
+
const blocked = report.terminalReports < gate.minimumReports || report.failureRate > gate.maximumFailureRate || observedMs < gate.observationMs;
|
|
29184
|
+
if (blocked) {
|
|
29185
|
+
if (strict)
|
|
29186
|
+
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`);
|
|
29187
|
+
return report;
|
|
29188
|
+
}
|
|
29189
|
+
const stage = context.currentStage + 1;
|
|
29190
|
+
const event = {
|
|
29191
|
+
createdAt: clock().toISOString(),
|
|
29192
|
+
failureRate: report.failureRate,
|
|
29193
|
+
format: 1,
|
|
29194
|
+
promotionId: context.promotionId,
|
|
29195
|
+
releaseId: channel.releaseId,
|
|
29196
|
+
rollout: nextStage.rollout,
|
|
29197
|
+
stage,
|
|
29198
|
+
terminalReports: report.terminalReports
|
|
29199
|
+
};
|
|
29200
|
+
const bytes = json(event);
|
|
29201
|
+
await options.store.put(`${rolloutRoot(input.appId, context.promotionId, channel.releaseId)}/advances/${String(stage).padStart(4, "0")}.json`, bytes, {
|
|
29202
|
+
cacheControl: "no-store",
|
|
29203
|
+
contentType: "application/json",
|
|
29204
|
+
maxBytes: bytes.byteLength,
|
|
29205
|
+
metadata: { releaseid: channel.releaseId, sha256: digest(bytes) },
|
|
29206
|
+
signal: input.signal
|
|
29207
|
+
});
|
|
29208
|
+
return await inspectUpdateRollout(input);
|
|
29209
|
+
};
|
|
29210
|
+
const advanceUpdateRollout = (input) => advanceRollout(input, true);
|
|
29211
|
+
const reconcileUpdateRollout = async (input) => {
|
|
29212
|
+
const report = await inspectUpdateRollout(input);
|
|
29213
|
+
if (!report || !report.automatic)
|
|
29214
|
+
return report;
|
|
29215
|
+
return advanceRollout(input, false);
|
|
29216
|
+
};
|
|
29217
|
+
const rolloutControl = (action) => async (input) => {
|
|
29218
|
+
input.signal?.throwIfAborted();
|
|
29219
|
+
const channel = await readChannel(input.appId, input.channel);
|
|
29220
|
+
if (!channel?.releaseId)
|
|
29221
|
+
throw new MobileUpdateRegistryError("Mobile update channel does not have an active release");
|
|
29222
|
+
await writeRolloutControl(channel, action, input.signal);
|
|
29223
|
+
return await inspectUpdateRollout(input);
|
|
29224
|
+
};
|
|
29225
|
+
const pauseUpdateRollout = rolloutControl("pause");
|
|
29226
|
+
const resumeUpdateRollout = rolloutControl("resume");
|
|
29227
|
+
const cancelUpdateRollout = rolloutControl("cancel");
|
|
29228
|
+
const recordUpdateHealth = async (input) => {
|
|
29229
|
+
const payload = verifyHealthToken(input.token);
|
|
29230
|
+
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")
|
|
29231
|
+
throw new MobileUpdateRegistryError("Mobile update health evidence does not match its token");
|
|
29232
|
+
const release = await readManifest2(input.appId, input.releaseId);
|
|
29233
|
+
if (!release || release.manifest.runtimeFingerprint !== input.runtimeFingerprint)
|
|
29234
|
+
throw new MobileUpdateRegistryError("Mobile update health release is invalid");
|
|
29235
|
+
const activeChannel = await readChannel(input.appId, input.channel);
|
|
29236
|
+
if (!activeChannel || activeChannel.releaseId !== input.releaseId || healthPromotionId(activeChannel) !== payload.promotionId)
|
|
29237
|
+
throw new MobileUpdateRegistryError("Mobile update health promotion is no longer active");
|
|
29238
|
+
const transfer = parseHealthTransfer(input.transfer);
|
|
29239
|
+
const installationHash = createHmac("sha256", health.secret).update(input.installationId).digest("hex");
|
|
29240
|
+
const evidence = {
|
|
29241
|
+
format: 1,
|
|
29242
|
+
installationHash,
|
|
29243
|
+
kind: input.kind,
|
|
29244
|
+
observedAt: clock().toISOString(),
|
|
29245
|
+
...input.reason ? { reason: input.reason } : {},
|
|
29246
|
+
...transfer ? { transfer } : {}
|
|
29247
|
+
};
|
|
29248
|
+
const bytes = json(evidence);
|
|
29249
|
+
await options.store.put(`${healthRoot(input.appId, payload.promotionId, input.releaseId)}/events/${installationHash}/${input.kind}.json`, bytes, {
|
|
29250
|
+
cacheControl: "no-store",
|
|
29251
|
+
contentType: "application/json",
|
|
29252
|
+
maxBytes: bytes.byteLength,
|
|
29253
|
+
metadata: { kind: input.kind, sha256: digest(bytes) }
|
|
29254
|
+
});
|
|
29255
|
+
let report = await inspectUpdateHealth({
|
|
29256
|
+
appId: input.appId,
|
|
29257
|
+
channel: input.channel,
|
|
29258
|
+
releaseId: input.releaseId
|
|
29259
|
+
});
|
|
29260
|
+
if (!report)
|
|
29261
|
+
throw new MobileUpdateRegistryError("Mobile update health promotion is no longer active");
|
|
29262
|
+
if (FAILURE_HEALTH_KINDS.has(input.kind) && report.terminalReports >= minimumReports && report.failureRate >= failureThreshold && !report.paused) {
|
|
29263
|
+
const marker = json({
|
|
29264
|
+
appId: input.appId,
|
|
29265
|
+
channel: input.channel,
|
|
29266
|
+
failureRate: report.failureRate,
|
|
29267
|
+
failures: report.failures,
|
|
29268
|
+
format: 1,
|
|
29269
|
+
pausedAt: clock().toISOString(),
|
|
29270
|
+
promotionId: payload.promotionId,
|
|
29271
|
+
releaseId: input.releaseId,
|
|
29272
|
+
reports: report.terminalReports
|
|
29273
|
+
});
|
|
29274
|
+
await options.store.put(pauseKey(input.appId, payload.promotionId, input.releaseId), marker, {
|
|
29275
|
+
cacheControl: "no-store",
|
|
29276
|
+
contentType: "application/json",
|
|
29277
|
+
maxBytes: marker.byteLength,
|
|
29278
|
+
metadata: { releaseid: input.releaseId, sha256: digest(marker) }
|
|
29279
|
+
});
|
|
29280
|
+
report = { ...report, paused: true };
|
|
29281
|
+
}
|
|
29282
|
+
if (rollout && (input.kind === "activated" || FAILURE_HEALTH_KINDS.has(input.kind)))
|
|
29283
|
+
return await reconcileUpdateRollout({
|
|
29284
|
+
appId: input.appId,
|
|
29285
|
+
channel: input.channel
|
|
29286
|
+
}) ?? report;
|
|
29287
|
+
return report;
|
|
29288
|
+
};
|
|
28756
29289
|
const retentionValues = (input) => {
|
|
28757
29290
|
if (!APP_ID.test(input.appId))
|
|
28758
29291
|
throw new MobileUpdateRegistryError("Mobile update appId is invalid");
|
|
@@ -28995,6 +29528,15 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
28995
29528
|
return result;
|
|
28996
29529
|
};
|
|
28997
29530
|
return {
|
|
29531
|
+
...health ? { inspectUpdateHealth, issueUpdateHealthToken, recordUpdateHealth } : {},
|
|
29532
|
+
...rollout ? {
|
|
29533
|
+
advanceUpdateRollout,
|
|
29534
|
+
cancelUpdateRollout,
|
|
29535
|
+
inspectUpdateRollout,
|
|
29536
|
+
pauseUpdateRollout,
|
|
29537
|
+
reconcileUpdateRollout,
|
|
29538
|
+
resumeUpdateRollout
|
|
29539
|
+
} : {},
|
|
28998
29540
|
inspectUpdateStorage: async (input) => (await inventory(input)).report,
|
|
28999
29541
|
pruneUpdates,
|
|
29000
29542
|
publishUpdate: async (input) => {
|
|
@@ -29272,7 +29814,7 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
29272
29814
|
const origin = request.headers.get("origin");
|
|
29273
29815
|
const cors = origin && allowedOrigins.has(origin) ? {
|
|
29274
29816
|
"access-control-allow-origin": origin,
|
|
29275
|
-
"access-control-expose-headers": "content-range,etag",
|
|
29817
|
+
"access-control-expose-headers": "content-range,etag,x-absolute-mobile-health-token",
|
|
29276
29818
|
vary: "Origin"
|
|
29277
29819
|
} : {};
|
|
29278
29820
|
if (request.method === "OPTIONS") {
|
|
@@ -29281,17 +29823,58 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
29281
29823
|
return new Response(null, {
|
|
29282
29824
|
headers: {
|
|
29283
29825
|
...cors,
|
|
29284
|
-
"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",
|
|
29285
|
-
"access-control-allow-methods": "GET,OPTIONS",
|
|
29826
|
+
"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",
|
|
29827
|
+
"access-control-allow-methods": "GET,POST,OPTIONS",
|
|
29286
29828
|
"access-control-max-age": "600"
|
|
29287
29829
|
},
|
|
29288
29830
|
status: 204
|
|
29289
29831
|
});
|
|
29290
29832
|
}
|
|
29291
|
-
if (request.method !== "GET")
|
|
29292
|
-
return new Response(null, { status: 405 });
|
|
29293
29833
|
const pathname = new URL(request.url).pathname.replace(/^\/+/, "");
|
|
29294
29834
|
const relative19 = pathname.startsWith(`${route}/`) ? pathname.slice(route.length + 1) : "";
|
|
29835
|
+
if (request.method === "POST" && relative19 === "health") {
|
|
29836
|
+
if (!options.registry.recordUpdateHealth)
|
|
29837
|
+
return new Response(null, { status: 404 });
|
|
29838
|
+
const appId = request.headers.get("x-absolute-mobile-app");
|
|
29839
|
+
const channel = request.headers.get("x-absolute-mobile-channel");
|
|
29840
|
+
const installationId = request.headers.get("x-absolute-mobile-installation");
|
|
29841
|
+
const runtimeFingerprint = request.headers.get("x-absolute-mobile-runtime");
|
|
29842
|
+
const token = request.headers.get("x-absolute-mobile-health-token");
|
|
29843
|
+
const declared = Number(request.headers.get("content-length"));
|
|
29844
|
+
if (appId !== options.appId || channel !== options.channel || !installationId || !runtimeFingerprint || !token || Number.isFinite(declared) && declared > 4096)
|
|
29845
|
+
return new Response(null, { status: 400 });
|
|
29846
|
+
const bodyBytes = new Uint8Array(await request.arrayBuffer());
|
|
29847
|
+
if (bodyBytes.byteLength > 4096)
|
|
29848
|
+
return new Response(null, { status: 413 });
|
|
29849
|
+
let body;
|
|
29850
|
+
try {
|
|
29851
|
+
body = JSON.parse(new TextDecoder().decode(bodyBytes));
|
|
29852
|
+
} catch {
|
|
29853
|
+
return new Response(null, { status: 400 });
|
|
29854
|
+
}
|
|
29855
|
+
if (!object3(body) || typeof body.releaseId !== "string" || typeof body.kind !== "string")
|
|
29856
|
+
return new Response(null, { status: 400 });
|
|
29857
|
+
try {
|
|
29858
|
+
const report = await options.registry.recordUpdateHealth({
|
|
29859
|
+
appId,
|
|
29860
|
+
channel,
|
|
29861
|
+
installationId,
|
|
29862
|
+
kind: body.kind,
|
|
29863
|
+
...body.reason === "boot-interrupted" || body.reason === "boot-timeout" ? { reason: body.reason } : {},
|
|
29864
|
+
releaseId: body.releaseId,
|
|
29865
|
+
runtimeFingerprint,
|
|
29866
|
+
token,
|
|
29867
|
+
...object3(body.transfer) ? { transfer: body.transfer } : {}
|
|
29868
|
+
});
|
|
29869
|
+
return Response.json({ paused: report.paused }, { headers: { ...cors, "cache-control": "no-store" }, status: 202 });
|
|
29870
|
+
} catch (error) {
|
|
29871
|
+
if (error instanceof MobileUpdateRegistryError)
|
|
29872
|
+
return new Response(null, { status: 403 });
|
|
29873
|
+
throw error;
|
|
29874
|
+
}
|
|
29875
|
+
}
|
|
29876
|
+
if (request.method !== "GET")
|
|
29877
|
+
return new Response(null, { status: 405 });
|
|
29295
29878
|
if (relative19 === "update.json") {
|
|
29296
29879
|
const expoProtocolVersion = request.headers.get("expo-protocol-version");
|
|
29297
29880
|
const expoProtocol = expoProtocolVersion !== null;
|
|
@@ -29317,6 +29900,13 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
29317
29900
|
installationId,
|
|
29318
29901
|
runtimeFingerprint
|
|
29319
29902
|
});
|
|
29903
|
+
const healthToken = selected && options.registry.issueUpdateHealthToken ? await options.registry.issueUpdateHealthToken({
|
|
29904
|
+
appId,
|
|
29905
|
+
channel,
|
|
29906
|
+
installationId,
|
|
29907
|
+
releaseId: selected.manifest.releaseId,
|
|
29908
|
+
runtimeFingerprint
|
|
29909
|
+
}) : null;
|
|
29320
29910
|
if (expoProtocol) {
|
|
29321
29911
|
let requestedCodeSigning;
|
|
29322
29912
|
try {
|
|
@@ -29368,6 +29958,7 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
29368
29958
|
extra: {
|
|
29369
29959
|
absolutejs: {
|
|
29370
29960
|
channel: selected.manifest.channel,
|
|
29961
|
+
...healthToken ? { healthToken } : {},
|
|
29371
29962
|
releaseId: selected.manifest.releaseId
|
|
29372
29963
|
},
|
|
29373
29964
|
expoClient: descriptor.expoConfig
|
|
@@ -29395,7 +29986,8 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
29395
29986
|
headers: {
|
|
29396
29987
|
...cors,
|
|
29397
29988
|
"cache-control": "no-store",
|
|
29398
|
-
etag: `"${selected.manifest.releaseId}"
|
|
29989
|
+
etag: `"${selected.manifest.releaseId}"`,
|
|
29990
|
+
...healthToken ? { "x-absolute-mobile-health-token": healthToken } : {}
|
|
29399
29991
|
}
|
|
29400
29992
|
});
|
|
29401
29993
|
}
|
|
@@ -29458,6 +30050,14 @@ var init_mobileUpdate = __esm(() => {
|
|
|
29458
30050
|
RELEASE = /^amu_[a-f0-9]{64}$/;
|
|
29459
30051
|
APP_ID = /^[A-Za-z][\w]*(?:\.[A-Za-z][\w]*)+$/;
|
|
29460
30052
|
NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
30053
|
+
HEALTH_KINDS = new Set([
|
|
30054
|
+
"activated",
|
|
30055
|
+
"downloaded",
|
|
30056
|
+
"download-failed",
|
|
30057
|
+
"quarantined",
|
|
30058
|
+
"rolled-back"
|
|
30059
|
+
]);
|
|
30060
|
+
FAILURE_HEALTH_KINDS = new Set(["quarantined", "rolled-back"]);
|
|
29461
30061
|
MobileUpdateRegistryError = class MobileUpdateRegistryError extends Error {
|
|
29462
30062
|
};
|
|
29463
30063
|
expoProtocolHeaders = {
|
|
@@ -29545,6 +30145,16 @@ var ABSOLUTE_MOBILE_UPDATE_SERVER_FORMAT = 1, DEFAULT_MOBILE_UPDATE_REGISTRY_MOD
|
|
|
29545
30145
|
} catch (error) {
|
|
29546
30146
|
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 });
|
|
29547
30147
|
}
|
|
30148
|
+
}, verifyHealthModule = (config, module) => {
|
|
30149
|
+
if (!config.updateServer?.health)
|
|
30150
|
+
return;
|
|
30151
|
+
if (typeof module.registry.inspectUpdateHealth !== "function" || typeof module.registry.issueUpdateHealthToken !== "function" || typeof module.registry.recordUpdateHealth !== "function")
|
|
30152
|
+
throw new TypeError("Mobile update fleet health is enabled but the registry is not provisioned for it. Run `absolute mobile update provision --force`.");
|
|
30153
|
+
}, verifyRolloutModule = (config, module) => {
|
|
30154
|
+
if (!config.updateServer?.rollout)
|
|
30155
|
+
return;
|
|
30156
|
+
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")
|
|
30157
|
+
throw new TypeError("Mobile update rollout orchestration is enabled but the registry is not provisioned for it. Run `absolute mobile update provision --force`.");
|
|
29548
30158
|
}, expoSigningOptions = (config) => {
|
|
29549
30159
|
if (!config.updates?.expoCodeSigning)
|
|
29550
30160
|
return;
|
|
@@ -29576,8 +30186,11 @@ var ABSOLUTE_MOBILE_UPDATE_SERVER_FORMAT = 1, DEFAULT_MOBILE_UPDATE_REGISTRY_MOD
|
|
|
29576
30186
|
if (!updates || !server?.autoMount)
|
|
29577
30187
|
return new Elysia5({ name: "absolutejs-mobile-updates-disabled" });
|
|
29578
30188
|
const module = await loadAbsoluteMobileUpdateServerModule(projectRoot, server.registryModule);
|
|
29579
|
-
if (options.production)
|
|
30189
|
+
if (options.production) {
|
|
29580
30190
|
await verifyDurableModule(module);
|
|
30191
|
+
verifyHealthModule(config, module);
|
|
30192
|
+
verifyRolloutModule(config, module);
|
|
30193
|
+
}
|
|
29581
30194
|
const manifest = new URL(updates.manifestUrl);
|
|
29582
30195
|
if (!manifest.pathname.endsWith("/update.json"))
|
|
29583
30196
|
throw new TypeError("Auto-mounted mobile update manifests must end in /update.json.");
|
|
@@ -29596,10 +30209,12 @@ var ABSOLUTE_MOBILE_UPDATE_SERVER_FORMAT = 1, DEFAULT_MOBILE_UPDATE_REGISTRY_MOD
|
|
|
29596
30209
|
return;
|
|
29597
30210
|
const module = await loadAbsoluteMobileUpdateServerModule(projectRoot, config.updateServer?.registryModule);
|
|
29598
30211
|
await verifyDurableModule(module);
|
|
30212
|
+
verifyHealthModule(config, module);
|
|
30213
|
+
verifyRolloutModule(config, module);
|
|
29599
30214
|
if (config.engine === "expo")
|
|
29600
30215
|
expoSigningOptions(config);
|
|
29601
30216
|
return module.metadata;
|
|
29602
|
-
}, publicKeysSource = (publicKeys) => JSON.stringify(publicKeys, null, "\t"),
|
|
30217
|
+
}, publicKeysSource = (publicKeys) => JSON.stringify(publicKeys, null, "\t"), renderAbsoluteMobileUpdateRegistryBase = (options) => {
|
|
29603
30218
|
const metadata = `export const absoluteMobileUpdateServer = {
|
|
29604
30219
|
format: 1,
|
|
29605
30220
|
provider: '${options.storage}',
|
|
@@ -29665,6 +30280,27 @@ export default createMobileUpdateRegistry({
|
|
|
29665
30280
|
store
|
|
29666
30281
|
});
|
|
29667
30282
|
`;
|
|
30283
|
+
}, renderAbsoluteMobileUpdateRegistry = (options) => {
|
|
30284
|
+
const source = renderAbsoluteMobileUpdateRegistryBase(options);
|
|
30285
|
+
let generated = "";
|
|
30286
|
+
if (options.health) {
|
|
30287
|
+
const secret = options.storage === "local" ? `process.env.${options.health.secretEnv} ?? 'absolutejs-local-health-secret-not-for-production'` : `required('${options.health.secretEnv}')`;
|
|
30288
|
+
generated += ` health: {
|
|
30289
|
+
autoPause: { failureRate: ${options.health.failureRate}, minimumReports: ${options.health.minimumReports} },
|
|
30290
|
+
secret: ${secret}
|
|
30291
|
+
},
|
|
30292
|
+
`;
|
|
30293
|
+
}
|
|
30294
|
+
if (options.rollout)
|
|
30295
|
+
generated += ` rollout: ${JSON.stringify(options.rollout, null, "\t").replaceAll(`
|
|
30296
|
+
`, `
|
|
30297
|
+
`)},
|
|
30298
|
+
`;
|
|
30299
|
+
if (!generated)
|
|
30300
|
+
return source;
|
|
30301
|
+
return source.replace(`export default createMobileUpdateRegistry({
|
|
30302
|
+
`, `export default createMobileUpdateRegistry({
|
|
30303
|
+
${generated}`);
|
|
29668
30304
|
}, writeAbsoluteMobileUpdateRegistry = async (options) => {
|
|
29669
30305
|
const path2 = projectPath(options.projectRoot, options.modulePath ?? DEFAULT_MOBILE_UPDATE_REGISTRY_MODULE);
|
|
29670
30306
|
if (!options.force) {
|
|
@@ -29676,6 +30312,8 @@ export default createMobileUpdateRegistry({
|
|
|
29676
30312
|
}
|
|
29677
30313
|
await mkdir13(dirname31(path2), { recursive: true });
|
|
29678
30314
|
await Bun.write(path2, renderAbsoluteMobileUpdateRegistry({
|
|
30315
|
+
...options.health ? { health: options.health } : {},
|
|
30316
|
+
...options.rollout ? { rollout: options.rollout } : {},
|
|
29679
30317
|
publicKeys: options.publicKeys,
|
|
29680
30318
|
storage: options.storage
|
|
29681
30319
|
}));
|
|
@@ -37278,5 +37916,5 @@ export {
|
|
|
37278
37916
|
devBuild
|
|
37279
37917
|
};
|
|
37280
37918
|
|
|
37281
|
-
//# debugId=
|
|
37919
|
+
//# debugId=EAFB7DB04FFF880464756E2164756E21
|
|
37282
37920
|
//# sourceMappingURL=build.js.map
|