@absolutejs/absolute 0.20.0-beta.85 → 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 +334 -13
- package/dist/build.js.map +5 -5
- package/dist/cli/{compile-d7g2fvqe.js → compile-e9cn3xpx.js} +3 -3
- 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 +334 -13
- package/dist/index.js.map +5 -5
- package/dist/mobile/index.js +480 -20
- package/dist/mobile/index.js.map +8 -8
- package/dist/mobile/remoteMacAgentEntry.js +237 -175
- package/dist/mobile/shellUpdate.js +138 -23
- package/dist/src/mobile/config.d.ts +5 -0
- package/dist/src/mobile/updateClient.d.ts +13 -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) => {
|
|
@@ -20762,7 +21003,7 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
20762
21003
|
const origin = request.headers.get("origin");
|
|
20763
21004
|
const cors = origin && allowedOrigins.has(origin) ? {
|
|
20764
21005
|
"access-control-allow-origin": origin,
|
|
20765
|
-
"access-control-expose-headers": "content-range,etag",
|
|
21006
|
+
"access-control-expose-headers": "content-range,etag,x-absolute-mobile-health-token",
|
|
20766
21007
|
vary: "Origin"
|
|
20767
21008
|
} : {};
|
|
20768
21009
|
if (request.method === "OPTIONS") {
|
|
@@ -20771,17 +21012,58 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
20771
21012
|
return new Response(null, {
|
|
20772
21013
|
headers: {
|
|
20773
21014
|
...cors,
|
|
20774
|
-
"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",
|
|
20775
|
-
"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",
|
|
20776
21017
|
"access-control-max-age": "600"
|
|
20777
21018
|
},
|
|
20778
21019
|
status: 204
|
|
20779
21020
|
});
|
|
20780
21021
|
}
|
|
20781
|
-
if (request.method !== "GET")
|
|
20782
|
-
return new Response(null, { status: 405 });
|
|
20783
21022
|
const pathname = new URL(request.url).pathname.replace(/^\/+/, "");
|
|
20784
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 });
|
|
20785
21067
|
if (relative17 === "update.json") {
|
|
20786
21068
|
const expoProtocolVersion = request.headers.get("expo-protocol-version");
|
|
20787
21069
|
const expoProtocol = expoProtocolVersion !== null;
|
|
@@ -20807,6 +21089,13 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
20807
21089
|
installationId,
|
|
20808
21090
|
runtimeFingerprint
|
|
20809
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;
|
|
20810
21099
|
if (expoProtocol) {
|
|
20811
21100
|
let requestedCodeSigning;
|
|
20812
21101
|
try {
|
|
@@ -20858,6 +21147,7 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
20858
21147
|
extra: {
|
|
20859
21148
|
absolutejs: {
|
|
20860
21149
|
channel: selected.manifest.channel,
|
|
21150
|
+
...healthToken ? { healthToken } : {},
|
|
20861
21151
|
releaseId: selected.manifest.releaseId
|
|
20862
21152
|
},
|
|
20863
21153
|
expoClient: descriptor.expoConfig
|
|
@@ -20885,7 +21175,8 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
20885
21175
|
headers: {
|
|
20886
21176
|
...cors,
|
|
20887
21177
|
"cache-control": "no-store",
|
|
20888
|
-
etag: `"${selected.manifest.releaseId}"
|
|
21178
|
+
etag: `"${selected.manifest.releaseId}"`,
|
|
21179
|
+
...healthToken ? { "x-absolute-mobile-health-token": healthToken } : {}
|
|
20889
21180
|
}
|
|
20890
21181
|
});
|
|
20891
21182
|
}
|
|
@@ -20948,6 +21239,14 @@ var init_mobileUpdate = __esm(() => {
|
|
|
20948
21239
|
RELEASE = /^amu_[a-f0-9]{64}$/;
|
|
20949
21240
|
APP_ID = /^[A-Za-z][\w]*(?:\.[A-Za-z][\w]*)+$/;
|
|
20950
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"]);
|
|
20951
21250
|
MobileUpdateRegistryError = class MobileUpdateRegistryError extends Error {
|
|
20952
21251
|
};
|
|
20953
21252
|
expoProtocolHeaders = {
|
|
@@ -21035,6 +21334,11 @@ var ABSOLUTE_MOBILE_UPDATE_SERVER_FORMAT = 1, DEFAULT_MOBILE_UPDATE_REGISTRY_MOD
|
|
|
21035
21334
|
} catch (error) {
|
|
21036
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 });
|
|
21037
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`.");
|
|
21038
21342
|
}, expoSigningOptions = (config) => {
|
|
21039
21343
|
if (!config.updates?.expoCodeSigning)
|
|
21040
21344
|
return;
|
|
@@ -21066,8 +21370,10 @@ var ABSOLUTE_MOBILE_UPDATE_SERVER_FORMAT = 1, DEFAULT_MOBILE_UPDATE_REGISTRY_MOD
|
|
|
21066
21370
|
if (!updates || !server?.autoMount)
|
|
21067
21371
|
return new Elysia4({ name: "absolutejs-mobile-updates-disabled" });
|
|
21068
21372
|
const module = await loadAbsoluteMobileUpdateServerModule(projectRoot, server.registryModule);
|
|
21069
|
-
if (options.production)
|
|
21373
|
+
if (options.production) {
|
|
21070
21374
|
await verifyDurableModule(module);
|
|
21375
|
+
verifyHealthModule(config, module);
|
|
21376
|
+
}
|
|
21071
21377
|
const manifest = new URL(updates.manifestUrl);
|
|
21072
21378
|
if (!manifest.pathname.endsWith("/update.json"))
|
|
21073
21379
|
throw new TypeError("Auto-mounted mobile update manifests must end in /update.json.");
|
|
@@ -21086,10 +21392,11 @@ var ABSOLUTE_MOBILE_UPDATE_SERVER_FORMAT = 1, DEFAULT_MOBILE_UPDATE_REGISTRY_MOD
|
|
|
21086
21392
|
return;
|
|
21087
21393
|
const module = await loadAbsoluteMobileUpdateServerModule(projectRoot, config.updateServer?.registryModule);
|
|
21088
21394
|
await verifyDurableModule(module);
|
|
21395
|
+
verifyHealthModule(config, module);
|
|
21089
21396
|
if (config.engine === "expo")
|
|
21090
21397
|
expoSigningOptions(config);
|
|
21091
21398
|
return module.metadata;
|
|
21092
|
-
}, publicKeysSource = (publicKeys) => JSON.stringify(publicKeys, null, "\t"),
|
|
21399
|
+
}, publicKeysSource = (publicKeys) => JSON.stringify(publicKeys, null, "\t"), renderAbsoluteMobileUpdateRegistryBase = (options) => {
|
|
21093
21400
|
const metadata = `export const absoluteMobileUpdateServer = {
|
|
21094
21401
|
format: 1,
|
|
21095
21402
|
provider: '${options.storage}',
|
|
@@ -21155,6 +21462,19 @@ export default createMobileUpdateRegistry({
|
|
|
21155
21462
|
store
|
|
21156
21463
|
});
|
|
21157
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}`);
|
|
21158
21478
|
}, writeAbsoluteMobileUpdateRegistry = async (options) => {
|
|
21159
21479
|
const path2 = projectPath3(options.projectRoot, options.modulePath ?? DEFAULT_MOBILE_UPDATE_REGISTRY_MODULE);
|
|
21160
21480
|
if (!options.force) {
|
|
@@ -21166,6 +21486,7 @@ export default createMobileUpdateRegistry({
|
|
|
21166
21486
|
}
|
|
21167
21487
|
await mkdir18(dirname19(path2), { recursive: true });
|
|
21168
21488
|
await Bun.write(path2, renderAbsoluteMobileUpdateRegistry({
|
|
21489
|
+
...options.health ? { health: options.health } : {},
|
|
21169
21490
|
publicKeys: options.publicKeys,
|
|
21170
21491
|
storage: options.storage
|
|
21171
21492
|
}));
|
|
@@ -27832,11 +28153,48 @@ export default function AbsoluteLayout() {
|
|
|
27832
28153
|
return <Stack screenOptions={{ headerShown: false }} />;
|
|
27833
28154
|
}
|
|
27834
28155
|
`;
|
|
27835
|
-
var updatesRuntimeSource = () => `${EXPO_GENERATED_HEADER}import { randomUUID } from 'expo-crypto';
|
|
28156
|
+
var updatesRuntimeSource = (config) => `${EXPO_GENERATED_HEADER}import { randomUUID } from 'expo-crypto';
|
|
27836
28157
|
import * as SecureStore from 'expo-secure-store';
|
|
27837
28158
|
import * as Updates from 'expo-updates';
|
|
27838
28159
|
|
|
27839
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
|
+
};
|
|
27840
28198
|
|
|
27841
28199
|
let startPromise: Promise<void> | undefined;
|
|
27842
28200
|
export const startAbsoluteExpoUpdates = () => {
|
|
@@ -27848,9 +28206,34 @@ export const startAbsoluteExpoUpdates = () => {
|
|
|
27848
28206
|
await SecureStore.setItemAsync(INSTALLATION_KEY, installationId);
|
|
27849
28207
|
}
|
|
27850
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
|
+
}
|
|
27851
28223
|
const result = await Updates.checkForUpdateAsync();
|
|
27852
28224
|
if (result.isAvailable || result.isRollBackToEmbedded) {
|
|
27853
|
-
|
|
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
|
+
}
|
|
27854
28237
|
if (result.isRollBackToEmbedded) {
|
|
27855
28238
|
await Updates.reloadAsync();
|
|
27856
28239
|
}
|
|
@@ -28795,7 +29178,7 @@ node_modules/
|
|
|
28795
29178
|
files.set(path, source);
|
|
28796
29179
|
}
|
|
28797
29180
|
if (config.updates) {
|
|
28798
|
-
files.set(join15(project, "src", "generated", "AbsoluteUpdates.ts"), updatesRuntimeSource());
|
|
29181
|
+
files.set(join15(project, "src", "generated", "AbsoluteUpdates.ts"), updatesRuntimeSource(config));
|
|
28799
29182
|
}
|
|
28800
29183
|
const expoCodeSigning = config.updates?.expoCodeSigning;
|
|
28801
29184
|
if (expoCodeSigning)
|
|
@@ -38780,6 +39163,30 @@ var requestHeaders = (config) => ({
|
|
|
38780
39163
|
"x-absolute-mobile-release": config.currentReleaseId,
|
|
38781
39164
|
"x-absolute-mobile-runtime": config.runtimeFingerprint
|
|
38782
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
|
+
};
|
|
38783
39190
|
var requireCompatible = (manifest, config) => {
|
|
38784
39191
|
if (manifest.appId !== config.appId)
|
|
38785
39192
|
throw new TypeError("Mobile update belongs to another app.");
|
|
@@ -38975,15 +39382,24 @@ var createAbsoluteMobileUpdateClient = (options) => {
|
|
|
38975
39382
|
throw new TypeError("Mobile update manifest is not valid JSON.");
|
|
38976
39383
|
}
|
|
38977
39384
|
const manifest = parseAbsoluteMobileUpdateManifest(manifestValue);
|
|
39385
|
+
const healthToken = response.headers.get("x-absolute-mobile-health-token");
|
|
38978
39386
|
requireCompatible(manifest, options.config);
|
|
38979
39387
|
if (!await options.verifier.verify(manifest))
|
|
38980
39388
|
throw new TypeError("Mobile update signature verification failed.");
|
|
38981
39389
|
if (manifest.releaseId === options.config.currentReleaseId)
|
|
38982
39390
|
return { kind: "current" };
|
|
38983
39391
|
if (options.config.blockedReleaseIds?.includes(manifest.releaseId))
|
|
38984
|
-
return {
|
|
39392
|
+
return {
|
|
39393
|
+
...healthToken ? { healthToken } : {},
|
|
39394
|
+
kind: "quarantined",
|
|
39395
|
+
releaseId: manifest.releaseId
|
|
39396
|
+
};
|
|
38985
39397
|
if (!download)
|
|
38986
|
-
return {
|
|
39398
|
+
return {
|
|
39399
|
+
...healthToken ? { healthToken } : {},
|
|
39400
|
+
kind: "update-available",
|
|
39401
|
+
manifest
|
|
39402
|
+
};
|
|
38987
39403
|
await options.store.begin(manifest);
|
|
38988
39404
|
let transfer;
|
|
38989
39405
|
try {
|
|
@@ -38994,14 +39410,28 @@ var createAbsoluteMobileUpdateClient = (options) => {
|
|
|
38994
39410
|
await options.store.suspend(manifest.releaseId);
|
|
38995
39411
|
else
|
|
38996
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
|
+
});
|
|
38997
39421
|
throw error;
|
|
38998
39422
|
}
|
|
38999
|
-
return {
|
|
39423
|
+
return {
|
|
39424
|
+
...healthToken ? { healthToken } : {},
|
|
39425
|
+
kind: "downloaded",
|
|
39426
|
+
manifest,
|
|
39427
|
+
transfer
|
|
39428
|
+
};
|
|
39000
39429
|
};
|
|
39001
39430
|
return {
|
|
39002
39431
|
check,
|
|
39003
39432
|
activate: (releaseId) => options.store.activate(releaseId),
|
|
39004
|
-
download: () => check(true)
|
|
39433
|
+
download: () => check(true),
|
|
39434
|
+
report: (input) => reportAbsoluteMobileUpdateHealth(options.config, input, request)
|
|
39005
39435
|
};
|
|
39006
39436
|
};
|
|
39007
39437
|
// src/mobile/updatePublisher.ts
|
|
@@ -39124,6 +39554,34 @@ var lifecycleMethod = (publisher, name) => {
|
|
|
39124
39554
|
throw new TypeError(`Mobile update registry does not support ${name}. Re-run \`absolute mobile update provision --force\` after upgrading @absolutejs/deploy.`);
|
|
39125
39555
|
return method;
|
|
39126
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
|
+
};
|
|
39127
39585
|
var validOptionalCounters = (values) => values.every((value) => value === undefined) || values.every((value) => Number.isSafeInteger(value) && (value ?? -1) >= 0);
|
|
39128
39586
|
var validateStorageIdentity = (result, appId) => {
|
|
39129
39587
|
if (!object5(result) || result.appId !== appId || !Array.isArray(result.releases) || ![
|
|
@@ -39385,6 +39843,7 @@ export {
|
|
|
39385
39843
|
hashAbsoluteMobilePropsSchema,
|
|
39386
39844
|
inspectAbsoluteAndroidInstalledApp,
|
|
39387
39845
|
inspectAbsoluteMobileRouteMetadata,
|
|
39846
|
+
inspectAbsoluteMobileUpdateHealth,
|
|
39388
39847
|
inspectAbsoluteMobileUpdateServer,
|
|
39389
39848
|
inspectAbsoluteMobileUpdateStorage,
|
|
39390
39849
|
inspectAbsoluteRemoteMac,
|
|
@@ -39455,6 +39914,7 @@ export {
|
|
|
39455
39914
|
removeAbsoluteRemoteMacProfile,
|
|
39456
39915
|
renderAbsoluteMobileUpdateRegistry,
|
|
39457
39916
|
repairAbsoluteIosDevSession,
|
|
39917
|
+
reportAbsoluteMobileUpdateHealth,
|
|
39458
39918
|
requestAbsoluteMobileBack,
|
|
39459
39919
|
requireAbsoluteIosReleaseMetadata,
|
|
39460
39920
|
resolveAbsoluteDeviceCapabilityPlan,
|
|
@@ -39493,5 +39953,5 @@ export {
|
|
|
39493
39953
|
writeAbsoluteMobileUpdateRegistry
|
|
39494
39954
|
};
|
|
39495
39955
|
|
|
39496
|
-
//# debugId=
|
|
39956
|
+
//# debugId=01F0CE93B9ED6BCC64756E2164756E21
|
|
39497
39957
|
//# sourceMappingURL=index.js.map
|