@absolutejs/absolute 0.20.0-beta.84 → 0.20.0-beta.86
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 +370 -17
- package/dist/build.js.map +5 -5
- package/dist/cli/{compile-t73ac1zb.js → compile-e9cn3xpx.js} +4 -4
- package/dist/cli/{config-naw5fer4.js → config-z2kf085h.js} +1 -1
- package/dist/cli/{dev-skb8dz33.js → dev-3hweza3s.js} +2 -2
- package/dist/cli/{expoProject-3mmwtemn.js → expoProject-dac8fqhq.js} +1 -1
- package/dist/cli/{index-czw3jd8f.js → index-3tsmbyze.js} +22 -1
- package/dist/cli/{index-q78x1k9q.js → index-9rk7v35t.js} +2 -2
- package/dist/cli/{index-p8ezgd5e.js → index-tame6e2c.js} +65 -3
- package/dist/cli/{index-8gksmws3.js → index-x48brywr.js} +2 -2
- package/dist/cli/index.js +5 -5
- package/dist/cli/{mobile-pdck6a35.js → mobile-2p3z33t1.js} +85 -8
- package/dist/cli/{start-jbgfw27x.js → start-7t186mhg.js} +4 -4
- package/dist/index.js +370 -17
- package/dist/index.js.map +5 -5
- package/dist/mobile/index.js +689 -69
- package/dist/mobile/index.js.map +8 -8
- package/dist/mobile/remoteMacAgentEntry.js +237 -175
- package/dist/mobile/shellUpdate.js +415 -67
- package/dist/src/mobile/config.d.ts +5 -0
- package/dist/src/mobile/updateClient.d.ts +33 -0
- package/dist/src/mobile/updatePublisher.d.ts +12 -1
- package/dist/src/mobile/updateServer.d.ts +10 -0
- package/dist/types/build.d.ts +9 -0
- package/package.json +2 -2
package/dist/mobile/index.js
CHANGED
|
@@ -604,7 +604,7 @@ __export(exports_config, {
|
|
|
604
604
|
import { readFileSync as readFileSync2 } from "fs";
|
|
605
605
|
import { resolve as resolve6 } from "path";
|
|
606
606
|
import { createHash as createHash8, createPublicKey, X509Certificate as X509Certificate2 } from "crypto";
|
|
607
|
-
var APP_ID_PATTERN, SCHEME_PATTERN, APPLE_APP_ID_PREFIX_PATTERN, CERTIFICATE_FINGERPRINT_PATTERN, UPDATE_NAME_PATTERN, UPDATE_PUBLIC_KEY_PATTERN, ENVIRONMENT_NAME_PATTERN, DEFAULT_UPDATE_BOOT_TIMEOUT_MS = 20000, MINIMUM_UPDATE_BOOT_TIMEOUT_MS = 5000, MAXIMUM_UPDATE_BOOT_TIMEOUT_MS = 120000, HOSTNAME_PATTERN, EXPO_RESERVED_ROUTE_PREFIXES, resolveProjectPath = (projectRoot, value, field2) => {
|
|
607
|
+
var APP_ID_PATTERN, SCHEME_PATTERN, APPLE_APP_ID_PREFIX_PATTERN, CERTIFICATE_FINGERPRINT_PATTERN, UPDATE_NAME_PATTERN, UPDATE_PUBLIC_KEY_PATTERN, ENVIRONMENT_NAME_PATTERN, DEFAULT_UPDATE_BOOT_TIMEOUT_MS = 20000, DEFAULT_UPDATE_HEALTH_FAILURE_RATE = 0.2, DEFAULT_UPDATE_HEALTH_MINIMUM_REPORTS = 20, MINIMUM_UPDATE_BOOT_TIMEOUT_MS = 5000, MAXIMUM_UPDATE_BOOT_TIMEOUT_MS = 120000, HOSTNAME_PATTERN, EXPO_RESERVED_ROUTE_PREFIXES, resolveProjectPath = (projectRoot, value, field2) => {
|
|
608
608
|
const root = resolve6(projectRoot);
|
|
609
609
|
const path = resolve6(root, value);
|
|
610
610
|
if (path !== root && !path.startsWith(`${root}/`)) {
|
|
@@ -798,6 +798,20 @@ var APP_ID_PATTERN, SCHEME_PATTERN, APPLE_APP_ID_PREFIX_PATTERN, CERTIFICATE_FIN
|
|
|
798
798
|
if (!ENVIRONMENT_NAME_PATTERN.test(expoPrivateKeyEnv))
|
|
799
799
|
throw new TypeError("mobile.updates.server.expoPrivateKeyEnv must be a valid environment variable name.");
|
|
800
800
|
const autoMount = config.updates.server?.autoMount ?? true;
|
|
801
|
+
const configuredHealth = config.updates.server?.health;
|
|
802
|
+
let health;
|
|
803
|
+
if (configuredHealth !== false) {
|
|
804
|
+
const failureRate = configuredHealth?.failureRate ?? DEFAULT_UPDATE_HEALTH_FAILURE_RATE;
|
|
805
|
+
const minimumReports = configuredHealth?.minimumReports ?? DEFAULT_UPDATE_HEALTH_MINIMUM_REPORTS;
|
|
806
|
+
const secretEnv = requireText(configuredHealth?.secretEnv ?? "ABSOLUTE_MOBILE_UPDATE_HEALTH_SECRET", "mobile.updates.server.health.secretEnv");
|
|
807
|
+
if (!Number.isFinite(failureRate) || failureRate <= 0 || failureRate > 1)
|
|
808
|
+
throw new TypeError("mobile.updates.server.health.failureRate must be greater than 0 and at most 1.");
|
|
809
|
+
if (!Number.isSafeInteger(minimumReports) || minimumReports < 1)
|
|
810
|
+
throw new TypeError("mobile.updates.server.health.minimumReports must be a positive integer.");
|
|
811
|
+
if (!ENVIRONMENT_NAME_PATTERN.test(secretEnv))
|
|
812
|
+
throw new TypeError("mobile.updates.server.health.secretEnv must be a valid environment variable name.");
|
|
813
|
+
health = { failureRate, minimumReports, secretEnv };
|
|
814
|
+
}
|
|
801
815
|
if (autoMount) {
|
|
802
816
|
const manifest = new URL(updates.manifestUrl);
|
|
803
817
|
if (manifest.origin !== productionOrigin)
|
|
@@ -830,7 +844,12 @@ var APP_ID_PATTERN, SCHEME_PATTERN, APPLE_APP_ID_PREFIX_PATTERN, CERTIFICATE_FIN
|
|
|
830
844
|
throw new TypeError(`mobile.updates.server.expoCodeSigningKeys.${keyId} certificate is not currently valid.`);
|
|
831
845
|
expoCodeSigningKeys[keyId] = { certificatePem, privateKeyEnv };
|
|
832
846
|
}
|
|
833
|
-
return {
|
|
847
|
+
return {
|
|
848
|
+
autoMount,
|
|
849
|
+
expoCodeSigningKeys,
|
|
850
|
+
...health ? { health } : {},
|
|
851
|
+
registryModule
|
|
852
|
+
};
|
|
834
853
|
}, validateExpoNativeRouteSegment = (path, segment, index, count, parameters) => {
|
|
835
854
|
if (segment === "*" && (index !== count - 1 || count === 1)) {
|
|
836
855
|
throw new TypeError(`mobile.routes.native route ${path} must use * once, as the final segment after a static or parameterized prefix.`);
|
|
@@ -19962,15 +19981,17 @@ __export(exports_mobileUpdate, {
|
|
|
19962
19981
|
});
|
|
19963
19982
|
import {
|
|
19964
19983
|
createHash as createHash15,
|
|
19984
|
+
createHmac,
|
|
19965
19985
|
createPrivateKey,
|
|
19966
19986
|
createPublicKey as createPublicKey2,
|
|
19967
19987
|
sign as sign2,
|
|
19988
|
+
timingSafeEqual,
|
|
19968
19989
|
verify as verify2,
|
|
19969
19990
|
X509Certificate as X509Certificate3
|
|
19970
19991
|
} from "crypto";
|
|
19971
19992
|
import { readFile as readFile23, stat as stat5 } from "fs/promises";
|
|
19972
19993
|
import path from "path";
|
|
19973
|
-
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, object6 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), text2 = (value, field2) => {
|
|
19994
|
+
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, object6 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), text2 = (value, field2) => {
|
|
19974
19995
|
if (typeof value !== "string" || value.length === 0)
|
|
19975
19996
|
throw new MobileUpdateRegistryError(`Mobile update ${field2} is invalid`);
|
|
19976
19997
|
return value;
|
|
@@ -20130,15 +20151,43 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
20130
20151
|
return true;
|
|
20131
20152
|
const value = createHash15("sha256").update(`${input.appId}\x00${input.channel}\x00${input.releaseId}\x00${input.installationId}`).digest().readUInt32BE(0);
|
|
20132
20153
|
return value / 4294967296 < input.rollout;
|
|
20154
|
+
}, base64Url = (value) => Buffer.from(value).toString("base64url"), healthPromotionId = (channel) => digest(new TextEncoder().encode(`${channel.appId}\x00${channel.channel}\x00${channel.releaseId ?? "embedded"}\x00${channel.promotedAt}`)), finiteMetric = (value, field2) => {
|
|
20155
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0)
|
|
20156
|
+
throw new MobileUpdateRegistryError(`Mobile update health ${field2} is invalid`);
|
|
20157
|
+
return value;
|
|
20158
|
+
}, parseHealthTransfer = (value) => {
|
|
20159
|
+
if (value === undefined)
|
|
20160
|
+
return;
|
|
20161
|
+
if (!object6(value))
|
|
20162
|
+
throw new MobileUpdateRegistryError("Mobile update health transfer is invalid");
|
|
20163
|
+
return {
|
|
20164
|
+
avoidedBytes: finiteMetric(value.avoidedBytes, "avoidedBytes"),
|
|
20165
|
+
downloadedBytes: finiteMetric(value.downloadedBytes, "downloadedBytes"),
|
|
20166
|
+
durationMs: finiteMetric(value.durationMs, "durationMs"),
|
|
20167
|
+
resumedBytes: finiteMetric(value.resumedBytes, "resumedBytes"),
|
|
20168
|
+
reusedBytes: finiteMetric(value.reusedBytes, "reusedBytes"),
|
|
20169
|
+
throughputBytesPerSecond: finiteMetric(value.throughputBytesPerSecond, "throughputBytesPerSecond")
|
|
20170
|
+
};
|
|
20133
20171
|
}, createMobileUpdateRegistry = (options) => {
|
|
20134
20172
|
const prefix = normalizedPrefix(options.prefix ?? DEFAULT_PREFIX);
|
|
20135
20173
|
const clock = options.clock ?? (() => new Date);
|
|
20174
|
+
const health = options.health;
|
|
20175
|
+
if (health && health.secret.length < 32)
|
|
20176
|
+
throw new MobileUpdateRegistryError("Mobile update health secret must contain at least 32 characters");
|
|
20177
|
+
if (health && !options.store.list)
|
|
20178
|
+
throw new MobileUpdateRegistryError("Mobile update health requires storage lifecycle listing");
|
|
20179
|
+
const minimumReports = health?.autoPause?.minimumReports ?? 20;
|
|
20180
|
+
const failureThreshold = health?.autoPause?.failureRate ?? 0.2;
|
|
20181
|
+
if (health && (!Number.isSafeInteger(minimumReports) || minimumReports < 1 || failureThreshold <= 0 || failureThreshold > 1))
|
|
20182
|
+
throw new MobileUpdateRegistryError("Mobile update health auto-pause policy is invalid");
|
|
20136
20183
|
const root = (appId) => `${prefix}/${appHash(appId)}`;
|
|
20137
20184
|
const releaseRoot = (manifest) => `${root(manifest.appId)}/releases/${manifest.releaseId}`;
|
|
20138
20185
|
const manifestKey = (manifest) => `${releaseRoot(manifest)}/update.json`;
|
|
20139
20186
|
const fileKey = (manifest, file) => `${releaseRoot(manifest)}/files/${file.path}`;
|
|
20140
20187
|
const contentBlobKey = (appId, sha2563) => `${root(appId)}/blobs/${sha2563}`;
|
|
20141
20188
|
const tombstoneKey = (appId, releaseId) => `${root(appId)}/gc/${releaseId}.json`;
|
|
20189
|
+
const healthRoot = (appId, promotionId, releaseId) => `${root(appId)}/health/${promotionId}/${releaseId}`;
|
|
20190
|
+
const pauseKey = (appId, promotionId, releaseId) => `${healthRoot(appId, promotionId, releaseId)}/paused.json`;
|
|
20142
20191
|
const channelKey = (appId, channel) => {
|
|
20143
20192
|
if (!APP_ID.test(appId) || !NAME.test(channel))
|
|
20144
20193
|
throw new MobileUpdateRegistryError("Mobile update channel identity is invalid");
|
|
@@ -20167,6 +20216,38 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
20167
20216
|
throw new MobileUpdateRegistryError("Stored mobile update channel identity changed");
|
|
20168
20217
|
return value;
|
|
20169
20218
|
};
|
|
20219
|
+
const signHealthToken = (payload) => {
|
|
20220
|
+
if (!health)
|
|
20221
|
+
return null;
|
|
20222
|
+
const encoded = base64Url(JSON.stringify(payload));
|
|
20223
|
+
const signature = createHmac("sha256", health.secret).update(encoded).digest("base64url");
|
|
20224
|
+
return `${encoded}.${signature}`;
|
|
20225
|
+
};
|
|
20226
|
+
const verifyHealthToken = (token) => {
|
|
20227
|
+
if (!health)
|
|
20228
|
+
throw new MobileUpdateRegistryError("Mobile update health reporting is not configured");
|
|
20229
|
+
const [encoded, provided, extra] = token.split(".");
|
|
20230
|
+
if (!encoded || !provided || extra)
|
|
20231
|
+
throw new MobileUpdateRegistryError("Mobile update health token is invalid");
|
|
20232
|
+
const expected = createHmac("sha256", health.secret).update(encoded).digest();
|
|
20233
|
+
let actual;
|
|
20234
|
+
try {
|
|
20235
|
+
actual = Buffer.from(provided, "base64url");
|
|
20236
|
+
} catch {
|
|
20237
|
+
actual = Buffer.alloc(0);
|
|
20238
|
+
}
|
|
20239
|
+
if (actual.byteLength !== expected.byteLength || !timingSafeEqual(actual, expected))
|
|
20240
|
+
throw new MobileUpdateRegistryError("Mobile update health token is invalid");
|
|
20241
|
+
let value;
|
|
20242
|
+
try {
|
|
20243
|
+
value = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
|
|
20244
|
+
} catch {
|
|
20245
|
+
throw new MobileUpdateRegistryError("Mobile update health token is invalid");
|
|
20246
|
+
}
|
|
20247
|
+
if (!object6(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")
|
|
20248
|
+
throw new MobileUpdateRegistryError("Mobile update health token is invalid");
|
|
20249
|
+
return value;
|
|
20250
|
+
};
|
|
20170
20251
|
const assertNotMarked = async (appId, releaseId) => {
|
|
20171
20252
|
if (!APP_ID.test(appId) || !RELEASE.test(releaseId))
|
|
20172
20253
|
throw new MobileUpdateRegistryError("Mobile update release identity is invalid");
|
|
@@ -20221,13 +20302,15 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
20221
20302
|
const channel = await readChannel(input.appId, input.channel);
|
|
20222
20303
|
if (!channel?.releaseId)
|
|
20223
20304
|
return { status: "empty" };
|
|
20224
|
-
|
|
20305
|
+
let selected = rolloutMember({
|
|
20225
20306
|
appId: input.appId,
|
|
20226
20307
|
channel: input.channel,
|
|
20227
20308
|
installationId: input.installationId,
|
|
20228
20309
|
releaseId: channel.releaseId,
|
|
20229
20310
|
rollout: channel.rollout
|
|
20230
20311
|
}) ? channel.releaseId : channel.fallbackReleaseId;
|
|
20312
|
+
if (health && selected === channel.releaseId && await options.store.head(pauseKey(input.appId, healthPromotionId(channel), channel.releaseId)))
|
|
20313
|
+
selected = channel.fallbackReleaseId;
|
|
20231
20314
|
if (!selected)
|
|
20232
20315
|
return { status: "empty" };
|
|
20233
20316
|
const release = await readManifest(input.appId, selected);
|
|
@@ -20243,6 +20326,163 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
20243
20326
|
status: "selected"
|
|
20244
20327
|
};
|
|
20245
20328
|
};
|
|
20329
|
+
const issueUpdateHealthToken = async (input) => {
|
|
20330
|
+
if (!health)
|
|
20331
|
+
return null;
|
|
20332
|
+
const channel = await readChannel(input.appId, input.channel);
|
|
20333
|
+
if (!channel?.releaseId || channel.releaseId !== input.releaseId)
|
|
20334
|
+
return null;
|
|
20335
|
+
const resolution = await resolveUpdateState(input);
|
|
20336
|
+
if (resolution.status !== "selected" || resolution.manifest.releaseId !== input.releaseId)
|
|
20337
|
+
return null;
|
|
20338
|
+
return signHealthToken({
|
|
20339
|
+
appId: input.appId,
|
|
20340
|
+
channel: input.channel,
|
|
20341
|
+
format: HEALTH_TOKEN_VERSION,
|
|
20342
|
+
installationId: input.installationId,
|
|
20343
|
+
promotionId: healthPromotionId(channel),
|
|
20344
|
+
releaseId: input.releaseId,
|
|
20345
|
+
runtimeFingerprint: input.runtimeFingerprint
|
|
20346
|
+
});
|
|
20347
|
+
};
|
|
20348
|
+
const inspectUpdateHealth = async (input) => {
|
|
20349
|
+
if (!health)
|
|
20350
|
+
throw new MobileUpdateRegistryError("Mobile update health reporting is not configured");
|
|
20351
|
+
const channel = await readChannel(input.appId, input.channel);
|
|
20352
|
+
const releaseId = input.releaseId ?? channel?.releaseId;
|
|
20353
|
+
if (!channel || !releaseId || channel.releaseId !== releaseId)
|
|
20354
|
+
return null;
|
|
20355
|
+
const promotionId = healthPromotionId(channel);
|
|
20356
|
+
const prefix2 = `${healthRoot(input.appId, promotionId, releaseId)}/events/`;
|
|
20357
|
+
const objects = [];
|
|
20358
|
+
const cursors = new Set;
|
|
20359
|
+
let cursor;
|
|
20360
|
+
do {
|
|
20361
|
+
const page = await options.store.list({
|
|
20362
|
+
...cursor ? { cursor } : {},
|
|
20363
|
+
prefix: prefix2
|
|
20364
|
+
});
|
|
20365
|
+
objects.push(...page.objects);
|
|
20366
|
+
if (!page.truncated)
|
|
20367
|
+
break;
|
|
20368
|
+
if (!page.cursor || cursors.has(page.cursor))
|
|
20369
|
+
throw new MobileUpdateRegistryError("Mobile update health storage returned an invalid cursor");
|
|
20370
|
+
cursors.add(page.cursor);
|
|
20371
|
+
cursor = page.cursor;
|
|
20372
|
+
} while (true);
|
|
20373
|
+
const installations = new Set;
|
|
20374
|
+
const byKind = new Map([...HEALTH_KINDS].map((kind) => [
|
|
20375
|
+
kind,
|
|
20376
|
+
new Set
|
|
20377
|
+
]));
|
|
20378
|
+
const transfer = {
|
|
20379
|
+
avoidedBytes: 0,
|
|
20380
|
+
downloadedBytes: 0,
|
|
20381
|
+
durationMs: 0,
|
|
20382
|
+
resumedBytes: 0,
|
|
20383
|
+
reusedBytes: 0,
|
|
20384
|
+
throughputBytesPerSecond: 0
|
|
20385
|
+
};
|
|
20386
|
+
for (const item of objects) {
|
|
20387
|
+
const bytes = await options.store.get(item.key);
|
|
20388
|
+
if (!bytes)
|
|
20389
|
+
continue;
|
|
20390
|
+
const head = await options.store.head(item.key);
|
|
20391
|
+
if (!head || head.size !== bytes.byteLength || head.metadata?.sha256 !== digest(bytes))
|
|
20392
|
+
throw new MobileUpdateRegistryError("Stored mobile update health evidence integrity failed");
|
|
20393
|
+
const value = decode2(bytes);
|
|
20394
|
+
if (!object6(value) || typeof value.installationHash !== "string" || !HEALTH_KINDS.has(String(value.kind)))
|
|
20395
|
+
throw new MobileUpdateRegistryError("Stored mobile update health evidence is invalid");
|
|
20396
|
+
const kind = value.kind;
|
|
20397
|
+
installations.add(value.installationHash);
|
|
20398
|
+
byKind.get(kind).add(value.installationHash);
|
|
20399
|
+
if (kind === "downloaded" && object6(value.transfer)) {
|
|
20400
|
+
const parsed = parseHealthTransfer(value.transfer);
|
|
20401
|
+
for (const key of Object.keys(transfer))
|
|
20402
|
+
transfer[key] += parsed[key];
|
|
20403
|
+
}
|
|
20404
|
+
}
|
|
20405
|
+
const failures = new Set([
|
|
20406
|
+
...byKind.get("quarantined"),
|
|
20407
|
+
...byKind.get("rolled-back")
|
|
20408
|
+
]);
|
|
20409
|
+
const terminals = new Set([...byKind.get("activated"), ...failures]);
|
|
20410
|
+
const failureRate = terminals.size === 0 ? 0 : failures.size / terminals.size;
|
|
20411
|
+
return {
|
|
20412
|
+
activated: byKind.get("activated").size,
|
|
20413
|
+
appId: input.appId,
|
|
20414
|
+
channel: input.channel,
|
|
20415
|
+
downloaded: byKind.get("downloaded").size,
|
|
20416
|
+
downloadFailed: byKind.get("download-failed").size,
|
|
20417
|
+
failureRate,
|
|
20418
|
+
failures: failures.size,
|
|
20419
|
+
paused: Boolean(await options.store.head(pauseKey(input.appId, promotionId, releaseId))),
|
|
20420
|
+
promotionId,
|
|
20421
|
+
quarantined: byKind.get("quarantined").size,
|
|
20422
|
+
releaseId,
|
|
20423
|
+
reportedInstallations: installations.size,
|
|
20424
|
+
rolledBack: byKind.get("rolled-back").size,
|
|
20425
|
+
rollout: channel.rollout,
|
|
20426
|
+
terminalReports: terminals.size,
|
|
20427
|
+
transfer
|
|
20428
|
+
};
|
|
20429
|
+
};
|
|
20430
|
+
const recordUpdateHealth = async (input) => {
|
|
20431
|
+
const payload = verifyHealthToken(input.token);
|
|
20432
|
+
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")
|
|
20433
|
+
throw new MobileUpdateRegistryError("Mobile update health evidence does not match its token");
|
|
20434
|
+
const release = await readManifest(input.appId, input.releaseId);
|
|
20435
|
+
if (!release || release.manifest.runtimeFingerprint !== input.runtimeFingerprint)
|
|
20436
|
+
throw new MobileUpdateRegistryError("Mobile update health release is invalid");
|
|
20437
|
+
const activeChannel = await readChannel(input.appId, input.channel);
|
|
20438
|
+
if (!activeChannel || activeChannel.releaseId !== input.releaseId || healthPromotionId(activeChannel) !== payload.promotionId)
|
|
20439
|
+
throw new MobileUpdateRegistryError("Mobile update health promotion is no longer active");
|
|
20440
|
+
const transfer = parseHealthTransfer(input.transfer);
|
|
20441
|
+
const installationHash = createHmac("sha256", health.secret).update(input.installationId).digest("hex");
|
|
20442
|
+
const evidence = {
|
|
20443
|
+
format: 1,
|
|
20444
|
+
installationHash,
|
|
20445
|
+
kind: input.kind,
|
|
20446
|
+
observedAt: clock().toISOString(),
|
|
20447
|
+
...input.reason ? { reason: input.reason } : {},
|
|
20448
|
+
...transfer ? { transfer } : {}
|
|
20449
|
+
};
|
|
20450
|
+
const bytes = json(evidence);
|
|
20451
|
+
await options.store.put(`${healthRoot(input.appId, payload.promotionId, input.releaseId)}/events/${installationHash}/${input.kind}.json`, bytes, {
|
|
20452
|
+
cacheControl: "no-store",
|
|
20453
|
+
contentType: "application/json",
|
|
20454
|
+
maxBytes: bytes.byteLength,
|
|
20455
|
+
metadata: { kind: input.kind, sha256: digest(bytes) }
|
|
20456
|
+
});
|
|
20457
|
+
let report = await inspectUpdateHealth({
|
|
20458
|
+
appId: input.appId,
|
|
20459
|
+
channel: input.channel,
|
|
20460
|
+
releaseId: input.releaseId
|
|
20461
|
+
});
|
|
20462
|
+
if (!report)
|
|
20463
|
+
throw new MobileUpdateRegistryError("Mobile update health promotion is no longer active");
|
|
20464
|
+
if (FAILURE_HEALTH_KINDS.has(input.kind) && report.terminalReports >= minimumReports && report.failureRate >= failureThreshold && !report.paused) {
|
|
20465
|
+
const marker = json({
|
|
20466
|
+
appId: input.appId,
|
|
20467
|
+
channel: input.channel,
|
|
20468
|
+
failureRate: report.failureRate,
|
|
20469
|
+
failures: report.failures,
|
|
20470
|
+
format: 1,
|
|
20471
|
+
pausedAt: clock().toISOString(),
|
|
20472
|
+
promotionId: payload.promotionId,
|
|
20473
|
+
releaseId: input.releaseId,
|
|
20474
|
+
reports: report.terminalReports
|
|
20475
|
+
});
|
|
20476
|
+
await options.store.put(pauseKey(input.appId, payload.promotionId, input.releaseId), marker, {
|
|
20477
|
+
cacheControl: "no-store",
|
|
20478
|
+
contentType: "application/json",
|
|
20479
|
+
maxBytes: marker.byteLength,
|
|
20480
|
+
metadata: { releaseid: input.releaseId, sha256: digest(marker) }
|
|
20481
|
+
});
|
|
20482
|
+
report = { ...report, paused: true };
|
|
20483
|
+
}
|
|
20484
|
+
return report;
|
|
20485
|
+
};
|
|
20246
20486
|
const retentionValues = (input) => {
|
|
20247
20487
|
if (!APP_ID.test(input.appId))
|
|
20248
20488
|
throw new MobileUpdateRegistryError("Mobile update appId is invalid");
|
|
@@ -20485,6 +20725,7 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
20485
20725
|
return result;
|
|
20486
20726
|
};
|
|
20487
20727
|
return {
|
|
20728
|
+
...health ? { inspectUpdateHealth, issueUpdateHealthToken, recordUpdateHealth } : {},
|
|
20488
20729
|
inspectUpdateStorage: async (input) => (await inventory(input)).report,
|
|
20489
20730
|
pruneUpdates,
|
|
20490
20731
|
publishUpdate: async (input) => {
|
|
@@ -20760,24 +21001,69 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
20760
21001
|
const expoCodeSigning = resolveExpoCodeSigning(options.expoCodeSigning);
|
|
20761
21002
|
return async (request) => {
|
|
20762
21003
|
const origin = request.headers.get("origin");
|
|
20763
|
-
const cors = origin && allowedOrigins.has(origin) ? {
|
|
21004
|
+
const cors = origin && allowedOrigins.has(origin) ? {
|
|
21005
|
+
"access-control-allow-origin": origin,
|
|
21006
|
+
"access-control-expose-headers": "content-range,etag,x-absolute-mobile-health-token",
|
|
21007
|
+
vary: "Origin"
|
|
21008
|
+
} : {};
|
|
20764
21009
|
if (request.method === "OPTIONS") {
|
|
20765
21010
|
if (!origin || !allowedOrigins.has(origin))
|
|
20766
21011
|
return new Response(null, { status: 403 });
|
|
20767
21012
|
return new Response(null, {
|
|
20768
21013
|
headers: {
|
|
20769
21014
|
...cors,
|
|
20770
|
-
"access-control-allow-headers": "x-absolute-mobile-app,x-absolute-mobile-channel,x-absolute-mobile-installation,x-absolute-mobile-release,x-absolute-mobile-runtime",
|
|
20771
|
-
"access-control-allow-methods": "GET,OPTIONS",
|
|
21015
|
+
"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",
|
|
21016
|
+
"access-control-allow-methods": "GET,POST,OPTIONS",
|
|
20772
21017
|
"access-control-max-age": "600"
|
|
20773
21018
|
},
|
|
20774
21019
|
status: 204
|
|
20775
21020
|
});
|
|
20776
21021
|
}
|
|
20777
|
-
if (request.method !== "GET")
|
|
20778
|
-
return new Response(null, { status: 405 });
|
|
20779
21022
|
const pathname = new URL(request.url).pathname.replace(/^\/+/, "");
|
|
20780
21023
|
const relative17 = pathname.startsWith(`${route}/`) ? pathname.slice(route.length + 1) : "";
|
|
21024
|
+
if (request.method === "POST" && relative17 === "health") {
|
|
21025
|
+
if (!options.registry.recordUpdateHealth)
|
|
21026
|
+
return new Response(null, { status: 404 });
|
|
21027
|
+
const appId = request.headers.get("x-absolute-mobile-app");
|
|
21028
|
+
const channel = request.headers.get("x-absolute-mobile-channel");
|
|
21029
|
+
const installationId = request.headers.get("x-absolute-mobile-installation");
|
|
21030
|
+
const runtimeFingerprint = request.headers.get("x-absolute-mobile-runtime");
|
|
21031
|
+
const token = request.headers.get("x-absolute-mobile-health-token");
|
|
21032
|
+
const declared = Number(request.headers.get("content-length"));
|
|
21033
|
+
if (appId !== options.appId || channel !== options.channel || !installationId || !runtimeFingerprint || !token || Number.isFinite(declared) && declared > 4096)
|
|
21034
|
+
return new Response(null, { status: 400 });
|
|
21035
|
+
const bodyBytes = new Uint8Array(await request.arrayBuffer());
|
|
21036
|
+
if (bodyBytes.byteLength > 4096)
|
|
21037
|
+
return new Response(null, { status: 413 });
|
|
21038
|
+
let body;
|
|
21039
|
+
try {
|
|
21040
|
+
body = JSON.parse(new TextDecoder().decode(bodyBytes));
|
|
21041
|
+
} catch {
|
|
21042
|
+
return new Response(null, { status: 400 });
|
|
21043
|
+
}
|
|
21044
|
+
if (!object6(body) || typeof body.releaseId !== "string" || typeof body.kind !== "string")
|
|
21045
|
+
return new Response(null, { status: 400 });
|
|
21046
|
+
try {
|
|
21047
|
+
const report = await options.registry.recordUpdateHealth({
|
|
21048
|
+
appId,
|
|
21049
|
+
channel,
|
|
21050
|
+
installationId,
|
|
21051
|
+
kind: body.kind,
|
|
21052
|
+
...body.reason === "boot-interrupted" || body.reason === "boot-timeout" ? { reason: body.reason } : {},
|
|
21053
|
+
releaseId: body.releaseId,
|
|
21054
|
+
runtimeFingerprint,
|
|
21055
|
+
token,
|
|
21056
|
+
...object6(body.transfer) ? { transfer: body.transfer } : {}
|
|
21057
|
+
});
|
|
21058
|
+
return Response.json({ paused: report.paused }, { headers: { ...cors, "cache-control": "no-store" }, status: 202 });
|
|
21059
|
+
} catch (error) {
|
|
21060
|
+
if (error instanceof MobileUpdateRegistryError)
|
|
21061
|
+
return new Response(null, { status: 403 });
|
|
21062
|
+
throw error;
|
|
21063
|
+
}
|
|
21064
|
+
}
|
|
21065
|
+
if (request.method !== "GET")
|
|
21066
|
+
return new Response(null, { status: 405 });
|
|
20781
21067
|
if (relative17 === "update.json") {
|
|
20782
21068
|
const expoProtocolVersion = request.headers.get("expo-protocol-version");
|
|
20783
21069
|
const expoProtocol = expoProtocolVersion !== null;
|
|
@@ -20803,6 +21089,13 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
20803
21089
|
installationId,
|
|
20804
21090
|
runtimeFingerprint
|
|
20805
21091
|
});
|
|
21092
|
+
const healthToken = selected && options.registry.issueUpdateHealthToken ? await options.registry.issueUpdateHealthToken({
|
|
21093
|
+
appId,
|
|
21094
|
+
channel,
|
|
21095
|
+
installationId,
|
|
21096
|
+
releaseId: selected.manifest.releaseId,
|
|
21097
|
+
runtimeFingerprint
|
|
21098
|
+
}) : null;
|
|
20806
21099
|
if (expoProtocol) {
|
|
20807
21100
|
let requestedCodeSigning;
|
|
20808
21101
|
try {
|
|
@@ -20854,6 +21147,7 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
20854
21147
|
extra: {
|
|
20855
21148
|
absolutejs: {
|
|
20856
21149
|
channel: selected.manifest.channel,
|
|
21150
|
+
...healthToken ? { healthToken } : {},
|
|
20857
21151
|
releaseId: selected.manifest.releaseId
|
|
20858
21152
|
},
|
|
20859
21153
|
expoClient: descriptor.expoConfig
|
|
@@ -20881,7 +21175,8 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
20881
21175
|
headers: {
|
|
20882
21176
|
...cors,
|
|
20883
21177
|
"cache-control": "no-store",
|
|
20884
|
-
etag: `"${selected.manifest.releaseId}"
|
|
21178
|
+
etag: `"${selected.manifest.releaseId}"`,
|
|
21179
|
+
...healthToken ? { "x-absolute-mobile-health-token": healthToken } : {}
|
|
20885
21180
|
}
|
|
20886
21181
|
});
|
|
20887
21182
|
}
|
|
@@ -20895,14 +21190,42 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
20895
21190
|
});
|
|
20896
21191
|
if (!file)
|
|
20897
21192
|
return new Response(null, { status: 404 });
|
|
20898
|
-
|
|
21193
|
+
const etag = `"${file.file.sha256}"`;
|
|
21194
|
+
const range = request.headers.get("range");
|
|
21195
|
+
const useRange = range !== null && (!request.headers.has("if-range") || request.headers.get("if-range") === etag);
|
|
21196
|
+
let contents = file.bytes;
|
|
21197
|
+
let status = 200;
|
|
21198
|
+
let contentRange;
|
|
21199
|
+
if (useRange) {
|
|
21200
|
+
const parsed = /^bytes=(\d+)-(\d*)$/.exec(range);
|
|
21201
|
+
const start = parsed?.[1] === undefined ? NaN : Number(parsed[1]);
|
|
21202
|
+
const requestedEnd = parsed?.[2] === undefined || parsed[2] === "" ? file.bytes.byteLength - 1 : Number(parsed[2]);
|
|
21203
|
+
if (!Number.isSafeInteger(start) || !Number.isSafeInteger(requestedEnd) || start < 0 || start >= file.bytes.byteLength || requestedEnd < start)
|
|
21204
|
+
return new Response(null, {
|
|
21205
|
+
headers: {
|
|
21206
|
+
...cors,
|
|
21207
|
+
"accept-ranges": "bytes",
|
|
21208
|
+
"content-range": `bytes */${file.bytes.byteLength}`,
|
|
21209
|
+
etag
|
|
21210
|
+
},
|
|
21211
|
+
status: 416
|
|
21212
|
+
});
|
|
21213
|
+
const end = Math.min(requestedEnd, file.bytes.byteLength - 1);
|
|
21214
|
+
contents = file.bytes.slice(start, end + 1);
|
|
21215
|
+
status = 206;
|
|
21216
|
+
contentRange = `bytes ${start}-${end}/${file.bytes.byteLength}`;
|
|
21217
|
+
}
|
|
21218
|
+
return new Response(new Blob([new Uint8Array(contents).buffer]), {
|
|
20899
21219
|
headers: {
|
|
20900
21220
|
...cors,
|
|
21221
|
+
"accept-ranges": "bytes",
|
|
20901
21222
|
"cache-control": "public, max-age=31536000, immutable",
|
|
20902
|
-
"content-length": String(
|
|
21223
|
+
"content-length": String(contents.byteLength),
|
|
21224
|
+
...contentRange ? { "content-range": contentRange } : {},
|
|
20903
21225
|
"content-type": expoContentType(file.file.path.includes(".") ? file.file.path.slice(file.file.path.lastIndexOf(".") + 1) : undefined, false),
|
|
20904
|
-
etag
|
|
20905
|
-
}
|
|
21226
|
+
etag
|
|
21227
|
+
},
|
|
21228
|
+
status
|
|
20906
21229
|
});
|
|
20907
21230
|
};
|
|
20908
21231
|
};
|
|
@@ -20916,6 +21239,14 @@ var init_mobileUpdate = __esm(() => {
|
|
|
20916
21239
|
RELEASE = /^amu_[a-f0-9]{64}$/;
|
|
20917
21240
|
APP_ID = /^[A-Za-z][\w]*(?:\.[A-Za-z][\w]*)+$/;
|
|
20918
21241
|
NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
21242
|
+
HEALTH_KINDS = new Set([
|
|
21243
|
+
"activated",
|
|
21244
|
+
"downloaded",
|
|
21245
|
+
"download-failed",
|
|
21246
|
+
"quarantined",
|
|
21247
|
+
"rolled-back"
|
|
21248
|
+
]);
|
|
21249
|
+
FAILURE_HEALTH_KINDS = new Set(["quarantined", "rolled-back"]);
|
|
20919
21250
|
MobileUpdateRegistryError = class MobileUpdateRegistryError extends Error {
|
|
20920
21251
|
};
|
|
20921
21252
|
expoProtocolHeaders = {
|
|
@@ -21003,6 +21334,11 @@ var ABSOLUTE_MOBILE_UPDATE_SERVER_FORMAT = 1, DEFAULT_MOBILE_UPDATE_REGISTRY_MOD
|
|
|
21003
21334
|
} catch (error) {
|
|
21004
21335
|
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 });
|
|
21005
21336
|
}
|
|
21337
|
+
}, verifyHealthModule = (config, module) => {
|
|
21338
|
+
if (!config.updateServer?.health)
|
|
21339
|
+
return;
|
|
21340
|
+
if (typeof module.registry.inspectUpdateHealth !== "function" || typeof module.registry.issueUpdateHealthToken !== "function" || typeof module.registry.recordUpdateHealth !== "function")
|
|
21341
|
+
throw new TypeError("Mobile update fleet health is enabled but the registry is not provisioned for it. Run `absolute mobile update provision --force`.");
|
|
21006
21342
|
}, expoSigningOptions = (config) => {
|
|
21007
21343
|
if (!config.updates?.expoCodeSigning)
|
|
21008
21344
|
return;
|
|
@@ -21034,8 +21370,10 @@ var ABSOLUTE_MOBILE_UPDATE_SERVER_FORMAT = 1, DEFAULT_MOBILE_UPDATE_REGISTRY_MOD
|
|
|
21034
21370
|
if (!updates || !server?.autoMount)
|
|
21035
21371
|
return new Elysia4({ name: "absolutejs-mobile-updates-disabled" });
|
|
21036
21372
|
const module = await loadAbsoluteMobileUpdateServerModule(projectRoot, server.registryModule);
|
|
21037
|
-
if (options.production)
|
|
21373
|
+
if (options.production) {
|
|
21038
21374
|
await verifyDurableModule(module);
|
|
21375
|
+
verifyHealthModule(config, module);
|
|
21376
|
+
}
|
|
21039
21377
|
const manifest = new URL(updates.manifestUrl);
|
|
21040
21378
|
if (!manifest.pathname.endsWith("/update.json"))
|
|
21041
21379
|
throw new TypeError("Auto-mounted mobile update manifests must end in /update.json.");
|
|
@@ -21054,10 +21392,11 @@ var ABSOLUTE_MOBILE_UPDATE_SERVER_FORMAT = 1, DEFAULT_MOBILE_UPDATE_REGISTRY_MOD
|
|
|
21054
21392
|
return;
|
|
21055
21393
|
const module = await loadAbsoluteMobileUpdateServerModule(projectRoot, config.updateServer?.registryModule);
|
|
21056
21394
|
await verifyDurableModule(module);
|
|
21395
|
+
verifyHealthModule(config, module);
|
|
21057
21396
|
if (config.engine === "expo")
|
|
21058
21397
|
expoSigningOptions(config);
|
|
21059
21398
|
return module.metadata;
|
|
21060
|
-
}, publicKeysSource = (publicKeys) => JSON.stringify(publicKeys, null, "\t"),
|
|
21399
|
+
}, publicKeysSource = (publicKeys) => JSON.stringify(publicKeys, null, "\t"), renderAbsoluteMobileUpdateRegistryBase = (options) => {
|
|
21061
21400
|
const metadata = `export const absoluteMobileUpdateServer = {
|
|
21062
21401
|
format: 1,
|
|
21063
21402
|
provider: '${options.storage}',
|
|
@@ -21123,6 +21462,19 @@ export default createMobileUpdateRegistry({
|
|
|
21123
21462
|
store
|
|
21124
21463
|
});
|
|
21125
21464
|
`;
|
|
21465
|
+
}, renderAbsoluteMobileUpdateRegistry = (options) => {
|
|
21466
|
+
const source = renderAbsoluteMobileUpdateRegistryBase(options);
|
|
21467
|
+
if (!options.health)
|
|
21468
|
+
return source;
|
|
21469
|
+
const secret = options.storage === "local" ? `process.env.${options.health.secretEnv} ?? 'absolutejs-local-health-secret-not-for-production'` : `required('${options.health.secretEnv}')`;
|
|
21470
|
+
const health = ` health: {
|
|
21471
|
+
autoPause: { failureRate: ${options.health.failureRate}, minimumReports: ${options.health.minimumReports} },
|
|
21472
|
+
secret: ${secret}
|
|
21473
|
+
},
|
|
21474
|
+
`;
|
|
21475
|
+
return source.replace(`export default createMobileUpdateRegistry({
|
|
21476
|
+
`, `export default createMobileUpdateRegistry({
|
|
21477
|
+
${health}`);
|
|
21126
21478
|
}, writeAbsoluteMobileUpdateRegistry = async (options) => {
|
|
21127
21479
|
const path2 = projectPath3(options.projectRoot, options.modulePath ?? DEFAULT_MOBILE_UPDATE_REGISTRY_MODULE);
|
|
21128
21480
|
if (!options.force) {
|
|
@@ -21134,6 +21486,7 @@ export default createMobileUpdateRegistry({
|
|
|
21134
21486
|
}
|
|
21135
21487
|
await mkdir18(dirname19(path2), { recursive: true });
|
|
21136
21488
|
await Bun.write(path2, renderAbsoluteMobileUpdateRegistry({
|
|
21489
|
+
...options.health ? { health: options.health } : {},
|
|
21137
21490
|
publicKeys: options.publicKeys,
|
|
21138
21491
|
storage: options.storage
|
|
21139
21492
|
}));
|
|
@@ -27800,11 +28153,48 @@ export default function AbsoluteLayout() {
|
|
|
27800
28153
|
return <Stack screenOptions={{ headerShown: false }} />;
|
|
27801
28154
|
}
|
|
27802
28155
|
`;
|
|
27803
|
-
var updatesRuntimeSource = () => `${EXPO_GENERATED_HEADER}import { randomUUID } from 'expo-crypto';
|
|
28156
|
+
var updatesRuntimeSource = (config) => `${EXPO_GENERATED_HEADER}import { randomUUID } from 'expo-crypto';
|
|
27804
28157
|
import * as SecureStore from 'expo-secure-store';
|
|
27805
28158
|
import * as Updates from 'expo-updates';
|
|
27806
28159
|
|
|
27807
28160
|
const INSTALLATION_KEY = 'absolutejs.mobile.update.installation.v1';
|
|
28161
|
+
const PENDING_HEALTH_KEY = 'absolutejs.mobile.update.pending-health.v1';
|
|
28162
|
+
const APP_ID = ${JSON.stringify(config.appId)};
|
|
28163
|
+
const CHANNEL = ${JSON.stringify(config.updates?.channel)};
|
|
28164
|
+
const MANIFEST_URL = ${JSON.stringify(config.updates?.manifestUrl)};
|
|
28165
|
+
|
|
28166
|
+
const updateIdentity = (value: unknown) => {
|
|
28167
|
+
if (!value || typeof value !== 'object') return undefined;
|
|
28168
|
+
const extra = Reflect.get(value, 'extra');
|
|
28169
|
+
if (!extra || typeof extra !== 'object') return undefined;
|
|
28170
|
+
const absolute = Reflect.get(extra, 'absolutejs');
|
|
28171
|
+
if (!absolute || typeof absolute !== 'object') return undefined;
|
|
28172
|
+
const healthToken = Reflect.get(absolute, 'healthToken');
|
|
28173
|
+
const releaseId = Reflect.get(absolute, 'releaseId');
|
|
28174
|
+
return typeof healthToken === 'string' && typeof releaseId === 'string'
|
|
28175
|
+
? { healthToken, releaseId }
|
|
28176
|
+
: undefined;
|
|
28177
|
+
};
|
|
28178
|
+
|
|
28179
|
+
const report = async (installationId: string, evidence: { healthToken: string; kind: string; releaseId: string }) => {
|
|
28180
|
+
const endpoint = new URL('./health', MANIFEST_URL);
|
|
28181
|
+
await fetch(endpoint.href, {
|
|
28182
|
+
body: JSON.stringify({ kind: evidence.kind, releaseId: evidence.releaseId }),
|
|
28183
|
+
cache: 'no-store',
|
|
28184
|
+
credentials: 'omit',
|
|
28185
|
+
headers: {
|
|
28186
|
+
'content-type': 'application/json',
|
|
28187
|
+
'x-absolute-mobile-app': APP_ID,
|
|
28188
|
+
'x-absolute-mobile-channel': CHANNEL,
|
|
28189
|
+
'x-absolute-mobile-health-token': evidence.healthToken,
|
|
28190
|
+
'x-absolute-mobile-installation': installationId,
|
|
28191
|
+
'x-absolute-mobile-release': updateIdentity(Updates.manifest)?.releaseId ?? 'embedded',
|
|
28192
|
+
'x-absolute-mobile-runtime': Updates.runtimeVersion ?? ''
|
|
28193
|
+
},
|
|
28194
|
+
method: 'POST',
|
|
28195
|
+
redirect: 'error'
|
|
28196
|
+
});
|
|
28197
|
+
};
|
|
27808
28198
|
|
|
27809
28199
|
let startPromise: Promise<void> | undefined;
|
|
27810
28200
|
export const startAbsoluteExpoUpdates = () => {
|
|
@@ -27816,9 +28206,34 @@ export const startAbsoluteExpoUpdates = () => {
|
|
|
27816
28206
|
await SecureStore.setItemAsync(INSTALLATION_KEY, installationId);
|
|
27817
28207
|
}
|
|
27818
28208
|
await Updates.setExtraParamAsync('absolute-installation', installationId);
|
|
28209
|
+
const pendingSource = await SecureStore.getItemAsync(PENDING_HEALTH_KEY);
|
|
28210
|
+
if (pendingSource) {
|
|
28211
|
+
try {
|
|
28212
|
+
const pending = JSON.parse(pendingSource);
|
|
28213
|
+
const active = updateIdentity(Updates.manifest);
|
|
28214
|
+
if (typeof pending?.healthToken === 'string' && typeof pending?.releaseId === 'string')
|
|
28215
|
+
await report(installationId, {
|
|
28216
|
+
healthToken: pending.healthToken,
|
|
28217
|
+
kind: active?.releaseId === pending.releaseId ? 'activated' : 'rolled-back',
|
|
28218
|
+
releaseId: pending.releaseId
|
|
28219
|
+
});
|
|
28220
|
+
} catch {}
|
|
28221
|
+
await SecureStore.deleteItemAsync(PENDING_HEALTH_KEY);
|
|
28222
|
+
}
|
|
27819
28223
|
const result = await Updates.checkForUpdateAsync();
|
|
27820
28224
|
if (result.isAvailable || result.isRollBackToEmbedded) {
|
|
27821
|
-
|
|
28225
|
+
const available = updateIdentity(Reflect.get(result, 'manifest'));
|
|
28226
|
+
try {
|
|
28227
|
+
const fetched = await Updates.fetchUpdateAsync();
|
|
28228
|
+
const identity = updateIdentity(Reflect.get(fetched, 'manifest')) ?? available;
|
|
28229
|
+
if (identity) {
|
|
28230
|
+
await SecureStore.setItemAsync(PENDING_HEALTH_KEY, JSON.stringify(identity));
|
|
28231
|
+
await report(installationId, { ...identity, kind: 'downloaded' });
|
|
28232
|
+
}
|
|
28233
|
+
} catch (error) {
|
|
28234
|
+
if (available) await report(installationId, { ...available, kind: 'download-failed' }).catch(() => undefined);
|
|
28235
|
+
throw error;
|
|
28236
|
+
}
|
|
27822
28237
|
if (result.isRollBackToEmbedded) {
|
|
27823
28238
|
await Updates.reloadAsync();
|
|
27824
28239
|
}
|
|
@@ -28763,7 +29178,7 @@ node_modules/
|
|
|
28763
29178
|
files.set(path, source);
|
|
28764
29179
|
}
|
|
28765
29180
|
if (config.updates) {
|
|
28766
|
-
files.set(join15(project, "src", "generated", "AbsoluteUpdates.ts"), updatesRuntimeSource());
|
|
29181
|
+
files.set(join15(project, "src", "generated", "AbsoluteUpdates.ts"), updatesRuntimeSource(config));
|
|
28767
29182
|
}
|
|
28768
29183
|
const expoCodeSigning = config.updates?.expoCodeSigning;
|
|
28769
29184
|
if (expoCodeSigning)
|
|
@@ -38705,17 +39120,18 @@ var fileUrl = (manifestUrl, releaseId, path) => {
|
|
|
38705
39120
|
throw new TypeError("Mobile update asset escaped its signed release origin.");
|
|
38706
39121
|
return result;
|
|
38707
39122
|
};
|
|
38708
|
-
var readChunks = async (reader, maximum2, chunks = [], received = 0) => {
|
|
39123
|
+
var readChunks = async (reader, maximum2, onChunk, chunks = [], received = 0) => {
|
|
38709
39124
|
const result = await reader.read();
|
|
38710
39125
|
if (result.done)
|
|
38711
39126
|
return { chunks, received };
|
|
38712
39127
|
const total = received + result.value.byteLength;
|
|
38713
39128
|
if (total > maximum2)
|
|
38714
39129
|
throw new TypeError("Mobile update response exceeds its signed size.");
|
|
39130
|
+
await onChunk?.(result.value, received);
|
|
38715
39131
|
chunks.push(result.value);
|
|
38716
|
-
return readChunks(reader, maximum2, chunks, total);
|
|
39132
|
+
return readChunks(reader, maximum2, onChunk, chunks, total);
|
|
38717
39133
|
};
|
|
38718
|
-
var readBounded = async (response, maximum2) => {
|
|
39134
|
+
var readBounded = async (response, maximum2, onChunk) => {
|
|
38719
39135
|
const declared = Number(response.headers.get("content-length"));
|
|
38720
39136
|
if (Number.isFinite(declared) && declared > maximum2)
|
|
38721
39137
|
throw new TypeError("Mobile update response exceeds its signed size.");
|
|
@@ -38723,8 +39139,9 @@ var readBounded = async (response, maximum2) => {
|
|
|
38723
39139
|
return new Uint8Array;
|
|
38724
39140
|
const reader = response.body.getReader();
|
|
38725
39141
|
let result;
|
|
39142
|
+
const chunks = [];
|
|
38726
39143
|
try {
|
|
38727
|
-
result = await readChunks(reader, maximum2);
|
|
39144
|
+
result = await readChunks(reader, maximum2, onChunk, chunks);
|
|
38728
39145
|
} catch (error) {
|
|
38729
39146
|
await reader.cancel().catch(() => {
|
|
38730
39147
|
return;
|
|
@@ -38733,7 +39150,7 @@ var readBounded = async (response, maximum2) => {
|
|
|
38733
39150
|
}
|
|
38734
39151
|
const contents = new Uint8Array(result.received);
|
|
38735
39152
|
let offset = 0;
|
|
38736
|
-
for (const chunk of
|
|
39153
|
+
for (const chunk of chunks) {
|
|
38737
39154
|
contents.set(chunk, offset);
|
|
38738
39155
|
offset += chunk.byteLength;
|
|
38739
39156
|
}
|
|
@@ -38746,6 +39163,30 @@ var requestHeaders = (config) => ({
|
|
|
38746
39163
|
"x-absolute-mobile-release": config.currentReleaseId,
|
|
38747
39164
|
"x-absolute-mobile-runtime": config.runtimeFingerprint
|
|
38748
39165
|
});
|
|
39166
|
+
var healthUrl = (manifestUrl) => new URL("./health", manifestUrl);
|
|
39167
|
+
var reportAbsoluteMobileUpdateHealth = async (config, input, request = globalThis.fetch) => {
|
|
39168
|
+
const manifestUrl = exactManifestUrl(config.manifestUrl);
|
|
39169
|
+
const headers = new Headers(requestHeaders(config));
|
|
39170
|
+
headers.set("content-type", "application/json");
|
|
39171
|
+
headers.set("x-absolute-mobile-health-token", input.healthToken);
|
|
39172
|
+
const response = await request(healthUrl(manifestUrl), {
|
|
39173
|
+
body: JSON.stringify({
|
|
39174
|
+
kind: input.kind,
|
|
39175
|
+
...input.reason ? { reason: input.reason } : {},
|
|
39176
|
+
releaseId: input.releaseId,
|
|
39177
|
+
...input.transfer ? { transfer: input.transfer } : {}
|
|
39178
|
+
}),
|
|
39179
|
+
cache: "no-store",
|
|
39180
|
+
credentials: "omit",
|
|
39181
|
+
headers,
|
|
39182
|
+
keepalive: true,
|
|
39183
|
+
method: "POST",
|
|
39184
|
+
redirect: "error",
|
|
39185
|
+
signal: AbortSignal.timeout(15000)
|
|
39186
|
+
});
|
|
39187
|
+
if (response.status !== 202)
|
|
39188
|
+
throw new TypeError(`Mobile update health report failed with HTTP ${response.status}.`);
|
|
39189
|
+
};
|
|
38749
39190
|
var requireCompatible = (manifest, config) => {
|
|
38750
39191
|
if (manifest.appId !== config.appId)
|
|
38751
39192
|
throw new TypeError("Mobile update belongs to another app.");
|
|
@@ -38754,49 +39195,172 @@ var requireCompatible = (manifest, config) => {
|
|
|
38754
39195
|
if (manifest.runtimeFingerprint !== config.runtimeFingerprint)
|
|
38755
39196
|
throw new TypeError("Mobile update requires a different native runtime.");
|
|
38756
39197
|
};
|
|
39198
|
+
var networkConcurrency = (requested) => {
|
|
39199
|
+
const bounded = Math.max(1, Math.min(6, Math.floor(requested ?? 3)));
|
|
39200
|
+
const navigatorValue = Reflect.get(globalThis, "navigator");
|
|
39201
|
+
const connection = typeof navigatorValue === "object" && navigatorValue !== null ? Reflect.get(navigatorValue, "connection") : undefined;
|
|
39202
|
+
if (typeof connection !== "object" || connection === null)
|
|
39203
|
+
return bounded;
|
|
39204
|
+
if (Reflect.get(connection, "saveData") === true)
|
|
39205
|
+
return 1;
|
|
39206
|
+
const effectiveType = Reflect.get(connection, "effectiveType");
|
|
39207
|
+
if (effectiveType === "slow-2g" || effectiveType === "2g")
|
|
39208
|
+
return 1;
|
|
39209
|
+
if (effectiveType === "3g")
|
|
39210
|
+
return Math.min(2, bounded);
|
|
39211
|
+
return bounded;
|
|
39212
|
+
};
|
|
39213
|
+
var combine = (prefix, suffix) => {
|
|
39214
|
+
const result = new Uint8Array(prefix.byteLength + suffix.byteLength);
|
|
39215
|
+
result.set(prefix);
|
|
39216
|
+
result.set(suffix, prefix.byteLength);
|
|
39217
|
+
return result;
|
|
39218
|
+
};
|
|
39219
|
+
var validContentRange = (value, start, total) => value === `bytes ${start}-${total - 1}/${total}`;
|
|
39220
|
+
var combineSignals = (signals) => {
|
|
39221
|
+
const nativeAny = Reflect.get(AbortSignal, "any");
|
|
39222
|
+
if (typeof nativeAny === "function")
|
|
39223
|
+
return Reflect.apply(nativeAny, AbortSignal, [signals]);
|
|
39224
|
+
const controller = new AbortController;
|
|
39225
|
+
const abort = () => controller.abort();
|
|
39226
|
+
if (signals.some((signal) => signal.aborted))
|
|
39227
|
+
abort();
|
|
39228
|
+
else
|
|
39229
|
+
signals.forEach((signal) => signal.addEventListener("abort", abort, { once: true }));
|
|
39230
|
+
return controller.signal;
|
|
39231
|
+
};
|
|
38757
39232
|
var createAbsoluteMobileUpdateClient = (options) => {
|
|
38758
39233
|
const manifestUrl = exactManifestUrl(options.config.manifestUrl);
|
|
38759
39234
|
const request = options.fetch ?? globalThis.fetch;
|
|
38760
|
-
const downloadFiles = async (manifest
|
|
38761
|
-
|
|
38762
|
-
|
|
38763
|
-
|
|
38764
|
-
|
|
38765
|
-
|
|
38766
|
-
|
|
38767
|
-
|
|
38768
|
-
|
|
38769
|
-
|
|
38770
|
-
|
|
38771
|
-
|
|
38772
|
-
|
|
38773
|
-
|
|
38774
|
-
|
|
38775
|
-
|
|
38776
|
-
|
|
38777
|
-
|
|
39235
|
+
const downloadFiles = async (manifest) => {
|
|
39236
|
+
const startedAt = performance.now();
|
|
39237
|
+
const transfer = {
|
|
39238
|
+
avoidedBytes: 0,
|
|
39239
|
+
completedFiles: 0,
|
|
39240
|
+
downloadedBytes: 0,
|
|
39241
|
+
downloadedFiles: 0,
|
|
39242
|
+
durationMs: 0,
|
|
39243
|
+
resumedBytes: 0,
|
|
39244
|
+
resumedFiles: 0,
|
|
39245
|
+
reusedBytes: 0,
|
|
39246
|
+
reusedFiles: 0,
|
|
39247
|
+
throughputBytesPerSecond: 0,
|
|
39248
|
+
totalBytes: manifest.files.reduce((total, file) => total + file.bytes, 0),
|
|
39249
|
+
totalFiles: manifest.files.length
|
|
39250
|
+
};
|
|
39251
|
+
const updateTiming = () => {
|
|
39252
|
+
transfer.durationMs = Math.max(0, performance.now() - startedAt);
|
|
39253
|
+
transfer.avoidedBytes = transfer.reusedBytes + transfer.resumedBytes;
|
|
39254
|
+
transfer.throughputBytesPerSecond = transfer.durationMs > 0 ? Math.round(transfer.downloadedBytes * 1000 / transfer.durationMs) : transfer.downloadedBytes;
|
|
39255
|
+
};
|
|
39256
|
+
const progress = () => {
|
|
39257
|
+
updateTiming();
|
|
39258
|
+
try {
|
|
39259
|
+
options.onProgress?.({
|
|
39260
|
+
...transfer,
|
|
39261
|
+
kind: "download-progress"
|
|
39262
|
+
});
|
|
39263
|
+
} catch {}
|
|
39264
|
+
};
|
|
39265
|
+
const controller = new AbortController;
|
|
39266
|
+
let next = 0;
|
|
39267
|
+
let firstError;
|
|
39268
|
+
const downloadFile = async (file) => {
|
|
39269
|
+
const staged = await options.store.readStaged?.(file);
|
|
39270
|
+
if (staged?.byteLength === file.bytes && await options.verifier.digest(staged) === file.sha256) {
|
|
39271
|
+
transfer.resumedBytes += staged.byteLength;
|
|
39272
|
+
transfer.resumedFiles += 1;
|
|
39273
|
+
transfer.completedFiles += 1;
|
|
39274
|
+
progress();
|
|
39275
|
+
return;
|
|
39276
|
+
}
|
|
39277
|
+
const reusable = await options.store.readReusable?.(file);
|
|
39278
|
+
if (reusable?.byteLength === file.bytes && await options.verifier.digest(reusable) === file.sha256) {
|
|
39279
|
+
await options.store.write(file, reusable);
|
|
39280
|
+
transfer.reusedBytes += reusable.byteLength;
|
|
39281
|
+
transfer.reusedFiles += 1;
|
|
39282
|
+
transfer.completedFiles += 1;
|
|
39283
|
+
progress();
|
|
39284
|
+
return;
|
|
39285
|
+
}
|
|
39286
|
+
const candidate = await options.store.readPartial?.(file);
|
|
39287
|
+
if (candidate?.byteLength === file.bytes && await options.verifier.digest(candidate) === file.sha256) {
|
|
39288
|
+
await options.store.write(file, candidate);
|
|
39289
|
+
transfer.resumedBytes += candidate.byteLength;
|
|
39290
|
+
transfer.resumedFiles += 1;
|
|
39291
|
+
transfer.completedFiles += 1;
|
|
39292
|
+
progress();
|
|
39293
|
+
return;
|
|
39294
|
+
}
|
|
39295
|
+
const partial = candidate && candidate.byteLength > 0 && candidate.byteLength < file.bytes ? candidate : new Uint8Array;
|
|
39296
|
+
const headers = new Headers;
|
|
39297
|
+
if (partial.byteLength > 0) {
|
|
39298
|
+
headers.set("if-range", `"${file.sha256}"`);
|
|
39299
|
+
headers.set("range", `bytes=${partial.byteLength}-`);
|
|
39300
|
+
}
|
|
39301
|
+
const asset2 = await request(fileUrl(manifestUrl, manifest.releaseId, file.path), {
|
|
39302
|
+
cache: "no-store",
|
|
39303
|
+
credentials: "omit",
|
|
39304
|
+
headers,
|
|
39305
|
+
redirect: "error",
|
|
39306
|
+
signal: combineSignals([
|
|
39307
|
+
controller.signal,
|
|
39308
|
+
AbortSignal.timeout(30000)
|
|
39309
|
+
])
|
|
38778
39310
|
});
|
|
38779
|
-
|
|
38780
|
-
|
|
38781
|
-
|
|
38782
|
-
|
|
38783
|
-
|
|
38784
|
-
|
|
38785
|
-
|
|
38786
|
-
|
|
38787
|
-
|
|
38788
|
-
|
|
38789
|
-
|
|
38790
|
-
|
|
38791
|
-
|
|
38792
|
-
|
|
38793
|
-
|
|
38794
|
-
|
|
38795
|
-
|
|
38796
|
-
|
|
38797
|
-
|
|
38798
|
-
|
|
38799
|
-
|
|
39311
|
+
if (!asset2.ok)
|
|
39312
|
+
throw new TypeError(`Mobile update asset ${file.path} failed with HTTP ${asset2.status}.`);
|
|
39313
|
+
const ranged = asset2.status === 206;
|
|
39314
|
+
if (ranged && (partial.byteLength === 0 || !validContentRange(asset2.headers.get("content-range"), partial.byteLength, file.bytes)))
|
|
39315
|
+
throw new TypeError(`Mobile update asset ${file.path} returned an invalid byte range.`);
|
|
39316
|
+
const prefix = ranged ? partial : new Uint8Array;
|
|
39317
|
+
if (ranged) {
|
|
39318
|
+
transfer.resumedBytes += partial.byteLength;
|
|
39319
|
+
transfer.resumedFiles += 1;
|
|
39320
|
+
}
|
|
39321
|
+
const downloaded = await readBounded(asset2, file.bytes - prefix.byteLength, options.store.appendPartial ? async (chunk, offset) => {
|
|
39322
|
+
await options.store.appendPartial?.(file, chunk, prefix.byteLength + offset);
|
|
39323
|
+
transfer.downloadedBytes += chunk.byteLength;
|
|
39324
|
+
progress();
|
|
39325
|
+
} : undefined);
|
|
39326
|
+
if (!options.store.appendPartial) {
|
|
39327
|
+
transfer.downloadedBytes += downloaded.byteLength;
|
|
39328
|
+
progress();
|
|
39329
|
+
}
|
|
39330
|
+
if (transfer.downloadedBytes > ABSOLUTE_MOBILE_UPDATE_MAX_TOTAL_BYTES)
|
|
39331
|
+
throw new TypeError("Mobile update exceeds the maximum transfer size.");
|
|
39332
|
+
const contents = combine(prefix, downloaded);
|
|
39333
|
+
if (contents.byteLength !== file.bytes)
|
|
39334
|
+
throw new TypeError(`Mobile update asset ${file.path} has an invalid size.`);
|
|
39335
|
+
if (await options.verifier.digest(contents) !== file.sha256)
|
|
39336
|
+
throw new TypeError(`Mobile update asset ${file.path} failed integrity verification.`);
|
|
39337
|
+
await options.store.write(file, contents);
|
|
39338
|
+
transfer.downloadedFiles += 1;
|
|
39339
|
+
transfer.completedFiles += 1;
|
|
39340
|
+
progress();
|
|
39341
|
+
};
|
|
39342
|
+
const worker = async () => {
|
|
39343
|
+
if (firstError)
|
|
39344
|
+
return;
|
|
39345
|
+
const index = next++;
|
|
39346
|
+
const file = manifest.files[index];
|
|
39347
|
+
if (!file)
|
|
39348
|
+
return;
|
|
39349
|
+
try {
|
|
39350
|
+
await downloadFile(file);
|
|
39351
|
+
} catch (error) {
|
|
39352
|
+
firstError ??= error;
|
|
39353
|
+
controller.abort();
|
|
39354
|
+
}
|
|
39355
|
+
await worker();
|
|
39356
|
+
};
|
|
39357
|
+
await Promise.all(Array.from({
|
|
39358
|
+
length: Math.min(networkConcurrency(options.concurrency), manifest.files.length)
|
|
39359
|
+
}, () => worker()));
|
|
39360
|
+
if (firstError)
|
|
39361
|
+
throw firstError;
|
|
39362
|
+
updateTiming();
|
|
39363
|
+
return transfer;
|
|
38800
39364
|
};
|
|
38801
39365
|
const check = async (download = false) => {
|
|
38802
39366
|
const response = await request(manifestUrl, {
|
|
@@ -38818,30 +39382,56 @@ var createAbsoluteMobileUpdateClient = (options) => {
|
|
|
38818
39382
|
throw new TypeError("Mobile update manifest is not valid JSON.");
|
|
38819
39383
|
}
|
|
38820
39384
|
const manifest = parseAbsoluteMobileUpdateManifest(manifestValue);
|
|
39385
|
+
const healthToken = response.headers.get("x-absolute-mobile-health-token");
|
|
38821
39386
|
requireCompatible(manifest, options.config);
|
|
38822
39387
|
if (!await options.verifier.verify(manifest))
|
|
38823
39388
|
throw new TypeError("Mobile update signature verification failed.");
|
|
38824
39389
|
if (manifest.releaseId === options.config.currentReleaseId)
|
|
38825
39390
|
return { kind: "current" };
|
|
38826
39391
|
if (options.config.blockedReleaseIds?.includes(manifest.releaseId))
|
|
38827
|
-
return {
|
|
39392
|
+
return {
|
|
39393
|
+
...healthToken ? { healthToken } : {},
|
|
39394
|
+
kind: "quarantined",
|
|
39395
|
+
releaseId: manifest.releaseId
|
|
39396
|
+
};
|
|
38828
39397
|
if (!download)
|
|
38829
|
-
return {
|
|
39398
|
+
return {
|
|
39399
|
+
...healthToken ? { healthToken } : {},
|
|
39400
|
+
kind: "update-available",
|
|
39401
|
+
manifest
|
|
39402
|
+
};
|
|
38830
39403
|
await options.store.begin(manifest);
|
|
38831
39404
|
let transfer;
|
|
38832
39405
|
try {
|
|
38833
39406
|
transfer = await downloadFiles(manifest);
|
|
38834
39407
|
await options.store.commit(manifest);
|
|
38835
39408
|
} catch (error) {
|
|
38836
|
-
|
|
39409
|
+
if (options.store.suspend)
|
|
39410
|
+
await options.store.suspend(manifest.releaseId);
|
|
39411
|
+
else
|
|
39412
|
+
await options.store.abort(manifest.releaseId);
|
|
39413
|
+
if (healthToken)
|
|
39414
|
+
reportAbsoluteMobileUpdateHealth(options.config, {
|
|
39415
|
+
healthToken,
|
|
39416
|
+
kind: "download-failed",
|
|
39417
|
+
releaseId: manifest.releaseId
|
|
39418
|
+
}, request).catch(() => {
|
|
39419
|
+
return;
|
|
39420
|
+
});
|
|
38837
39421
|
throw error;
|
|
38838
39422
|
}
|
|
38839
|
-
return {
|
|
39423
|
+
return {
|
|
39424
|
+
...healthToken ? { healthToken } : {},
|
|
39425
|
+
kind: "downloaded",
|
|
39426
|
+
manifest,
|
|
39427
|
+
transfer
|
|
39428
|
+
};
|
|
38840
39429
|
};
|
|
38841
39430
|
return {
|
|
38842
39431
|
check,
|
|
38843
39432
|
activate: (releaseId) => options.store.activate(releaseId),
|
|
38844
|
-
download: () => check(true)
|
|
39433
|
+
download: () => check(true),
|
|
39434
|
+
report: (input) => reportAbsoluteMobileUpdateHealth(options.config, input, request)
|
|
38845
39435
|
};
|
|
38846
39436
|
};
|
|
38847
39437
|
// src/mobile/updatePublisher.ts
|
|
@@ -38964,6 +39554,34 @@ var lifecycleMethod = (publisher, name) => {
|
|
|
38964
39554
|
throw new TypeError(`Mobile update registry does not support ${name}. Re-run \`absolute mobile update provision --force\` after upgrading @absolutejs/deploy.`);
|
|
38965
39555
|
return method;
|
|
38966
39556
|
};
|
|
39557
|
+
var inspectAbsoluteMobileUpdateHealth = async (options) => {
|
|
39558
|
+
const report = await lifecycleMethod(options.publisher, "inspectUpdateHealth")({
|
|
39559
|
+
appId: options.appId,
|
|
39560
|
+
channel: options.channel,
|
|
39561
|
+
...options.releaseId ? { releaseId: options.releaseId } : {}
|
|
39562
|
+
});
|
|
39563
|
+
if (report === null)
|
|
39564
|
+
return null;
|
|
39565
|
+
if (!object5(report) || report.appId !== options.appId || report.channel !== options.channel || options.releaseId !== undefined && report.releaseId !== options.releaseId || typeof report.paused !== "boolean" || ![
|
|
39566
|
+
report.activated,
|
|
39567
|
+
report.downloaded,
|
|
39568
|
+
report.downloadFailed,
|
|
39569
|
+
report.failures,
|
|
39570
|
+
report.quarantined,
|
|
39571
|
+
report.reportedInstallations,
|
|
39572
|
+
report.rolledBack,
|
|
39573
|
+
report.terminalReports
|
|
39574
|
+
].every((value) => Number.isSafeInteger(value) && value >= 0) || !Number.isFinite(report.failureRate) || report.failureRate < 0 || report.failureRate > 1 || !Number.isFinite(report.rollout) || report.rollout < 0 || report.rollout > 1 || !object5(report.transfer) || ![
|
|
39575
|
+
report.transfer.avoidedBytes,
|
|
39576
|
+
report.transfer.downloadedBytes,
|
|
39577
|
+
report.transfer.durationMs,
|
|
39578
|
+
report.transfer.resumedBytes,
|
|
39579
|
+
report.transfer.reusedBytes,
|
|
39580
|
+
report.transfer.throughputBytesPerSecond
|
|
39581
|
+
].every((value) => Number.isFinite(value) && value >= 0))
|
|
39582
|
+
throw new TypeError("Mobile update registry returned an invalid health report.");
|
|
39583
|
+
return report;
|
|
39584
|
+
};
|
|
38967
39585
|
var validOptionalCounters = (values) => values.every((value) => value === undefined) || values.every((value) => Number.isSafeInteger(value) && (value ?? -1) >= 0);
|
|
38968
39586
|
var validateStorageIdentity = (result, appId) => {
|
|
38969
39587
|
if (!object5(result) || result.appId !== appId || !Array.isArray(result.releases) || ![
|
|
@@ -39225,6 +39843,7 @@ export {
|
|
|
39225
39843
|
hashAbsoluteMobilePropsSchema,
|
|
39226
39844
|
inspectAbsoluteAndroidInstalledApp,
|
|
39227
39845
|
inspectAbsoluteMobileRouteMetadata,
|
|
39846
|
+
inspectAbsoluteMobileUpdateHealth,
|
|
39228
39847
|
inspectAbsoluteMobileUpdateServer,
|
|
39229
39848
|
inspectAbsoluteMobileUpdateStorage,
|
|
39230
39849
|
inspectAbsoluteRemoteMac,
|
|
@@ -39295,6 +39914,7 @@ export {
|
|
|
39295
39914
|
removeAbsoluteRemoteMacProfile,
|
|
39296
39915
|
renderAbsoluteMobileUpdateRegistry,
|
|
39297
39916
|
repairAbsoluteIosDevSession,
|
|
39917
|
+
reportAbsoluteMobileUpdateHealth,
|
|
39298
39918
|
requestAbsoluteMobileBack,
|
|
39299
39919
|
requireAbsoluteIosReleaseMetadata,
|
|
39300
39920
|
resolveAbsoluteDeviceCapabilityPlan,
|
|
@@ -39333,5 +39953,5 @@ export {
|
|
|
39333
39953
|
writeAbsoluteMobileUpdateRegistry
|
|
39334
39954
|
};
|
|
39335
39955
|
|
|
39336
|
-
//# debugId=
|
|
39956
|
+
//# debugId=01F0CE93B9ED6BCC64756E2164756E21
|
|
39337
39957
|
//# sourceMappingURL=index.js.map
|