@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/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) => {
|
|
@@ -13078,24 +13319,69 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
13078
13319
|
const expoCodeSigning = resolveExpoCodeSigning(options.expoCodeSigning);
|
|
13079
13320
|
return async (request) => {
|
|
13080
13321
|
const origin = request.headers.get("origin");
|
|
13081
|
-
const cors = origin && allowedOrigins.has(origin) ? {
|
|
13322
|
+
const cors = origin && allowedOrigins.has(origin) ? {
|
|
13323
|
+
"access-control-allow-origin": origin,
|
|
13324
|
+
"access-control-expose-headers": "content-range,etag,x-absolute-mobile-health-token",
|
|
13325
|
+
vary: "Origin"
|
|
13326
|
+
} : {};
|
|
13082
13327
|
if (request.method === "OPTIONS") {
|
|
13083
13328
|
if (!origin || !allowedOrigins.has(origin))
|
|
13084
13329
|
return new Response(null, { status: 403 });
|
|
13085
13330
|
return new Response(null, {
|
|
13086
13331
|
headers: {
|
|
13087
13332
|
...cors,
|
|
13088
|
-
"access-control-allow-headers": "x-absolute-mobile-app,x-absolute-mobile-channel,x-absolute-mobile-installation,x-absolute-mobile-release,x-absolute-mobile-runtime",
|
|
13089
|
-
"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",
|
|
13090
13335
|
"access-control-max-age": "600"
|
|
13091
13336
|
},
|
|
13092
13337
|
status: 204
|
|
13093
13338
|
});
|
|
13094
13339
|
}
|
|
13095
|
-
if (request.method !== "GET")
|
|
13096
|
-
return new Response(null, { status: 405 });
|
|
13097
13340
|
const pathname = new URL(request.url).pathname.replace(/^\/+/, "");
|
|
13098
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 });
|
|
13099
13385
|
if (relative7 === "update.json") {
|
|
13100
13386
|
const expoProtocolVersion = request.headers.get("expo-protocol-version");
|
|
13101
13387
|
const expoProtocol = expoProtocolVersion !== null;
|
|
@@ -13121,6 +13407,13 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
13121
13407
|
installationId,
|
|
13122
13408
|
runtimeFingerprint
|
|
13123
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;
|
|
13124
13417
|
if (expoProtocol) {
|
|
13125
13418
|
let requestedCodeSigning;
|
|
13126
13419
|
try {
|
|
@@ -13172,6 +13465,7 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
13172
13465
|
extra: {
|
|
13173
13466
|
absolutejs: {
|
|
13174
13467
|
channel: selected.manifest.channel,
|
|
13468
|
+
...healthToken ? { healthToken } : {},
|
|
13175
13469
|
releaseId: selected.manifest.releaseId
|
|
13176
13470
|
},
|
|
13177
13471
|
expoClient: descriptor.expoConfig
|
|
@@ -13199,7 +13493,8 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
13199
13493
|
headers: {
|
|
13200
13494
|
...cors,
|
|
13201
13495
|
"cache-control": "no-store",
|
|
13202
|
-
etag: `"${selected.manifest.releaseId}"
|
|
13496
|
+
etag: `"${selected.manifest.releaseId}"`,
|
|
13497
|
+
...healthToken ? { "x-absolute-mobile-health-token": healthToken } : {}
|
|
13203
13498
|
}
|
|
13204
13499
|
});
|
|
13205
13500
|
}
|
|
@@ -13213,14 +13508,42 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
13213
13508
|
});
|
|
13214
13509
|
if (!file2)
|
|
13215
13510
|
return new Response(null, { status: 404 });
|
|
13216
|
-
|
|
13511
|
+
const etag = `"${file2.file.sha256}"`;
|
|
13512
|
+
const range = request.headers.get("range");
|
|
13513
|
+
const useRange = range !== null && (!request.headers.has("if-range") || request.headers.get("if-range") === etag);
|
|
13514
|
+
let contents = file2.bytes;
|
|
13515
|
+
let status = 200;
|
|
13516
|
+
let contentRange;
|
|
13517
|
+
if (useRange) {
|
|
13518
|
+
const parsed = /^bytes=(\d+)-(\d*)$/.exec(range);
|
|
13519
|
+
const start = parsed?.[1] === undefined ? NaN : Number(parsed[1]);
|
|
13520
|
+
const requestedEnd = parsed?.[2] === undefined || parsed[2] === "" ? file2.bytes.byteLength - 1 : Number(parsed[2]);
|
|
13521
|
+
if (!Number.isSafeInteger(start) || !Number.isSafeInteger(requestedEnd) || start < 0 || start >= file2.bytes.byteLength || requestedEnd < start)
|
|
13522
|
+
return new Response(null, {
|
|
13523
|
+
headers: {
|
|
13524
|
+
...cors,
|
|
13525
|
+
"accept-ranges": "bytes",
|
|
13526
|
+
"content-range": `bytes */${file2.bytes.byteLength}`,
|
|
13527
|
+
etag
|
|
13528
|
+
},
|
|
13529
|
+
status: 416
|
|
13530
|
+
});
|
|
13531
|
+
const end = Math.min(requestedEnd, file2.bytes.byteLength - 1);
|
|
13532
|
+
contents = file2.bytes.slice(start, end + 1);
|
|
13533
|
+
status = 206;
|
|
13534
|
+
contentRange = `bytes ${start}-${end}/${file2.bytes.byteLength}`;
|
|
13535
|
+
}
|
|
13536
|
+
return new Response(new Blob([new Uint8Array(contents).buffer]), {
|
|
13217
13537
|
headers: {
|
|
13218
13538
|
...cors,
|
|
13539
|
+
"accept-ranges": "bytes",
|
|
13219
13540
|
"cache-control": "public, max-age=31536000, immutable",
|
|
13220
|
-
"content-length": String(
|
|
13541
|
+
"content-length": String(contents.byteLength),
|
|
13542
|
+
...contentRange ? { "content-range": contentRange } : {},
|
|
13221
13543
|
"content-type": expoContentType(file2.file.path.includes(".") ? file2.file.path.slice(file2.file.path.lastIndexOf(".") + 1) : undefined, false),
|
|
13222
|
-
etag
|
|
13223
|
-
}
|
|
13544
|
+
etag
|
|
13545
|
+
},
|
|
13546
|
+
status
|
|
13224
13547
|
});
|
|
13225
13548
|
};
|
|
13226
13549
|
};
|
|
@@ -13234,6 +13557,14 @@ var init_mobileUpdate = __esm(() => {
|
|
|
13234
13557
|
RELEASE = /^amu_[a-f0-9]{64}$/;
|
|
13235
13558
|
APP_ID = /^[A-Za-z][\w]*(?:\.[A-Za-z][\w]*)+$/;
|
|
13236
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"]);
|
|
13237
13568
|
MobileUpdateRegistryError = class MobileUpdateRegistryError extends Error {
|
|
13238
13569
|
};
|
|
13239
13570
|
expoProtocolHeaders = {
|
|
@@ -13321,6 +13652,11 @@ var ABSOLUTE_MOBILE_UPDATE_SERVER_FORMAT = 1, DEFAULT_MOBILE_UPDATE_REGISTRY_MOD
|
|
|
13321
13652
|
} catch (error) {
|
|
13322
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 });
|
|
13323
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`.");
|
|
13324
13660
|
}, expoSigningOptions = (config) => {
|
|
13325
13661
|
if (!config.updates?.expoCodeSigning)
|
|
13326
13662
|
return;
|
|
@@ -13352,8 +13688,10 @@ var ABSOLUTE_MOBILE_UPDATE_SERVER_FORMAT = 1, DEFAULT_MOBILE_UPDATE_REGISTRY_MOD
|
|
|
13352
13688
|
if (!updates || !server?.autoMount)
|
|
13353
13689
|
return new Elysia4({ name: "absolutejs-mobile-updates-disabled" });
|
|
13354
13690
|
const module = await loadAbsoluteMobileUpdateServerModule(projectRoot, server.registryModule);
|
|
13355
|
-
if (options.production)
|
|
13691
|
+
if (options.production) {
|
|
13356
13692
|
await verifyDurableModule(module);
|
|
13693
|
+
verifyHealthModule(config, module);
|
|
13694
|
+
}
|
|
13357
13695
|
const manifest = new URL(updates.manifestUrl);
|
|
13358
13696
|
if (!manifest.pathname.endsWith("/update.json"))
|
|
13359
13697
|
throw new TypeError("Auto-mounted mobile update manifests must end in /update.json.");
|
|
@@ -13372,10 +13710,11 @@ var ABSOLUTE_MOBILE_UPDATE_SERVER_FORMAT = 1, DEFAULT_MOBILE_UPDATE_REGISTRY_MOD
|
|
|
13372
13710
|
return;
|
|
13373
13711
|
const module = await loadAbsoluteMobileUpdateServerModule(projectRoot, config.updateServer?.registryModule);
|
|
13374
13712
|
await verifyDurableModule(module);
|
|
13713
|
+
verifyHealthModule(config, module);
|
|
13375
13714
|
if (config.engine === "expo")
|
|
13376
13715
|
expoSigningOptions(config);
|
|
13377
13716
|
return module.metadata;
|
|
13378
|
-
}, publicKeysSource = (publicKeys) => JSON.stringify(publicKeys, null, "\t"),
|
|
13717
|
+
}, publicKeysSource = (publicKeys) => JSON.stringify(publicKeys, null, "\t"), renderAbsoluteMobileUpdateRegistryBase = (options) => {
|
|
13379
13718
|
const metadata2 = `export const absoluteMobileUpdateServer = {
|
|
13380
13719
|
format: 1,
|
|
13381
13720
|
provider: '${options.storage}',
|
|
@@ -13441,6 +13780,19 @@ export default createMobileUpdateRegistry({
|
|
|
13441
13780
|
store
|
|
13442
13781
|
});
|
|
13443
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}`);
|
|
13444
13796
|
}, writeAbsoluteMobileUpdateRegistry = async (options) => {
|
|
13445
13797
|
const path2 = projectPath(options.projectRoot, options.modulePath ?? DEFAULT_MOBILE_UPDATE_REGISTRY_MODULE);
|
|
13446
13798
|
if (!options.force) {
|
|
@@ -13452,6 +13804,7 @@ export default createMobileUpdateRegistry({
|
|
|
13452
13804
|
}
|
|
13453
13805
|
await mkdir6(dirname12(path2), { recursive: true });
|
|
13454
13806
|
await Bun.write(path2, renderAbsoluteMobileUpdateRegistry({
|
|
13807
|
+
...options.health ? { health: options.health } : {},
|
|
13455
13808
|
publicKeys: options.publicKeys,
|
|
13456
13809
|
storage: options.storage
|
|
13457
13810
|
}));
|
|
@@ -44864,5 +45217,5 @@ export {
|
|
|
44864
45217
|
wrapPageHandlerWithStreamingSlots
|
|
44865
45218
|
};
|
|
44866
45219
|
|
|
44867
|
-
//# debugId=
|
|
45220
|
+
//# debugId=CC09B0266A0CC45B64756E2164756E21
|
|
44868
45221
|
//# sourceMappingURL=index.js.map
|