@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/index.js
CHANGED
|
@@ -10636,7 +10636,7 @@ __export(exports_config, {
|
|
|
10636
10636
|
import { readFileSync as readFileSync11 } from "fs";
|
|
10637
10637
|
import { resolve as resolve12 } from "path";
|
|
10638
10638
|
import { createHash as createHash4, createPublicKey, X509Certificate } from "crypto";
|
|
10639
|
-
var APP_ID_PATTERN, SCHEME_PATTERN, APPLE_APP_ID_PREFIX_PATTERN, CERTIFICATE_FINGERPRINT_PATTERN, UPDATE_NAME_PATTERN, UPDATE_PUBLIC_KEY_PATTERN, ENVIRONMENT_NAME_PATTERN, DEFAULT_UPDATE_BOOT_TIMEOUT_MS = 20000, MINIMUM_UPDATE_BOOT_TIMEOUT_MS = 5000, MAXIMUM_UPDATE_BOOT_TIMEOUT_MS = 120000, HOSTNAME_PATTERN, EXPO_RESERVED_ROUTE_PREFIXES, resolveProjectPath = (projectRoot, value, field) => {
|
|
10639
|
+
var APP_ID_PATTERN, SCHEME_PATTERN, APPLE_APP_ID_PREFIX_PATTERN, CERTIFICATE_FINGERPRINT_PATTERN, UPDATE_NAME_PATTERN, UPDATE_PUBLIC_KEY_PATTERN, ENVIRONMENT_NAME_PATTERN, DEFAULT_UPDATE_BOOT_TIMEOUT_MS = 20000, DEFAULT_UPDATE_HEALTH_FAILURE_RATE = 0.2, DEFAULT_UPDATE_HEALTH_MINIMUM_REPORTS = 20, MINIMUM_UPDATE_BOOT_TIMEOUT_MS = 5000, MAXIMUM_UPDATE_BOOT_TIMEOUT_MS = 120000, HOSTNAME_PATTERN, EXPO_RESERVED_ROUTE_PREFIXES, resolveProjectPath = (projectRoot, value, field) => {
|
|
10640
10640
|
const root = resolve12(projectRoot);
|
|
10641
10641
|
const path = resolve12(root, value);
|
|
10642
10642
|
if (path !== root && !path.startsWith(`${root}/`)) {
|
|
@@ -10830,6 +10830,20 @@ var APP_ID_PATTERN, SCHEME_PATTERN, APPLE_APP_ID_PREFIX_PATTERN, CERTIFICATE_FIN
|
|
|
10830
10830
|
if (!ENVIRONMENT_NAME_PATTERN.test(expoPrivateKeyEnv))
|
|
10831
10831
|
throw new TypeError("mobile.updates.server.expoPrivateKeyEnv must be a valid environment variable name.");
|
|
10832
10832
|
const autoMount = config.updates.server?.autoMount ?? true;
|
|
10833
|
+
const configuredHealth = config.updates.server?.health;
|
|
10834
|
+
let health;
|
|
10835
|
+
if (configuredHealth !== false) {
|
|
10836
|
+
const failureRate = configuredHealth?.failureRate ?? DEFAULT_UPDATE_HEALTH_FAILURE_RATE;
|
|
10837
|
+
const minimumReports = configuredHealth?.minimumReports ?? DEFAULT_UPDATE_HEALTH_MINIMUM_REPORTS;
|
|
10838
|
+
const secretEnv = requireText(configuredHealth?.secretEnv ?? "ABSOLUTE_MOBILE_UPDATE_HEALTH_SECRET", "mobile.updates.server.health.secretEnv");
|
|
10839
|
+
if (!Number.isFinite(failureRate) || failureRate <= 0 || failureRate > 1)
|
|
10840
|
+
throw new TypeError("mobile.updates.server.health.failureRate must be greater than 0 and at most 1.");
|
|
10841
|
+
if (!Number.isSafeInteger(minimumReports) || minimumReports < 1)
|
|
10842
|
+
throw new TypeError("mobile.updates.server.health.minimumReports must be a positive integer.");
|
|
10843
|
+
if (!ENVIRONMENT_NAME_PATTERN.test(secretEnv))
|
|
10844
|
+
throw new TypeError("mobile.updates.server.health.secretEnv must be a valid environment variable name.");
|
|
10845
|
+
health = { failureRate, minimumReports, secretEnv };
|
|
10846
|
+
}
|
|
10833
10847
|
if (autoMount) {
|
|
10834
10848
|
const manifest = new URL(updates.manifestUrl);
|
|
10835
10849
|
if (manifest.origin !== productionOrigin)
|
|
@@ -10862,7 +10876,12 @@ var APP_ID_PATTERN, SCHEME_PATTERN, APPLE_APP_ID_PREFIX_PATTERN, CERTIFICATE_FIN
|
|
|
10862
10876
|
throw new TypeError(`mobile.updates.server.expoCodeSigningKeys.${keyId} certificate is not currently valid.`);
|
|
10863
10877
|
expoCodeSigningKeys[keyId] = { certificatePem, privateKeyEnv };
|
|
10864
10878
|
}
|
|
10865
|
-
return {
|
|
10879
|
+
return {
|
|
10880
|
+
autoMount,
|
|
10881
|
+
expoCodeSigningKeys,
|
|
10882
|
+
...health ? { health } : {},
|
|
10883
|
+
registryModule
|
|
10884
|
+
};
|
|
10866
10885
|
}, validateExpoNativeRouteSegment = (path, segment, index, count, parameters) => {
|
|
10867
10886
|
if (segment === "*" && (index !== count - 1 || count === 1)) {
|
|
10868
10887
|
throw new TypeError(`mobile.routes.native route ${path} must use * once, as the final segment after a static or parameterized prefix.`);
|
|
@@ -12280,15 +12299,17 @@ __export(exports_mobileUpdate, {
|
|
|
12280
12299
|
});
|
|
12281
12300
|
import {
|
|
12282
12301
|
createHash as createHash5,
|
|
12302
|
+
createHmac,
|
|
12283
12303
|
createPrivateKey,
|
|
12284
12304
|
createPublicKey as createPublicKey2,
|
|
12285
12305
|
sign,
|
|
12306
|
+
timingSafeEqual,
|
|
12286
12307
|
verify,
|
|
12287
12308
|
X509Certificate as X509Certificate2
|
|
12288
12309
|
} from "crypto";
|
|
12289
12310
|
import { readFile as readFile4, stat as stat2 } from "fs/promises";
|
|
12290
12311
|
import path from "path";
|
|
12291
|
-
var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updates", MAX_FILE_BYTES, MAX_TOTAL_BYTES, DAY_MS, DEFAULT_MIN_AGE_MS, DEFAULT_GRACE_PERIOD_MS, DEFAULT_RETAIN_RECENT = 5, HASH, RELEASE, APP_ID, NAME, EXPO_DESCRIPTOR = "_absolute/expo-update.json", EXPO_CODE_SIGNING_ALGORITHM = "rsa-v1_5-sha256", MobileUpdateRegistryError, object3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), text2 = (value, field) => {
|
|
12312
|
+
var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updates", MAX_FILE_BYTES, MAX_TOTAL_BYTES, DAY_MS, DEFAULT_MIN_AGE_MS, DEFAULT_GRACE_PERIOD_MS, DEFAULT_RETAIN_RECENT = 5, HASH, RELEASE, APP_ID, NAME, EXPO_DESCRIPTOR = "_absolute/expo-update.json", EXPO_CODE_SIGNING_ALGORITHM = "rsa-v1_5-sha256", HEALTH_TOKEN_VERSION = 1, HEALTH_KINDS, FAILURE_HEALTH_KINDS, MobileUpdateRegistryError, object3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), text2 = (value, field) => {
|
|
12292
12313
|
if (typeof value !== "string" || value.length === 0)
|
|
12293
12314
|
throw new MobileUpdateRegistryError(`Mobile update ${field} is invalid`);
|
|
12294
12315
|
return value;
|
|
@@ -12448,15 +12469,43 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
12448
12469
|
return true;
|
|
12449
12470
|
const value = createHash5("sha256").update(`${input.appId}\x00${input.channel}\x00${input.releaseId}\x00${input.installationId}`).digest().readUInt32BE(0);
|
|
12450
12471
|
return value / 4294967296 < input.rollout;
|
|
12472
|
+
}, 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, field) => {
|
|
12473
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0)
|
|
12474
|
+
throw new MobileUpdateRegistryError(`Mobile update health ${field} is invalid`);
|
|
12475
|
+
return value;
|
|
12476
|
+
}, parseHealthTransfer = (value) => {
|
|
12477
|
+
if (value === undefined)
|
|
12478
|
+
return;
|
|
12479
|
+
if (!object3(value))
|
|
12480
|
+
throw new MobileUpdateRegistryError("Mobile update health transfer is invalid");
|
|
12481
|
+
return {
|
|
12482
|
+
avoidedBytes: finiteMetric(value.avoidedBytes, "avoidedBytes"),
|
|
12483
|
+
downloadedBytes: finiteMetric(value.downloadedBytes, "downloadedBytes"),
|
|
12484
|
+
durationMs: finiteMetric(value.durationMs, "durationMs"),
|
|
12485
|
+
resumedBytes: finiteMetric(value.resumedBytes, "resumedBytes"),
|
|
12486
|
+
reusedBytes: finiteMetric(value.reusedBytes, "reusedBytes"),
|
|
12487
|
+
throughputBytesPerSecond: finiteMetric(value.throughputBytesPerSecond, "throughputBytesPerSecond")
|
|
12488
|
+
};
|
|
12451
12489
|
}, createMobileUpdateRegistry = (options) => {
|
|
12452
12490
|
const prefix = normalizedPrefix(options.prefix ?? DEFAULT_PREFIX);
|
|
12453
12491
|
const clock = options.clock ?? (() => new Date);
|
|
12492
|
+
const health = options.health;
|
|
12493
|
+
if (health && health.secret.length < 32)
|
|
12494
|
+
throw new MobileUpdateRegistryError("Mobile update health secret must contain at least 32 characters");
|
|
12495
|
+
if (health && !options.store.list)
|
|
12496
|
+
throw new MobileUpdateRegistryError("Mobile update health requires storage lifecycle listing");
|
|
12497
|
+
const minimumReports = health?.autoPause?.minimumReports ?? 20;
|
|
12498
|
+
const failureThreshold = health?.autoPause?.failureRate ?? 0.2;
|
|
12499
|
+
if (health && (!Number.isSafeInteger(minimumReports) || minimumReports < 1 || failureThreshold <= 0 || failureThreshold > 1))
|
|
12500
|
+
throw new MobileUpdateRegistryError("Mobile update health auto-pause policy is invalid");
|
|
12454
12501
|
const root = (appId) => `${prefix}/${appHash(appId)}`;
|
|
12455
12502
|
const releaseRoot = (manifest) => `${root(manifest.appId)}/releases/${manifest.releaseId}`;
|
|
12456
12503
|
const manifestKey = (manifest) => `${releaseRoot(manifest)}/update.json`;
|
|
12457
12504
|
const fileKey = (manifest, file2) => `${releaseRoot(manifest)}/files/${file2.path}`;
|
|
12458
12505
|
const contentBlobKey = (appId, sha256) => `${root(appId)}/blobs/${sha256}`;
|
|
12459
12506
|
const tombstoneKey = (appId, releaseId) => `${root(appId)}/gc/${releaseId}.json`;
|
|
12507
|
+
const healthRoot = (appId, promotionId, releaseId) => `${root(appId)}/health/${promotionId}/${releaseId}`;
|
|
12508
|
+
const pauseKey = (appId, promotionId, releaseId) => `${healthRoot(appId, promotionId, releaseId)}/paused.json`;
|
|
12460
12509
|
const channelKey = (appId, channel) => {
|
|
12461
12510
|
if (!APP_ID.test(appId) || !NAME.test(channel))
|
|
12462
12511
|
throw new MobileUpdateRegistryError("Mobile update channel identity is invalid");
|
|
@@ -12485,6 +12534,38 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
12485
12534
|
throw new MobileUpdateRegistryError("Stored mobile update channel identity changed");
|
|
12486
12535
|
return value;
|
|
12487
12536
|
};
|
|
12537
|
+
const signHealthToken = (payload) => {
|
|
12538
|
+
if (!health)
|
|
12539
|
+
return null;
|
|
12540
|
+
const encoded = base64Url(JSON.stringify(payload));
|
|
12541
|
+
const signature = createHmac("sha256", health.secret).update(encoded).digest("base64url");
|
|
12542
|
+
return `${encoded}.${signature}`;
|
|
12543
|
+
};
|
|
12544
|
+
const verifyHealthToken = (token) => {
|
|
12545
|
+
if (!health)
|
|
12546
|
+
throw new MobileUpdateRegistryError("Mobile update health reporting is not configured");
|
|
12547
|
+
const [encoded, provided, extra] = token.split(".");
|
|
12548
|
+
if (!encoded || !provided || extra)
|
|
12549
|
+
throw new MobileUpdateRegistryError("Mobile update health token is invalid");
|
|
12550
|
+
const expected = createHmac("sha256", health.secret).update(encoded).digest();
|
|
12551
|
+
let actual;
|
|
12552
|
+
try {
|
|
12553
|
+
actual = Buffer.from(provided, "base64url");
|
|
12554
|
+
} catch {
|
|
12555
|
+
actual = Buffer.alloc(0);
|
|
12556
|
+
}
|
|
12557
|
+
if (actual.byteLength !== expected.byteLength || !timingSafeEqual(actual, expected))
|
|
12558
|
+
throw new MobileUpdateRegistryError("Mobile update health token is invalid");
|
|
12559
|
+
let value;
|
|
12560
|
+
try {
|
|
12561
|
+
value = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
|
|
12562
|
+
} catch {
|
|
12563
|
+
throw new MobileUpdateRegistryError("Mobile update health token is invalid");
|
|
12564
|
+
}
|
|
12565
|
+
if (!object3(value) || value.format !== HEALTH_TOKEN_VERSION || typeof value.appId !== "string" || typeof value.channel !== "string" || typeof value.installationId !== "string" || typeof value.promotionId !== "string" || typeof value.releaseId !== "string" || typeof value.runtimeFingerprint !== "string")
|
|
12566
|
+
throw new MobileUpdateRegistryError("Mobile update health token is invalid");
|
|
12567
|
+
return value;
|
|
12568
|
+
};
|
|
12488
12569
|
const assertNotMarked = async (appId, releaseId) => {
|
|
12489
12570
|
if (!APP_ID.test(appId) || !RELEASE.test(releaseId))
|
|
12490
12571
|
throw new MobileUpdateRegistryError("Mobile update release identity is invalid");
|
|
@@ -12539,13 +12620,15 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
12539
12620
|
const channel = await readChannel(input.appId, input.channel);
|
|
12540
12621
|
if (!channel?.releaseId)
|
|
12541
12622
|
return { status: "empty" };
|
|
12542
|
-
|
|
12623
|
+
let selected = rolloutMember({
|
|
12543
12624
|
appId: input.appId,
|
|
12544
12625
|
channel: input.channel,
|
|
12545
12626
|
installationId: input.installationId,
|
|
12546
12627
|
releaseId: channel.releaseId,
|
|
12547
12628
|
rollout: channel.rollout
|
|
12548
12629
|
}) ? channel.releaseId : channel.fallbackReleaseId;
|
|
12630
|
+
if (health && selected === channel.releaseId && await options.store.head(pauseKey(input.appId, healthPromotionId(channel), channel.releaseId)))
|
|
12631
|
+
selected = channel.fallbackReleaseId;
|
|
12549
12632
|
if (!selected)
|
|
12550
12633
|
return { status: "empty" };
|
|
12551
12634
|
const release = await readManifest(input.appId, selected);
|
|
@@ -12561,6 +12644,163 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
12561
12644
|
status: "selected"
|
|
12562
12645
|
};
|
|
12563
12646
|
};
|
|
12647
|
+
const issueUpdateHealthToken = async (input) => {
|
|
12648
|
+
if (!health)
|
|
12649
|
+
return null;
|
|
12650
|
+
const channel = await readChannel(input.appId, input.channel);
|
|
12651
|
+
if (!channel?.releaseId || channel.releaseId !== input.releaseId)
|
|
12652
|
+
return null;
|
|
12653
|
+
const resolution = await resolveUpdateState(input);
|
|
12654
|
+
if (resolution.status !== "selected" || resolution.manifest.releaseId !== input.releaseId)
|
|
12655
|
+
return null;
|
|
12656
|
+
return signHealthToken({
|
|
12657
|
+
appId: input.appId,
|
|
12658
|
+
channel: input.channel,
|
|
12659
|
+
format: HEALTH_TOKEN_VERSION,
|
|
12660
|
+
installationId: input.installationId,
|
|
12661
|
+
promotionId: healthPromotionId(channel),
|
|
12662
|
+
releaseId: input.releaseId,
|
|
12663
|
+
runtimeFingerprint: input.runtimeFingerprint
|
|
12664
|
+
});
|
|
12665
|
+
};
|
|
12666
|
+
const inspectUpdateHealth = async (input) => {
|
|
12667
|
+
if (!health)
|
|
12668
|
+
throw new MobileUpdateRegistryError("Mobile update health reporting is not configured");
|
|
12669
|
+
const channel = await readChannel(input.appId, input.channel);
|
|
12670
|
+
const releaseId = input.releaseId ?? channel?.releaseId;
|
|
12671
|
+
if (!channel || !releaseId || channel.releaseId !== releaseId)
|
|
12672
|
+
return null;
|
|
12673
|
+
const promotionId = healthPromotionId(channel);
|
|
12674
|
+
const prefix2 = `${healthRoot(input.appId, promotionId, releaseId)}/events/`;
|
|
12675
|
+
const objects = [];
|
|
12676
|
+
const cursors = new Set;
|
|
12677
|
+
let cursor;
|
|
12678
|
+
do {
|
|
12679
|
+
const page = await options.store.list({
|
|
12680
|
+
...cursor ? { cursor } : {},
|
|
12681
|
+
prefix: prefix2
|
|
12682
|
+
});
|
|
12683
|
+
objects.push(...page.objects);
|
|
12684
|
+
if (!page.truncated)
|
|
12685
|
+
break;
|
|
12686
|
+
if (!page.cursor || cursors.has(page.cursor))
|
|
12687
|
+
throw new MobileUpdateRegistryError("Mobile update health storage returned an invalid cursor");
|
|
12688
|
+
cursors.add(page.cursor);
|
|
12689
|
+
cursor = page.cursor;
|
|
12690
|
+
} while (true);
|
|
12691
|
+
const installations = new Set;
|
|
12692
|
+
const byKind = new Map([...HEALTH_KINDS].map((kind) => [
|
|
12693
|
+
kind,
|
|
12694
|
+
new Set
|
|
12695
|
+
]));
|
|
12696
|
+
const transfer = {
|
|
12697
|
+
avoidedBytes: 0,
|
|
12698
|
+
downloadedBytes: 0,
|
|
12699
|
+
durationMs: 0,
|
|
12700
|
+
resumedBytes: 0,
|
|
12701
|
+
reusedBytes: 0,
|
|
12702
|
+
throughputBytesPerSecond: 0
|
|
12703
|
+
};
|
|
12704
|
+
for (const item of objects) {
|
|
12705
|
+
const bytes = await options.store.get(item.key);
|
|
12706
|
+
if (!bytes)
|
|
12707
|
+
continue;
|
|
12708
|
+
const head = await options.store.head(item.key);
|
|
12709
|
+
if (!head || head.size !== bytes.byteLength || head.metadata?.sha256 !== digest(bytes))
|
|
12710
|
+
throw new MobileUpdateRegistryError("Stored mobile update health evidence integrity failed");
|
|
12711
|
+
const value = decode(bytes);
|
|
12712
|
+
if (!object3(value) || typeof value.installationHash !== "string" || !HEALTH_KINDS.has(String(value.kind)))
|
|
12713
|
+
throw new MobileUpdateRegistryError("Stored mobile update health evidence is invalid");
|
|
12714
|
+
const kind = value.kind;
|
|
12715
|
+
installations.add(value.installationHash);
|
|
12716
|
+
byKind.get(kind).add(value.installationHash);
|
|
12717
|
+
if (kind === "downloaded" && object3(value.transfer)) {
|
|
12718
|
+
const parsed = parseHealthTransfer(value.transfer);
|
|
12719
|
+
for (const key of Object.keys(transfer))
|
|
12720
|
+
transfer[key] += parsed[key];
|
|
12721
|
+
}
|
|
12722
|
+
}
|
|
12723
|
+
const failures = new Set([
|
|
12724
|
+
...byKind.get("quarantined"),
|
|
12725
|
+
...byKind.get("rolled-back")
|
|
12726
|
+
]);
|
|
12727
|
+
const terminals = new Set([...byKind.get("activated"), ...failures]);
|
|
12728
|
+
const failureRate = terminals.size === 0 ? 0 : failures.size / terminals.size;
|
|
12729
|
+
return {
|
|
12730
|
+
activated: byKind.get("activated").size,
|
|
12731
|
+
appId: input.appId,
|
|
12732
|
+
channel: input.channel,
|
|
12733
|
+
downloaded: byKind.get("downloaded").size,
|
|
12734
|
+
downloadFailed: byKind.get("download-failed").size,
|
|
12735
|
+
failureRate,
|
|
12736
|
+
failures: failures.size,
|
|
12737
|
+
paused: Boolean(await options.store.head(pauseKey(input.appId, promotionId, releaseId))),
|
|
12738
|
+
promotionId,
|
|
12739
|
+
quarantined: byKind.get("quarantined").size,
|
|
12740
|
+
releaseId,
|
|
12741
|
+
reportedInstallations: installations.size,
|
|
12742
|
+
rolledBack: byKind.get("rolled-back").size,
|
|
12743
|
+
rollout: channel.rollout,
|
|
12744
|
+
terminalReports: terminals.size,
|
|
12745
|
+
transfer
|
|
12746
|
+
};
|
|
12747
|
+
};
|
|
12748
|
+
const recordUpdateHealth = async (input) => {
|
|
12749
|
+
const payload = verifyHealthToken(input.token);
|
|
12750
|
+
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")
|
|
12751
|
+
throw new MobileUpdateRegistryError("Mobile update health evidence does not match its token");
|
|
12752
|
+
const release = await readManifest(input.appId, input.releaseId);
|
|
12753
|
+
if (!release || release.manifest.runtimeFingerprint !== input.runtimeFingerprint)
|
|
12754
|
+
throw new MobileUpdateRegistryError("Mobile update health release is invalid");
|
|
12755
|
+
const activeChannel = await readChannel(input.appId, input.channel);
|
|
12756
|
+
if (!activeChannel || activeChannel.releaseId !== input.releaseId || healthPromotionId(activeChannel) !== payload.promotionId)
|
|
12757
|
+
throw new MobileUpdateRegistryError("Mobile update health promotion is no longer active");
|
|
12758
|
+
const transfer = parseHealthTransfer(input.transfer);
|
|
12759
|
+
const installationHash = createHmac("sha256", health.secret).update(input.installationId).digest("hex");
|
|
12760
|
+
const evidence = {
|
|
12761
|
+
format: 1,
|
|
12762
|
+
installationHash,
|
|
12763
|
+
kind: input.kind,
|
|
12764
|
+
observedAt: clock().toISOString(),
|
|
12765
|
+
...input.reason ? { reason: input.reason } : {},
|
|
12766
|
+
...transfer ? { transfer } : {}
|
|
12767
|
+
};
|
|
12768
|
+
const bytes = json(evidence);
|
|
12769
|
+
await options.store.put(`${healthRoot(input.appId, payload.promotionId, input.releaseId)}/events/${installationHash}/${input.kind}.json`, bytes, {
|
|
12770
|
+
cacheControl: "no-store",
|
|
12771
|
+
contentType: "application/json",
|
|
12772
|
+
maxBytes: bytes.byteLength,
|
|
12773
|
+
metadata: { kind: input.kind, sha256: digest(bytes) }
|
|
12774
|
+
});
|
|
12775
|
+
let report = await inspectUpdateHealth({
|
|
12776
|
+
appId: input.appId,
|
|
12777
|
+
channel: input.channel,
|
|
12778
|
+
releaseId: input.releaseId
|
|
12779
|
+
});
|
|
12780
|
+
if (!report)
|
|
12781
|
+
throw new MobileUpdateRegistryError("Mobile update health promotion is no longer active");
|
|
12782
|
+
if (FAILURE_HEALTH_KINDS.has(input.kind) && report.terminalReports >= minimumReports && report.failureRate >= failureThreshold && !report.paused) {
|
|
12783
|
+
const marker = json({
|
|
12784
|
+
appId: input.appId,
|
|
12785
|
+
channel: input.channel,
|
|
12786
|
+
failureRate: report.failureRate,
|
|
12787
|
+
failures: report.failures,
|
|
12788
|
+
format: 1,
|
|
12789
|
+
pausedAt: clock().toISOString(),
|
|
12790
|
+
promotionId: payload.promotionId,
|
|
12791
|
+
releaseId: input.releaseId,
|
|
12792
|
+
reports: report.terminalReports
|
|
12793
|
+
});
|
|
12794
|
+
await options.store.put(pauseKey(input.appId, payload.promotionId, input.releaseId), marker, {
|
|
12795
|
+
cacheControl: "no-store",
|
|
12796
|
+
contentType: "application/json",
|
|
12797
|
+
maxBytes: marker.byteLength,
|
|
12798
|
+
metadata: { releaseid: input.releaseId, sha256: digest(marker) }
|
|
12799
|
+
});
|
|
12800
|
+
report = { ...report, paused: true };
|
|
12801
|
+
}
|
|
12802
|
+
return report;
|
|
12803
|
+
};
|
|
12564
12804
|
const retentionValues = (input) => {
|
|
12565
12805
|
if (!APP_ID.test(input.appId))
|
|
12566
12806
|
throw new MobileUpdateRegistryError("Mobile update appId is invalid");
|
|
@@ -12803,6 +13043,7 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
12803
13043
|
return result;
|
|
12804
13044
|
};
|
|
12805
13045
|
return {
|
|
13046
|
+
...health ? { inspectUpdateHealth, issueUpdateHealthToken, recordUpdateHealth } : {},
|
|
12806
13047
|
inspectUpdateStorage: async (input) => (await inventory(input)).report,
|
|
12807
13048
|
pruneUpdates,
|
|
12808
13049
|
publishUpdate: async (input) => {
|
|
@@ -13080,7 +13321,7 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
13080
13321
|
const origin = request.headers.get("origin");
|
|
13081
13322
|
const cors = origin && allowedOrigins.has(origin) ? {
|
|
13082
13323
|
"access-control-allow-origin": origin,
|
|
13083
|
-
"access-control-expose-headers": "content-range,etag",
|
|
13324
|
+
"access-control-expose-headers": "content-range,etag,x-absolute-mobile-health-token",
|
|
13084
13325
|
vary: "Origin"
|
|
13085
13326
|
} : {};
|
|
13086
13327
|
if (request.method === "OPTIONS") {
|
|
@@ -13089,17 +13330,58 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
13089
13330
|
return new Response(null, {
|
|
13090
13331
|
headers: {
|
|
13091
13332
|
...cors,
|
|
13092
|
-
"access-control-allow-headers": "if-range,range,x-absolute-mobile-app,x-absolute-mobile-channel,x-absolute-mobile-installation,x-absolute-mobile-release,x-absolute-mobile-runtime",
|
|
13093
|
-
"access-control-allow-methods": "GET,OPTIONS",
|
|
13333
|
+
"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",
|
|
13334
|
+
"access-control-allow-methods": "GET,POST,OPTIONS",
|
|
13094
13335
|
"access-control-max-age": "600"
|
|
13095
13336
|
},
|
|
13096
13337
|
status: 204
|
|
13097
13338
|
});
|
|
13098
13339
|
}
|
|
13099
|
-
if (request.method !== "GET")
|
|
13100
|
-
return new Response(null, { status: 405 });
|
|
13101
13340
|
const pathname = new URL(request.url).pathname.replace(/^\/+/, "");
|
|
13102
13341
|
const relative7 = pathname.startsWith(`${route}/`) ? pathname.slice(route.length + 1) : "";
|
|
13342
|
+
if (request.method === "POST" && relative7 === "health") {
|
|
13343
|
+
if (!options.registry.recordUpdateHealth)
|
|
13344
|
+
return new Response(null, { status: 404 });
|
|
13345
|
+
const appId = request.headers.get("x-absolute-mobile-app");
|
|
13346
|
+
const channel = request.headers.get("x-absolute-mobile-channel");
|
|
13347
|
+
const installationId = request.headers.get("x-absolute-mobile-installation");
|
|
13348
|
+
const runtimeFingerprint = request.headers.get("x-absolute-mobile-runtime");
|
|
13349
|
+
const token = request.headers.get("x-absolute-mobile-health-token");
|
|
13350
|
+
const declared = Number(request.headers.get("content-length"));
|
|
13351
|
+
if (appId !== options.appId || channel !== options.channel || !installationId || !runtimeFingerprint || !token || Number.isFinite(declared) && declared > 4096)
|
|
13352
|
+
return new Response(null, { status: 400 });
|
|
13353
|
+
const bodyBytes = new Uint8Array(await request.arrayBuffer());
|
|
13354
|
+
if (bodyBytes.byteLength > 4096)
|
|
13355
|
+
return new Response(null, { status: 413 });
|
|
13356
|
+
let body;
|
|
13357
|
+
try {
|
|
13358
|
+
body = JSON.parse(new TextDecoder().decode(bodyBytes));
|
|
13359
|
+
} catch {
|
|
13360
|
+
return new Response(null, { status: 400 });
|
|
13361
|
+
}
|
|
13362
|
+
if (!object3(body) || typeof body.releaseId !== "string" || typeof body.kind !== "string")
|
|
13363
|
+
return new Response(null, { status: 400 });
|
|
13364
|
+
try {
|
|
13365
|
+
const report = await options.registry.recordUpdateHealth({
|
|
13366
|
+
appId,
|
|
13367
|
+
channel,
|
|
13368
|
+
installationId,
|
|
13369
|
+
kind: body.kind,
|
|
13370
|
+
...body.reason === "boot-interrupted" || body.reason === "boot-timeout" ? { reason: body.reason } : {},
|
|
13371
|
+
releaseId: body.releaseId,
|
|
13372
|
+
runtimeFingerprint,
|
|
13373
|
+
token,
|
|
13374
|
+
...object3(body.transfer) ? { transfer: body.transfer } : {}
|
|
13375
|
+
});
|
|
13376
|
+
return Response.json({ paused: report.paused }, { headers: { ...cors, "cache-control": "no-store" }, status: 202 });
|
|
13377
|
+
} catch (error) {
|
|
13378
|
+
if (error instanceof MobileUpdateRegistryError)
|
|
13379
|
+
return new Response(null, { status: 403 });
|
|
13380
|
+
throw error;
|
|
13381
|
+
}
|
|
13382
|
+
}
|
|
13383
|
+
if (request.method !== "GET")
|
|
13384
|
+
return new Response(null, { status: 405 });
|
|
13103
13385
|
if (relative7 === "update.json") {
|
|
13104
13386
|
const expoProtocolVersion = request.headers.get("expo-protocol-version");
|
|
13105
13387
|
const expoProtocol = expoProtocolVersion !== null;
|
|
@@ -13125,6 +13407,13 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
13125
13407
|
installationId,
|
|
13126
13408
|
runtimeFingerprint
|
|
13127
13409
|
});
|
|
13410
|
+
const healthToken = selected && options.registry.issueUpdateHealthToken ? await options.registry.issueUpdateHealthToken({
|
|
13411
|
+
appId,
|
|
13412
|
+
channel,
|
|
13413
|
+
installationId,
|
|
13414
|
+
releaseId: selected.manifest.releaseId,
|
|
13415
|
+
runtimeFingerprint
|
|
13416
|
+
}) : null;
|
|
13128
13417
|
if (expoProtocol) {
|
|
13129
13418
|
let requestedCodeSigning;
|
|
13130
13419
|
try {
|
|
@@ -13176,6 +13465,7 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
13176
13465
|
extra: {
|
|
13177
13466
|
absolutejs: {
|
|
13178
13467
|
channel: selected.manifest.channel,
|
|
13468
|
+
...healthToken ? { healthToken } : {},
|
|
13179
13469
|
releaseId: selected.manifest.releaseId
|
|
13180
13470
|
},
|
|
13181
13471
|
expoClient: descriptor.expoConfig
|
|
@@ -13203,7 +13493,8 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
13203
13493
|
headers: {
|
|
13204
13494
|
...cors,
|
|
13205
13495
|
"cache-control": "no-store",
|
|
13206
|
-
etag: `"${selected.manifest.releaseId}"
|
|
13496
|
+
etag: `"${selected.manifest.releaseId}"`,
|
|
13497
|
+
...healthToken ? { "x-absolute-mobile-health-token": healthToken } : {}
|
|
13207
13498
|
}
|
|
13208
13499
|
});
|
|
13209
13500
|
}
|
|
@@ -13266,6 +13557,14 @@ var init_mobileUpdate = __esm(() => {
|
|
|
13266
13557
|
RELEASE = /^amu_[a-f0-9]{64}$/;
|
|
13267
13558
|
APP_ID = /^[A-Za-z][\w]*(?:\.[A-Za-z][\w]*)+$/;
|
|
13268
13559
|
NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
13560
|
+
HEALTH_KINDS = new Set([
|
|
13561
|
+
"activated",
|
|
13562
|
+
"downloaded",
|
|
13563
|
+
"download-failed",
|
|
13564
|
+
"quarantined",
|
|
13565
|
+
"rolled-back"
|
|
13566
|
+
]);
|
|
13567
|
+
FAILURE_HEALTH_KINDS = new Set(["quarantined", "rolled-back"]);
|
|
13269
13568
|
MobileUpdateRegistryError = class MobileUpdateRegistryError extends Error {
|
|
13270
13569
|
};
|
|
13271
13570
|
expoProtocolHeaders = {
|
|
@@ -13353,6 +13652,11 @@ var ABSOLUTE_MOBILE_UPDATE_SERVER_FORMAT = 1, DEFAULT_MOBILE_UPDATE_REGISTRY_MOD
|
|
|
13353
13652
|
} catch (error) {
|
|
13354
13653
|
throw new TypeError(`Durable mobile update storage verification failed for ${module.metadata.provider}. Check the bucket, endpoint, credentials, and read/write/delete permissions.`, { cause: error });
|
|
13355
13654
|
}
|
|
13655
|
+
}, verifyHealthModule = (config, module) => {
|
|
13656
|
+
if (!config.updateServer?.health)
|
|
13657
|
+
return;
|
|
13658
|
+
if (typeof module.registry.inspectUpdateHealth !== "function" || typeof module.registry.issueUpdateHealthToken !== "function" || typeof module.registry.recordUpdateHealth !== "function")
|
|
13659
|
+
throw new TypeError("Mobile update fleet health is enabled but the registry is not provisioned for it. Run `absolute mobile update provision --force`.");
|
|
13356
13660
|
}, expoSigningOptions = (config) => {
|
|
13357
13661
|
if (!config.updates?.expoCodeSigning)
|
|
13358
13662
|
return;
|
|
@@ -13384,8 +13688,10 @@ var ABSOLUTE_MOBILE_UPDATE_SERVER_FORMAT = 1, DEFAULT_MOBILE_UPDATE_REGISTRY_MOD
|
|
|
13384
13688
|
if (!updates || !server?.autoMount)
|
|
13385
13689
|
return new Elysia4({ name: "absolutejs-mobile-updates-disabled" });
|
|
13386
13690
|
const module = await loadAbsoluteMobileUpdateServerModule(projectRoot, server.registryModule);
|
|
13387
|
-
if (options.production)
|
|
13691
|
+
if (options.production) {
|
|
13388
13692
|
await verifyDurableModule(module);
|
|
13693
|
+
verifyHealthModule(config, module);
|
|
13694
|
+
}
|
|
13389
13695
|
const manifest = new URL(updates.manifestUrl);
|
|
13390
13696
|
if (!manifest.pathname.endsWith("/update.json"))
|
|
13391
13697
|
throw new TypeError("Auto-mounted mobile update manifests must end in /update.json.");
|
|
@@ -13404,10 +13710,11 @@ var ABSOLUTE_MOBILE_UPDATE_SERVER_FORMAT = 1, DEFAULT_MOBILE_UPDATE_REGISTRY_MOD
|
|
|
13404
13710
|
return;
|
|
13405
13711
|
const module = await loadAbsoluteMobileUpdateServerModule(projectRoot, config.updateServer?.registryModule);
|
|
13406
13712
|
await verifyDurableModule(module);
|
|
13713
|
+
verifyHealthModule(config, module);
|
|
13407
13714
|
if (config.engine === "expo")
|
|
13408
13715
|
expoSigningOptions(config);
|
|
13409
13716
|
return module.metadata;
|
|
13410
|
-
}, publicKeysSource = (publicKeys) => JSON.stringify(publicKeys, null, "\t"),
|
|
13717
|
+
}, publicKeysSource = (publicKeys) => JSON.stringify(publicKeys, null, "\t"), renderAbsoluteMobileUpdateRegistryBase = (options) => {
|
|
13411
13718
|
const metadata2 = `export const absoluteMobileUpdateServer = {
|
|
13412
13719
|
format: 1,
|
|
13413
13720
|
provider: '${options.storage}',
|
|
@@ -13473,6 +13780,19 @@ export default createMobileUpdateRegistry({
|
|
|
13473
13780
|
store
|
|
13474
13781
|
});
|
|
13475
13782
|
`;
|
|
13783
|
+
}, renderAbsoluteMobileUpdateRegistry = (options) => {
|
|
13784
|
+
const source = renderAbsoluteMobileUpdateRegistryBase(options);
|
|
13785
|
+
if (!options.health)
|
|
13786
|
+
return source;
|
|
13787
|
+
const secret = options.storage === "local" ? `process.env.${options.health.secretEnv} ?? 'absolutejs-local-health-secret-not-for-production'` : `required('${options.health.secretEnv}')`;
|
|
13788
|
+
const health = ` health: {
|
|
13789
|
+
autoPause: { failureRate: ${options.health.failureRate}, minimumReports: ${options.health.minimumReports} },
|
|
13790
|
+
secret: ${secret}
|
|
13791
|
+
},
|
|
13792
|
+
`;
|
|
13793
|
+
return source.replace(`export default createMobileUpdateRegistry({
|
|
13794
|
+
`, `export default createMobileUpdateRegistry({
|
|
13795
|
+
${health}`);
|
|
13476
13796
|
}, writeAbsoluteMobileUpdateRegistry = async (options) => {
|
|
13477
13797
|
const path2 = projectPath(options.projectRoot, options.modulePath ?? DEFAULT_MOBILE_UPDATE_REGISTRY_MODULE);
|
|
13478
13798
|
if (!options.force) {
|
|
@@ -13484,6 +13804,7 @@ export default createMobileUpdateRegistry({
|
|
|
13484
13804
|
}
|
|
13485
13805
|
await mkdir6(dirname12(path2), { recursive: true });
|
|
13486
13806
|
await Bun.write(path2, renderAbsoluteMobileUpdateRegistry({
|
|
13807
|
+
...options.health ? { health: options.health } : {},
|
|
13487
13808
|
publicKeys: options.publicKeys,
|
|
13488
13809
|
storage: options.storage
|
|
13489
13810
|
}));
|
|
@@ -44896,5 +45217,5 @@ export {
|
|
|
44896
45217
|
wrapPageHandlerWithStreamingSlots
|
|
44897
45218
|
};
|
|
44898
45219
|
|
|
44899
|
-
//# debugId=
|
|
45220
|
+
//# debugId=CC09B0266A0CC45B64756E2164756E21
|
|
44900
45221
|
//# sourceMappingURL=index.js.map
|