@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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
var __require = import.meta.require;
|
|
3
3
|
|
|
4
|
-
// .angular-partial-tmp-
|
|
4
|
+
// .angular-partial-tmp-sMuxmM/src/core/streamingSlotRegistrar.ts
|
|
5
5
|
var STREAMING_SLOT_REGISTRAR_KEY = Symbol.for("absolutejs.streamingSlotRegistrar");
|
|
6
6
|
var STREAMING_SLOT_WARNING_STORAGE_KEY = Symbol.for("absolutejs.streamingSlotWarningController");
|
|
7
7
|
var STREAMING_SLOT_COLLECTION_STORAGE_KEY = Symbol.for("absolutejs.streamingSlotCollectionController");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
var __require = import.meta.require;
|
|
3
3
|
|
|
4
|
-
// .angular-partial-tmp-
|
|
4
|
+
// .angular-partial-tmp-sMuxmM/src/core/streamingSlotRegistrar.ts
|
|
5
5
|
var STREAMING_SLOT_REGISTRAR_KEY = Symbol.for("absolutejs.streamingSlotRegistrar");
|
|
6
6
|
var STREAMING_SLOT_WARNING_STORAGE_KEY = Symbol.for("absolutejs.streamingSlotWarningController");
|
|
7
7
|
var STREAMING_SLOT_COLLECTION_STORAGE_KEY = Symbol.for("absolutejs.streamingSlotCollectionController");
|
|
@@ -48,7 +48,7 @@ var warnMissingStreamingSlotCollector = (primitiveName) => {
|
|
|
48
48
|
getWarningController()?.maybeWarn(primitiveName);
|
|
49
49
|
};
|
|
50
50
|
|
|
51
|
-
// .angular-partial-tmp-
|
|
51
|
+
// .angular-partial-tmp-sMuxmM/src/core/streamingSlotRegistry.ts
|
|
52
52
|
var STREAMING_SLOT_STORAGE_KEY = Symbol.for("absolutejs.streamingSlotAsyncLocalStorage");
|
|
53
53
|
var isObjectRecord2 = (value) => Boolean(value) && typeof value === "object";
|
|
54
54
|
var isAsyncLocalStorage = (value) => isObjectRecord2(value) && ("getStore" in value) && typeof value.getStore === "function" && ("run" in value) && typeof value.run === "function";
|
package/dist/build.js
CHANGED
|
@@ -27625,7 +27625,7 @@ __export(exports_config, {
|
|
|
27625
27625
|
import { readFileSync as readFileSync35 } from "fs";
|
|
27626
27626
|
import { resolve as resolve44 } from "path";
|
|
27627
27627
|
import { createHash as createHash5, createPublicKey, X509Certificate } from "crypto";
|
|
27628
|
-
var APP_ID_PATTERN, SCHEME_PATTERN, APPLE_APP_ID_PREFIX_PATTERN, CERTIFICATE_FINGERPRINT_PATTERN, UPDATE_NAME_PATTERN, UPDATE_PUBLIC_KEY_PATTERN, ENVIRONMENT_NAME_PATTERN, DEFAULT_UPDATE_BOOT_TIMEOUT_MS = 20000, MINIMUM_UPDATE_BOOT_TIMEOUT_MS = 5000, MAXIMUM_UPDATE_BOOT_TIMEOUT_MS = 120000, HOSTNAME_PATTERN, EXPO_RESERVED_ROUTE_PREFIXES, resolveProjectPath = (projectRoot, value, field) => {
|
|
27628
|
+
var APP_ID_PATTERN, SCHEME_PATTERN, APPLE_APP_ID_PREFIX_PATTERN, CERTIFICATE_FINGERPRINT_PATTERN, UPDATE_NAME_PATTERN, UPDATE_PUBLIC_KEY_PATTERN, ENVIRONMENT_NAME_PATTERN, DEFAULT_UPDATE_BOOT_TIMEOUT_MS = 20000, DEFAULT_UPDATE_HEALTH_FAILURE_RATE = 0.2, DEFAULT_UPDATE_HEALTH_MINIMUM_REPORTS = 20, MINIMUM_UPDATE_BOOT_TIMEOUT_MS = 5000, MAXIMUM_UPDATE_BOOT_TIMEOUT_MS = 120000, HOSTNAME_PATTERN, EXPO_RESERVED_ROUTE_PREFIXES, resolveProjectPath = (projectRoot, value, field) => {
|
|
27629
27629
|
const root = resolve44(projectRoot);
|
|
27630
27630
|
const path = resolve44(root, value);
|
|
27631
27631
|
if (path !== root && !path.startsWith(`${root}/`)) {
|
|
@@ -27819,6 +27819,20 @@ var APP_ID_PATTERN, SCHEME_PATTERN, APPLE_APP_ID_PREFIX_PATTERN, CERTIFICATE_FIN
|
|
|
27819
27819
|
if (!ENVIRONMENT_NAME_PATTERN.test(expoPrivateKeyEnv))
|
|
27820
27820
|
throw new TypeError("mobile.updates.server.expoPrivateKeyEnv must be a valid environment variable name.");
|
|
27821
27821
|
const autoMount = config.updates.server?.autoMount ?? true;
|
|
27822
|
+
const configuredHealth = config.updates.server?.health;
|
|
27823
|
+
let health;
|
|
27824
|
+
if (configuredHealth !== false) {
|
|
27825
|
+
const failureRate = configuredHealth?.failureRate ?? DEFAULT_UPDATE_HEALTH_FAILURE_RATE;
|
|
27826
|
+
const minimumReports = configuredHealth?.minimumReports ?? DEFAULT_UPDATE_HEALTH_MINIMUM_REPORTS;
|
|
27827
|
+
const secretEnv = requireText(configuredHealth?.secretEnv ?? "ABSOLUTE_MOBILE_UPDATE_HEALTH_SECRET", "mobile.updates.server.health.secretEnv");
|
|
27828
|
+
if (!Number.isFinite(failureRate) || failureRate <= 0 || failureRate > 1)
|
|
27829
|
+
throw new TypeError("mobile.updates.server.health.failureRate must be greater than 0 and at most 1.");
|
|
27830
|
+
if (!Number.isSafeInteger(minimumReports) || minimumReports < 1)
|
|
27831
|
+
throw new TypeError("mobile.updates.server.health.minimumReports must be a positive integer.");
|
|
27832
|
+
if (!ENVIRONMENT_NAME_PATTERN.test(secretEnv))
|
|
27833
|
+
throw new TypeError("mobile.updates.server.health.secretEnv must be a valid environment variable name.");
|
|
27834
|
+
health = { failureRate, minimumReports, secretEnv };
|
|
27835
|
+
}
|
|
27822
27836
|
if (autoMount) {
|
|
27823
27837
|
const manifest = new URL(updates.manifestUrl);
|
|
27824
27838
|
if (manifest.origin !== productionOrigin)
|
|
@@ -27851,7 +27865,12 @@ var APP_ID_PATTERN, SCHEME_PATTERN, APPLE_APP_ID_PREFIX_PATTERN, CERTIFICATE_FIN
|
|
|
27851
27865
|
throw new TypeError(`mobile.updates.server.expoCodeSigningKeys.${keyId} certificate is not currently valid.`);
|
|
27852
27866
|
expoCodeSigningKeys[keyId] = { certificatePem, privateKeyEnv };
|
|
27853
27867
|
}
|
|
27854
|
-
return {
|
|
27868
|
+
return {
|
|
27869
|
+
autoMount,
|
|
27870
|
+
expoCodeSigningKeys,
|
|
27871
|
+
...health ? { health } : {},
|
|
27872
|
+
registryModule
|
|
27873
|
+
};
|
|
27855
27874
|
}, validateExpoNativeRouteSegment = (path, segment, index, count, parameters) => {
|
|
27856
27875
|
if (segment === "*" && (index !== count - 1 || count === 1)) {
|
|
27857
27876
|
throw new TypeError(`mobile.routes.native route ${path} must use * once, as the final segment after a static or parameterized prefix.`);
|
|
@@ -28472,15 +28491,17 @@ __export(exports_mobileUpdate, {
|
|
|
28472
28491
|
});
|
|
28473
28492
|
import {
|
|
28474
28493
|
createHash as createHash6,
|
|
28494
|
+
createHmac,
|
|
28475
28495
|
createPrivateKey,
|
|
28476
28496
|
createPublicKey as createPublicKey2,
|
|
28477
28497
|
sign,
|
|
28498
|
+
timingSafeEqual,
|
|
28478
28499
|
verify,
|
|
28479
28500
|
X509Certificate as X509Certificate2
|
|
28480
28501
|
} from "crypto";
|
|
28481
28502
|
import { readFile as readFile9, stat as stat3 } from "fs/promises";
|
|
28482
28503
|
import path from "path";
|
|
28483
|
-
var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updates", MAX_FILE_BYTES, MAX_TOTAL_BYTES, DAY_MS, DEFAULT_MIN_AGE_MS, DEFAULT_GRACE_PERIOD_MS, DEFAULT_RETAIN_RECENT = 5, HASH, RELEASE, APP_ID, NAME, EXPO_DESCRIPTOR = "_absolute/expo-update.json", EXPO_CODE_SIGNING_ALGORITHM = "rsa-v1_5-sha256", MobileUpdateRegistryError, object3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), text2 = (value, field) => {
|
|
28504
|
+
var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updates", MAX_FILE_BYTES, MAX_TOTAL_BYTES, DAY_MS, DEFAULT_MIN_AGE_MS, DEFAULT_GRACE_PERIOD_MS, DEFAULT_RETAIN_RECENT = 5, HASH, RELEASE, APP_ID, NAME, EXPO_DESCRIPTOR = "_absolute/expo-update.json", EXPO_CODE_SIGNING_ALGORITHM = "rsa-v1_5-sha256", HEALTH_TOKEN_VERSION = 1, HEALTH_KINDS, FAILURE_HEALTH_KINDS, MobileUpdateRegistryError, object3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), text2 = (value, field) => {
|
|
28484
28505
|
if (typeof value !== "string" || value.length === 0)
|
|
28485
28506
|
throw new MobileUpdateRegistryError(`Mobile update ${field} is invalid`);
|
|
28486
28507
|
return value;
|
|
@@ -28640,15 +28661,43 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
28640
28661
|
return true;
|
|
28641
28662
|
const value = createHash6("sha256").update(`${input.appId}\x00${input.channel}\x00${input.releaseId}\x00${input.installationId}`).digest().readUInt32BE(0);
|
|
28642
28663
|
return value / 4294967296 < input.rollout;
|
|
28664
|
+
}, 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) => {
|
|
28665
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0)
|
|
28666
|
+
throw new MobileUpdateRegistryError(`Mobile update health ${field} is invalid`);
|
|
28667
|
+
return value;
|
|
28668
|
+
}, parseHealthTransfer = (value) => {
|
|
28669
|
+
if (value === undefined)
|
|
28670
|
+
return;
|
|
28671
|
+
if (!object3(value))
|
|
28672
|
+
throw new MobileUpdateRegistryError("Mobile update health transfer is invalid");
|
|
28673
|
+
return {
|
|
28674
|
+
avoidedBytes: finiteMetric(value.avoidedBytes, "avoidedBytes"),
|
|
28675
|
+
downloadedBytes: finiteMetric(value.downloadedBytes, "downloadedBytes"),
|
|
28676
|
+
durationMs: finiteMetric(value.durationMs, "durationMs"),
|
|
28677
|
+
resumedBytes: finiteMetric(value.resumedBytes, "resumedBytes"),
|
|
28678
|
+
reusedBytes: finiteMetric(value.reusedBytes, "reusedBytes"),
|
|
28679
|
+
throughputBytesPerSecond: finiteMetric(value.throughputBytesPerSecond, "throughputBytesPerSecond")
|
|
28680
|
+
};
|
|
28643
28681
|
}, createMobileUpdateRegistry = (options) => {
|
|
28644
28682
|
const prefix = normalizedPrefix(options.prefix ?? DEFAULT_PREFIX);
|
|
28645
28683
|
const clock = options.clock ?? (() => new Date);
|
|
28684
|
+
const health = options.health;
|
|
28685
|
+
if (health && health.secret.length < 32)
|
|
28686
|
+
throw new MobileUpdateRegistryError("Mobile update health secret must contain at least 32 characters");
|
|
28687
|
+
if (health && !options.store.list)
|
|
28688
|
+
throw new MobileUpdateRegistryError("Mobile update health requires storage lifecycle listing");
|
|
28689
|
+
const minimumReports = health?.autoPause?.minimumReports ?? 20;
|
|
28690
|
+
const failureThreshold = health?.autoPause?.failureRate ?? 0.2;
|
|
28691
|
+
if (health && (!Number.isSafeInteger(minimumReports) || minimumReports < 1 || failureThreshold <= 0 || failureThreshold > 1))
|
|
28692
|
+
throw new MobileUpdateRegistryError("Mobile update health auto-pause policy is invalid");
|
|
28646
28693
|
const root = (appId) => `${prefix}/${appHash(appId)}`;
|
|
28647
28694
|
const releaseRoot = (manifest) => `${root(manifest.appId)}/releases/${manifest.releaseId}`;
|
|
28648
28695
|
const manifestKey = (manifest) => `${releaseRoot(manifest)}/update.json`;
|
|
28649
28696
|
const fileKey = (manifest, file5) => `${releaseRoot(manifest)}/files/${file5.path}`;
|
|
28650
28697
|
const contentBlobKey = (appId, sha256) => `${root(appId)}/blobs/${sha256}`;
|
|
28651
28698
|
const tombstoneKey = (appId, releaseId) => `${root(appId)}/gc/${releaseId}.json`;
|
|
28699
|
+
const healthRoot = (appId, promotionId, releaseId) => `${root(appId)}/health/${promotionId}/${releaseId}`;
|
|
28700
|
+
const pauseKey = (appId, promotionId, releaseId) => `${healthRoot(appId, promotionId, releaseId)}/paused.json`;
|
|
28652
28701
|
const channelKey = (appId, channel) => {
|
|
28653
28702
|
if (!APP_ID.test(appId) || !NAME.test(channel))
|
|
28654
28703
|
throw new MobileUpdateRegistryError("Mobile update channel identity is invalid");
|
|
@@ -28677,6 +28726,38 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
28677
28726
|
throw new MobileUpdateRegistryError("Stored mobile update channel identity changed");
|
|
28678
28727
|
return value;
|
|
28679
28728
|
};
|
|
28729
|
+
const signHealthToken = (payload) => {
|
|
28730
|
+
if (!health)
|
|
28731
|
+
return null;
|
|
28732
|
+
const encoded = base64Url(JSON.stringify(payload));
|
|
28733
|
+
const signature = createHmac("sha256", health.secret).update(encoded).digest("base64url");
|
|
28734
|
+
return `${encoded}.${signature}`;
|
|
28735
|
+
};
|
|
28736
|
+
const verifyHealthToken = (token) => {
|
|
28737
|
+
if (!health)
|
|
28738
|
+
throw new MobileUpdateRegistryError("Mobile update health reporting is not configured");
|
|
28739
|
+
const [encoded, provided, extra] = token.split(".");
|
|
28740
|
+
if (!encoded || !provided || extra)
|
|
28741
|
+
throw new MobileUpdateRegistryError("Mobile update health token is invalid");
|
|
28742
|
+
const expected = createHmac("sha256", health.secret).update(encoded).digest();
|
|
28743
|
+
let actual;
|
|
28744
|
+
try {
|
|
28745
|
+
actual = Buffer.from(provided, "base64url");
|
|
28746
|
+
} catch {
|
|
28747
|
+
actual = Buffer.alloc(0);
|
|
28748
|
+
}
|
|
28749
|
+
if (actual.byteLength !== expected.byteLength || !timingSafeEqual(actual, expected))
|
|
28750
|
+
throw new MobileUpdateRegistryError("Mobile update health token is invalid");
|
|
28751
|
+
let value;
|
|
28752
|
+
try {
|
|
28753
|
+
value = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
|
|
28754
|
+
} catch {
|
|
28755
|
+
throw new MobileUpdateRegistryError("Mobile update health token is invalid");
|
|
28756
|
+
}
|
|
28757
|
+
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")
|
|
28758
|
+
throw new MobileUpdateRegistryError("Mobile update health token is invalid");
|
|
28759
|
+
return value;
|
|
28760
|
+
};
|
|
28680
28761
|
const assertNotMarked = async (appId, releaseId) => {
|
|
28681
28762
|
if (!APP_ID.test(appId) || !RELEASE.test(releaseId))
|
|
28682
28763
|
throw new MobileUpdateRegistryError("Mobile update release identity is invalid");
|
|
@@ -28731,13 +28812,15 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
28731
28812
|
const channel = await readChannel(input.appId, input.channel);
|
|
28732
28813
|
if (!channel?.releaseId)
|
|
28733
28814
|
return { status: "empty" };
|
|
28734
|
-
|
|
28815
|
+
let selected = rolloutMember({
|
|
28735
28816
|
appId: input.appId,
|
|
28736
28817
|
channel: input.channel,
|
|
28737
28818
|
installationId: input.installationId,
|
|
28738
28819
|
releaseId: channel.releaseId,
|
|
28739
28820
|
rollout: channel.rollout
|
|
28740
28821
|
}) ? channel.releaseId : channel.fallbackReleaseId;
|
|
28822
|
+
if (health && selected === channel.releaseId && await options.store.head(pauseKey(input.appId, healthPromotionId(channel), channel.releaseId)))
|
|
28823
|
+
selected = channel.fallbackReleaseId;
|
|
28741
28824
|
if (!selected)
|
|
28742
28825
|
return { status: "empty" };
|
|
28743
28826
|
const release = await readManifest2(input.appId, selected);
|
|
@@ -28753,6 +28836,163 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
28753
28836
|
status: "selected"
|
|
28754
28837
|
};
|
|
28755
28838
|
};
|
|
28839
|
+
const issueUpdateHealthToken = async (input) => {
|
|
28840
|
+
if (!health)
|
|
28841
|
+
return null;
|
|
28842
|
+
const channel = await readChannel(input.appId, input.channel);
|
|
28843
|
+
if (!channel?.releaseId || channel.releaseId !== input.releaseId)
|
|
28844
|
+
return null;
|
|
28845
|
+
const resolution = await resolveUpdateState(input);
|
|
28846
|
+
if (resolution.status !== "selected" || resolution.manifest.releaseId !== input.releaseId)
|
|
28847
|
+
return null;
|
|
28848
|
+
return signHealthToken({
|
|
28849
|
+
appId: input.appId,
|
|
28850
|
+
channel: input.channel,
|
|
28851
|
+
format: HEALTH_TOKEN_VERSION,
|
|
28852
|
+
installationId: input.installationId,
|
|
28853
|
+
promotionId: healthPromotionId(channel),
|
|
28854
|
+
releaseId: input.releaseId,
|
|
28855
|
+
runtimeFingerprint: input.runtimeFingerprint
|
|
28856
|
+
});
|
|
28857
|
+
};
|
|
28858
|
+
const inspectUpdateHealth = async (input) => {
|
|
28859
|
+
if (!health)
|
|
28860
|
+
throw new MobileUpdateRegistryError("Mobile update health reporting is not configured");
|
|
28861
|
+
const channel = await readChannel(input.appId, input.channel);
|
|
28862
|
+
const releaseId = input.releaseId ?? channel?.releaseId;
|
|
28863
|
+
if (!channel || !releaseId || channel.releaseId !== releaseId)
|
|
28864
|
+
return null;
|
|
28865
|
+
const promotionId = healthPromotionId(channel);
|
|
28866
|
+
const prefix2 = `${healthRoot(input.appId, promotionId, releaseId)}/events/`;
|
|
28867
|
+
const objects = [];
|
|
28868
|
+
const cursors = new Set;
|
|
28869
|
+
let cursor;
|
|
28870
|
+
do {
|
|
28871
|
+
const page = await options.store.list({
|
|
28872
|
+
...cursor ? { cursor } : {},
|
|
28873
|
+
prefix: prefix2
|
|
28874
|
+
});
|
|
28875
|
+
objects.push(...page.objects);
|
|
28876
|
+
if (!page.truncated)
|
|
28877
|
+
break;
|
|
28878
|
+
if (!page.cursor || cursors.has(page.cursor))
|
|
28879
|
+
throw new MobileUpdateRegistryError("Mobile update health storage returned an invalid cursor");
|
|
28880
|
+
cursors.add(page.cursor);
|
|
28881
|
+
cursor = page.cursor;
|
|
28882
|
+
} while (true);
|
|
28883
|
+
const installations = new Set;
|
|
28884
|
+
const byKind = new Map([...HEALTH_KINDS].map((kind) => [
|
|
28885
|
+
kind,
|
|
28886
|
+
new Set
|
|
28887
|
+
]));
|
|
28888
|
+
const transfer = {
|
|
28889
|
+
avoidedBytes: 0,
|
|
28890
|
+
downloadedBytes: 0,
|
|
28891
|
+
durationMs: 0,
|
|
28892
|
+
resumedBytes: 0,
|
|
28893
|
+
reusedBytes: 0,
|
|
28894
|
+
throughputBytesPerSecond: 0
|
|
28895
|
+
};
|
|
28896
|
+
for (const item of objects) {
|
|
28897
|
+
const bytes = await options.store.get(item.key);
|
|
28898
|
+
if (!bytes)
|
|
28899
|
+
continue;
|
|
28900
|
+
const head = await options.store.head(item.key);
|
|
28901
|
+
if (!head || head.size !== bytes.byteLength || head.metadata?.sha256 !== digest(bytes))
|
|
28902
|
+
throw new MobileUpdateRegistryError("Stored mobile update health evidence integrity failed");
|
|
28903
|
+
const value = decode(bytes);
|
|
28904
|
+
if (!object3(value) || typeof value.installationHash !== "string" || !HEALTH_KINDS.has(String(value.kind)))
|
|
28905
|
+
throw new MobileUpdateRegistryError("Stored mobile update health evidence is invalid");
|
|
28906
|
+
const kind = value.kind;
|
|
28907
|
+
installations.add(value.installationHash);
|
|
28908
|
+
byKind.get(kind).add(value.installationHash);
|
|
28909
|
+
if (kind === "downloaded" && object3(value.transfer)) {
|
|
28910
|
+
const parsed = parseHealthTransfer(value.transfer);
|
|
28911
|
+
for (const key of Object.keys(transfer))
|
|
28912
|
+
transfer[key] += parsed[key];
|
|
28913
|
+
}
|
|
28914
|
+
}
|
|
28915
|
+
const failures = new Set([
|
|
28916
|
+
...byKind.get("quarantined"),
|
|
28917
|
+
...byKind.get("rolled-back")
|
|
28918
|
+
]);
|
|
28919
|
+
const terminals = new Set([...byKind.get("activated"), ...failures]);
|
|
28920
|
+
const failureRate = terminals.size === 0 ? 0 : failures.size / terminals.size;
|
|
28921
|
+
return {
|
|
28922
|
+
activated: byKind.get("activated").size,
|
|
28923
|
+
appId: input.appId,
|
|
28924
|
+
channel: input.channel,
|
|
28925
|
+
downloaded: byKind.get("downloaded").size,
|
|
28926
|
+
downloadFailed: byKind.get("download-failed").size,
|
|
28927
|
+
failureRate,
|
|
28928
|
+
failures: failures.size,
|
|
28929
|
+
paused: Boolean(await options.store.head(pauseKey(input.appId, promotionId, releaseId))),
|
|
28930
|
+
promotionId,
|
|
28931
|
+
quarantined: byKind.get("quarantined").size,
|
|
28932
|
+
releaseId,
|
|
28933
|
+
reportedInstallations: installations.size,
|
|
28934
|
+
rolledBack: byKind.get("rolled-back").size,
|
|
28935
|
+
rollout: channel.rollout,
|
|
28936
|
+
terminalReports: terminals.size,
|
|
28937
|
+
transfer
|
|
28938
|
+
};
|
|
28939
|
+
};
|
|
28940
|
+
const recordUpdateHealth = async (input) => {
|
|
28941
|
+
const payload = verifyHealthToken(input.token);
|
|
28942
|
+
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")
|
|
28943
|
+
throw new MobileUpdateRegistryError("Mobile update health evidence does not match its token");
|
|
28944
|
+
const release = await readManifest2(input.appId, input.releaseId);
|
|
28945
|
+
if (!release || release.manifest.runtimeFingerprint !== input.runtimeFingerprint)
|
|
28946
|
+
throw new MobileUpdateRegistryError("Mobile update health release is invalid");
|
|
28947
|
+
const activeChannel = await readChannel(input.appId, input.channel);
|
|
28948
|
+
if (!activeChannel || activeChannel.releaseId !== input.releaseId || healthPromotionId(activeChannel) !== payload.promotionId)
|
|
28949
|
+
throw new MobileUpdateRegistryError("Mobile update health promotion is no longer active");
|
|
28950
|
+
const transfer = parseHealthTransfer(input.transfer);
|
|
28951
|
+
const installationHash = createHmac("sha256", health.secret).update(input.installationId).digest("hex");
|
|
28952
|
+
const evidence = {
|
|
28953
|
+
format: 1,
|
|
28954
|
+
installationHash,
|
|
28955
|
+
kind: input.kind,
|
|
28956
|
+
observedAt: clock().toISOString(),
|
|
28957
|
+
...input.reason ? { reason: input.reason } : {},
|
|
28958
|
+
...transfer ? { transfer } : {}
|
|
28959
|
+
};
|
|
28960
|
+
const bytes = json(evidence);
|
|
28961
|
+
await options.store.put(`${healthRoot(input.appId, payload.promotionId, input.releaseId)}/events/${installationHash}/${input.kind}.json`, bytes, {
|
|
28962
|
+
cacheControl: "no-store",
|
|
28963
|
+
contentType: "application/json",
|
|
28964
|
+
maxBytes: bytes.byteLength,
|
|
28965
|
+
metadata: { kind: input.kind, sha256: digest(bytes) }
|
|
28966
|
+
});
|
|
28967
|
+
let report = await inspectUpdateHealth({
|
|
28968
|
+
appId: input.appId,
|
|
28969
|
+
channel: input.channel,
|
|
28970
|
+
releaseId: input.releaseId
|
|
28971
|
+
});
|
|
28972
|
+
if (!report)
|
|
28973
|
+
throw new MobileUpdateRegistryError("Mobile update health promotion is no longer active");
|
|
28974
|
+
if (FAILURE_HEALTH_KINDS.has(input.kind) && report.terminalReports >= minimumReports && report.failureRate >= failureThreshold && !report.paused) {
|
|
28975
|
+
const marker = json({
|
|
28976
|
+
appId: input.appId,
|
|
28977
|
+
channel: input.channel,
|
|
28978
|
+
failureRate: report.failureRate,
|
|
28979
|
+
failures: report.failures,
|
|
28980
|
+
format: 1,
|
|
28981
|
+
pausedAt: clock().toISOString(),
|
|
28982
|
+
promotionId: payload.promotionId,
|
|
28983
|
+
releaseId: input.releaseId,
|
|
28984
|
+
reports: report.terminalReports
|
|
28985
|
+
});
|
|
28986
|
+
await options.store.put(pauseKey(input.appId, payload.promotionId, input.releaseId), marker, {
|
|
28987
|
+
cacheControl: "no-store",
|
|
28988
|
+
contentType: "application/json",
|
|
28989
|
+
maxBytes: marker.byteLength,
|
|
28990
|
+
metadata: { releaseid: input.releaseId, sha256: digest(marker) }
|
|
28991
|
+
});
|
|
28992
|
+
report = { ...report, paused: true };
|
|
28993
|
+
}
|
|
28994
|
+
return report;
|
|
28995
|
+
};
|
|
28756
28996
|
const retentionValues = (input) => {
|
|
28757
28997
|
if (!APP_ID.test(input.appId))
|
|
28758
28998
|
throw new MobileUpdateRegistryError("Mobile update appId is invalid");
|
|
@@ -28995,6 +29235,7 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
28995
29235
|
return result;
|
|
28996
29236
|
};
|
|
28997
29237
|
return {
|
|
29238
|
+
...health ? { inspectUpdateHealth, issueUpdateHealthToken, recordUpdateHealth } : {},
|
|
28998
29239
|
inspectUpdateStorage: async (input) => (await inventory(input)).report,
|
|
28999
29240
|
pruneUpdates,
|
|
29000
29241
|
publishUpdate: async (input) => {
|
|
@@ -29272,7 +29513,7 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
29272
29513
|
const origin = request.headers.get("origin");
|
|
29273
29514
|
const cors = origin && allowedOrigins.has(origin) ? {
|
|
29274
29515
|
"access-control-allow-origin": origin,
|
|
29275
|
-
"access-control-expose-headers": "content-range,etag",
|
|
29516
|
+
"access-control-expose-headers": "content-range,etag,x-absolute-mobile-health-token",
|
|
29276
29517
|
vary: "Origin"
|
|
29277
29518
|
} : {};
|
|
29278
29519
|
if (request.method === "OPTIONS") {
|
|
@@ -29281,17 +29522,58 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
29281
29522
|
return new Response(null, {
|
|
29282
29523
|
headers: {
|
|
29283
29524
|
...cors,
|
|
29284
|
-
"access-control-allow-headers": "if-range,range,x-absolute-mobile-app,x-absolute-mobile-channel,x-absolute-mobile-installation,x-absolute-mobile-release,x-absolute-mobile-runtime",
|
|
29285
|
-
"access-control-allow-methods": "GET,OPTIONS",
|
|
29525
|
+
"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",
|
|
29526
|
+
"access-control-allow-methods": "GET,POST,OPTIONS",
|
|
29286
29527
|
"access-control-max-age": "600"
|
|
29287
29528
|
},
|
|
29288
29529
|
status: 204
|
|
29289
29530
|
});
|
|
29290
29531
|
}
|
|
29291
|
-
if (request.method !== "GET")
|
|
29292
|
-
return new Response(null, { status: 405 });
|
|
29293
29532
|
const pathname = new URL(request.url).pathname.replace(/^\/+/, "");
|
|
29294
29533
|
const relative19 = pathname.startsWith(`${route}/`) ? pathname.slice(route.length + 1) : "";
|
|
29534
|
+
if (request.method === "POST" && relative19 === "health") {
|
|
29535
|
+
if (!options.registry.recordUpdateHealth)
|
|
29536
|
+
return new Response(null, { status: 404 });
|
|
29537
|
+
const appId = request.headers.get("x-absolute-mobile-app");
|
|
29538
|
+
const channel = request.headers.get("x-absolute-mobile-channel");
|
|
29539
|
+
const installationId = request.headers.get("x-absolute-mobile-installation");
|
|
29540
|
+
const runtimeFingerprint = request.headers.get("x-absolute-mobile-runtime");
|
|
29541
|
+
const token = request.headers.get("x-absolute-mobile-health-token");
|
|
29542
|
+
const declared = Number(request.headers.get("content-length"));
|
|
29543
|
+
if (appId !== options.appId || channel !== options.channel || !installationId || !runtimeFingerprint || !token || Number.isFinite(declared) && declared > 4096)
|
|
29544
|
+
return new Response(null, { status: 400 });
|
|
29545
|
+
const bodyBytes = new Uint8Array(await request.arrayBuffer());
|
|
29546
|
+
if (bodyBytes.byteLength > 4096)
|
|
29547
|
+
return new Response(null, { status: 413 });
|
|
29548
|
+
let body;
|
|
29549
|
+
try {
|
|
29550
|
+
body = JSON.parse(new TextDecoder().decode(bodyBytes));
|
|
29551
|
+
} catch {
|
|
29552
|
+
return new Response(null, { status: 400 });
|
|
29553
|
+
}
|
|
29554
|
+
if (!object3(body) || typeof body.releaseId !== "string" || typeof body.kind !== "string")
|
|
29555
|
+
return new Response(null, { status: 400 });
|
|
29556
|
+
try {
|
|
29557
|
+
const report = await options.registry.recordUpdateHealth({
|
|
29558
|
+
appId,
|
|
29559
|
+
channel,
|
|
29560
|
+
installationId,
|
|
29561
|
+
kind: body.kind,
|
|
29562
|
+
...body.reason === "boot-interrupted" || body.reason === "boot-timeout" ? { reason: body.reason } : {},
|
|
29563
|
+
releaseId: body.releaseId,
|
|
29564
|
+
runtimeFingerprint,
|
|
29565
|
+
token,
|
|
29566
|
+
...object3(body.transfer) ? { transfer: body.transfer } : {}
|
|
29567
|
+
});
|
|
29568
|
+
return Response.json({ paused: report.paused }, { headers: { ...cors, "cache-control": "no-store" }, status: 202 });
|
|
29569
|
+
} catch (error) {
|
|
29570
|
+
if (error instanceof MobileUpdateRegistryError)
|
|
29571
|
+
return new Response(null, { status: 403 });
|
|
29572
|
+
throw error;
|
|
29573
|
+
}
|
|
29574
|
+
}
|
|
29575
|
+
if (request.method !== "GET")
|
|
29576
|
+
return new Response(null, { status: 405 });
|
|
29295
29577
|
if (relative19 === "update.json") {
|
|
29296
29578
|
const expoProtocolVersion = request.headers.get("expo-protocol-version");
|
|
29297
29579
|
const expoProtocol = expoProtocolVersion !== null;
|
|
@@ -29317,6 +29599,13 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
29317
29599
|
installationId,
|
|
29318
29600
|
runtimeFingerprint
|
|
29319
29601
|
});
|
|
29602
|
+
const healthToken = selected && options.registry.issueUpdateHealthToken ? await options.registry.issueUpdateHealthToken({
|
|
29603
|
+
appId,
|
|
29604
|
+
channel,
|
|
29605
|
+
installationId,
|
|
29606
|
+
releaseId: selected.manifest.releaseId,
|
|
29607
|
+
runtimeFingerprint
|
|
29608
|
+
}) : null;
|
|
29320
29609
|
if (expoProtocol) {
|
|
29321
29610
|
let requestedCodeSigning;
|
|
29322
29611
|
try {
|
|
@@ -29368,6 +29657,7 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
29368
29657
|
extra: {
|
|
29369
29658
|
absolutejs: {
|
|
29370
29659
|
channel: selected.manifest.channel,
|
|
29660
|
+
...healthToken ? { healthToken } : {},
|
|
29371
29661
|
releaseId: selected.manifest.releaseId
|
|
29372
29662
|
},
|
|
29373
29663
|
expoClient: descriptor.expoConfig
|
|
@@ -29395,7 +29685,8 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
|
|
|
29395
29685
|
headers: {
|
|
29396
29686
|
...cors,
|
|
29397
29687
|
"cache-control": "no-store",
|
|
29398
|
-
etag: `"${selected.manifest.releaseId}"
|
|
29688
|
+
etag: `"${selected.manifest.releaseId}"`,
|
|
29689
|
+
...healthToken ? { "x-absolute-mobile-health-token": healthToken } : {}
|
|
29399
29690
|
}
|
|
29400
29691
|
});
|
|
29401
29692
|
}
|
|
@@ -29458,6 +29749,14 @@ var init_mobileUpdate = __esm(() => {
|
|
|
29458
29749
|
RELEASE = /^amu_[a-f0-9]{64}$/;
|
|
29459
29750
|
APP_ID = /^[A-Za-z][\w]*(?:\.[A-Za-z][\w]*)+$/;
|
|
29460
29751
|
NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
29752
|
+
HEALTH_KINDS = new Set([
|
|
29753
|
+
"activated",
|
|
29754
|
+
"downloaded",
|
|
29755
|
+
"download-failed",
|
|
29756
|
+
"quarantined",
|
|
29757
|
+
"rolled-back"
|
|
29758
|
+
]);
|
|
29759
|
+
FAILURE_HEALTH_KINDS = new Set(["quarantined", "rolled-back"]);
|
|
29461
29760
|
MobileUpdateRegistryError = class MobileUpdateRegistryError extends Error {
|
|
29462
29761
|
};
|
|
29463
29762
|
expoProtocolHeaders = {
|
|
@@ -29545,6 +29844,11 @@ var ABSOLUTE_MOBILE_UPDATE_SERVER_FORMAT = 1, DEFAULT_MOBILE_UPDATE_REGISTRY_MOD
|
|
|
29545
29844
|
} catch (error) {
|
|
29546
29845
|
throw new TypeError(`Durable mobile update storage verification failed for ${module.metadata.provider}. Check the bucket, endpoint, credentials, and read/write/delete permissions.`, { cause: error });
|
|
29547
29846
|
}
|
|
29847
|
+
}, verifyHealthModule = (config, module) => {
|
|
29848
|
+
if (!config.updateServer?.health)
|
|
29849
|
+
return;
|
|
29850
|
+
if (typeof module.registry.inspectUpdateHealth !== "function" || typeof module.registry.issueUpdateHealthToken !== "function" || typeof module.registry.recordUpdateHealth !== "function")
|
|
29851
|
+
throw new TypeError("Mobile update fleet health is enabled but the registry is not provisioned for it. Run `absolute mobile update provision --force`.");
|
|
29548
29852
|
}, expoSigningOptions = (config) => {
|
|
29549
29853
|
if (!config.updates?.expoCodeSigning)
|
|
29550
29854
|
return;
|
|
@@ -29576,8 +29880,10 @@ var ABSOLUTE_MOBILE_UPDATE_SERVER_FORMAT = 1, DEFAULT_MOBILE_UPDATE_REGISTRY_MOD
|
|
|
29576
29880
|
if (!updates || !server?.autoMount)
|
|
29577
29881
|
return new Elysia5({ name: "absolutejs-mobile-updates-disabled" });
|
|
29578
29882
|
const module = await loadAbsoluteMobileUpdateServerModule(projectRoot, server.registryModule);
|
|
29579
|
-
if (options.production)
|
|
29883
|
+
if (options.production) {
|
|
29580
29884
|
await verifyDurableModule(module);
|
|
29885
|
+
verifyHealthModule(config, module);
|
|
29886
|
+
}
|
|
29581
29887
|
const manifest = new URL(updates.manifestUrl);
|
|
29582
29888
|
if (!manifest.pathname.endsWith("/update.json"))
|
|
29583
29889
|
throw new TypeError("Auto-mounted mobile update manifests must end in /update.json.");
|
|
@@ -29596,10 +29902,11 @@ var ABSOLUTE_MOBILE_UPDATE_SERVER_FORMAT = 1, DEFAULT_MOBILE_UPDATE_REGISTRY_MOD
|
|
|
29596
29902
|
return;
|
|
29597
29903
|
const module = await loadAbsoluteMobileUpdateServerModule(projectRoot, config.updateServer?.registryModule);
|
|
29598
29904
|
await verifyDurableModule(module);
|
|
29905
|
+
verifyHealthModule(config, module);
|
|
29599
29906
|
if (config.engine === "expo")
|
|
29600
29907
|
expoSigningOptions(config);
|
|
29601
29908
|
return module.metadata;
|
|
29602
|
-
}, publicKeysSource = (publicKeys) => JSON.stringify(publicKeys, null, "\t"),
|
|
29909
|
+
}, publicKeysSource = (publicKeys) => JSON.stringify(publicKeys, null, "\t"), renderAbsoluteMobileUpdateRegistryBase = (options) => {
|
|
29603
29910
|
const metadata = `export const absoluteMobileUpdateServer = {
|
|
29604
29911
|
format: 1,
|
|
29605
29912
|
provider: '${options.storage}',
|
|
@@ -29665,6 +29972,19 @@ export default createMobileUpdateRegistry({
|
|
|
29665
29972
|
store
|
|
29666
29973
|
});
|
|
29667
29974
|
`;
|
|
29975
|
+
}, renderAbsoluteMobileUpdateRegistry = (options) => {
|
|
29976
|
+
const source = renderAbsoluteMobileUpdateRegistryBase(options);
|
|
29977
|
+
if (!options.health)
|
|
29978
|
+
return source;
|
|
29979
|
+
const secret = options.storage === "local" ? `process.env.${options.health.secretEnv} ?? 'absolutejs-local-health-secret-not-for-production'` : `required('${options.health.secretEnv}')`;
|
|
29980
|
+
const health = ` health: {
|
|
29981
|
+
autoPause: { failureRate: ${options.health.failureRate}, minimumReports: ${options.health.minimumReports} },
|
|
29982
|
+
secret: ${secret}
|
|
29983
|
+
},
|
|
29984
|
+
`;
|
|
29985
|
+
return source.replace(`export default createMobileUpdateRegistry({
|
|
29986
|
+
`, `export default createMobileUpdateRegistry({
|
|
29987
|
+
${health}`);
|
|
29668
29988
|
}, writeAbsoluteMobileUpdateRegistry = async (options) => {
|
|
29669
29989
|
const path2 = projectPath(options.projectRoot, options.modulePath ?? DEFAULT_MOBILE_UPDATE_REGISTRY_MODULE);
|
|
29670
29990
|
if (!options.force) {
|
|
@@ -29676,6 +29996,7 @@ export default createMobileUpdateRegistry({
|
|
|
29676
29996
|
}
|
|
29677
29997
|
await mkdir13(dirname31(path2), { recursive: true });
|
|
29678
29998
|
await Bun.write(path2, renderAbsoluteMobileUpdateRegistry({
|
|
29999
|
+
...options.health ? { health: options.health } : {},
|
|
29679
30000
|
publicKeys: options.publicKeys,
|
|
29680
30001
|
storage: options.storage
|
|
29681
30002
|
}));
|
|
@@ -37278,5 +37599,5 @@ export {
|
|
|
37278
37599
|
devBuild
|
|
37279
37600
|
};
|
|
37280
37601
|
|
|
37281
|
-
//# debugId=
|
|
37602
|
+
//# debugId=C5E73CF77BAB352E64756E2164756E21
|
|
37282
37603
|
//# sourceMappingURL=build.js.map
|