@absolutejs/absolute 0.20.0-beta.63 → 0.20.0-beta.64

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/cli/index.js CHANGED
@@ -1693,6 +1693,158 @@ var init_deviceCapabilities = __esm(() => {
1693
1693
  ];
1694
1694
  });
1695
1695
 
1696
+ // src/mobile/updateProtocol.ts
1697
+ var ABSOLUTE_MOBILE_UPDATE_FORMAT = 1, ABSOLUTE_MOBILE_UPDATE_MAX_FILE_BYTES, ABSOLUTE_MOBILE_UPDATE_MAX_FILES = 1e4, ABSOLUTE_MOBILE_UPDATE_MAX_TOTAL_BYTES, ABSOLUTE_MOBILE_UPDATE_SIGNATURE_ALGORITHM = "ecdsa-p256-sha256", HASH_PATTERN, RELEASE_PATTERN, KEY_ID_PATTERN, CHANNEL_PATTERN, isClassification = (value) => value === "bug-fix" || value === "content" || value === "security", object3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), canonicalValue = (value) => {
1698
+ if (Array.isArray(value))
1699
+ return value.map(canonicalValue);
1700
+ if (!object3(value))
1701
+ return value;
1702
+ return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonicalValue(value[key])]));
1703
+ }, absoluteMobileUpdateSigningPayload = (manifest) => new TextEncoder().encode(canonicalizeAbsoluteMobileUpdate(manifest)), canonicalizeAbsoluteMobileUpdate = (value) => JSON.stringify(canonicalValue(value)), requireText2 = (value, field) => {
1704
+ if (typeof value !== "string" || value.length === 0)
1705
+ throw new TypeError(`${field} must be a non-empty string.`);
1706
+ return value;
1707
+ }, normalizeAbsoluteMobileUpdatePath = (value) => {
1708
+ const path = requireText2(value, "Update file path").replaceAll("\\", "/");
1709
+ if (path.startsWith("/") || path.includes("\x00") || path.split("/").some((segment) => !segment || segment === "." || segment === "..")) {
1710
+ throw new TypeError("Update file paths must be normalized relative paths.");
1711
+ }
1712
+ return path;
1713
+ }, parseFile = (value) => {
1714
+ if (!object3(value))
1715
+ throw new TypeError("Update files must be objects.");
1716
+ const path = normalizeAbsoluteMobileUpdatePath(value.path);
1717
+ if (typeof value.bytes !== "number" || !Number.isSafeInteger(value.bytes) || value.bytes < 0 || value.bytes > ABSOLUTE_MOBILE_UPDATE_MAX_FILE_BYTES) {
1718
+ throw new TypeError(`Update file ${path} has an invalid byte length.`);
1719
+ }
1720
+ if (typeof value.sha256 !== "string" || !HASH_PATTERN.test(value.sha256))
1721
+ throw new TypeError(`Update file ${path} has an invalid SHA-256 digest.`);
1722
+ return { bytes: value.bytes, path, sha256: value.sha256 };
1723
+ }, parseAbsoluteMobileUnsignedUpdateManifest = (value) => {
1724
+ if (!object3(value) || value.format !== ABSOLUTE_MOBILE_UPDATE_FORMAT)
1725
+ throw new TypeError("Invalid AbsoluteJS mobile update manifest.");
1726
+ const appId = requireText2(value.appId, "Update appId");
1727
+ const channel = requireText2(value.channel, "Update channel");
1728
+ if (!CHANNEL_PATTERN.test(channel))
1729
+ throw new TypeError("Update channel contains unsupported characters.");
1730
+ if (!isClassification(value.classification))
1731
+ throw new TypeError("Update classification is invalid.");
1732
+ const createdAt = requireText2(value.createdAt, "Update createdAt");
1733
+ if (!Number.isFinite(Date.parse(createdAt)) || new Date(createdAt).toISOString() !== createdAt)
1734
+ throw new TypeError("Update createdAt must be a canonical ISO timestamp.");
1735
+ if (!Array.isArray(value.files) || value.files.length === 0)
1736
+ throw new TypeError("An update must contain at least one file.");
1737
+ if (value.files.length > ABSOLUTE_MOBILE_UPDATE_MAX_FILES)
1738
+ throw new TypeError("Update contains too many files.");
1739
+ const files = value.files.map(parseFile);
1740
+ const sorted = [...files].sort((left, right) => left.path.localeCompare(right.path));
1741
+ if (files.some((file, index) => file.path !== sorted[index]?.path))
1742
+ throw new TypeError("Update files must be sorted by path.");
1743
+ if (new Set(files.map(({ path }) => path)).size !== files.length)
1744
+ throw new TypeError("Update file paths must be unique.");
1745
+ if (files.reduce((total, file) => total + file.bytes, 0) > ABSOLUTE_MOBILE_UPDATE_MAX_TOTAL_BYTES)
1746
+ throw new TypeError("Update exceeds the maximum uncompressed size.");
1747
+ if (typeof value.releaseId !== "string" || !RELEASE_PATTERN.test(value.releaseId))
1748
+ throw new TypeError("Update releaseId is invalid.");
1749
+ if (typeof value.runtimeFingerprint !== "string" || !HASH_PATTERN.test(value.runtimeFingerprint))
1750
+ throw new TypeError("Update runtime fingerprint is invalid.");
1751
+ if (value.withinSubmittedPurpose !== true)
1752
+ throw new TypeError("OTA updates must attest that they remain within the submitted app purpose.");
1753
+ return {
1754
+ appId,
1755
+ channel,
1756
+ classification: value.classification,
1757
+ createdAt,
1758
+ files,
1759
+ format: ABSOLUTE_MOBILE_UPDATE_FORMAT,
1760
+ releaseId: value.releaseId,
1761
+ runtimeFingerprint: value.runtimeFingerprint,
1762
+ withinSubmittedPurpose: true
1763
+ };
1764
+ }, parseAbsoluteMobileUpdateManifest = (value) => {
1765
+ if (!object3(value))
1766
+ throw new TypeError("Invalid AbsoluteJS mobile update manifest.");
1767
+ const { signature: signatureValue, ...unsignedValue } = value;
1768
+ const unsigned = parseAbsoluteMobileUnsignedUpdateManifest(unsignedValue);
1769
+ if (!object3(signatureValue))
1770
+ throw new TypeError("Update signature is missing.");
1771
+ if (signatureValue.algorithm !== ABSOLUTE_MOBILE_UPDATE_SIGNATURE_ALGORITHM)
1772
+ throw new TypeError("Update signature algorithm is unsupported.");
1773
+ const keyId = requireText2(signatureValue.keyId, "Update signature keyId");
1774
+ if (!KEY_ID_PATTERN.test(keyId))
1775
+ throw new TypeError("Update signature keyId is invalid.");
1776
+ const signature = requireText2(signatureValue.value, "Update signature value");
1777
+ let signatureBytes;
1778
+ try {
1779
+ signatureBytes = Uint8Array.from(atob(signature), (character) => character.charCodeAt(0));
1780
+ } catch {
1781
+ signatureBytes = new Uint8Array;
1782
+ }
1783
+ if (!/^[A-Za-z0-9+/]+={0,2}$/u.test(signature) || signatureBytes.byteLength !== 64 || btoa(String.fromCharCode(...signatureBytes)) !== signature)
1784
+ throw new TypeError("Update signature is not canonical base64.");
1785
+ return {
1786
+ ...unsigned,
1787
+ signature: {
1788
+ algorithm: ABSOLUTE_MOBILE_UPDATE_SIGNATURE_ALGORITHM,
1789
+ keyId,
1790
+ value: signature
1791
+ }
1792
+ };
1793
+ }, unsignedAbsoluteMobileUpdate = (manifest) => {
1794
+ const { signature: _signature, ...unsigned } = manifest;
1795
+ return unsigned;
1796
+ };
1797
+ var init_updateProtocol = __esm(() => {
1798
+ ABSOLUTE_MOBILE_UPDATE_MAX_FILE_BYTES = 32 * 1024 * 1024;
1799
+ ABSOLUTE_MOBILE_UPDATE_MAX_TOTAL_BYTES = 128 * 1024 * 1024;
1800
+ HASH_PATTERN = /^[a-f0-9]{64}$/u;
1801
+ RELEASE_PATTERN = /^amu_[a-f0-9]{64}$/u;
1802
+ KEY_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u;
1803
+ CHANNEL_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u;
1804
+ });
1805
+
1806
+ // src/mobile/updateRuntime.ts
1807
+ import { createHash } from "crypto";
1808
+ var ABSOLUTE_MOBILE_SHELL_ABI = 2, ABSOLUTE_MOBILE_UPDATE_RUNTIME_FORMAT = 1, createAbsoluteMobileUpdateRuntimeDescriptor = (options) => ({
1809
+ appId: options.config.appId,
1810
+ auth: options.auth ?? null,
1811
+ deepLinks: {
1812
+ hosts: options.config.deepLinkHosts,
1813
+ ...options.config.deepLinkScheme ? { scheme: options.config.deepLinkScheme } : {}
1814
+ },
1815
+ deviceCapabilities: options.deviceCapabilities,
1816
+ engine: options.config.engine,
1817
+ format: ABSOLUTE_MOBILE_UPDATE_RUNTIME_FORMAT,
1818
+ shellAbi: ABSOLUTE_MOBILE_SHELL_ABI,
1819
+ syncSchema: options.syncSchema ?? null,
1820
+ updates: options.config.updates ?? null
1821
+ }), fingerprintAbsoluteMobileUpdateRuntime = (descriptor) => createHash("sha256").update(canonicalizeAbsoluteMobileUpdate(descriptor)).digest("hex"), resolveAbsoluteMobileUpdateRuntime = (config, projectRoot) => {
1822
+ const auth = resolveAbsoluteMobileAuthManifest(projectRoot, config);
1823
+ const sync = auth !== undefined && projectUsesAbsoluteSync(projectRoot);
1824
+ const syncSchema = sync ? discoverAbsoluteSyncSchema(projectRoot) : undefined;
1825
+ const deviceCapabilities = resolveAbsoluteDeviceCapabilityPlan(projectRoot, config.engine);
1826
+ const descriptor = createAbsoluteMobileUpdateRuntimeDescriptor({
1827
+ ...auth ? { auth } : {},
1828
+ config,
1829
+ deviceCapabilities,
1830
+ ...syncSchema ? { syncSchema } : {}
1831
+ });
1832
+ return {
1833
+ auth,
1834
+ descriptor,
1835
+ deviceCapabilities,
1836
+ fingerprint: fingerprintAbsoluteMobileUpdateRuntime(descriptor),
1837
+ sync,
1838
+ syncSchema
1839
+ };
1840
+ };
1841
+ var init_updateRuntime = __esm(() => {
1842
+ init_deviceCapabilities();
1843
+ init_nativeAuth();
1844
+ init_updateProtocol();
1845
+ init_syncSchema();
1846
+ });
1847
+
1696
1848
  // src/mobile/expoProject.ts
1697
1849
  import {
1698
1850
  access,
@@ -1705,7 +1857,7 @@ import {
1705
1857
  rm,
1706
1858
  writeFile
1707
1859
  } from "fs/promises";
1708
- import { createHash } from "crypto";
1860
+ import { createHash as createHash2 } from "crypto";
1709
1861
  import { basename as basename2, dirname as dirname4, join as join8, relative as relative2, resolve as resolve5, sep } from "path";
1710
1862
  var EXPO_GENERATED_HEADER = `// Generated by AbsoluteJS. Edit absolute.config.ts, then run absolute mobile sync.
1711
1863
  `, EXPO_ASSET_EXTENSION = ".absasset", EXPO_PROJECT_MARKER = ".absolutejs-expo-project", exists = async (path) => {
@@ -1772,7 +1924,7 @@ var EXPO_GENERATED_HEADER = `// Generated by AbsoluteJS. Edit absolute.config.ts
1772
1924
  }, routeFile = (project, route) => join8(project, "app", ...routeSegments(route), "index.tsx"), packageDependencies = (plan) => Object.fromEntries(plan.requiredPackages.map((spec) => {
1773
1925
  const separator = spec.lastIndexOf("@");
1774
1926
  return [spec.slice(0, separator), spec.slice(separator + 1)];
1775
- })), expoPackage = (auth, sync, devices) => ({
1927
+ })), expoPackage = (auth, sync, devices, updates) => ({
1776
1928
  dependencies: {
1777
1929
  "@absolutejs/devices": "0.7.0",
1778
1930
  "@absolutejs/devices-expo": "0.0.2",
@@ -1787,6 +1939,7 @@ var EXPO_GENERATED_HEADER = `// Generated by AbsoluteJS. Edit absolute.config.ts
1787
1939
  expo: "~57.0.9",
1788
1940
  "expo-asset": "~57.0.15",
1789
1941
  "expo-constants": "~57.0.16",
1942
+ ...updates ? { "expo-crypto": "~57.0.1" } : {},
1790
1943
  "expo-dev-client": "~57.0.16",
1791
1944
  "expo-file-system": "~57.0.6",
1792
1945
  "expo-haptics": "~57.0.2",
@@ -1795,13 +1948,13 @@ var EXPO_GENERATED_HEADER = `// Generated by AbsoluteJS. Edit absolute.config.ts
1795
1948
  "expo-background-task": "~57.0.14",
1796
1949
  "expo-network": "~57.0.1",
1797
1950
  "expo-sqlite": "~57.0.2",
1798
- "expo-task-manager": "~57.0.14",
1799
- "expo-updates": "~57.0.19"
1951
+ "expo-task-manager": "~57.0.14"
1800
1952
  } : {},
1953
+ ...updates || sync ? { "expo-updates": "~57.0.19" } : {},
1801
1954
  "expo-router": "~57.0.17",
1802
- ...auth ? {
1955
+ ...auth || updates ? {
1803
1956
  "expo-secure-store": "~57.0.2",
1804
- "expo-web-browser": "~57.0.2"
1957
+ ...auth ? { "expo-web-browser": "~57.0.2" } : {}
1805
1958
  } : {},
1806
1959
  react: "19.2.3",
1807
1960
  "react-native": "0.86.3",
@@ -1823,7 +1976,7 @@ var EXPO_GENERATED_HEADER = `// Generated by AbsoluteJS. Edit absolute.config.ts
1823
1976
  start: "expo start --dev-client"
1824
1977
  },
1825
1978
  version: "0.0.0"
1826
- }), expoAppConfig = (config, auth, sync, devices) => {
1979
+ }), expoAppConfig = (config, auth, sync, devices, runtimeFingerprint) => {
1827
1980
  const requirements = absoluteDeviceNativeRequirements(devices);
1828
1981
  const usageKey = (purpose) => {
1829
1982
  if (purpose === "camera")
@@ -1909,7 +2062,7 @@ var EXPO_GENERATED_HEADER = `// Generated by AbsoluteJS. Edit absolute.config.ts
1909
2062
  plugins: [
1910
2063
  "expo-router",
1911
2064
  ["expo-dev-client", { launchMode: "most-recent" }],
1912
- ...auth ? ["expo-secure-store"] : [],
2065
+ ...auth || config.updates ? ["expo-secure-store"] : [],
1913
2066
  ...sync ? [
1914
2067
  "expo-sqlite",
1915
2068
  "expo-background-task",
@@ -1917,9 +2070,20 @@ var EXPO_GENERATED_HEADER = `// Generated by AbsoluteJS. Edit absolute.config.ts
1917
2070
  ] : [],
1918
2071
  ...configuredPlugins
1919
2072
  ],
1920
- runtimeVersion: { policy: "appVersion" },
2073
+ runtimeVersion: runtimeFingerprint,
1921
2074
  scheme: config.deepLinkScheme,
1922
2075
  slug: config.appId.toLowerCase().replaceAll(".", "-"),
2076
+ ...config.updates ? {
2077
+ updates: {
2078
+ checkAutomatically: "NEVER",
2079
+ fallbackToCacheTimeout: config.updates.bootTimeoutMs,
2080
+ requestHeaders: {
2081
+ "x-absolute-mobile-app": config.appId,
2082
+ "x-absolute-mobile-channel": config.updates.channel
2083
+ },
2084
+ url: config.updates.manifestUrl
2085
+ }
2086
+ } : {},
1923
2087
  version: config.iosVersion ?? "0.1.0"
1924
2088
  }
1925
2089
  };
@@ -1937,13 +2101,16 @@ config.resolver.nodeModulesPaths = [
1937
2101
  config.watchFolders = [appRoot];
1938
2102
 
1939
2103
  module.exports = config;
1940
- `, layoutSource = (auth, sync) => `${EXPO_GENERATED_HEADER}import { Stack } from 'expo-router';
2104
+ `, layoutSource = (auth, sync, updates) => `${EXPO_GENERATED_HEADER}import { Stack } from 'expo-router';
1941
2105
  import '../src/generated/AbsoluteDevices';
1942
2106
  ${auth ? `import { useEffect, useState } from 'react';
1943
2107
  import { startAbsoluteExpoAuth } from '../src/generated/AbsoluteAuth';` : ""}
1944
2108
  ${sync ? "import { startAbsoluteExpoSync } from '../src/generated/AbsoluteSync';" : ""}
2109
+ ${updates ? `import { useEffect as useUpdateEffect } from 'react';
2110
+ import { startAbsoluteExpoUpdates } from '../src/generated/AbsoluteUpdates';` : ""}
1945
2111
 
1946
2112
  export default function AbsoluteLayout() {
2113
+ ${updates ? "useUpdateEffect(() => { void startAbsoluteExpoUpdates(); }, []);" : ""}
1947
2114
  ${auth ? `const [ready, setReady] = useState(false);
1948
2115
  useEffect(() => {
1949
2116
  let active = true;
@@ -1953,6 +2120,39 @@ export default function AbsoluteLayout() {
1953
2120
  if (!ready) return null;` : ""}
1954
2121
  return <Stack screenOptions={{ headerShown: false }} />;
1955
2122
  }
2123
+ `, updatesRuntimeSource = (config) => `${EXPO_GENERATED_HEADER}import { randomUUID } from 'expo-crypto';
2124
+ import * as SecureStore from 'expo-secure-store';
2125
+ import * as Updates from 'expo-updates';
2126
+
2127
+ const INSTALLATION_KEY = 'absolutejs.mobile.update.installation.v1';
2128
+ const REQUEST_HEADERS = ${JSON.stringify({
2129
+ "x-absolute-mobile-app": config.appId,
2130
+ "x-absolute-mobile-channel": config.updates?.channel
2131
+ })};
2132
+
2133
+ let startPromise: Promise<void> | undefined;
2134
+ export const startAbsoluteExpoUpdates = () => {
2135
+ startPromise ??= (async () => {
2136
+ if (!Updates.isEnabled || __DEV__) return;
2137
+ let installationId = await SecureStore.getItemAsync(INSTALLATION_KEY);
2138
+ if (!installationId) {
2139
+ installationId = randomUUID();
2140
+ await SecureStore.setItemAsync(INSTALLATION_KEY, installationId);
2141
+ }
2142
+ await Updates.setUpdateRequestHeadersOverride({
2143
+ ...REQUEST_HEADERS,
2144
+ 'x-absolute-mobile-installation': installationId
2145
+ });
2146
+ const result = await Updates.checkForUpdateAsync();
2147
+ if (result.isAvailable || result.isRollBackToEmbedded) {
2148
+ await Updates.fetchUpdateAsync();
2149
+ }
2150
+ })().catch(error => {
2151
+ console.warn('[absolute:mobile-update] Expo update check failed', error instanceof Error ? error.message : 'unknown error');
2152
+ });
2153
+
2154
+ return startPromise;
2155
+ };
1956
2156
  `, nativeDiagnosticSource, devicesRuntimeSource = (config, plan, auth) => {
1957
2157
  const imports = plan.capabilities.map((name, index) => {
1958
2158
  const provider = plan.providers[name];
@@ -2023,7 +2223,7 @@ ${push ? `absoluteExpoAuth.onPrincipalChange(principal => {
2023
2223
  });` : ""}
2024
2224
  `;
2025
2225
  }, authRuntimeSource = (auth, appId) => {
2026
- const storageIdentity = createHash("sha256").update(appId).digest("hex").slice(0, 24);
2226
+ const storageIdentity = createHash2("sha256").update(appId).digest("hex").slice(0, 24);
2027
2227
  return `${EXPO_GENERATED_HEADER}import { createAbsoluteExpoAuthClient } from '@absolutejs/auth-expo';
2028
2228
  import { createMobileAuthTransport, installAuthClientRuntimeTransport } from '@absolutejs/auth/client/mobile';
2029
2229
 
@@ -2054,7 +2254,7 @@ export const startAbsoluteExpoAuth = () => {
2054
2254
  };
2055
2255
  `;
2056
2256
  }, syncRuntimeSource = (config, schema) => {
2057
- const storageIdentity = createHash("sha256").update(config.appId).digest("hex").slice(0, 24);
2257
+ const storageIdentity = createHash2("sha256").update(config.appId).digest("hex").slice(0, 24);
2058
2258
  const backgroundEndpoint = new URL("/__absolute/sync/background", config.productionOrigin).href;
2059
2259
  const backgroundTask = `${config.appId}.absolutejs.sync`;
2060
2260
  return `${EXPO_GENERATED_HEADER}import { runHeadlessSync } from '@absolutejs/sync/client/headless';
@@ -2669,6 +2869,7 @@ export default createAbsoluteNativeRoute(ApplicationNativeRoute, ${JSON.stringif
2669
2869
  const devices = resolveAbsoluteDeviceCapabilityPlan(projectRoot, "expo");
2670
2870
  const syncEnabled = Boolean(auth && projectUsesAbsoluteSync(projectRoot));
2671
2871
  const syncSchema = syncEnabled ? { components: discoverAbsoluteSyncSchema(projectRoot).components } : undefined;
2872
+ const runtimeFingerprint = resolveAbsoluteMobileUpdateRuntime(config, projectRoot).fingerprint;
2672
2873
  const routeModules = Object.entries(config.expoNativeRoutes);
2673
2874
  const moduleChecks = await Promise.all(routeModules.map(async ([route, module]) => ({
2674
2875
  exists: await exists(module),
@@ -2701,12 +2902,12 @@ node_modules/
2701
2902
  ],
2702
2903
  [
2703
2904
  join8(project, "app.json"),
2704
- jsonSource(expoAppConfig(config, authEnabled, syncEnabled, devices))
2905
+ jsonSource(expoAppConfig(config, authEnabled, syncEnabled, devices, runtimeFingerprint))
2705
2906
  ],
2706
2907
  [join8(project, "app.config.js"), expoDynamicAppConfig],
2707
2908
  [
2708
2909
  join8(project, "package.json"),
2709
- jsonSource(expoPackage(authEnabled, syncEnabled, devices))
2910
+ jsonSource(expoPackage(authEnabled, syncEnabled, devices, config.updates !== undefined))
2710
2911
  ],
2711
2912
  [join8(project, "metro.config.js"), metroConfig(projectRoot)],
2712
2913
  [
@@ -2719,7 +2920,7 @@ node_modules/
2719
2920
  ],
2720
2921
  [
2721
2922
  join8(project, "app", "_layout.tsx"),
2722
- layoutSource(authEnabled, syncEnabled)
2923
+ layoutSource(authEnabled, syncEnabled, config.updates !== undefined)
2723
2924
  ],
2724
2925
  [
2725
2926
  join8(project, "app", "__absolute", "native", "index.tsx"),
@@ -2738,6 +2939,9 @@ node_modules/
2738
2939
  webHostSource(config, auth, syncEnabled)
2739
2940
  ]
2740
2941
  ]);
2942
+ if (config.updates) {
2943
+ files.set(join8(project, "src", "generated", "AbsoluteUpdates.ts"), updatesRuntimeSource(config));
2944
+ }
2741
2945
  if (auth) {
2742
2946
  files.set(join8(project, "src", "generated", "AbsoluteAuth.ts"), authRuntimeSource(auth, config.appId));
2743
2947
  }
@@ -2836,7 +3040,7 @@ export const materializeAbsoluteWebBundle = async () => {
2836
3040
  const nativeDataManifest = expoNativeDataManifest(manifest);
2837
3041
  const { appBuild } = nativeDataManifest;
2838
3042
  const files = await walkFiles(config.bundleDirectory);
2839
- const bundleHash = createHash("sha256");
3043
+ const bundleHash = createHash2("sha256");
2840
3044
  const filesWithContents = await Promise.all(files.map(async (file) => ({ contents: await readFile(file), file })));
2841
3045
  filesWithContents.forEach(({ contents, file }) => {
2842
3046
  bundleHash.update(relative2(config.bundleDirectory, file).replaceAll("\\", "/"));
@@ -2871,6 +3075,7 @@ var init_expoProject = __esm(() => {
2871
3075
  init_nativeAuth();
2872
3076
  init_syncSchema();
2873
3077
  init_deviceCapabilities();
3078
+ init_updateRuntime();
2874
3079
  expoDynamicAppConfig = `${EXPO_GENERATED_HEADER}const config = require('./app.json');
2875
3080
 
2876
3081
  if (process.env.ABSOLUTE_EXPO_DEVELOPMENT === '1' && process.env.ABSOLUTE_EXPO_DEVELOPMENT_CA_PATH) {
@@ -3471,7 +3676,7 @@ import {
3471
3676
  rm as rm2,
3472
3677
  writeFile as writeFile3
3473
3678
  } from "fs/promises";
3474
- import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
3679
+ import { createHash as createHash3, randomUUID as randomUUID2 } from "crypto";
3475
3680
  import {
3476
3681
  dirname as dirname5,
3477
3682
  isAbsolute,
@@ -3727,7 +3932,7 @@ var ANDROID_BOOT_TIMEOUT_MS = 180000, ANDROID_BOOT_POLL_MS = 1000, ANDROID_CAPTU
3727
3932
  lstat(path),
3728
3933
  readFile4(path)
3729
3934
  ]);
3730
- const contentDigest = createHash2("sha256").update(contents).digest("hex");
3935
+ const contentDigest = createHash3("sha256").update(contents).digest("hex");
3731
3936
  return [
3732
3937
  `file\x00${identity}${metadata.mode & EXECUTABLE_MODE_MASK}\x00${contentDigest}\x00`
3733
3938
  ];
@@ -3739,7 +3944,7 @@ var ANDROID_BOOT_TIMEOUT_MS = 180000, ANDROID_BOOT_POLL_MS = 1000, ANDROID_CAPTU
3739
3944
  }, hashNativeTree = async (root, label, ignorePublicBundle) => {
3740
3945
  const resolvedRoot = await realpath(root);
3741
3946
  const records = await collectNativeDirectory(resolvedRoot, label, resolvedRoot, ignorePublicBundle);
3742
- return createHash2("sha256").update(records.join("")).digest("hex");
3947
+ return createHash3("sha256").update(records.join("")).digest("hex");
3743
3948
  }, fingerprintAbsoluteAndroidNativeProject = async (project, options = {}) => {
3744
3949
  const { dependencies } = await nativeDependencySources(project.nativeDirectory);
3745
3950
  const roots = [
@@ -3755,7 +3960,7 @@ var ANDROID_BOOT_TIMEOUT_MS = 180000, ANDROID_BOOT_POLL_MS = 1000, ANDROID_CAPTU
3755
3960
  }))
3756
3961
  ];
3757
3962
  const treeDigests = await Promise.all(roots.map((root) => hashNativeTree(root.source, root.label, root.ignorePublicBundle)));
3758
- return createHash2("sha256").update(treeDigests.join("\x00")).digest("hex");
3963
+ return createHash3("sha256").update(treeDigests.join("\x00")).digest("hex");
3759
3964
  }, parseJournal = (value) => {
3760
3965
  if (typeof value !== "object" || value === null)
3761
3966
  return null;
@@ -4476,7 +4681,7 @@ var init_androidEmulatorController = __esm(() => {
4476
4681
  });
4477
4682
 
4478
4683
  // src/mobile/emulatorInstaller.ts
4479
- import { createHash as createHash3 } from "crypto";
4684
+ import { createHash as createHash4 } from "crypto";
4480
4685
  import { cp as cp2, mkdir as mkdir3, mkdtemp as mkdtemp2, readFile as readFile5, rm as rm3 } from "fs/promises";
4481
4686
  import { tmpdir } from "os";
4482
4687
  import { basename as basename3, join as join12 } from "path";
@@ -4581,7 +4786,7 @@ var ANDROID_API = 36, ANDROID_BUILD_TOOLS = "36.0.0", COMMAND_LINE_TOOLS_VERSION
4581
4786
  const release = releaseFor(plan.host, input.arch);
4582
4787
  input.log(`Downloading ${basename3(release.url)}...`);
4583
4788
  const bytes = await input.download(release.url);
4584
- const digest = createHash3("sha256").update(bytes).digest("hex");
4789
+ const digest = createHash4("sha256").update(bytes).digest("hex");
4585
4790
  if (digest !== release.sha256) {
4586
4791
  throw new Error(`Android command-line-tools checksum mismatch: expected ${release.sha256}, received ${digest}.`);
4587
4792
  }
@@ -4782,7 +4987,7 @@ var init_emulatorInstaller = __esm(() => {
4782
4987
  });
4783
4988
 
4784
4989
  // src/mobile/iosRelease.ts
4785
- import { createHash as createHash4 } from "crypto";
4990
+ import { createHash as createHash5 } from "crypto";
4786
4991
  import {
4787
4992
  access as access6,
4788
4993
  copyFile as copyFile2,
@@ -4863,7 +5068,7 @@ var ABSOLUTE_IOS_RELEASE_FORMAT = 1, developmentTeamArgument = (value) => {
4863
5068
  }));
4864
5069
  return nested.flat();
4865
5070
  }, fingerprintAbsoluteIosNativeProject = async (nativeDirectory, options = {}) => {
4866
- const hasher = createHash4("sha256");
5071
+ const hasher = createHash5("sha256");
4867
5072
  const files = await fingerprintFiles(nativeDirectory, nativeDirectory, options);
4868
5073
  const contents = await Promise.all(files.map((file) => readFile6(file)));
4869
5074
  files.forEach((file, index) => {
@@ -4881,7 +5086,7 @@ var ABSOLUTE_IOS_RELEASE_FORMAT = 1, developmentTeamArgument = (value) => {
4881
5086
  throw new TypeError("mobile build --outdir must remain inside the project.");
4882
5087
  }
4883
5088
  return output;
4884
- }, sha256File = async (path) => createHash4("sha256").update(await readFile6(path)).digest("hex"), findByExtension = async (root, extension) => {
5089
+ }, sha256File = async (path) => createHash5("sha256").update(await readFile6(path)).digest("hex"), findByExtension = async (root, extension) => {
4885
5090
  if (!await pathExists3(root))
4886
5091
  return;
4887
5092
  const entries = await readdir3(root, { withFileTypes: true });
@@ -5026,7 +5231,7 @@ var ABSOLUTE_IOS_RELEASE_FORMAT = 1, developmentTeamArgument = (value) => {
5026
5231
  let buildNumber = requireBuildNumber(options.buildNumber);
5027
5232
  if (options.prepareBuildNumber) {
5028
5233
  const nativeFingerprint = await fingerprintAbsoluteIosNativeProject(nativeDirectory);
5029
- const buildIdentity = createHash4("sha256").update(`${manifest.appBuild}\x00${nativeFingerprint}\x00${marketingVersion}`).digest("hex");
5234
+ const buildIdentity = createHash5("sha256").update(`${manifest.appBuild}\x00${nativeFingerprint}\x00${marketingVersion}`).digest("hex");
5030
5235
  buildNumber = requireBuildNumber(await options.prepareBuildNumber(buildIdentity));
5031
5236
  }
5032
5237
  const stagingParent = resolve9(options.projectRoot, ".absolutejs/mobile");
@@ -5135,7 +5340,7 @@ var init_iosRelease = __esm(() => {
5135
5340
  });
5136
5341
 
5137
5342
  // src/mobile/iosSimulatorController.ts
5138
- import { createHash as createHash5, randomUUID as randomUUID3 } from "crypto";
5343
+ import { createHash as createHash6, randomUUID as randomUUID3 } from "crypto";
5139
5344
  import {
5140
5345
  access as access7,
5141
5346
  copyFile as copyFile3,
@@ -5590,7 +5795,7 @@ var ABSOLUTE_IOS_SIMULATOR_NAME = "AbsoluteJS iPhone", BOOT_TIMEOUT_MS = 180000,
5590
5795
  ]);
5591
5796
  return result.exitCode === 0 && result.stdout.includes(project.config.appId);
5592
5797
  }, buildIosDebugApp = async (project, udid, fingerprint, run, signal) => {
5593
- const derivedDataPath = join14(project.projectRoot, ".absolutejs", "mobile", "ios-derived-data", createHash5("sha256").update(project.config.appId).digest("hex").slice(0, 16));
5798
+ const derivedDataPath = join14(project.projectRoot, ".absolutejs", "mobile", "ios-derived-data", createHash6("sha256").update(project.config.appId).digest("hex").slice(0, 16));
5594
5799
  await mkdir5(derivedDataPath, { recursive: true });
5595
5800
  await requireSuccess2([
5596
5801
  project.xcodebuild,
@@ -5611,7 +5816,7 @@ var ABSOLUTE_IOS_SIMULATOR_NAME = "AbsoluteJS iPhone", BOOT_TIMEOUT_MS = 180000,
5611
5816
  throw new Error(`Xcode did not produce the simulator app at ${appPath}.`);
5612
5817
  return appPath;
5613
5818
  }, buildPhysicalIosDebugApp = async (project, identifier, run, signal) => {
5614
- const derivedDataPath = join14(project.projectRoot, ".absolutejs", "mobile", "ios-derived-data", createHash5("sha256").update(project.config.appId).digest("hex").slice(0, 16));
5819
+ const derivedDataPath = join14(project.projectRoot, ".absolutejs", "mobile", "ios-derived-data", createHash6("sha256").update(project.config.appId).digest("hex").slice(0, 16));
5615
5820
  await mkdir5(derivedDataPath, { recursive: true });
5616
5821
  await requireSuccess2([
5617
5822
  project.xcodebuild,
@@ -6039,7 +6244,7 @@ var init_iosSimulatorController = __esm(() => {
6039
6244
  var ABSOLUTE_REMOTE_MAC_EVENT_PREFIX = "ABSOLUTE_REMOTE_MAC\t", ABSOLUTE_REMOTE_MAC_PROTOCOL_VERSION = 3;
6040
6245
 
6041
6246
  // src/mobile/remoteMacProtocol.ts
6042
- import { createHash as createHash6, randomUUID as randomUUID4 } from "crypto";
6247
+ import { createHash as createHash7, randomUUID as randomUUID4 } from "crypto";
6043
6248
  import {
6044
6249
  chmod,
6045
6250
  mkdir as mkdir6,
@@ -6231,7 +6436,7 @@ var PROFILE_FORMAT = 1, REMOTE_STDIN_FLUSH_ATTEMPTS = 3, REMOTE_SESSION_CLOSE_TI
6231
6436
  }
6232
6437
  await saveStore(store, profilePath);
6233
6438
  return true;
6234
- }, projectIdentity = (projectRoot, appId) => createHash6("sha256").update(`${resolvePath(projectRoot)}\x00${appId}`).digest("hex").slice(0, 20), createAbsoluteRemoteExpoIosDevProject = (config, projectRoot, profile) => {
6439
+ }, projectIdentity = (projectRoot, appId) => createHash7("sha256").update(`${resolvePath(projectRoot)}\x00${appId}`).digest("hex").slice(0, 20), createAbsoluteRemoteExpoIosDevProject = (config, projectRoot, profile) => {
6235
6440
  if (config.engine !== "expo")
6236
6441
  throw new TypeError("Remote Expo execution requires mobile.engine: expo.");
6237
6442
  return createAbsoluteRemoteIosDevProject(config, projectRoot, profile);
@@ -6420,7 +6625,7 @@ var PROFILE_FORMAT = 1, REMOTE_STDIN_FLUSH_ATTEMPTS = 3, REMOTE_SESSION_CLOSE_TI
6420
6625
  path = join15(outdir, "remoteMacAgentEntry.js");
6421
6626
  }
6422
6627
  const bytes = await Bun.file(path).arrayBuffer();
6423
- const sha256 = createHash6("sha256").update(new Uint8Array(bytes)).digest("hex");
6628
+ const sha256 = createHash7("sha256").update(new Uint8Array(bytes)).digest("hex");
6424
6629
  return { bytes: bytes.byteLength, path, sha256 };
6425
6630
  }, portableRelativePath = (root, path) => relative7(root, path).split(sep5).join(posix.sep), portableMobileConfig = (project) => ({
6426
6631
  appId: project.config.appId,
@@ -7657,7 +7862,7 @@ var init_eslintChunked = __esm(() => {
7657
7862
  });
7658
7863
 
7659
7864
  // src/cli/scripts/eslint.ts
7660
- import { createHash as createHash7 } from "crypto";
7865
+ import { createHash as createHash8 } from "crypto";
7661
7866
  import {
7662
7867
  existsSync as existsSync8,
7663
7868
  mkdirSync as mkdirSync5,
@@ -7756,7 +7961,7 @@ var DEFAULT_CACHE_LOCATION = ".absolutejs/eslint-cache", CACHE_CONTRACT_VERSION
7756
7961
  directory = parent;
7757
7962
  }
7758
7963
  }, createEslintCacheFingerprint = (cwd = process.cwd()) => {
7759
- const hash = createHash7("sha256");
7964
+ const hash = createHash8("sha256");
7760
7965
  hash.update(`absolute-eslint-cache:${CACHE_CONTRACT_VERSION}\x00`);
7761
7966
  const configPath2 = findEslintConfigPath(cwd);
7762
7967
  for (const dependency of lintDependencyNames(cwd, configPath2).sort()) {
@@ -7766,7 +7971,7 @@ var DEFAULT_CACHE_LOCATION = ".absolutejs/eslint-cache", CACHE_CONTRACT_VERSION
7766
7971
  }
7767
7972
  return hash.digest("hex");
7768
7973
  }, createEslintConfigDigest = (cwd = process.cwd()) => {
7769
- const hash = createHash7("sha256");
7974
+ const hash = createHash8("sha256");
7770
7975
  hash.update(`absolute-eslint-config:${CACHE_CONTRACT_VERSION}\x00`);
7771
7976
  const configPath2 = findEslintConfigPath(cwd);
7772
7977
  if (configPath2)
@@ -8630,7 +8835,7 @@ var init_buildMetadata = __esm(() => {
8630
8835
  });
8631
8836
 
8632
8837
  // src/mobile/releaseArtifact.ts
8633
- import { createHash as createHash8 } from "crypto";
8838
+ import { createHash as createHash9 } from "crypto";
8634
8839
  var ABSOLUTE_MOBILE_COMPATIBILITY_FORMAT = 1, ABSOLUTE_MOBILE_RETAINED_GENERATIONS = 3, SHA_256 = "sha256", RELEASE_ID_PREFIX = "amc_", MODULE_SEGMENT_PARENT = "..", EXPORT_NAME_PATTERN, frameworks3, isCanonicalRecord = (value) => {
8635
8840
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
8636
8841
  return false;
@@ -8667,7 +8872,7 @@ var ABSOLUTE_MOBILE_COMPATIBILITY_FORMAT = 1, ABSOLUTE_MOBILE_RETAINED_GENERATIO
8667
8872
  return normalizeCanonicalRecord(value, ancestors);
8668
8873
  }
8669
8874
  throw new TypeError("Compatibility metadata must contain only finite JSON values.");
8670
- }, canonicalJson = (value) => JSON.stringify(normalizeCanonicalValue(value, new Set)), hashCanonicalValue = (value) => createHash8(SHA_256).update(canonicalJson(value)).digest("hex"), requireNonEmpty = (value, field) => {
8875
+ }, canonicalJson = (value) => JSON.stringify(normalizeCanonicalValue(value, new Set)), hashCanonicalValue = (value) => createHash9(SHA_256).update(canonicalJson(value)).digest("hex"), requireNonEmpty = (value, field) => {
8671
8876
  if (!value.trim()) {
8672
8877
  throw new TypeError(`${field} must not be empty.`);
8673
8878
  }
@@ -8839,10 +9044,10 @@ var normalizeSlug = (str) => str.trim().replace(/\s+/g, "-").replace(/[^A-Za-z0-
8839
9044
  };
8840
9045
 
8841
9046
  // src/mobile/buildRelease.ts
8842
- import { createHash as createHash9 } from "crypto";
9047
+ import { createHash as createHash10 } from "crypto";
8843
9048
  import { mkdir as mkdir8, readFile as readFile9, writeFile as writeFile7 } from "fs/promises";
8844
9049
  import { basename as basename7, dirname as dirname11, extname as extname4, join as join20, relative as relative11, resolve as resolve15 } from "path";
8845
- var sha256 = (bytes) => createHash9("sha256").update(bytes).digest("hex"), STATIC_SCRIPT_PATTERN, rewriteStaticScriptPaths = (source, manifest) => source.replace(STATIC_SCRIPT_PATTERN, (match, prefix, path, suffix) => {
9050
+ var sha256 = (bytes) => createHash10("sha256").update(bytes).digest("hex"), STATIC_SCRIPT_PATTERN, rewriteStaticScriptPaths = (source, manifest) => source.replace(STATIC_SCRIPT_PATTERN, (match, prefix, path, suffix) => {
8846
9051
  if (path.endsWith("/htmx.min.js"))
8847
9052
  return match;
8848
9053
  const key = toPascal(basename7(path, extname4(path)));
@@ -9408,10 +9613,10 @@ var init_capacitorBundle = __esm(() => {
9408
9613
  });
9409
9614
 
9410
9615
  // src/mobile/artifactStore.ts
9411
- import { createHash as createHash10 } from "crypto";
9616
+ import { createHash as createHash11 } from "crypto";
9412
9617
  var DEFAULT_MAX_PRODUCER_BYTES = 134217728, SHA_2562 = "sha256", hashBlob = async (blob) => {
9413
9618
  const bytes = new Uint8Array(await blob.arrayBuffer());
9414
- return createHash10(SHA_2562).update(bytes).digest("hex");
9619
+ return createHash11(SHA_2562).update(bytes).digest("hex");
9415
9620
  }, verifyAbsoluteMobileCompatibilityProducer = async (release, maxProducerBytes = DEFAULT_MAX_PRODUCER_BYTES) => {
9416
9621
  const { artifact, producer } = release;
9417
9622
  if (producer.size !== artifact.producer.bytes || producer.size > maxProducerBytes) {
@@ -9427,7 +9632,7 @@ var init_artifactStore = __esm(() => {
9427
9632
  });
9428
9633
 
9429
9634
  // src/mobile/materializedBundle.ts
9430
- import { createHash as createHash11 } from "crypto";
9635
+ import { createHash as createHash12 } from "crypto";
9431
9636
  import {
9432
9637
  access as access8,
9433
9638
  mkdir as mkdir10,
@@ -9443,7 +9648,7 @@ var ABSOLUTE_MOBILE_MATERIALIZED_BUNDLE_FORMAT = 1, CURRENT_BUNDLE_FILE = "curre
9443
9648
  currentReleaseId,
9444
9649
  releases: releases.map(({ releaseId }) => releaseId)
9445
9650
  });
9446
- return `amb_${createHash11("sha256").update(identity).digest("hex")}`;
9651
+ return `amb_${createHash12("sha256").update(identity).digest("hex")}`;
9447
9652
  }, parseBundleIndex = (value) => {
9448
9653
  if (!isRecord8(value) || value.format !== ABSOLUTE_MOBILE_MATERIALIZED_BUNDLE_FORMAT || typeof value.bundleId !== "string" || !BUNDLE_ID_PATTERN.test(value.bundleId) || typeof value.currentReleaseId !== "string" || !Array.isArray(value.releases)) {
9449
9654
  throw new TypeError("Invalid materialized mobile compatibility bundle.");
@@ -9556,136 +9761,6 @@ var init_materializedBundle = __esm(() => {
9556
9761
  BUNDLE_ID_PATTERN = /^amb_[a-f0-9]{64}$/;
9557
9762
  });
9558
9763
 
9559
- // src/mobile/updateProtocol.ts
9560
- var ABSOLUTE_MOBILE_UPDATE_FORMAT = 1, ABSOLUTE_MOBILE_UPDATE_MAX_FILE_BYTES, ABSOLUTE_MOBILE_UPDATE_MAX_FILES = 1e4, ABSOLUTE_MOBILE_UPDATE_MAX_TOTAL_BYTES, ABSOLUTE_MOBILE_UPDATE_SIGNATURE_ALGORITHM = "ecdsa-p256-sha256", HASH_PATTERN, RELEASE_PATTERN, KEY_ID_PATTERN, CHANNEL_PATTERN, isClassification = (value) => value === "bug-fix" || value === "content" || value === "security", object3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), canonicalValue = (value) => {
9561
- if (Array.isArray(value))
9562
- return value.map(canonicalValue);
9563
- if (!object3(value))
9564
- return value;
9565
- return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonicalValue(value[key])]));
9566
- }, absoluteMobileUpdateSigningPayload = (manifest) => new TextEncoder().encode(canonicalizeAbsoluteMobileUpdate(manifest)), canonicalizeAbsoluteMobileUpdate = (value) => JSON.stringify(canonicalValue(value)), requireText2 = (value, field) => {
9567
- if (typeof value !== "string" || value.length === 0)
9568
- throw new TypeError(`${field} must be a non-empty string.`);
9569
- return value;
9570
- }, normalizeAbsoluteMobileUpdatePath = (value) => {
9571
- const path = requireText2(value, "Update file path").replaceAll("\\", "/");
9572
- if (path.startsWith("/") || path.includes("\x00") || path.split("/").some((segment) => !segment || segment === "." || segment === "..")) {
9573
- throw new TypeError("Update file paths must be normalized relative paths.");
9574
- }
9575
- return path;
9576
- }, parseFile = (value) => {
9577
- if (!object3(value))
9578
- throw new TypeError("Update files must be objects.");
9579
- const path = normalizeAbsoluteMobileUpdatePath(value.path);
9580
- if (typeof value.bytes !== "number" || !Number.isSafeInteger(value.bytes) || value.bytes < 0 || value.bytes > ABSOLUTE_MOBILE_UPDATE_MAX_FILE_BYTES) {
9581
- throw new TypeError(`Update file ${path} has an invalid byte length.`);
9582
- }
9583
- if (typeof value.sha256 !== "string" || !HASH_PATTERN.test(value.sha256))
9584
- throw new TypeError(`Update file ${path} has an invalid SHA-256 digest.`);
9585
- return { bytes: value.bytes, path, sha256: value.sha256 };
9586
- }, parseAbsoluteMobileUnsignedUpdateManifest = (value) => {
9587
- if (!object3(value) || value.format !== ABSOLUTE_MOBILE_UPDATE_FORMAT)
9588
- throw new TypeError("Invalid AbsoluteJS mobile update manifest.");
9589
- const appId = requireText2(value.appId, "Update appId");
9590
- const channel = requireText2(value.channel, "Update channel");
9591
- if (!CHANNEL_PATTERN.test(channel))
9592
- throw new TypeError("Update channel contains unsupported characters.");
9593
- if (!isClassification(value.classification))
9594
- throw new TypeError("Update classification is invalid.");
9595
- const createdAt = requireText2(value.createdAt, "Update createdAt");
9596
- if (!Number.isFinite(Date.parse(createdAt)) || new Date(createdAt).toISOString() !== createdAt)
9597
- throw new TypeError("Update createdAt must be a canonical ISO timestamp.");
9598
- if (!Array.isArray(value.files) || value.files.length === 0)
9599
- throw new TypeError("An update must contain at least one file.");
9600
- if (value.files.length > ABSOLUTE_MOBILE_UPDATE_MAX_FILES)
9601
- throw new TypeError("Update contains too many files.");
9602
- const files = value.files.map(parseFile);
9603
- const sorted = [...files].sort((left, right) => left.path.localeCompare(right.path));
9604
- if (files.some((file, index) => file.path !== sorted[index]?.path))
9605
- throw new TypeError("Update files must be sorted by path.");
9606
- if (new Set(files.map(({ path }) => path)).size !== files.length)
9607
- throw new TypeError("Update file paths must be unique.");
9608
- if (files.reduce((total, file) => total + file.bytes, 0) > ABSOLUTE_MOBILE_UPDATE_MAX_TOTAL_BYTES)
9609
- throw new TypeError("Update exceeds the maximum uncompressed size.");
9610
- if (typeof value.releaseId !== "string" || !RELEASE_PATTERN.test(value.releaseId))
9611
- throw new TypeError("Update releaseId is invalid.");
9612
- if (typeof value.runtimeFingerprint !== "string" || !HASH_PATTERN.test(value.runtimeFingerprint))
9613
- throw new TypeError("Update runtime fingerprint is invalid.");
9614
- if (value.withinSubmittedPurpose !== true)
9615
- throw new TypeError("OTA updates must attest that they remain within the submitted app purpose.");
9616
- return {
9617
- appId,
9618
- channel,
9619
- classification: value.classification,
9620
- createdAt,
9621
- files,
9622
- format: ABSOLUTE_MOBILE_UPDATE_FORMAT,
9623
- releaseId: value.releaseId,
9624
- runtimeFingerprint: value.runtimeFingerprint,
9625
- withinSubmittedPurpose: true
9626
- };
9627
- }, parseAbsoluteMobileUpdateManifest = (value) => {
9628
- if (!object3(value))
9629
- throw new TypeError("Invalid AbsoluteJS mobile update manifest.");
9630
- const { signature: signatureValue, ...unsignedValue } = value;
9631
- const unsigned = parseAbsoluteMobileUnsignedUpdateManifest(unsignedValue);
9632
- if (!object3(signatureValue))
9633
- throw new TypeError("Update signature is missing.");
9634
- if (signatureValue.algorithm !== ABSOLUTE_MOBILE_UPDATE_SIGNATURE_ALGORITHM)
9635
- throw new TypeError("Update signature algorithm is unsupported.");
9636
- const keyId = requireText2(signatureValue.keyId, "Update signature keyId");
9637
- if (!KEY_ID_PATTERN.test(keyId))
9638
- throw new TypeError("Update signature keyId is invalid.");
9639
- const signature = requireText2(signatureValue.value, "Update signature value");
9640
- let signatureBytes;
9641
- try {
9642
- signatureBytes = Uint8Array.from(atob(signature), (character) => character.charCodeAt(0));
9643
- } catch {
9644
- signatureBytes = new Uint8Array;
9645
- }
9646
- if (!/^[A-Za-z0-9+/]+={0,2}$/u.test(signature) || signatureBytes.byteLength !== 64 || btoa(String.fromCharCode(...signatureBytes)) !== signature)
9647
- throw new TypeError("Update signature is not canonical base64.");
9648
- return {
9649
- ...unsigned,
9650
- signature: {
9651
- algorithm: ABSOLUTE_MOBILE_UPDATE_SIGNATURE_ALGORITHM,
9652
- keyId,
9653
- value: signature
9654
- }
9655
- };
9656
- }, unsignedAbsoluteMobileUpdate = (manifest) => {
9657
- const { signature: _signature, ...unsigned } = manifest;
9658
- return unsigned;
9659
- };
9660
- var init_updateProtocol = __esm(() => {
9661
- ABSOLUTE_MOBILE_UPDATE_MAX_FILE_BYTES = 32 * 1024 * 1024;
9662
- ABSOLUTE_MOBILE_UPDATE_MAX_TOTAL_BYTES = 128 * 1024 * 1024;
9663
- HASH_PATTERN = /^[a-f0-9]{64}$/u;
9664
- RELEASE_PATTERN = /^amu_[a-f0-9]{64}$/u;
9665
- KEY_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u;
9666
- CHANNEL_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u;
9667
- });
9668
-
9669
- // src/mobile/updateRuntime.ts
9670
- import { createHash as createHash12 } from "crypto";
9671
- var ABSOLUTE_MOBILE_SHELL_ABI = 2, ABSOLUTE_MOBILE_UPDATE_RUNTIME_FORMAT = 1, createAbsoluteMobileUpdateRuntimeDescriptor = (options) => ({
9672
- appId: options.config.appId,
9673
- auth: options.auth ?? null,
9674
- deepLinks: {
9675
- hosts: options.config.deepLinkHosts,
9676
- ...options.config.deepLinkScheme ? { scheme: options.config.deepLinkScheme } : {}
9677
- },
9678
- deviceCapabilities: options.deviceCapabilities,
9679
- engine: options.config.engine,
9680
- format: ABSOLUTE_MOBILE_UPDATE_RUNTIME_FORMAT,
9681
- shellAbi: ABSOLUTE_MOBILE_SHELL_ABI,
9682
- syncSchema: options.syncSchema ?? null,
9683
- updates: options.config.updates ?? null
9684
- }), fingerprintAbsoluteMobileUpdateRuntime = (descriptor) => createHash12("sha256").update(canonicalizeAbsoluteMobileUpdate(descriptor)).digest("hex");
9685
- var init_updateRuntime = __esm(() => {
9686
- init_updateProtocol();
9687
- });
9688
-
9689
9764
  // src/mobile/buildPipeline.ts
9690
9765
  import { readFile as readFile12 } from "fs/promises";
9691
9766
  import { join as join23, resolve as resolve17 } from "path";
@@ -9754,16 +9829,13 @@ var isElysiaApp = (value) => typeof value === "object" && value !== null && type
9754
9829
  producerPath: resolve17(options.producerPath),
9755
9830
  runtime: String(ABSOLUTE_MOBILE_PAGE_PROTOCOL_VERSION)
9756
9831
  });
9757
- const auth = resolveAbsoluteMobileAuthManifest(options.projectRoot, mobile);
9758
- const sync = auth !== undefined && projectUsesAbsoluteSync(options.projectRoot);
9759
- const syncSchema = sync ? discoverAbsoluteSyncSchema(options.projectRoot) : undefined;
9760
- const deviceCapabilities = resolveAbsoluteDeviceCapabilityPlan(options.projectRoot, mobile.engine);
9761
- const runtimeFingerprint = fingerprintAbsoluteMobileUpdateRuntime(createAbsoluteMobileUpdateRuntimeDescriptor({
9762
- ...auth ? { auth } : {},
9763
- config: mobile,
9832
+ const {
9833
+ auth,
9764
9834
  deviceCapabilities,
9765
- ...syncSchema ? { syncSchema } : {}
9766
- }));
9835
+ fingerprint: runtimeFingerprint,
9836
+ sync,
9837
+ syncSchema
9838
+ } = resolveAbsoluteMobileUpdateRuntime(mobile, options.projectRoot);
9767
9839
  const usesPush = deviceCapabilities.capabilities.includes("pushNotifications");
9768
9840
  if (usesPush && !auth)
9769
9841
  throw new TypeError("Portable push notifications require @absolutejs/auth so provider tokens can be registered without exposing identity controls to page code.");
@@ -9811,8 +9883,6 @@ var init_buildPipeline = __esm(() => {
9811
9883
  init_materializedBundle();
9812
9884
  init_pageProtocol();
9813
9885
  init_releaseArtifact();
9814
- init_nativeAuth();
9815
- init_syncSchema();
9816
9886
  init_deviceCapabilities();
9817
9887
  init_expoProject();
9818
9888
  init_updateRuntime();
@@ -21099,7 +21169,7 @@ var ABSOLUTE_MOBILE_COMPLIANCE_REPORT_FORMAT = 1, HMR_ASSET_PATTERN, RELEASE_ASS
21099
21169
  } catch (error) {
21100
21170
  return fail5(`${platform6}.content-security-policy`, error instanceof Error ? error.message : "Packaged shell CSP could not be validated.", path, "Rebuild the production mobile bundle with the AbsoluteJS-generated shell.");
21101
21171
  }
21102
- }, expoApplicationConfigCheck = async (config) => {
21172
+ }, expoApplicationConfigCheck = async (config, projectRoot) => {
21103
21173
  const path = join53(config.nativeProjectDirectory, "app.json");
21104
21174
  try {
21105
21175
  const root = await readJsonObject(path);
@@ -21109,13 +21179,14 @@ var ABSOLUTE_MOBILE_COMPLIANCE_REPORT_FORMAT = 1, HMR_ASSET_PATTERN, RELEASE_ASS
21109
21179
  if (expo.name !== config.appName || config.platforms.includes("android") && (!isRecord15(expo.android) || expo.android.package !== config.appId) || config.platforms.includes("ios") && (!isRecord15(expo.ios) || expo.ios.bundleIdentifier !== config.appId)) {
21110
21180
  throw new TypeError("Generated Expo application identity does not match mobile config.");
21111
21181
  }
21112
- if (!isRecord15(expo.runtimeVersion) || expo.runtimeVersion.policy !== "appVersion") {
21113
- throw new TypeError("Generated Expo runtimeVersion must follow the native app version.");
21182
+ const expectedRuntime = resolveAbsoluteMobileUpdateRuntime(config, projectRoot).fingerprint;
21183
+ if (expo.runtimeVersion !== expectedRuntime) {
21184
+ throw new TypeError("Generated Expo runtimeVersion does not match the AbsoluteJS native contract.");
21114
21185
  }
21115
21186
  if (Array.isArray(expo.plugins) && expo.plugins.some((plugin) => typeof plugin === "string" && plugin.includes("withAbsoluteDevelopmentCa"))) {
21116
21187
  throw new TypeError("Generated Expo production config includes the development CA plugin.");
21117
21188
  }
21118
- return pass("expo.app-config", "Expo application identity and runtime policy match the production mobile config.", path);
21189
+ return pass("expo.app-config", "Expo application identity and generated native runtime match the production mobile config.", path);
21119
21190
  } catch (error) {
21120
21191
  return fail5("expo.app-config", error instanceof Error ? error.message : "Generated Expo application config could not be validated.", path, "Run `absolute mobile build <platform>`; do not edit the generated Expo project.");
21121
21192
  }
@@ -21405,7 +21476,7 @@ var ABSOLUTE_MOBILE_COMPLIANCE_REPORT_FORMAT = 1, HMR_ASSET_PATTERN, RELEASE_ASS
21405
21476
  const journalPath = join53(projectRoot, ".absolutejs", "mobile", "expo-dev-session", "journal.json");
21406
21477
  const checks = await Promise.all([
21407
21478
  journalReleaseCheck(journalPath, "android"),
21408
- expoApplicationConfigCheck(config),
21479
+ expoApplicationConfigCheck(config, projectRoot),
21409
21480
  expoEmbeddedAssetsCheck(config),
21410
21481
  manifestReleaseCheck(manifestPath),
21411
21482
  hmrAssetsReleaseCheck(config.bundleDirectory),
@@ -21459,7 +21530,7 @@ var ABSOLUTE_MOBILE_COMPLIANCE_REPORT_FORMAT = 1, HMR_ASSET_PATTERN, RELEASE_ASS
21459
21530
  ];
21460
21531
  checks.push(config.iosVersion ? pass("ios.marketing-version", `The iOS marketing version is ${config.iosVersion}.`) : fail5("ios.marketing-version", "iOS has no explicit App Store marketing version.", projectRoot, "Add mobile.ios.version to absolutejs.config.ts, for example 1.0.0."));
21461
21532
  checks.push(...await Promise.all([
21462
- expoApplicationConfigCheck(config),
21533
+ expoApplicationConfigCheck(config, projectRoot),
21463
21534
  expoEmbeddedAssetsCheck(config),
21464
21535
  hmrAssetsReleaseCheck(config.bundleDirectory),
21465
21536
  embeddedBundleReleaseCheck(config, projectRoot, "ios", config.bundleDirectory),
@@ -21565,6 +21636,7 @@ var init_releaseDoctor = __esm(() => {
21565
21636
  init_nativeAuth();
21566
21637
  init_syncSchema();
21567
21638
  init_deviceCapabilities();
21639
+ init_updateRuntime();
21568
21640
  HMR_ASSET_PATTERN = /(?:__HMR_WS__|hmr-timing|__absolute_target|absolutejs-error-overlay)/u;
21569
21641
  RELEASE_ASSET_EXTENSIONS = new Set([".html", ".js", ".mjs"]);
21570
21642
  EXACT_VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u;
@@ -23136,11 +23208,93 @@ var init_updateSigning = __esm(() => {
23136
23208
  init_updateProtocol();
23137
23209
  });
23138
23210
 
23211
+ // src/mobile/expoUpdate.ts
23212
+ import { access as access16, mkdir as mkdir18, readFile as readFile26, writeFile as writeFile20 } from "fs/promises";
23213
+ import { dirname as dirname35, extname as extname10, join as join58 } from "path";
23214
+ var ABSOLUTE_EXPO_UPDATE_DESCRIPTOR = "_absolute/expo-update.json", ABSOLUTE_EXPO_UPDATE_FORMAT = 1, object4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), safePath = (value, field) => {
23215
+ if (typeof value !== "string" || value.length === 0)
23216
+ throw new TypeError(`Expo update ${field} is invalid.`);
23217
+ const path = value.replaceAll("\\", "/");
23218
+ if (path.startsWith("/") || path.split("/").some((segment) => !segment || segment === "." || segment === ".."))
23219
+ throw new TypeError(`Expo update ${field} must be a relative path.`);
23220
+ return path;
23221
+ }, extension = (value, path) => {
23222
+ if (value === null || value === undefined) {
23223
+ const inferred = extname10(path);
23224
+ return inferred ? inferred.slice(1) : undefined;
23225
+ }
23226
+ if (typeof value !== "string" || !/^[A-Za-z0-9]+$/u.test(value))
23227
+ throw new TypeError("Expo update asset extension is invalid.");
23228
+ return value;
23229
+ }, asset = (value) => {
23230
+ if (!object4(value))
23231
+ throw new TypeError("Expo update asset is invalid.");
23232
+ const path = safePath(value.path, "asset path");
23233
+ const normalizedExtension = extension(value.extension ?? value.ext, path);
23234
+ return {
23235
+ ...normalizedExtension ? { extension: normalizedExtension } : {},
23236
+ path
23237
+ };
23238
+ }, platform6 = (value) => {
23239
+ if (!object4(value) || !Array.isArray(value.assets))
23240
+ throw new TypeError("Expo update platform metadata is invalid.");
23241
+ const launchValue = object4(value.launchAsset) ? value.launchAsset : { path: value.bundle };
23242
+ const launchPath = safePath(launchValue.path, "launch bundle");
23243
+ const launchExtension = extension(launchValue.extension ?? null, launchPath);
23244
+ const assets = value.assets.map(asset);
23245
+ return {
23246
+ assets,
23247
+ launchAsset: {
23248
+ ...launchExtension ? { extension: launchExtension } : {},
23249
+ path: launchPath
23250
+ }
23251
+ };
23252
+ }, parseAbsoluteExpoUpdateDescriptor = (value) => {
23253
+ if (!object4(value) || value.engine !== "expo" || value.format !== ABSOLUTE_EXPO_UPDATE_FORMAT || !object4(value.expoConfig) || !object4(value.platforms) || typeof value.runtimeVersion !== "string" || !/^[a-f0-9]{64}$/u.test(value.runtimeVersion))
23254
+ throw new TypeError("AbsoluteJS Expo update descriptor is invalid.");
23255
+ const platforms = {};
23256
+ for (const name of ["android", "ios"]) {
23257
+ const candidate = value.platforms[name];
23258
+ if (candidate !== undefined)
23259
+ platforms[name] = platform6(candidate);
23260
+ }
23261
+ if (Object.keys(platforms).length === 0)
23262
+ throw new TypeError("Expo update descriptor has no native platforms.");
23263
+ return {
23264
+ engine: "expo",
23265
+ expoConfig: value.expoConfig,
23266
+ format: ABSOLUTE_EXPO_UPDATE_FORMAT,
23267
+ platforms,
23268
+ runtimeVersion: value.runtimeVersion
23269
+ };
23270
+ }, finalizeAbsoluteExpoUpdateExport = async (options) => {
23271
+ const metadata = JSON.parse(await readFile26(join58(options.exportDirectory, "metadata.json"), "utf8"));
23272
+ if (!object4(metadata) || !object4(metadata.fileMetadata))
23273
+ throw new TypeError("Expo export metadata.json is invalid.");
23274
+ const descriptor = parseAbsoluteExpoUpdateDescriptor({
23275
+ engine: "expo",
23276
+ expoConfig: options.expoConfig,
23277
+ format: ABSOLUTE_EXPO_UPDATE_FORMAT,
23278
+ platforms: metadata.fileMetadata,
23279
+ runtimeVersion: options.runtimeVersion
23280
+ });
23281
+ const referenced = Object.values(descriptor.platforms).flatMap((entry) => entry ? [entry.launchAsset, ...entry.assets] : []);
23282
+ await Promise.all(referenced.map(({ path }) => access16(join58(options.exportDirectory, path)).catch(() => {
23283
+ throw new TypeError(`Expo export metadata references missing asset ${path}.`);
23284
+ })));
23285
+ const destination = join58(options.exportDirectory, ABSOLUTE_EXPO_UPDATE_DESCRIPTOR);
23286
+ await mkdir18(dirname35(destination), { recursive: true });
23287
+ await writeFile20(destination, `${JSON.stringify(descriptor, null, "\t")}
23288
+ `);
23289
+ return { descriptor, path: destination };
23290
+ };
23291
+ var init_expoUpdate = () => {};
23292
+
23139
23293
  // src/mobile/updatePublisher.ts
23140
- import { access as access16 } from "fs/promises";
23294
+ import { access as access17 } from "fs/promises";
23141
23295
  import { isAbsolute as isAbsolute9, relative as relative32, resolve as resolve45, sep as sep10 } from "path";
23142
23296
  import { pathToFileURL as pathToFileURL3 } from "url";
23143
- var object4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), isPublisher2 = (value) => object4(value) && typeof value.publishUpdate === "function" && typeof value.promoteUpdate === "function" && typeof value.rollbackUpdate === "function", projectPath2 = (projectRoot, requested, label) => {
23297
+ var object5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), isPublisher2 = (value) => object5(value) && typeof value.publishUpdate === "function" && typeof value.promoteUpdate === "function" && typeof value.rollbackUpdate === "function", projectPath2 = (projectRoot, requested, label) => {
23144
23298
  const root = resolve45(projectRoot);
23145
23299
  const path = resolve45(root, requested);
23146
23300
  const projectRelative = relative32(root, path);
@@ -23149,11 +23303,11 @@ var object4 = (value) => typeof value === "object" && value !== null && !Array.i
23149
23303
  return path;
23150
23304
  }, loadAbsoluteMobileUpdatePublisher = async (projectRoot, requestedModulePath) => {
23151
23305
  const modulePath = projectPath2(projectRoot, requestedModulePath, "mobile update registry");
23152
- await access16(modulePath).catch(() => {
23306
+ await access17(modulePath).catch(() => {
23153
23307
  throw new TypeError(`Mobile update registry does not exist: ${modulePath}`);
23154
23308
  });
23155
23309
  const loaded = await import(pathToFileURL3(modulePath).href);
23156
- const publisher = object4(loaded) ? loaded.default ?? loaded.registry : undefined;
23310
+ const publisher = object5(loaded) ? loaded.default ?? loaded.registry : undefined;
23157
23311
  if (!isPublisher2(publisher))
23158
23312
  throw new TypeError("Mobile update registry must implement publishUpdate, promoteUpdate, and rollbackUpdate.");
23159
23313
  return publisher;
@@ -23200,12 +23354,19 @@ var exports_mobile = {};
23200
23354
  __export(exports_mobile, {
23201
23355
  runMobile: () => runMobile
23202
23356
  });
23203
- import { access as access17, mkdir as mkdir18, readFile as readFile26, writeFile as writeFile20 } from "fs/promises";
23357
+ import {
23358
+ access as access18,
23359
+ mkdir as mkdir19,
23360
+ mkdtemp as mkdtemp9,
23361
+ readFile as readFile27,
23362
+ rm as rm13,
23363
+ writeFile as writeFile21
23364
+ } from "fs/promises";
23204
23365
  import { createPublicKey as createPublicKey3 } from "crypto";
23205
- import { join as join58, relative as relative33, resolve as resolve46 } from "path";
23366
+ import { join as join59, relative as relative33, resolve as resolve46 } from "path";
23206
23367
  import { createInterface } from "readline/promises";
23207
23368
  var NOT_FOUND4 = -1, isRecord18 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), CAPACITOR_PACKAGES, CAPACITOR_PACKAGE_SPECS, CAPACITOR_SYNC_PACKAGE_SPECS, packageNameFromSpec = (spec) => spec.slice(0, spec.lastIndexOf("@")), directProjectPackages = async (projectRoot) => {
23208
- const manifest = JSON.parse(await readFile26(join58(projectRoot, "package.json"), "utf8"));
23369
+ const manifest = JSON.parse(await readFile27(join59(projectRoot, "package.json"), "utf8"));
23209
23370
  if (!isRecord18(manifest))
23210
23371
  throw new TypeError("Application package.json must contain an object.");
23211
23372
  const names = new Set;
@@ -23218,7 +23379,7 @@ var NOT_FOUND4 = -1, isRecord18 = (value) => typeof value === "object" && value
23218
23379
  return names;
23219
23380
  }, resolvedPackageVersion = async (projectRoot, packageName) => {
23220
23381
  try {
23221
- const manifest = JSON.parse(await readFile26(join58(projectRoot, "node_modules", packageName, "package.json"), "utf8"));
23382
+ const manifest = JSON.parse(await readFile27(join59(projectRoot, "node_modules", packageName, "package.json"), "utf8"));
23222
23383
  return isRecord18(manifest) && typeof manifest.version === "string" ? manifest.version : undefined;
23223
23384
  } catch {
23224
23385
  return;
@@ -23260,9 +23421,9 @@ var NOT_FOUND4 = -1, isRecord18 = (value) => typeof value === "object" && value
23260
23421
  }
23261
23422
  return value;
23262
23423
  }, capacitorExecutable = async (projectRoot) => {
23263
- const executable = join58(projectRoot, "node_modules", ".bin", "cap");
23424
+ const executable = join59(projectRoot, "node_modules", ".bin", "cap");
23264
23425
  try {
23265
- await access17(executable);
23426
+ await access18(executable);
23266
23427
  return executable;
23267
23428
  } catch {
23268
23429
  throw new TypeError(`Capacitor is not installed in this app. Run: bun add ${CAPACITOR_PACKAGES.join(" ")}`);
@@ -23279,10 +23440,10 @@ var NOT_FOUND4 = -1, isRecord18 = (value) => typeof value === "object" && value
23279
23440
  if (exitCode !== 0) {
23280
23441
  throw new TypeError(`Capacitor exited with status ${exitCode}.`);
23281
23442
  }
23282
- }, runCapacitorForPlatforms = (projectRoot, command, platforms) => platforms.reduce((pending, platform6) => pending.then(() => runCapacitor(projectRoot, [command, platform6])), Promise.resolve()), expoExecutable = async (project) => {
23283
- const executable = join58(project, "node_modules", ".bin", "expo");
23443
+ }, runCapacitorForPlatforms = (projectRoot, command, platforms) => platforms.reduce((pending, platform7) => pending.then(() => runCapacitor(projectRoot, [command, platform7])), Promise.resolve()), expoExecutable = async (project) => {
23444
+ const executable = join59(project, "node_modules", ".bin", "expo");
23284
23445
  try {
23285
- await access17(executable);
23446
+ await access18(executable);
23286
23447
  return executable;
23287
23448
  } catch {
23288
23449
  throw new TypeError("Expo dependencies are not installed in the generated shell. Run `absolute mobile init --yes`.");
@@ -23311,14 +23472,14 @@ var NOT_FOUND4 = -1, isRecord18 = (value) => typeof value === "object" && value
23311
23472
  throw new TypeError(`Expo exited with status ${exitCode}.`);
23312
23473
  }, ensureExpoPackages = async (project, args) => {
23313
23474
  try {
23314
- const manifest = JSON.parse(await readFile26(join58(project, "package.json"), "utf8"));
23475
+ const manifest = JSON.parse(await readFile27(join59(project, "package.json"), "utf8"));
23315
23476
  const dependencies = typeof manifest === "object" && manifest !== null ? Reflect.get(manifest, "dependencies") : undefined;
23316
23477
  if (typeof dependencies !== "object" || dependencies === null || Array.isArray(dependencies))
23317
23478
  throw new TypeError("Generated Expo dependencies are invalid.");
23318
23479
  await Promise.all(Object.entries(dependencies).map(async ([name, expected]) => {
23319
23480
  if (typeof expected !== "string")
23320
23481
  throw new TypeError("Generated Expo dependency version is invalid.");
23321
- const installed = JSON.parse(await readFile26(join58(project, "node_modules", name, "package.json"), "utf8"));
23482
+ const installed = JSON.parse(await readFile27(join59(project, "node_modules", name, "package.json"), "utf8"));
23322
23483
  const actual = typeof installed === "object" && installed !== null ? Reflect.get(installed, "version") : undefined;
23323
23484
  if (typeof actual !== "string")
23324
23485
  throw new TypeError("Installed Expo dependency version is invalid.");
@@ -23478,8 +23639,8 @@ var NOT_FOUND4 = -1, isRecord18 = (value) => typeof value === "object" && value
23478
23639
  await ensureExpoPackages(mobile.nativeProjectDirectory, args);
23479
23640
  const assets = await syncAbsoluteExpoWebAssets(mobile);
23480
23641
  console.log(`Synced ${assets.assets} embedded AbsoluteJS assets for ${assets.appBuild}.`);
23481
- const platform7 = args.find((value) => value === "android" || value === "ios");
23482
- const platforms2 = platform7 ? [platform7] : mobile.platforms;
23642
+ const platform8 = args.find((value) => value === "android" || value === "ios");
23643
+ const platforms2 = platform8 ? [platform8] : mobile.platforms;
23483
23644
  await runExpo(mobile.nativeProjectDirectory, [
23484
23645
  "prebuild",
23485
23646
  "--no-install",
@@ -23489,8 +23650,8 @@ var NOT_FOUND4 = -1, isRecord18 = (value) => typeof value === "object" && value
23489
23650
  return;
23490
23651
  }
23491
23652
  await ensureCapacitorPackages(projectRoot, args, mobile);
23492
- const platform6 = args.find((value) => value === "android" || value === "ios");
23493
- const platforms = platform6 ? [platform6] : mobile.platforms;
23653
+ const platform7 = args.find((value) => value === "android" || value === "ios");
23654
+ const platforms = platform7 ? [platform7] : mobile.platforms;
23494
23655
  if (platforms.includes("android"))
23495
23656
  await repairAbsoluteAndroidDevSession(projectRoot);
23496
23657
  if (platforms.includes("ios"))
@@ -23598,8 +23759,8 @@ var NOT_FOUND4 = -1, isRecord18 = (value) => typeof value === "object" && value
23598
23759
  }
23599
23760
  }, runReleaseDoctor = async (args) => {
23600
23761
  const { mobile, projectRoot } = await loadMobile(valueAfter(args, "--config"));
23601
- const platform6 = args.find((value) => value === "android" || value === "ios");
23602
- const effectiveMobile = platform6 ? { ...mobile, platforms: [platform6] } : mobile;
23762
+ const platform7 = args.find((value) => value === "android" || value === "ios");
23763
+ const effectiveMobile = platform7 ? { ...mobile, platforms: [platform7] } : mobile;
23603
23764
  const result = await inspectAbsoluteMobileRelease(effectiveMobile, projectRoot);
23604
23765
  if (args.includes("--json")) {
23605
23766
  console.log(JSON.stringify(createAbsoluteMobileComplianceReport(effectiveMobile, result), null, 2));
@@ -23648,10 +23809,51 @@ Mobile release security and compliance checks failed.`);
23648
23809
  if (value === "bug-fix" || value === "content" || value === "security")
23649
23810
  return value;
23650
23811
  throw new TypeError("mobile update build requires --classification bug-fix|content|security.");
23812
+ }, prepareExpoMobileUpdateExport = async (options) => {
23813
+ const { mobile } = options;
23814
+ if (mobile.engine !== "expo")
23815
+ return {
23816
+ bundleDirectory: mobile.bundleDirectory,
23817
+ temporaryDirectory: undefined
23818
+ };
23819
+ await ensureExpoPackages(mobile.nativeProjectDirectory, options.args);
23820
+ const temporaryParent = join59(options.projectRoot, ".absolutejs", "mobile", "expo-update-exports");
23821
+ await mkdir19(temporaryParent, { recursive: true });
23822
+ const temporaryDirectory = await mkdtemp9(join59(temporaryParent, ".stage-"));
23823
+ try {
23824
+ await runExpo(mobile.nativeProjectDirectory, [
23825
+ "export",
23826
+ "--platform",
23827
+ "ios",
23828
+ "--platform",
23829
+ "android",
23830
+ "--output-dir",
23831
+ temporaryDirectory,
23832
+ "--clear"
23833
+ ], { production: true });
23834
+ const generatedApp = JSON.parse(await readFile27(join59(mobile.nativeProjectDirectory, "app.json"), "utf8"));
23835
+ const expoConfig = isRecord18(generatedApp) ? generatedApp.expo : undefined;
23836
+ if (!isRecord18(expoConfig))
23837
+ throw new TypeError("Generated Expo app configuration is invalid.");
23838
+ const computedRuntime = resolveAbsoluteMobileUpdateRuntime(mobile, options.projectRoot).fingerprint;
23839
+ if (computedRuntime !== options.runtimeFingerprint)
23840
+ throw new TypeError("Expo native runtime changed while exporting the update.");
23841
+ await finalizeAbsoluteExpoUpdateExport({
23842
+ expoConfig,
23843
+ exportDirectory: temporaryDirectory,
23844
+ runtimeVersion: options.runtimeFingerprint
23845
+ });
23846
+ return {
23847
+ bundleDirectory: temporaryDirectory,
23848
+ temporaryDirectory
23849
+ };
23850
+ } catch (error) {
23851
+ await rm13(temporaryDirectory, { force: true, recursive: true });
23852
+ throw error;
23853
+ }
23651
23854
  }, buildMobileUpdate = async (args) => {
23652
23855
  const configPath2 = valueAfter(args, "--config");
23653
23856
  const { mobile, projectRoot } = await loadMobile(configPath2);
23654
- requireCapacitorEngine(mobile, "mobile update build");
23655
23857
  if (!mobile.updates)
23656
23858
  throw new TypeError("mobile update build requires mobile.updates.publicKeys in absolute.config.ts.");
23657
23859
  if (!args.includes("--within-submitted-purpose"))
@@ -23665,13 +23867,14 @@ Mobile release security and compliance checks failed.`);
23665
23867
  throw new TypeError("mobile update build requires --signing-key <private-key.pem>.");
23666
23868
  const startedAt = performance.now();
23667
23869
  let success = false;
23870
+ let expoExportDirectory;
23668
23871
  try {
23669
23872
  await start(mobileBuildServerEntry(args), valueAfter(args, "--web-outdir"), configPath2, { prepareOnly: true });
23670
- const embedded = JSON.parse(await readFile26(join58(mobile.bundleDirectory, "absolute-mobile-manifest.json"), "utf8"));
23873
+ const embedded = JSON.parse(await readFile27(join59(mobile.bundleDirectory, "absolute-mobile-manifest.json"), "utf8"));
23671
23874
  const runtimeFingerprint = isRecord18(embedded) ? embedded.nativeRuntime : undefined;
23672
23875
  if (typeof runtimeFingerprint !== "string" || !/^[a-f0-9]{64}$/u.test(runtimeFingerprint))
23673
23876
  throw new TypeError("Prepared mobile bundle is missing its native runtime fingerprint.");
23674
- const privateKey = await readFile26(resolve46(projectRoot, signingKeyPath));
23877
+ const privateKey = await readFile27(resolve46(projectRoot, signingKeyPath));
23675
23878
  const configuredPublicKey = Buffer.from(mobile.updates.publicKeys[keyId], "base64");
23676
23879
  const derivedPublicKey = createPublicKey3(privateKey).export({
23677
23880
  format: "der",
@@ -23679,13 +23882,20 @@ Mobile release security and compliance checks failed.`);
23679
23882
  });
23680
23883
  if (!configuredPublicKey.equals(derivedPublicKey))
23681
23884
  throw new TypeError(`The private key does not match mobile.updates.publicKeys.${keyId}.`);
23885
+ const expoExport = await prepareExpoMobileUpdateExport({
23886
+ args,
23887
+ mobile,
23888
+ projectRoot,
23889
+ runtimeFingerprint
23890
+ });
23891
+ expoExportDirectory = expoExport.temporaryDirectory;
23682
23892
  const result = await buildAbsoluteMobileUpdate({
23683
23893
  appId: mobile.appId,
23684
- bundleDirectory: mobile.bundleDirectory,
23894
+ bundleDirectory: expoExport.bundleDirectory,
23685
23895
  channel: mobile.updates.channel,
23686
23896
  classification,
23687
23897
  keyId,
23688
- outputDirectory: valueAfter(args, "--outdir") ?? join58(projectRoot, ".absolutejs", "mobile", "updates"),
23898
+ outputDirectory: valueAfter(args, "--outdir") ?? join59(projectRoot, ".absolutejs", "mobile", "updates"),
23689
23899
  privateKey,
23690
23900
  runtimeFingerprint
23691
23901
  });
@@ -23696,6 +23906,8 @@ Mobile release security and compliance checks failed.`);
23696
23906
  console.log(`Manifest: ${result.manifestPath}`);
23697
23907
  return result;
23698
23908
  } finally {
23909
+ if (expoExportDirectory)
23910
+ await rm13(expoExportDirectory, { force: true, recursive: true });
23699
23911
  sendTelemetryEvent("mobile:update-build", {
23700
23912
  classification: classification ?? "invalid",
23701
23913
  durationMs: Math.round(performance.now() - startedAt),
@@ -23962,7 +24174,7 @@ Mobile release security and compliance checks failed.`);
23962
24174
  const durationMs = Math.round(performance.now() - startedAt);
23963
24175
  console.log(`Built ${release.metadata.signed ? "signed" : "unsigned"} Android App Bundle in ${getDurationString(durationMs)}.`);
23964
24176
  console.log(`Artifact: ${release.artifactPath}`);
23965
- console.log(`Metadata: ${join58(release.releaseRoot, "release.json")}`);
24177
+ console.log(`Metadata: ${join59(release.releaseRoot, "release.json")}`);
23966
24178
  return release;
23967
24179
  } finally {
23968
24180
  sendTelemetryEvent("mobile:android-release-build", {
@@ -24110,7 +24322,7 @@ Mobile release security and compliance checks failed.`);
24110
24322
  const durationMs = Math.round(performance.now() - startedAt);
24111
24323
  console.log(`Built ${release.metadata.signed ? "signed" : "unsigned"} iOS IPA ${release.metadata.marketingVersion}${release.metadata.buildNumber ? ` (${release.metadata.buildNumber})` : ""} in ${getDurationString(durationMs)}.`);
24112
24324
  console.log(`Artifact: ${release.artifactPath}`);
24113
- console.log(`Metadata: ${join58(release.releaseRoot, "release.json")}`);
24325
+ console.log(`Metadata: ${join59(release.releaseRoot, "release.json")}`);
24114
24326
  return release;
24115
24327
  } finally {
24116
24328
  stopListeningForCancellation();
@@ -24219,7 +24431,7 @@ Mobile release security and compliance checks failed.`);
24219
24431
  checks.push({
24220
24432
  id: "sync.storage-schema",
24221
24433
  label: `Offline schema ${schema.components.map((component2) => `${component2.id}@${component2.version}`).join(", ")}`,
24222
- path: join58(projectRoot, "package.json"),
24434
+ path: join59(projectRoot, "package.json"),
24223
24435
  platform: "host",
24224
24436
  status: "pass"
24225
24437
  });
@@ -24227,7 +24439,7 @@ Mobile release security and compliance checks failed.`);
24227
24439
  checks.push({
24228
24440
  id: "sync.storage-schema",
24229
24441
  label: "Offline schema metadata is invalid",
24230
- path: join58(projectRoot, "package.json"),
24442
+ path: join59(projectRoot, "package.json"),
24231
24443
  platform: "host",
24232
24444
  remediation: error instanceof Error ? error.message : String(error),
24233
24445
  status: "fail"
@@ -24238,9 +24450,9 @@ Mobile release security and compliance checks failed.`);
24238
24450
  await runReleaseDoctor(args);
24239
24451
  return;
24240
24452
  }
24241
- const platform6 = args.find((value) => value === "android" || value === "ios");
24453
+ const platform7 = args.find((value) => value === "android" || value === "ios");
24242
24454
  const requestedRemote = valueAfter(args, "--remote");
24243
- const remoteProfile = platform6 === "ios" && (requestedRemote !== undefined || process.platform !== "darwin") ? await getAbsoluteRemoteMacProfile(requestedRemote, remoteProfilePath()) : undefined;
24455
+ const remoteProfile = platform7 === "ios" && (requestedRemote !== undefined || process.platform !== "darwin") ? await getAbsoluteRemoteMacProfile(requestedRemote, remoteProfilePath()) : undefined;
24244
24456
  if (remoteProfile) {
24245
24457
  if (args.includes("--fix"))
24246
24458
  throw new TypeError("Remote Mac doctor is read-only. Configure Xcode or Bun on the Mac, then rerun doctor.");
@@ -24253,7 +24465,7 @@ Mobile release security and compliance checks failed.`);
24253
24465
  }
24254
24466
  const checks = await inspectAbsoluteMobileToolchain();
24255
24467
  appendSyncSchemaDoctorCheck(checks, process.cwd());
24256
- const selected = platform6 ? checks.filter((check2) => check2.platform === "host" || check2.platform === platform6) : checks;
24468
+ const selected = platform7 ? checks.filter((check2) => check2.platform === "host" || check2.platform === platform7) : checks;
24257
24469
  if (args.includes("--json")) {
24258
24470
  if (args.includes("--fix")) {
24259
24471
  throw new TypeError("mobile doctor --json cannot be combined with --fix.");
@@ -24264,7 +24476,7 @@ Mobile release security and compliance checks failed.`);
24264
24476
  printDoctorChecks(selected);
24265
24477
  if (!args.includes("--fix"))
24266
24478
  return;
24267
- const installPlatform = platform6 ?? "android";
24479
+ const installPlatform = platform7 ?? "android";
24268
24480
  const relevantFailures = selected.filter((check2) => check2.platform === installPlatform && (check2.status === "fail" || check2.status === "warn"));
24269
24481
  if (relevantFailures.length === 0) {
24270
24482
  console.log(`
@@ -24380,12 +24592,12 @@ Emulator setup verification:`);
24380
24592
  timeoutMs
24381
24593
  });
24382
24594
  }, writeAndroidFailureArtifacts = async (options) => {
24383
- await mkdir18(options.artifactRoot, { recursive: true });
24384
- const screenshot = options.session ? await options.session.screenshot(join58(options.artifactRoot, "android-failure.png")).catch(() => {
24595
+ await mkdir19(options.artifactRoot, { recursive: true });
24596
+ const screenshot = options.session ? await options.session.screenshot(join59(options.artifactRoot, "android-failure.png")).catch(() => {
24385
24597
  return;
24386
24598
  }) : undefined;
24387
- const diagnosticPath = join58(options.artifactRoot, "android-failure.json");
24388
- await writeFile20(diagnosticPath, `${JSON.stringify({
24599
+ const diagnosticPath = join59(options.artifactRoot, "android-failure.json");
24600
+ await writeFile21(diagnosticPath, `${JSON.stringify({
24389
24601
  diagnostics: options.session?.diagnostics ?? [],
24390
24602
  error: options.error instanceof Error ? options.error.message : String(options.error),
24391
24603
  platform: "android",
@@ -24449,7 +24661,7 @@ Emulator setup verification:`);
24449
24661
  console.log(JSON.stringify(report, null, 2));
24450
24662
  else
24451
24663
  printAndroidTestReport(report);
24452
- const screenshot = reportRoot ? await session.screenshot(join58(artifactRoot, "android-emulator.png")) : undefined;
24664
+ const screenshot = reportRoot ? await session.screenshot(join59(artifactRoot, "android-emulator.png")) : undefined;
24453
24665
  await writeRequestedAndroidReport({
24454
24666
  adb,
24455
24667
  args,
@@ -24638,8 +24850,8 @@ Emulator setup verification:`);
24638
24850
  throw new Error(`${label} failed: ${result.stderr.trim() || result.stdout.trim() || `status ${result.exitCode}`}`);
24639
24851
  return result;
24640
24852
  }, writeIosFailureArtifacts = async (options) => {
24641
- await mkdir18(options.artifactRoot, { recursive: true });
24642
- const screenshot = join58(options.artifactRoot, "ios-failure.png");
24853
+ await mkdir19(options.artifactRoot, { recursive: true });
24854
+ const screenshot = join59(options.artifactRoot, "ios-failure.png");
24643
24855
  const screenshotResult = captureCommand4([
24644
24856
  options.xcrun,
24645
24857
  "simctl",
@@ -24648,8 +24860,8 @@ Emulator setup verification:`);
24648
24860
  "screenshot",
24649
24861
  screenshot
24650
24862
  ]);
24651
- const diagnosticPath = join58(options.artifactRoot, "ios-failure.json");
24652
- await writeFile20(diagnosticPath, `${JSON.stringify({
24863
+ const diagnosticPath = join59(options.artifactRoot, "ios-failure.json");
24864
+ await writeFile21(diagnosticPath, `${JSON.stringify({
24653
24865
  appId: options.appId,
24654
24866
  error: options.error instanceof Error ? options.error.message : String(options.error),
24655
24867
  platform: "ios",
@@ -24663,14 +24875,14 @@ Emulator setup verification:`);
24663
24875
  diagnosticPath,
24664
24876
  screenshot: screenshotResult.exitCode === 0 ? screenshot : undefined
24665
24877
  };
24666
- }, nativeReportRoot = (args, projectRoot, platform6) => {
24878
+ }, nativeReportRoot = (args, projectRoot, platform7) => {
24667
24879
  const index = args.indexOf("--report");
24668
24880
  if (index === NOT_FOUND4)
24669
24881
  return;
24670
24882
  const candidate = args[index + 1];
24671
24883
  const explicit = candidate?.startsWith("--") ? undefined : candidate;
24672
24884
  const timestamp = new Date().toISOString().replaceAll(":", "-");
24673
- return safeArtifactRoot(projectRoot, explicit ?? `.absolutejs/mobile/test-reports/${platform6}-${timestamp}`);
24885
+ return safeArtifactRoot(projectRoot, explicit ?? `.absolutejs/mobile/test-reports/${platform7}-${timestamp}`);
24674
24886
  }, absolutejsVersionForReport = async () => {
24675
24887
  let absolutejsVersion = process.env.ABSOLUTE_VERSION ?? "unknown";
24676
24888
  const versions = await Promise.all([
@@ -24894,8 +25106,8 @@ Emulator setup verification:`);
24894
25106
  mobile.appId
24895
25107
  ], "iOS app launch");
24896
25108
  await waitForIosHmrClient({ https, port, timeoutMs });
24897
- await mkdir18(artifactRoot, { recursive: true });
24898
- const screenshot = join58(artifactRoot, "ios-simulator.png");
25109
+ await mkdir19(artifactRoot, { recursive: true });
25110
+ const screenshot = join59(artifactRoot, "ios-simulator.png");
24899
25111
  requireCapturedCommand([xcrun, "simctl", "io", simulator.udid, "screenshot", screenshot], "iOS simulator screenshot");
24900
25112
  const hmrApply = await waitForRequestedIosHmr(args, instance, timeoutMs);
24901
25113
  const report = {
@@ -25095,6 +25307,8 @@ var init_mobile = __esm(() => {
25095
25307
  init_mobileInspect();
25096
25308
  init_ciWorkflow();
25097
25309
  init_updateSigning();
25310
+ init_expoUpdate();
25311
+ init_updateRuntime();
25098
25312
  init_updatePublisher();
25099
25313
  CAPACITOR_PACKAGES = [
25100
25314
  "@capacitor/core",
@@ -25131,9 +25345,9 @@ var exports_typecheck = {};
25131
25345
  __export(exports_typecheck, {
25132
25346
  typecheck: () => typecheck
25133
25347
  });
25134
- import { resolve as resolve47, join as join59 } from "path";
25348
+ import { resolve as resolve47, join as join60 } from "path";
25135
25349
  import { existsSync as existsSync44, readFileSync as readFileSync40 } from "fs";
25136
- import { mkdir as mkdir19, writeFile as writeFile21 } from "fs/promises";
25350
+ import { mkdir as mkdir20, writeFile as writeFile22 } from "fs/promises";
25137
25351
  var isCommandService3 = (service) => service.kind === "command" || Array.isArray(service.command), resolveConfigPath = (configPath2) => resolve47(configPath2 ?? process.env.ABSOLUTE_CONFIG ?? "absolute.config.ts"), getTypecheckTargets = async (configPath2) => {
25138
25352
  if (!existsSync44(resolveConfigPath(configPath2))) {
25139
25353
  const defaultService = {};
@@ -25240,8 +25454,8 @@ Found ${errorCount} error${suffix}.`;
25240
25454
  console.error("\x1B[31m\u2717\x1B[0m vue-tsc is required for Vue type checking. Install it: bun add -d vue-tsc");
25241
25455
  process.exit(1);
25242
25456
  }
25243
- const vueTsconfigPath = join59(cacheDir, "tsconfig.vue-check.json");
25244
- await writeFile21(vueTsconfigPath, JSON.stringify({
25457
+ const vueTsconfigPath = join60(cacheDir, "tsconfig.vue-check.json");
25458
+ await writeFile22(vueTsconfigPath, JSON.stringify({
25245
25459
  compilerOptions: {
25246
25460
  rootDir: ".."
25247
25461
  },
@@ -25260,7 +25474,7 @@ Found ${errorCount} error${suffix}.`;
25260
25474
  ...base,
25261
25475
  "--incremental",
25262
25476
  "--tsBuildInfoFile",
25263
- join59(cacheDir, "vue-tsc.tsbuildinfo")
25477
+ join60(cacheDir, "vue-tsc.tsbuildinfo")
25264
25478
  ]);
25265
25479
  if (cached.exitCode === 0 || cached.output.length > 0)
25266
25480
  return cached;
@@ -25271,8 +25485,8 @@ Found ${errorCount} error${suffix}.`;
25271
25485
  console.error("\x1B[31m\u2717\x1B[0m @angular/compiler-cli is required for Angular type checking. Install it: bun add -d @angular/compiler-cli");
25272
25486
  process.exit(1);
25273
25487
  }
25274
- const angularTsconfigPath = join59(cacheDir, "tsconfig.angular-check.json");
25275
- await writeFile21(angularTsconfigPath, JSON.stringify({
25488
+ const angularTsconfigPath = join60(cacheDir, "tsconfig.angular-check.json");
25489
+ await writeFile22(angularTsconfigPath, JSON.stringify({
25276
25490
  angularCompilerOptions: {
25277
25491
  strictTemplates: true
25278
25492
  },
@@ -25291,8 +25505,8 @@ Found ${errorCount} error${suffix}.`;
25291
25505
  console.error("\x1B[31m\u2717\x1B[0m typescript is required for type checking. Install it: bun add -d typescript");
25292
25506
  process.exit(1);
25293
25507
  }
25294
- const tscConfigPath = join59(cacheDir, "tsconfig.typecheck.json");
25295
- return writeFile21(tscConfigPath, JSON.stringify({
25508
+ const tscConfigPath = join60(cacheDir, "tsconfig.typecheck.json");
25509
+ return writeFile22(tscConfigPath, JSON.stringify({
25296
25510
  compilerOptions: {
25297
25511
  rootDir: ".."
25298
25512
  },
@@ -25306,7 +25520,7 @@ Found ${errorCount} error${suffix}.`;
25306
25520
  resolve47(tscConfigPath),
25307
25521
  "--incremental",
25308
25522
  "--tsBuildInfoFile",
25309
- join59(cacheDir, "tsc.tsbuildinfo"),
25523
+ join60(cacheDir, "tsc.tsbuildinfo"),
25310
25524
  "--pretty"
25311
25525
  ]));
25312
25526
  }, buildSvelteCheck = async (cacheDir, svelteDir) => {
@@ -25315,8 +25529,8 @@ Found ${errorCount} error${suffix}.`;
25315
25529
  console.error("\x1B[31m\u2717\x1B[0m svelte-check is required for Svelte type checking. Install it: bun add -d svelte-check");
25316
25530
  process.exit(1);
25317
25531
  }
25318
- const svelteTsconfigPath = join59(cacheDir, "tsconfig.svelte-check.json");
25319
- await writeFile21(svelteTsconfigPath, JSON.stringify({
25532
+ const svelteTsconfigPath = join60(cacheDir, "tsconfig.svelte-check.json");
25533
+ await writeFile22(svelteTsconfigPath, JSON.stringify({
25320
25534
  extends: resolve47("tsconfig.json"),
25321
25535
  files: ABSOLUTE_TYPECHECK_FILES,
25322
25536
  include: [`../${svelteDir}/**/*`]
@@ -25345,7 +25559,7 @@ Found ${errorCount} error${suffix}.`;
25345
25559
  ...new Set(targets.map((config) => config.angularDirectory).filter((dir) => typeof dir === "string" && dir.length > 0))
25346
25560
  ];
25347
25561
  const cacheDir = ".absolutejs";
25348
- await mkdir19(cacheDir, { recursive: true });
25562
+ await mkdir20(cacheDir, { recursive: true });
25349
25563
  const checks = [];
25350
25564
  checks.push(hasVue ? buildVueTscCheck(cacheDir) : buildTscCheck(cacheDir));
25351
25565
  for (const svelteDir of hasSvelte ? svelteDirs : []) {