@absolutejs/absolute 0.20.0-beta.59 → 0.20.0-beta.60

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
@@ -718,7 +718,8 @@ var init_portScan = () => {};
718
718
 
719
719
  // src/mobile/config.ts
720
720
  import { resolve as resolve2 } from "path";
721
- var APP_ID_PATTERN, SCHEME_PATTERN, APPLE_APP_ID_PREFIX_PATTERN, CERTIFICATE_FINGERPRINT_PATTERN, HOSTNAME_PATTERN, EXPO_RESERVED_ROUTE_PREFIXES, resolveProjectPath = (projectRoot, value, field) => {
721
+ import { createPublicKey } from "crypto";
722
+ var APP_ID_PATTERN, SCHEME_PATTERN, APPLE_APP_ID_PREFIX_PATTERN, CERTIFICATE_FINGERPRINT_PATTERN, UPDATE_NAME_PATTERN, UPDATE_PUBLIC_KEY_PATTERN, HOSTNAME_PATTERN, EXPO_RESERVED_ROUTE_PREFIXES, resolveProjectPath = (projectRoot, value, field) => {
722
723
  const root = resolve2(projectRoot);
723
724
  const path = resolve2(root, value);
724
725
  if (path !== root && !path.startsWith(`${root}/`)) {
@@ -791,7 +792,52 @@ var APP_ID_PATTERN, SCHEME_PATTERN, APPLE_APP_ID_PREFIX_PATTERN, CERTIFICATE_FIN
791
792
  }
792
793
  return value.match(/.{2}/g)?.join(":") ?? value;
793
794
  }))
794
- ].sort(), validateExpoNativeRouteSegment = (path, segment, index, count, parameters) => {
795
+ ].sort(), normalizeUpdates = (config, productionOrigin) => {
796
+ if (!config.updates)
797
+ return;
798
+ const channel = requireText(config.updates.channel ?? "production", "mobile.updates.channel");
799
+ if (!UPDATE_NAME_PATTERN.test(channel))
800
+ throw new TypeError("mobile.updates.channel contains unsupported characters.");
801
+ const manifestUrl = new URL(config.updates.manifestUrl ?? `/__absolute/mobile/updates/${encodeURIComponent(channel)}/update.json`, `${productionOrigin}/`);
802
+ const loopback = manifestUrl.hostname === "localhost" || manifestUrl.hostname === "127.0.0.1" || manifestUrl.hostname === "[::1]";
803
+ if (manifestUrl.protocol !== "https:" && !loopback)
804
+ throw new TypeError("mobile.updates.manifestUrl must use HTTPS outside loopback development.");
805
+ if (manifestUrl.username || manifestUrl.password || manifestUrl.hash)
806
+ throw new TypeError("mobile.updates.manifestUrl cannot contain credentials or a fragment.");
807
+ const entries = Object.entries(config.updates.publicKeys).sort(([left], [right]) => left.localeCompare(right));
808
+ if (entries.length === 0)
809
+ throw new TypeError("mobile.updates.publicKeys must contain at least one key.");
810
+ const publicKeys = Object.fromEntries(entries.map(([keyId, key]) => {
811
+ if (!UPDATE_NAME_PATTERN.test(keyId))
812
+ throw new TypeError("mobile.updates.publicKeys contains an invalid key ID.");
813
+ const normalized = requireText(key, `mobile.updates.publicKeys.${keyId}`);
814
+ if (!UPDATE_PUBLIC_KEY_PATTERN.test(normalized))
815
+ throw new TypeError(`mobile.updates.publicKeys.${keyId} must be base64-encoded ECDSA P-256 SPKI DER.`);
816
+ let decoded;
817
+ try {
818
+ decoded = Buffer.from(normalized, "base64");
819
+ } catch {
820
+ throw new TypeError(`mobile.updates.publicKeys.${keyId} must be canonical base64.`);
821
+ }
822
+ let keyType;
823
+ let namedCurve;
824
+ try {
825
+ const publicKey = createPublicKey({
826
+ format: "der",
827
+ key: decoded,
828
+ type: "spki"
829
+ });
830
+ keyType = publicKey.asymmetricKeyType;
831
+ namedCurve = publicKey.asymmetricKeyDetails?.namedCurve;
832
+ } catch {
833
+ keyType = undefined;
834
+ }
835
+ if (decoded.toString("base64") !== normalized || keyType !== "ec" || namedCurve !== "prime256v1")
836
+ throw new TypeError(`mobile.updates.publicKeys.${keyId} is not an ECDSA P-256 SPKI public key.`);
837
+ return [keyId, normalized];
838
+ }));
839
+ return { channel, manifestUrl: manifestUrl.href, publicKeys };
840
+ }, validateExpoNativeRouteSegment = (path, segment, index, count, parameters) => {
795
841
  if (segment === "*" && (index !== count - 1 || count === 1)) {
796
842
  throw new TypeError(`mobile.routes.native route ${path} must use * once, as the final segment after a static or parameterized prefix.`);
797
843
  }
@@ -849,6 +895,7 @@ var APP_ID_PATTERN, SCHEME_PATTERN, APPLE_APP_ID_PREFIX_PATTERN, CERTIFICATE_FIN
849
895
  if (deepLinkScheme && !SCHEME_PATTERN.test(deepLinkScheme)) {
850
896
  throw new TypeError("mobile.deepLinks.scheme is not a valid URL scheme.");
851
897
  }
898
+ const updates = normalizeUpdates(config, productionOrigin);
852
899
  return {
853
900
  androidCertificateFingerprints: normalizeCertificateFingerprints(config.deepLinks?.android?.sha256CertificateFingerprints),
854
901
  appId,
@@ -865,7 +912,8 @@ var APP_ID_PATTERN, SCHEME_PATTERN, APPLE_APP_ID_PREFIX_PATTERN, CERTIFICATE_FIN
865
912
  nativeProjectDirectory: resolveProjectPath(projectRoot, config.nativeProject?.directory ?? (config.engine === "expo" ? ".absolutejs/mobile/expo" : "mobile"), "mobile.nativeProject.directory"),
866
913
  platforms: normalizePlatforms(config.platforms),
867
914
  productionOrigin,
868
- pushAndroidGoogleServicesFile: resolveProjectPath(projectRoot, config.pushNotifications?.android?.googleServicesFile ?? "google-services.json", "mobile.pushNotifications.android.googleServicesFile")
915
+ pushAndroidGoogleServicesFile: resolveProjectPath(projectRoot, config.pushNotifications?.android?.googleServicesFile ?? "google-services.json", "mobile.pushNotifications.android.googleServicesFile"),
916
+ ...updates ? { updates } : {}
869
917
  };
870
918
  };
871
919
  var init_config = __esm(() => {
@@ -873,6 +921,8 @@ var init_config = __esm(() => {
873
921
  SCHEME_PATTERN = /^[a-z][a-z0-9+.-]*$/;
874
922
  APPLE_APP_ID_PREFIX_PATTERN = /^[A-Z0-9]{10}$/;
875
923
  CERTIFICATE_FINGERPRINT_PATTERN = /^[0-9A-F]{64}$/;
924
+ UPDATE_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u;
925
+ UPDATE_PUBLIC_KEY_PATTERN = /^[A-Za-z0-9+/]+={0,2}$/u;
876
926
  HOSTNAME_PATTERN = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(?:\.(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?))*$/;
877
927
  EXPO_RESERVED_ROUTE_PREFIXES = new Set([
878
928
  "_expo",
@@ -9060,6 +9110,11 @@ var MANIFEST_FILE = "absolute-mobile-manifest.json", BOOTSTRAP_FILE = "absolute-
9060
9110
  if (candidate)
9061
9111
  return candidate;
9062
9112
  throw new TypeError("AbsoluteJS Expo device bridge module is missing.");
9113
+ }, shellUpdateModule = () => {
9114
+ const candidate = ["js", "ts"].map((extension) => join21(import.meta.dir, `shellUpdate.${extension}`)).find(existsSync10);
9115
+ if (candidate)
9116
+ return candidate;
9117
+ throw new TypeError("AbsoluteJS mobile update shell module is missing.");
9063
9118
  }, escapeHtml = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;"), contentSecurityPolicy = (productionOrigin) => {
9064
9119
  const backend = new URL(productionOrigin);
9065
9120
  const socketOrigin = `${backend.protocol === "https:" ? "wss:" : "ws:"}//${backend.host}`;
@@ -9114,7 +9169,7 @@ var MANIFEST_FILE = "absolute-mobile-manifest.json", BOOTSTRAP_FILE = "absolute-
9114
9169
  if (!resolved.startsWith(`${resolve16(packageDirectory)}/`))
9115
9170
  throw new TypeError(`${specifier} has an unsafe import entry.`);
9116
9171
  return resolved;
9117
- }, buildShellBootstrap = async (staging, auth, sync, storagePrefix, engine, deviceCapabilities, projectRoot) => {
9172
+ }, buildShellBootstrap = async (staging, auth, sync, storagePrefix, engine, updates, deviceCapabilities, projectRoot) => {
9118
9173
  const capacitor = engine !== "expo";
9119
9174
  const shellCapabilities = capacitor ? deviceCapabilities.capabilities : [];
9120
9175
  const modulePath = shellBootstrapModule();
@@ -9122,12 +9177,13 @@ var MANIFEST_FILE = "absolute-mobile-manifest.json", BOOTSTRAP_FILE = "absolute-
9122
9177
  const syncFactory = capacitor ? "installAbsoluteMobileShellSync" : "installAbsoluteExpoShellSync";
9123
9178
  const authImport = auth ? `import { ${authFactory} } from ${JSON.stringify(capacitor ? shellAuthModule() : shellExpoAuthModule())};
9124
9179
  ` : "";
9125
- const options = auth ? `{ createAuth: ${authFactory}${sync ? `, installSync: ${syncFactory}` : ""} }` : "";
9126
9180
  const syncImport = sync ? `import { ${syncFactory} } from ${JSON.stringify(capacitor ? shellSyncModule() : shellExpoSyncModule())};
9127
9181
  ` : "";
9128
9182
  const pushIndex = shellCapabilities.indexOf("pushNotifications");
9129
9183
  const push = pushIndex !== -1;
9130
9184
  const pushImport = push ? `import { createAbsoluteMobileShellPush } from ${JSON.stringify(shellPushModule())};
9185
+ ` : "";
9186
+ const updateImport = updates ? `import { installAbsoluteMobileShellUpdates } from ${JSON.stringify(shellUpdateModule())};
9131
9187
  ` : "";
9132
9188
  const capabilityImports = (await Promise.all(shellCapabilities.map(async (name, index) => {
9133
9189
  const provider = deviceCapabilities.providers[name];
@@ -9144,15 +9200,21 @@ const absoluteMobilePushCapability = absoluteDeviceCapability${pushIndex}(absolu
9144
9200
  const baseAdapterModule = capacitor ? await resolveProjectImport(projectRoot, "@absolutejs/devices-capacitor") : shellExpoDevicesModule();
9145
9201
  const adapterImport = capacitor ? `import { installCapacitorDeviceAdapterIfNative } from ${JSON.stringify(baseAdapterModule)};` : `import { createAbsoluteExpoBridgeFetch, installAbsoluteExpoWebDeviceAdapter } from ${JSON.stringify(baseAdapterModule)};`;
9146
9202
  const adapterInstall = capacitor ? `installCapacitorDeviceAdapterIfNative({ storagePrefix: ${JSON.stringify(storagePrefix)}${capabilityOptions ? `, ${capabilityOptions}` : ""} });` : `installAbsoluteExpoWebDeviceAdapter(${JSON.stringify(deviceCapabilities.capabilities)});`;
9147
- let shellOptions = auth ? options : "{ createFetch: createAbsoluteExpoBridgeFetch }";
9148
- if (capacitor)
9149
- shellOptions = options;
9203
+ const shellOptionProperties = [];
9150
9204
  if (push) {
9151
- shellOptions = `{ createAuth: (config, options) => createAbsoluteMobileShellAuth(config, options), beforeSignOut: absoluteMobilePush.beforeSignOut, connectPush: (auth) => absoluteMobilePush.connect(auth, absoluteMobilePushCapability)${sync ? `, installSync: ${syncFactory}` : ""} }`;
9152
- }
9205
+ shellOptionProperties.push("createAuth: (config, options) => createAbsoluteMobileShellAuth(config, options)", "beforeSignOut: absoluteMobilePush.beforeSignOut", "connectPush: (auth) => absoluteMobilePush.connect(auth, absoluteMobilePushCapability)");
9206
+ } else if (auth)
9207
+ shellOptionProperties.push(`createAuth: ${authFactory}`);
9208
+ else if (!capacitor)
9209
+ shellOptionProperties.push("createFetch: createAbsoluteExpoBridgeFetch");
9210
+ if (sync)
9211
+ shellOptionProperties.push(`installSync: ${syncFactory}`);
9212
+ if (updates)
9213
+ shellOptionProperties.push("installUpdates: installAbsoluteMobileShellUpdates");
9214
+ const shellOptions = `{ ${shellOptionProperties.join(", ")} }`;
9153
9215
  await writeFile8(entryPath, `import { startAbsoluteMobileShell } from ${JSON.stringify(modulePath)};
9154
9216
  ${adapterImport}
9155
- ${authImport}${syncImport}${pushImport}${capabilityImports}
9217
+ ${authImport}${syncImport}${pushImport}${updateImport}${capabilityImports}
9156
9218
  ${pushSetup}${adapterInstall}
9157
9219
  void startAbsoluteMobileShell(${shellOptions});
9158
9220
  `);
@@ -9287,10 +9349,12 @@ void startAbsoluteMobileShell(${shellOptions});
9287
9349
  deviceCapabilities: options.deviceCapabilities.capabilities,
9288
9350
  entry: options.config.entry,
9289
9351
  format: ABSOLUTE_MOBILE_CLIENT_MANIFEST_FORMAT,
9352
+ nativeRuntime: options.runtimeFingerprint,
9290
9353
  pages,
9291
9354
  productionOrigin: options.config.productionOrigin,
9292
9355
  routes: options.artifact.routes,
9293
9356
  runtime: options.artifact.runtime,
9357
+ ...options.config.updates ? { updates: options.config.updates } : {},
9294
9358
  ...options.sync ? {
9295
9359
  sync: {
9296
9360
  background: {
@@ -9310,7 +9374,7 @@ void startAbsoluteMobileShell(${shellOptions});
9310
9374
  writeFile8(join21(staging, MANIFEST_FILE), `${JSON.stringify(manifest, null, "\t")}
9311
9375
  `),
9312
9376
  writeFile8(join21(staging, INDEX_FILE), indexHtml(options.config.appName, options.config.productionOrigin)),
9313
- buildShellBootstrap(staging, options.auth !== undefined, options.auth !== undefined && options.sync === true, `absolutejs.${options.auth?.clientId ?? options.config.appId}.`, options.config.engine, options.deviceCapabilities, options.projectRoot)
9377
+ buildShellBootstrap(staging, options.auth !== undefined, options.auth !== undefined && options.sync === true, `absolutejs.${options.auth?.clientId ?? options.config.appId}.`, options.config.engine, options.config.engine === "capacitor" && options.config.updates !== undefined, options.deviceCapabilities, options.projectRoot)
9314
9378
  ]);
9315
9379
  await installBundle(staging, destination);
9316
9380
  return manifest;
@@ -9484,6 +9548,136 @@ var init_materializedBundle = __esm(() => {
9484
9548
  BUNDLE_ID_PATTERN = /^amb_[a-f0-9]{64}$/;
9485
9549
  });
9486
9550
 
9551
+ // src/mobile/updateProtocol.ts
9552
+ 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) => {
9553
+ if (Array.isArray(value))
9554
+ return value.map(canonicalValue);
9555
+ if (!object3(value))
9556
+ return value;
9557
+ return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonicalValue(value[key])]));
9558
+ }, absoluteMobileUpdateSigningPayload = (manifest) => new TextEncoder().encode(canonicalizeAbsoluteMobileUpdate(manifest)), canonicalizeAbsoluteMobileUpdate = (value) => JSON.stringify(canonicalValue(value)), requireText2 = (value, field) => {
9559
+ if (typeof value !== "string" || value.length === 0)
9560
+ throw new TypeError(`${field} must be a non-empty string.`);
9561
+ return value;
9562
+ }, normalizeAbsoluteMobileUpdatePath = (value) => {
9563
+ const path = requireText2(value, "Update file path").replaceAll("\\", "/");
9564
+ if (path.startsWith("/") || path.includes("\x00") || path.split("/").some((segment) => !segment || segment === "." || segment === "..")) {
9565
+ throw new TypeError("Update file paths must be normalized relative paths.");
9566
+ }
9567
+ return path;
9568
+ }, parseFile = (value) => {
9569
+ if (!object3(value))
9570
+ throw new TypeError("Update files must be objects.");
9571
+ const path = normalizeAbsoluteMobileUpdatePath(value.path);
9572
+ if (typeof value.bytes !== "number" || !Number.isSafeInteger(value.bytes) || value.bytes < 0 || value.bytes > ABSOLUTE_MOBILE_UPDATE_MAX_FILE_BYTES) {
9573
+ throw new TypeError(`Update file ${path} has an invalid byte length.`);
9574
+ }
9575
+ if (typeof value.sha256 !== "string" || !HASH_PATTERN.test(value.sha256))
9576
+ throw new TypeError(`Update file ${path} has an invalid SHA-256 digest.`);
9577
+ return { bytes: value.bytes, path, sha256: value.sha256 };
9578
+ }, parseAbsoluteMobileUnsignedUpdateManifest = (value) => {
9579
+ if (!object3(value) || value.format !== ABSOLUTE_MOBILE_UPDATE_FORMAT)
9580
+ throw new TypeError("Invalid AbsoluteJS mobile update manifest.");
9581
+ const appId = requireText2(value.appId, "Update appId");
9582
+ const channel = requireText2(value.channel, "Update channel");
9583
+ if (!CHANNEL_PATTERN.test(channel))
9584
+ throw new TypeError("Update channel contains unsupported characters.");
9585
+ if (!isClassification(value.classification))
9586
+ throw new TypeError("Update classification is invalid.");
9587
+ const createdAt = requireText2(value.createdAt, "Update createdAt");
9588
+ if (!Number.isFinite(Date.parse(createdAt)) || new Date(createdAt).toISOString() !== createdAt)
9589
+ throw new TypeError("Update createdAt must be a canonical ISO timestamp.");
9590
+ if (!Array.isArray(value.files) || value.files.length === 0)
9591
+ throw new TypeError("An update must contain at least one file.");
9592
+ if (value.files.length > ABSOLUTE_MOBILE_UPDATE_MAX_FILES)
9593
+ throw new TypeError("Update contains too many files.");
9594
+ const files = value.files.map(parseFile);
9595
+ const sorted = [...files].sort((left, right) => left.path.localeCompare(right.path));
9596
+ if (files.some((file, index) => file.path !== sorted[index]?.path))
9597
+ throw new TypeError("Update files must be sorted by path.");
9598
+ if (new Set(files.map(({ path }) => path)).size !== files.length)
9599
+ throw new TypeError("Update file paths must be unique.");
9600
+ if (files.reduce((total, file) => total + file.bytes, 0) > ABSOLUTE_MOBILE_UPDATE_MAX_TOTAL_BYTES)
9601
+ throw new TypeError("Update exceeds the maximum uncompressed size.");
9602
+ if (typeof value.releaseId !== "string" || !RELEASE_PATTERN.test(value.releaseId))
9603
+ throw new TypeError("Update releaseId is invalid.");
9604
+ if (typeof value.runtimeFingerprint !== "string" || !HASH_PATTERN.test(value.runtimeFingerprint))
9605
+ throw new TypeError("Update runtime fingerprint is invalid.");
9606
+ if (value.withinSubmittedPurpose !== true)
9607
+ throw new TypeError("OTA updates must attest that they remain within the submitted app purpose.");
9608
+ return {
9609
+ appId,
9610
+ channel,
9611
+ classification: value.classification,
9612
+ createdAt,
9613
+ files,
9614
+ format: ABSOLUTE_MOBILE_UPDATE_FORMAT,
9615
+ releaseId: value.releaseId,
9616
+ runtimeFingerprint: value.runtimeFingerprint,
9617
+ withinSubmittedPurpose: true
9618
+ };
9619
+ }, parseAbsoluteMobileUpdateManifest = (value) => {
9620
+ if (!object3(value))
9621
+ throw new TypeError("Invalid AbsoluteJS mobile update manifest.");
9622
+ const { signature: signatureValue, ...unsignedValue } = value;
9623
+ const unsigned = parseAbsoluteMobileUnsignedUpdateManifest(unsignedValue);
9624
+ if (!object3(signatureValue))
9625
+ throw new TypeError("Update signature is missing.");
9626
+ if (signatureValue.algorithm !== ABSOLUTE_MOBILE_UPDATE_SIGNATURE_ALGORITHM)
9627
+ throw new TypeError("Update signature algorithm is unsupported.");
9628
+ const keyId = requireText2(signatureValue.keyId, "Update signature keyId");
9629
+ if (!KEY_ID_PATTERN.test(keyId))
9630
+ throw new TypeError("Update signature keyId is invalid.");
9631
+ const signature = requireText2(signatureValue.value, "Update signature value");
9632
+ let signatureBytes;
9633
+ try {
9634
+ signatureBytes = Uint8Array.from(atob(signature), (character) => character.charCodeAt(0));
9635
+ } catch {
9636
+ signatureBytes = new Uint8Array;
9637
+ }
9638
+ if (!/^[A-Za-z0-9+/]+={0,2}$/u.test(signature) || signatureBytes.byteLength !== 64 || btoa(String.fromCharCode(...signatureBytes)) !== signature)
9639
+ throw new TypeError("Update signature is not canonical base64.");
9640
+ return {
9641
+ ...unsigned,
9642
+ signature: {
9643
+ algorithm: ABSOLUTE_MOBILE_UPDATE_SIGNATURE_ALGORITHM,
9644
+ keyId,
9645
+ value: signature
9646
+ }
9647
+ };
9648
+ }, unsignedAbsoluteMobileUpdate = (manifest) => {
9649
+ const { signature: _signature, ...unsigned } = manifest;
9650
+ return unsigned;
9651
+ };
9652
+ var init_updateProtocol = __esm(() => {
9653
+ ABSOLUTE_MOBILE_UPDATE_MAX_FILE_BYTES = 32 * 1024 * 1024;
9654
+ ABSOLUTE_MOBILE_UPDATE_MAX_TOTAL_BYTES = 128 * 1024 * 1024;
9655
+ HASH_PATTERN = /^[a-f0-9]{64}$/u;
9656
+ RELEASE_PATTERN = /^amu_[a-f0-9]{64}$/u;
9657
+ KEY_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u;
9658
+ CHANNEL_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u;
9659
+ });
9660
+
9661
+ // src/mobile/updateRuntime.ts
9662
+ import { createHash as createHash12 } from "crypto";
9663
+ var ABSOLUTE_MOBILE_SHELL_ABI = 1, ABSOLUTE_MOBILE_UPDATE_RUNTIME_FORMAT = 1, createAbsoluteMobileUpdateRuntimeDescriptor = (options) => ({
9664
+ appId: options.config.appId,
9665
+ auth: options.auth ?? null,
9666
+ deepLinks: {
9667
+ hosts: options.config.deepLinkHosts,
9668
+ ...options.config.deepLinkScheme ? { scheme: options.config.deepLinkScheme } : {}
9669
+ },
9670
+ deviceCapabilities: options.deviceCapabilities,
9671
+ engine: options.config.engine,
9672
+ format: ABSOLUTE_MOBILE_UPDATE_RUNTIME_FORMAT,
9673
+ shellAbi: ABSOLUTE_MOBILE_SHELL_ABI,
9674
+ syncSchema: options.syncSchema ?? null,
9675
+ updates: options.config.updates ?? null
9676
+ }), fingerprintAbsoluteMobileUpdateRuntime = (descriptor) => createHash12("sha256").update(canonicalizeAbsoluteMobileUpdate(descriptor)).digest("hex");
9677
+ var init_updateRuntime = __esm(() => {
9678
+ init_updateProtocol();
9679
+ });
9680
+
9487
9681
  // src/mobile/buildPipeline.ts
9488
9682
  import { readFile as readFile12 } from "fs/promises";
9489
9683
  import { join as join23, resolve as resolve17 } from "path";
@@ -9556,6 +9750,12 @@ var isElysiaApp = (value) => typeof value === "object" && value !== null && type
9556
9750
  const sync = auth !== undefined && projectUsesAbsoluteSync(options.projectRoot);
9557
9751
  const syncSchema = sync ? discoverAbsoluteSyncSchema(options.projectRoot) : undefined;
9558
9752
  const deviceCapabilities = resolveAbsoluteDeviceCapabilityPlan(options.projectRoot, mobile.engine);
9753
+ const runtimeFingerprint = fingerprintAbsoluteMobileUpdateRuntime(createAbsoluteMobileUpdateRuntimeDescriptor({
9754
+ ...auth ? { auth } : {},
9755
+ config: mobile,
9756
+ deviceCapabilities,
9757
+ ...syncSchema ? { syncSchema } : {}
9758
+ }));
9559
9759
  const usesPush = deviceCapabilities.capabilities.includes("pushNotifications");
9560
9760
  if (usesPush && !auth)
9561
9761
  throw new TypeError("Portable push notifications require @absolutejs/auth so provider tokens can be registered without exposing identity controls to page code.");
@@ -9584,6 +9784,7 @@ var isElysiaApp = (value) => typeof value === "object" && value !== null && type
9584
9784
  config: mobile,
9585
9785
  deviceCapabilities,
9586
9786
  projectRoot: options.projectRoot,
9787
+ runtimeFingerprint,
9587
9788
  ...sync ? { sync: true } : {},
9588
9789
  ...syncSchema ? { syncSchema: { components: syncSchema.components } } : {}
9589
9790
  });
@@ -9606,6 +9807,7 @@ var init_buildPipeline = __esm(() => {
9606
9807
  init_syncSchema();
9607
9808
  init_deviceCapabilities();
9608
9809
  init_expoProject();
9810
+ init_updateRuntime();
9609
9811
  });
9610
9812
 
9611
9813
  // src/mobile/routeMetadataTransform.ts
@@ -9635,8 +9837,8 @@ var ROUTE_METHODS, SOURCE_FILTER, PAGE_HANDLERS, posixPath = (value) => value.re
9635
9837
  if (ts5.isStringLiteralLike(property.name))
9636
9838
  return property.name.text;
9637
9839
  return;
9638
- }, objectPropertyExpression = (object3, name) => {
9639
- const property = object3.properties.find((candidate) => propertyName(candidate) === name);
9840
+ }, objectPropertyExpression = (object4, name) => {
9841
+ const property = object4.properties.find((candidate) => propertyName(candidate) === name);
9640
9842
  if (property && ts5.isPropertyAssignment(property)) {
9641
9843
  return property.initializer;
9642
9844
  }
@@ -9822,8 +10024,8 @@ var ROUTE_METHODS, SOURCE_FILTER, PAGE_HANDLERS, posixPath = (value) => value.re
9822
10024
  if (!ts5.isCallExpression(expression))
9823
10025
  return;
9824
10026
  return callableObject(expression, checker);
9825
- }, objectAssetKey = (object3, name, checker, bindings = new Map) => {
9826
- for (const property of [...object3.properties].reverse()) {
10027
+ }, objectAssetKey = (object4, name, checker, bindings = new Map) => {
10028
+ for (const property of [...object4.properties].reverse()) {
9827
10029
  if (propertyName(property) === name && ts5.isShorthandPropertyAssignment(property)) {
9828
10030
  return assetKeyWithBindings(property.name, checker, bindings);
9829
10031
  }
@@ -11512,9 +11714,9 @@ __export(exports_lintProof, {
11512
11714
  writeLintProof: () => writeLintProof
11513
11715
  });
11514
11716
  import {
11515
- createHash as createHash12,
11717
+ createHash as createHash13,
11516
11718
  createPrivateKey,
11517
- createPublicKey,
11719
+ createPublicKey as createPublicKey2,
11518
11720
  sign,
11519
11721
  verify
11520
11722
  } from "crypto";
@@ -11555,7 +11757,7 @@ var DEFAULT_PROOF_LOCATION = ".absolutejs/lint-proof.json", PROOF_CONTRACT_VERSI
11555
11757
  lintFingerprint: proof.lintFingerprint,
11556
11758
  sourceTree: proof.sourceTree
11557
11759
  })
11558
- ].join("\x00")), publicKeyId = (key) => createHash12("sha256").update(key.export({ format: "der", type: "spki" })).digest("hex"), readEd25519PrivateKey = (cwd, location) => {
11760
+ ].join("\x00")), publicKeyId = (key) => createHash13("sha256").update(key.export({ format: "der", type: "spki" })).digest("hex"), readEd25519PrivateKey = (cwd, location) => {
11559
11761
  const path = resolve24(cwd, location);
11560
11762
  if (isInside3(realpathSync(gitRoot(cwd)), realpathSync(path))) {
11561
11763
  throw new Error("lint proof signing key must live outside the Git working tree");
@@ -11566,7 +11768,7 @@ var DEFAULT_PROOF_LOCATION = ".absolutejs/lint-proof.json", PROOF_CONTRACT_VERSI
11566
11768
  }
11567
11769
  return key;
11568
11770
  }, readEd25519PublicKey = (cwd, location) => {
11569
- const key = createPublicKey(readFileSync18(resolve24(cwd, location)));
11771
+ const key = createPublicKey2(readFileSync18(resolve24(cwd, location)));
11570
11772
  if (key.asymmetricKeyType !== "ed25519") {
11571
11773
  throw new Error("trusted lint proof key must be an Ed25519 public key");
11572
11774
  }
@@ -11649,7 +11851,7 @@ var DEFAULT_PROOF_LOCATION = ".absolutejs/lint-proof.json", PROOF_CONTRACT_VERSI
11649
11851
  } finally {
11650
11852
  rmSync5(temporaryDirectory, { force: true, recursive: true });
11651
11853
  }
11652
- }, proofFingerprint = (cwd) => createHash12("sha256").update(`absolute-lint-proof:${PROOF_CONTRACT_VERSION}\x00`).update(createEslintCacheFingerprint(cwd)).update("\x00").update(createEslintConfigDigest(cwd)).digest("hex"), createLintProof = (command, options = {}) => {
11854
+ }, proofFingerprint = (cwd) => createHash13("sha256").update(`absolute-lint-proof:${PROOF_CONTRACT_VERSION}\x00`).update(createEslintCacheFingerprint(cwd)).update("\x00").update(createEslintConfigDigest(cwd)).digest("hex"), createLintProof = (command, options = {}) => {
11653
11855
  const cwd = options.cwd ?? process.cwd();
11654
11856
  const proofLocation = options.proofLocation ?? DEFAULT_PROOF_LOCATION;
11655
11857
  return {
@@ -11667,7 +11869,7 @@ var DEFAULT_PROOF_LOCATION = ".absolutejs/lint-proof.json", PROOF_CONTRACT_VERSI
11667
11869
  const proof = createLintProof(command, { cwd, proofLocation });
11668
11870
  if (options.signingKeyLocation) {
11669
11871
  const privateKey = readEd25519PrivateKey(cwd, options.signingKeyLocation);
11670
- const publicKey = createPublicKey(privateKey);
11872
+ const publicKey = createPublicKey2(privateKey);
11671
11873
  proof.attestation = {
11672
11874
  algorithm: "ed25519",
11673
11875
  keyId: publicKeyId(publicKey),
@@ -13357,7 +13559,7 @@ var CONFIG_CANDIDATES2, RUNTIME_FIELDS, findConfigPath = (cwd, override) => {
13357
13559
  return { opaque: false, value: items };
13358
13560
  }
13359
13561
  if (ts7.isObjectLiteralExpression(node)) {
13360
- const object3 = {};
13562
+ const object4 = {};
13361
13563
  for (const property of node.properties) {
13362
13564
  if (!ts7.isPropertyAssignment(property) || !(ts7.isIdentifier(property.name) || ts7.isStringLiteral(property.name))) {
13363
13565
  return { opaque: true, value: undefined };
@@ -13365,18 +13567,18 @@ var CONFIG_CANDIDATES2, RUNTIME_FIELDS, findConfigPath = (cwd, override) => {
13365
13567
  const result = evalLiteral(property.initializer);
13366
13568
  if (result.opaque)
13367
13569
  return { opaque: true, value: undefined };
13368
- object3[property.name.text] = result.value;
13570
+ object4[property.name.text] = result.value;
13369
13571
  }
13370
- return { opaque: false, value: object3 };
13572
+ return { opaque: false, value: object4 };
13371
13573
  }
13372
13574
  return { opaque: true, value: undefined };
13373
13575
  }, readCurrent = (configPath2) => {
13374
13576
  const current = {};
13375
13577
  const opaqueKeys = [];
13376
- const { object: object3 } = parseConfigObject(configPath2);
13377
- if (!object3)
13578
+ const { object: object4 } = parseConfigObject(configPath2);
13579
+ if (!object4)
13378
13580
  return { current, opaqueKeys };
13379
- for (const property of object3.properties) {
13581
+ for (const property of object4.properties) {
13380
13582
  if (!ts7.isPropertyAssignment(property) || !(ts7.isIdentifier(property.name) || ts7.isStringLiteral(property.name))) {
13381
13583
  continue;
13382
13584
  }
@@ -14094,8 +14296,8 @@ export const navData: NavItem[] = [];
14094
14296
  };
14095
14297
  visit(sourceFile);
14096
14298
  return found;
14097
- }, readStringProperty = (object3, name) => {
14098
- const property = object3.properties.find((candidate) => ts10.isPropertyAssignment(candidate) && ts10.isIdentifier(candidate.name) && candidate.name.text === name);
14299
+ }, readStringProperty = (object4, name) => {
14300
+ const property = object4.properties.find((candidate) => ts10.isPropertyAssignment(candidate) && ts10.isIdentifier(candidate.name) && candidate.name.text === name);
14099
14301
  if (!property || !ts10.isStringLiteralLike(property.initializer)) {
14100
14302
  return null;
14101
14303
  }
@@ -14558,18 +14760,18 @@ var lineStartOffset = (text2, position) => {
14558
14760
  `)
14559
14761
  index -= 1;
14560
14762
  return index;
14561
- }, indentBefore2 = (text2, position) => text2.slice(lineStartOffset(text2, position), position), findProperty = (object3, name) => object3.properties.find((property) => ts11.isPropertyAssignment(property) && (ts11.isIdentifier(property.name) || ts11.isStringLiteral(property.name)) && property.name.text === name), applyAbsoluteConfigEdit = (configPath2, request) => {
14763
+ }, indentBefore2 = (text2, position) => text2.slice(lineStartOffset(text2, position), position), findProperty = (object4, name) => object4.properties.find((property) => ts11.isPropertyAssignment(property) && (ts11.isIdentifier(property.name) || ts11.isStringLiteral(property.name)) && property.name.text === name), applyAbsoluteConfigEdit = (configPath2, request) => {
14562
14764
  try {
14563
14765
  const text2 = readFileSync26(configPath2, "utf-8");
14564
14766
  const sourceFile = ts11.createSourceFile(configPath2, text2, ts11.ScriptTarget.Latest, true);
14565
- const object3 = findConfigObject(sourceFile);
14566
- if (!object3) {
14767
+ const object4 = findConfigObject(sourceFile);
14768
+ if (!object4) {
14567
14769
  return {
14568
14770
  message: "Could not find defineConfig({ ... }) in the config file.",
14569
14771
  ok: false
14570
14772
  };
14571
14773
  }
14572
- const existing = findProperty(object3, request.name);
14774
+ const existing = findProperty(object4, request.name);
14573
14775
  if (request.remove) {
14574
14776
  if (!existing)
14575
14777
  return { message: `${request.name} is not set`, ok: true };
@@ -14590,7 +14792,7 @@ var lineStartOffset = (text2, position) => {
14590
14792
  writeFileSync14(configPath2, text2.slice(0, start2) + valueText + text2.slice(end), "utf-8");
14591
14793
  return { message: `Updated ${request.name}`, ok: true };
14592
14794
  }
14593
- const { properties } = object3;
14795
+ const { properties } = object4;
14594
14796
  const entry = `${request.name}: ${valueText}`;
14595
14797
  if (properties.length > 0) {
14596
14798
  const last = properties[properties.length - 1];
@@ -14609,11 +14811,11 @@ var lineStartOffset = (text2, position) => {
14609
14811
  ${indent}${entry}`;
14610
14812
  writeFileSync14(configPath2, text2.slice(0, insertionIndex) + insertion + text2.slice(insertionIndex), "utf-8");
14611
14813
  } else {
14612
- const insertionIndex = object3.getStart(sourceFile) + 1;
14613
- const indent = `${indentBefore2(text2, object3.getStart(sourceFile))} `;
14814
+ const insertionIndex = object4.getStart(sourceFile) + 1;
14815
+ const indent = `${indentBefore2(text2, object4.getStart(sourceFile))} `;
14614
14816
  const insertion = `
14615
14817
  ${indent}${entry}
14616
- ${indentBefore2(text2, object3.getStart(sourceFile))}`;
14818
+ ${indentBefore2(text2, object4.getStart(sourceFile))}`;
14617
14819
  writeFileSync14(configPath2, text2.slice(0, insertionIndex) + insertion + text2.slice(insertionIndex), "utf-8");
14618
14820
  }
14619
14821
  return { message: `Updated ${request.name}`, ok: true };
@@ -15309,10 +15511,10 @@ var AUTH_PACKAGE = "@absolutejs/auth", CONFIG_CANDIDATES3, findAuthSettingsPath
15309
15511
  }, readCurrent2 = (configPath2) => {
15310
15512
  const current = {};
15311
15513
  const opaqueKeys = [];
15312
- const { object: object3 } = parseAuthSettingsObject(configPath2);
15313
- if (!object3)
15514
+ const { object: object4 } = parseAuthSettingsObject(configPath2);
15515
+ if (!object4)
15314
15516
  return { current, opaqueKeys };
15315
- for (const property of object3.properties) {
15517
+ for (const property of object4.properties) {
15316
15518
  if (!ts12.isPropertyAssignment(property) || !(ts12.isIdentifier(property.name) || ts12.isStringLiteral(property.name))) {
15317
15519
  continue;
15318
15520
  }
@@ -15425,11 +15627,11 @@ var AUTH_PACKAGE2 = "@absolutejs/auth", REPO_URL = "https://github.com/absolutej
15425
15627
  return null;
15426
15628
  }
15427
15629
  return property.initializer.properties.length;
15428
- }, readConfigKeys = (object3) => {
15630
+ }, readConfigKeys = (object4) => {
15429
15631
  const keys = new Set;
15430
15632
  let providerCount = null;
15431
- const usesSpread = object3.properties.some((property) => ts13.isSpreadAssignment(property));
15432
- for (const property of object3.properties) {
15633
+ const usesSpread = object4.properties.some((property) => ts13.isSpreadAssignment(property));
15634
+ for (const property of object4.properties) {
15433
15635
  const { name } = property;
15434
15636
  if (name === undefined || !ts13.isIdentifier(name))
15435
15637
  continue;
@@ -19461,14 +19663,72 @@ var init_nativeBackgroundSync = __esm(() => {
19461
19663
  init_nativeAuth();
19462
19664
  });
19463
19665
 
19666
+ // src/mobile/nativeUpdates.ts
19667
+ import { readFile as readFile16, rename as rename12, writeFile as writeFile13 } from "fs/promises";
19668
+ import { join as join51 } from "path";
19669
+ var START = "// absolutejs:mobile-updates:start", END = "// absolutejs:mobile-updates:end", writeChanged2 = async (path, source) => {
19670
+ const current = await readFile16(path, "utf8");
19671
+ if (current === source)
19672
+ return false;
19673
+ const temporary = `${path}.${crypto.randomUUID()}.tmp`;
19674
+ await writeFile13(temporary, source, { flag: "wx" });
19675
+ await rename12(temporary, path);
19676
+ return true;
19677
+ }, replaceRegion2 = (source, region) => {
19678
+ const start2 = source.indexOf(START);
19679
+ const end = source.indexOf(END);
19680
+ if (start2 < 0 !== end < 0 || end < start2)
19681
+ throw new TypeError("AbsoluteJS mobile update markers are malformed.");
19682
+ if (start2 >= 0) {
19683
+ const from = source.lastIndexOf(`
19684
+ `, start2) + 1;
19685
+ const newline = source.indexOf(`
19686
+ `, end + END.length);
19687
+ return `${source.slice(0, from)}${region}${source.slice(newline < 0 ? source.length : newline + 1)}`;
19688
+ }
19689
+ if (!region)
19690
+ return source;
19691
+ const launch = source.indexOf("didFinishLaunchingWithOptions");
19692
+ const brace = launch < 0 ? -1 : source.indexOf("{", launch);
19693
+ const insert = brace < 0 ? -1 : source.indexOf(`
19694
+ `, brace) + 1;
19695
+ if (insert <= 0)
19696
+ throw new TypeError("Could not find a safe iOS location for mobile update recovery.");
19697
+ return `${source.slice(0, insert)}${region}${source.slice(insert)}`;
19698
+ }, iosRecoveryRegion, applyAbsoluteNativeUpdates = async (config, platforms = config.platforms) => {
19699
+ if (config.engine !== "capacitor" || !platforms.includes("ios"))
19700
+ return { changed: false };
19701
+ const path = join51(config.nativeProjectDirectory, "ios/App/App/AppDelegate.swift");
19702
+ const source = await readFile16(path, "utf8");
19703
+ const updated = replaceRegion2(source, config.updates ? iosRecoveryRegion : "");
19704
+ return { changed: await writeChanged2(path, updated) };
19705
+ };
19706
+ var init_nativeUpdates = __esm(() => {
19707
+ iosRecoveryRegion = ` ${START}
19708
+ // A confirmed Capacitor snapshot lives in Library/NoCloud and is not
19709
+ // restored during device migration. Clear only a dangling pointer so
19710
+ // Capacitor falls back to the store-signed embedded bundle.
19711
+ if let persisted = UserDefaults.standard.string(forKey: "serverBasePath"), !persisted.isEmpty,
19712
+ let library = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first {
19713
+ let snapshot = library
19714
+ .appendingPathComponent("NoCloud/ionic_built_snapshots", isDirectory: true)
19715
+ .appendingPathComponent(URL(fileURLWithPath: persisted).lastPathComponent, isDirectory: true)
19716
+ if !FileManager.default.fileExists(atPath: snapshot.path) {
19717
+ UserDefaults.standard.removeObject(forKey: "serverBasePath")
19718
+ }
19719
+ }
19720
+ ${END}
19721
+ `;
19722
+ });
19723
+
19464
19724
  // src/mobile/associationFiles.ts
19465
19725
  import {
19466
19726
  access as access9,
19467
19727
  mkdir as mkdir11,
19468
- readFile as readFile16,
19469
- rename as rename12,
19728
+ readFile as readFile17,
19729
+ rename as rename13,
19470
19730
  rm as rm9,
19471
- writeFile as writeFile13
19731
+ writeFile as writeFile14
19472
19732
  } from "fs/promises";
19473
19733
  import { resolve as resolve37 } from "path";
19474
19734
  import { Elysia } from "elysia";
@@ -19523,7 +19783,7 @@ var OWNERSHIP_FILE = ".absolutejs-mobile-associations.json", HTTP_OK = 200, VERI
19523
19783
  }, writeAtomic = async (path, source) => {
19524
19784
  let current;
19525
19785
  try {
19526
- current = await readFile16(path, "utf8");
19786
+ current = await readFile17(path, "utf8");
19527
19787
  } catch (error) {
19528
19788
  if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT") {
19529
19789
  throw error;
@@ -19532,8 +19792,8 @@ var OWNERSHIP_FILE = ".absolutejs-mobile-associations.json", HTTP_OK = 200, VERI
19532
19792
  if (current === source)
19533
19793
  return false;
19534
19794
  const temporary = `${path}.${crypto.randomUUID()}.tmp`;
19535
- await writeFile13(temporary, source, { flag: "wx" });
19536
- await rename12(temporary, path);
19795
+ await writeFile14(temporary, source, { flag: "wx" });
19796
+ await rename13(temporary, path);
19537
19797
  return true;
19538
19798
  }, exists3 = async (path) => {
19539
19799
  try {
@@ -19546,7 +19806,7 @@ var OWNERSHIP_FILE = ".absolutejs-mobile-associations.json", HTTP_OK = 200, VERI
19546
19806
  const path = resolve37(root, OWNERSHIP_FILE);
19547
19807
  let ownership;
19548
19808
  try {
19549
- ownership = JSON.parse(await readFile16(path, "utf8"));
19809
+ ownership = JSON.parse(await readFile17(path, "utf8"));
19550
19810
  } catch {
19551
19811
  throw new TypeError(`Association output ${root} already exists and is not owned by AbsoluteJS.`);
19552
19812
  }
@@ -19559,12 +19819,12 @@ var OWNERSHIP_FILE = ".absolutejs-mobile-associations.json", HTTP_OK = 200, VERI
19559
19819
  await assertOwnedOutput(root);
19560
19820
  const backup = `${root}.${crypto.randomUUID()}.previous`;
19561
19821
  if (hasCurrent)
19562
- await rename12(root, backup);
19822
+ await rename13(root, backup);
19563
19823
  try {
19564
- await rename12(temporary, root);
19824
+ await rename13(temporary, root);
19565
19825
  } catch (error) {
19566
19826
  if (hasCurrent)
19567
- await rename12(backup, root);
19827
+ await rename13(backup, root);
19568
19828
  throw error;
19569
19829
  }
19570
19830
  if (hasCurrent)
@@ -19654,7 +19914,7 @@ var init_associationFiles = __esm(() => {
19654
19914
  });
19655
19915
 
19656
19916
  // src/mobile/androidWebView.ts
19657
- import { mkdir as mkdir12, writeFile as writeFile14 } from "fs/promises";
19917
+ import { mkdir as mkdir12, writeFile as writeFile15 } from "fs/promises";
19658
19918
  import { dirname as dirname29, resolve as resolve38 } from "path";
19659
19919
 
19660
19920
  class CdpConnection {
@@ -19930,7 +20190,7 @@ var CDP_COMMAND_TIMEOUT_MS = 1e4, WEBVIEW_ATTACH_TIMEOUT_MS = 30000, WEBVIEW_POL
19930
20190
  }
19931
20191
  const absolutePath = resolve38(path);
19932
20192
  await mkdir12(dirname29(absolutePath), { recursive: true });
19933
- await writeFile14(absolutePath, Buffer.from(data, "base64"));
20193
+ await writeFile15(absolutePath, Buffer.from(data, "base64"));
19934
20194
  return absolutePath;
19935
20195
  },
19936
20196
  tap: async (coordinateX, coordinateY) => {
@@ -20061,9 +20321,9 @@ var DEFAULT_ROUTE_TIMEOUT_MS = 30000, DEFAULT_HMR_TIMEOUT_MS = 30000, routeExpre
20061
20321
  };
20062
20322
 
20063
20323
  // src/mobile/mobileBundleInspection.ts
20064
- import { createHash as createHash13 } from "crypto";
20065
- import { access as access10, readFile as readFile17, stat as stat2 } from "fs/promises";
20066
- import { join as join51, relative as relative25, resolve as resolve39 } from "path";
20324
+ import { createHash as createHash14 } from "crypto";
20325
+ import { access as access10, readFile as readFile18, stat as stat2 } from "fs/promises";
20326
+ import { join as join52, relative as relative25, resolve as resolve39 } from "path";
20067
20327
  var MOBILE_FRAMEWORKS, SHA256_PATTERN, isObject2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), portablePath = (projectRoot, path) => {
20068
20328
  const value = relative25(resolve39(projectRoot), resolve39(path)).replaceAll("\\", "/");
20069
20329
  return value || ".";
@@ -20075,7 +20335,7 @@ var MOBILE_FRAMEWORKS, SHA256_PATTERN, isObject2 = (value) => typeof value === "
20075
20335
  return false;
20076
20336
  }
20077
20337
  }, readObject = async (path) => {
20078
- const value = JSON.parse(await readFile17(path, "utf8"));
20338
+ const value = JSON.parse(await readFile18(path, "utf8"));
20079
20339
  if (!isObject2(value))
20080
20340
  throw new TypeError("JSON root must be an object.");
20081
20341
  return value;
@@ -20100,13 +20360,13 @@ var MOBILE_FRAMEWORKS, SHA256_PATTERN, isObject2 = (value) => typeof value === "
20100
20360
  if (expectedHash !== undefined) {
20101
20361
  if (!SHA256_PATTERN.test(expectedHash))
20102
20362
  throw new TypeError(`${field} has an invalid SHA-256 digest.`);
20103
- const actual = createHash13("sha256").update(await readFile17(path)).digest("hex");
20363
+ const actual = createHash14("sha256").update(await readFile18(path)).digest("hex");
20104
20364
  if (actual !== expectedHash)
20105
20365
  throw new TypeError(`${field} failed its SHA-256 integrity check.`);
20106
20366
  }
20107
20367
  return portable;
20108
20368
  }, inspectAbsoluteMobileBundle = async (config, projectRoot) => {
20109
- const manifestPath = join51(config.bundleDirectory, "absolute-mobile-manifest.json");
20369
+ const manifestPath = join52(config.bundleDirectory, "absolute-mobile-manifest.json");
20110
20370
  const manifest = portablePath(projectRoot, manifestPath);
20111
20371
  if (!await pathExists5(manifestPath))
20112
20372
  return { manifest, status: "missing" };
@@ -20120,9 +20380,14 @@ var MOBILE_FRAMEWORKS, SHA256_PATTERN, isObject2 = (value) => typeof value === "
20120
20380
  throw new TypeError("productionOrigin does not match the effective mobile config.");
20121
20381
  const appBuild = requireString(value.appBuild, "appBuild");
20122
20382
  const runtime = requireString(value.runtime, "runtime");
20383
+ const nativeRuntime = requireString(value.nativeRuntime, "nativeRuntime");
20384
+ if (!SHA256_PATTERN.test(nativeRuntime))
20385
+ throw new TypeError("nativeRuntime must be a SHA-256 fingerprint.");
20123
20386
  if (runtime !== String(ABSOLUTE_MOBILE_PAGE_PROTOCOL_VERSION))
20124
20387
  throw new TypeError("runtime is not supported by this AbsoluteJS build.");
20125
20388
  const capabilities = requireStringArray(value.deviceCapabilities, "deviceCapabilities").sort();
20389
+ if (JSON.stringify(value.updates ?? null) !== JSON.stringify(config.updates ?? null))
20390
+ throw new TypeError("Embedded update trust configuration does not match mobile config.");
20126
20391
  if (!Array.isArray(value.pages) || !Array.isArray(value.routes))
20127
20392
  throw new TypeError("pages and routes must be arrays.");
20128
20393
  const pageIds = new Set;
@@ -20175,11 +20440,13 @@ var MOBILE_FRAMEWORKS, SHA256_PATTERN, isObject2 = (value) => typeof value === "
20175
20440
  entryResolved,
20176
20441
  frameworks: [...frameworks7].sort(),
20177
20442
  manifest,
20443
+ nativeRuntime,
20178
20444
  pageCount: value.pages.length,
20179
20445
  routeCount: value.routes.length,
20180
20446
  runtime,
20181
20447
  status: "valid",
20182
- sync: isObject2(value.sync)
20448
+ sync: isObject2(value.sync),
20449
+ updates: isObject2(value.updates)
20183
20450
  };
20184
20451
  } catch (error) {
20185
20452
  return {
@@ -20206,8 +20473,8 @@ var init_mobileBundleInspection = __esm(() => {
20206
20473
  });
20207
20474
 
20208
20475
  // src/mobile/releaseDoctor.ts
20209
- import { access as access11, readFile as readFile18, readdir as readdir5 } from "fs/promises";
20210
- import { dirname as dirname30, extname as extname8, join as join52, relative as relative26 } from "path";
20476
+ import { access as access11, readFile as readFile19, readdir as readdir5 } from "fs/promises";
20477
+ import { dirname as dirname30, extname as extname8, join as join53, relative as relative26 } from "path";
20211
20478
  var ABSOLUTE_MOBILE_COMPLIANCE_REPORT_FORMAT = 1, HMR_ASSET_PATTERN, RELEASE_ASSET_EXTENSIONS, EXACT_VERSION_PATTERN, NOT_FOUND3 = -1, LOCK_FILES, MANUAL_REVIEW, pathExists6 = async (path) => {
20212
20479
  try {
20213
20480
  await access11(path);
@@ -20220,13 +20487,13 @@ var ABSOLUTE_MOBILE_COMPLIANCE_REPORT_FORMAT = 1, HMR_ASSET_PATTERN, RELEASE_ASS
20220
20487
  return findHmrAsset(path);
20221
20488
  if (!isFile2 || !RELEASE_ASSET_EXTENSIONS.has(extname8(path)))
20222
20489
  return;
20223
- const source = await readFile18(path, "utf8");
20490
+ const source = await readFile19(path, "utf8");
20224
20491
  return HMR_ASSET_PATTERN.test(source) ? path : undefined;
20225
20492
  }, findHmrAsset = async (root) => {
20226
20493
  if (!await pathExists6(root))
20227
20494
  return;
20228
20495
  const entries = await readdir5(root, { withFileTypes: true });
20229
- const matches = await Promise.all(entries.map((entry) => inspectReleaseAsset(join52(root, entry.name), entry.isDirectory(), entry.isFile())));
20496
+ const matches = await Promise.all(entries.map((entry) => inspectReleaseAsset(join53(root, entry.name), entry.isDirectory(), entry.isFile())));
20230
20497
  return matches.find((match) => match !== undefined);
20231
20498
  }, pass = (id, detail, path) => ({ detail, id, path, status: "pass" }), fail5 = (id, detail, path, remediation) => ({
20232
20499
  detail,
@@ -20241,7 +20508,7 @@ var ABSOLUTE_MOBILE_COMPLIANCE_REPORT_FORMAT = 1, HMR_ASSET_PATTERN, RELEASE_ASS
20241
20508
  remediation,
20242
20509
  status: "warn"
20243
20510
  }), isRecord14 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), readJsonObject = async (path) => {
20244
- const value = JSON.parse(await readFile18(path, "utf8"));
20511
+ const value = JSON.parse(await readFile19(path, "utf8"));
20245
20512
  if (typeof value !== "object" || value === null || Array.isArray(value))
20246
20513
  throw new TypeError("JSON root must be an object.");
20247
20514
  return Object.fromEntries(Object.entries(value));
@@ -20257,7 +20524,7 @@ var ABSOLUTE_MOBILE_COMPLIANCE_REPORT_FORMAT = 1, HMR_ASSET_PATTERN, RELEASE_ASS
20257
20524
  }
20258
20525
  return declarations;
20259
20526
  }, versionCore = (version2) => version2.split("-")[0]?.split(".").slice(0, 2).join("."), capacitorVersionCheck = async (config, projectRoot) => {
20260
- const manifestPath = join52(projectRoot, "package.json");
20527
+ const manifestPath = join53(projectRoot, "package.json");
20261
20528
  try {
20262
20529
  const manifest = await readJsonObject(manifestPath);
20263
20530
  const declarations = packageDeclarations(manifest);
@@ -20270,7 +20537,7 @@ var ABSOLUTE_MOBILE_COMPLIANCE_REPORT_FORMAT = 1, HMR_ASSET_PATTERN, RELEASE_ASS
20270
20537
  const declared = declarations.get(name);
20271
20538
  if (!declared || !EXACT_VERSION_PATTERN.test(declared))
20272
20539
  throw new TypeError(`${name} must be a direct exact dependency.`);
20273
- const installed = await readJsonObject(join52(projectRoot, "node_modules", name, "package.json"));
20540
+ const installed = await readJsonObject(join53(projectRoot, "node_modules", name, "package.json"));
20274
20541
  if (installed.version !== declared)
20275
20542
  throw new TypeError(`${name} does not match its installed version.`);
20276
20543
  return declared;
@@ -20291,7 +20558,7 @@ var ABSOLUTE_MOBILE_COMPLIANCE_REPORT_FORMAT = 1, HMR_ASSET_PATTERN, RELEASE_ASS
20291
20558
  const actual = installed.split(".").map(Number);
20292
20559
  return actual[0] === expected[0] && actual[1] === expected[1] && (actual[2] ?? NOT_FOUND3) >= (expected[2] ?? 0);
20293
20560
  }, expoVersionCheck = async (config) => {
20294
- const manifestPath = join52(config.nativeProjectDirectory, "package.json");
20561
+ const manifestPath = join53(config.nativeProjectDirectory, "package.json");
20295
20562
  try {
20296
20563
  const manifest = await readJsonObject(manifestPath);
20297
20564
  const declarations = packageDeclarations(manifest);
@@ -20301,13 +20568,13 @@ var ABSOLUTE_MOBILE_COMPLIANCE_REPORT_FORMAT = 1, HMR_ASSET_PATTERN, RELEASE_ASS
20301
20568
  throw new TypeError(`Generated Expo project is missing ${missingRequired}.`);
20302
20569
  const installedVersions = await Promise.all([...declarations].map(async ([name, declared]) => ({
20303
20570
  declared,
20304
- installed: await readJsonObject(join52(config.nativeProjectDirectory, "node_modules", name, "package.json")),
20571
+ installed: await readJsonObject(join53(config.nativeProjectDirectory, "node_modules", name, "package.json")),
20305
20572
  name
20306
20573
  })));
20307
20574
  const mismatch = installedVersions.find(({ declared, installed }) => typeof installed.version !== "string" || !satisfiesGeneratedVersion(declared, installed.version));
20308
20575
  if (mismatch)
20309
20576
  throw new TypeError(`Generated Expo dependency ${mismatch.name}@${mismatch.declared} does not match its installed version.`);
20310
- if (!await pathExists6(join52(config.nativeProjectDirectory, "bun.lock")))
20577
+ if (!await pathExists6(join53(config.nativeProjectDirectory, "bun.lock")))
20311
20578
  throw new TypeError("Generated Expo dependency lockfile is missing.");
20312
20579
  return pass("mobile.expo-versions", `Generated Expo SDK dependencies are pinned, installed, and locked (${declarations.get("expo")}).`, manifestPath);
20313
20580
  } catch (error) {
@@ -20315,10 +20582,10 @@ var ABSOLUTE_MOBILE_COMPLIANCE_REPORT_FORMAT = 1, HMR_ASSET_PATTERN, RELEASE_ASS
20315
20582
  }
20316
20583
  }, dependencyLockCheck = async (projectRoot) => {
20317
20584
  const present = (await Promise.all(LOCK_FILES.map(async (name) => ({
20318
- exists: await pathExists6(join52(projectRoot, name)),
20585
+ exists: await pathExists6(join53(projectRoot, name)),
20319
20586
  name
20320
20587
  })))).find(({ exists: exists4 }) => exists4);
20321
- return present ? pass("mobile.dependency-lock", `Dependency graph is locked by ${present.name}.`, join52(projectRoot, present.name)) : fail5("mobile.dependency-lock", "No supported dependency lockfile is present.", projectRoot, "Install dependencies with the project package manager and commit its lockfile before release.");
20588
+ return present ? pass("mobile.dependency-lock", `Dependency graph is locked by ${present.name}.`, join53(projectRoot, present.name)) : fail5("mobile.dependency-lock", "No supported dependency lockfile is present.", projectRoot, "Install dependencies with the project package manager and commit its lockfile before release.");
20322
20589
  }, productionOriginCheck = (config, projectRoot) => {
20323
20590
  const origin = new URL(config.productionOrigin);
20324
20591
  return origin.protocol === "https:" ? pass("mobile.production-origin", "Production transport uses an HTTPS origin.") : fail5("mobile.production-origin", "A loopback development origin cannot be used for a signed release.", projectRoot, "Configure mobile.server.productionOrigin with the deployed HTTPS origin.");
@@ -20353,7 +20620,7 @@ var ABSOLUTE_MOBILE_COMPLIANCE_REPORT_FORMAT = 1, HMR_ASSET_PATTERN, RELEASE_ASS
20353
20620
  if (!await pathExists6(nativeConfigPath)) {
20354
20621
  return fail5("android.capacitor-config", "The generated Android Capacitor config is missing.", nativeConfigPath, "Run `absolute mobile sync android` before release validation.");
20355
20622
  }
20356
- const unsafe = isUnsafeCapacitorConfig(await readFile18(nativeConfigPath, "utf8"));
20623
+ const unsafe = isUnsafeCapacitorConfig(await readFile19(nativeConfigPath, "utf8"));
20357
20624
  return unsafe ? fail5("android.capacitor-config", "Android Capacitor config contains a development server URL, cleartext transport, navigation allowlist, or invalid JSON.", nativeConfigPath, "Run `absolute mobile sync android`; do not ship development transport overrides.") : pass("android.capacitor-config", "Android Capacitor config contains no development transport overrides.", nativeConfigPath);
20358
20625
  }, capacitorIdentityCheck = async (config, platform6, nativeConfigPath) => {
20359
20626
  try {
@@ -20368,12 +20635,12 @@ var ABSOLUTE_MOBILE_COMPLIANCE_REPORT_FORMAT = 1, HMR_ASSET_PATTERN, RELEASE_ASS
20368
20635
  if (!await pathExists6(manifestPath)) {
20369
20636
  return fail5("android.cleartext", "The Android manifest is missing.", manifestPath, "Run `absolute mobile sync android` before release validation.");
20370
20637
  }
20371
- const source = await readFile18(manifestPath, "utf8");
20638
+ const source = await readFile19(manifestPath, "utf8");
20372
20639
  const cleartext = /android:usesCleartextTraffic=["']true["']/u.test(source);
20373
20640
  const networkConfigName = source.match(/android:networkSecurityConfig=["']@xml\/([a-z0-9_]+)["']/u)?.[1];
20374
- const networkConfigPath = networkConfigName ? join52(dirname30(manifestPath), "res", "xml", `${networkConfigName}.xml`) : undefined;
20641
+ const networkConfigPath = networkConfigName ? join53(dirname30(manifestPath), "res", "xml", `${networkConfigName}.xml`) : undefined;
20375
20642
  const developmentTrustReference = /android:networkSecurityConfig=["']@xml\/absolutejs_dev_network_security["']/u.test(source);
20376
- const developmentTrustContents = networkConfigPath ? await readFile18(networkConfigPath, "utf8").then((value) => value.includes("@raw/absolutejs_dev_ca")).catch(() => false) : false;
20643
+ const developmentTrustContents = networkConfigPath ? await readFile19(networkConfigPath, "utf8").then((value) => value.includes("@raw/absolutejs_dev_ca")).catch(() => false) : false;
20377
20644
  const developmentTrust = developmentTrustReference || developmentTrustContents;
20378
20645
  return cleartext || developmentTrust ? fail5("android.cleartext", developmentTrust ? "Android still references the AbsoluteJS development certificate authority." : "Android explicitly permits cleartext traffic.", manifestPath, "Run `absolute mobile sync android`; do not ship development transport or trust overrides.") : pass("android.cleartext", "Android does not explicitly permit cleartext traffic.", manifestPath);
20379
20646
  }, hmrAssetsReleaseCheck = async (publicRoot) => {
@@ -20382,12 +20649,12 @@ var ABSOLUTE_MOBILE_COMPLIANCE_REPORT_FORMAT = 1, HMR_ASSET_PATTERN, RELEASE_ASS
20382
20649
  }, embeddedBundleReleaseCheck = async (config, projectRoot, platform6, publicRoot) => {
20383
20650
  const inspection = await inspectAbsoluteMobileBundle({ ...config, bundleDirectory: publicRoot }, projectRoot);
20384
20651
  if (inspection.status === "valid")
20385
- return pass(`${platform6}.bundle-integrity`, `Packaged mobile manifest, runtime, routes, and ${inspection.pageCount ?? 0} page asset(s) passed structural and SHA-256 validation.`, join52(publicRoot, "absolute-mobile-manifest.json"));
20386
- return fail5(`${platform6}.bundle-integrity`, inspection.status === "missing" ? "The packaged mobile manifest is missing." : `The packaged mobile bundle is invalid: ${inspection.issue ?? "unknown validation error"}`, join52(publicRoot, "absolute-mobile-manifest.json"), "Rebuild the production mobile bundle and run Capacitor sync for this platform.");
20652
+ return pass(`${platform6}.bundle-integrity`, `Packaged mobile manifest, runtime, routes, and ${inspection.pageCount ?? 0} page asset(s) passed structural and SHA-256 validation.`, join53(publicRoot, "absolute-mobile-manifest.json"));
20653
+ return fail5(`${platform6}.bundle-integrity`, inspection.status === "missing" ? "The packaged mobile manifest is missing." : `The packaged mobile bundle is invalid: ${inspection.issue ?? "unknown validation error"}`, join53(publicRoot, "absolute-mobile-manifest.json"), "Rebuild the production mobile bundle and run Capacitor sync for this platform.");
20387
20654
  }, contentSecurityPolicyCheck = async (config, platform6, publicRoot) => {
20388
- const path = join52(publicRoot, "index.html");
20655
+ const path = join53(publicRoot, "index.html");
20389
20656
  try {
20390
- const source = await readFile18(path, "utf8");
20657
+ const source = await readFile19(path, "utf8");
20391
20658
  const requirements = [
20392
20659
  "Content-Security-Policy",
20393
20660
  "default-src 'self'",
@@ -20405,7 +20672,7 @@ var ABSOLUTE_MOBILE_COMPLIANCE_REPORT_FORMAT = 1, HMR_ASSET_PATTERN, RELEASE_ASS
20405
20672
  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.");
20406
20673
  }
20407
20674
  }, expoApplicationConfigCheck = async (config) => {
20408
- const path = join52(config.nativeProjectDirectory, "app.json");
20675
+ const path = join53(config.nativeProjectDirectory, "app.json");
20409
20676
  try {
20410
20677
  const root = await readJsonObject(path);
20411
20678
  if (!isRecord14(root.expo))
@@ -20425,12 +20692,12 @@ var ABSOLUTE_MOBILE_COMPLIANCE_REPORT_FORMAT = 1, HMR_ASSET_PATTERN, RELEASE_ASS
20425
20692
  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.");
20426
20693
  }
20427
20694
  }, expoEmbeddedAssetsCheck = async (config) => {
20428
- const generated = join52(config.nativeProjectDirectory, "src", "generated", "webAssets.ts");
20429
- const assetsRoot = join52(config.nativeProjectDirectory, "assets", "absolute");
20695
+ const generated = join53(config.nativeProjectDirectory, "src", "generated", "webAssets.ts");
20696
+ const assetsRoot = join53(config.nativeProjectDirectory, "assets", "absolute");
20430
20697
  try {
20431
20698
  const [source, manifest] = await Promise.all([
20432
- readFile18(generated, "utf8"),
20433
- readJsonObject(join52(config.bundleDirectory, "absolute-mobile-manifest.json"))
20699
+ readFile19(generated, "utf8"),
20700
+ readJsonObject(join53(config.bundleDirectory, "absolute-mobile-manifest.json"))
20434
20701
  ]);
20435
20702
  if (source.includes("embedded AbsoluteJS bundle is unavailable") || typeof manifest.appBuild !== "string" || !source.includes(JSON.stringify(manifest.appBuild)) || !source.includes(JSON.stringify(config.productionOrigin))) {
20436
20703
  throw new TypeError("Generated Expo assets do not contain the prepared production release identity.");
@@ -20450,8 +20717,8 @@ var ABSOLUTE_MOBILE_COMPLIANCE_REPORT_FORMAT = 1, HMR_ASSET_PATTERN, RELEASE_ASS
20450
20717
  if (!embedded)
20451
20718
  return false;
20452
20719
  const [left, right] = await Promise.all([
20453
- readFile18(join52(config.bundleDirectory, path)),
20454
- readFile18(join52(assetsRoot, embedded))
20720
+ readFile19(join53(config.bundleDirectory, path)),
20721
+ readFile19(join53(assetsRoot, embedded))
20455
20722
  ]);
20456
20723
  return left.equals(right);
20457
20724
  }));
@@ -20465,18 +20732,18 @@ var ABSOLUTE_MOBILE_COMPLIANCE_REPORT_FORMAT = 1, HMR_ASSET_PATTERN, RELEASE_ASS
20465
20732
  if (!await pathExists6(root))
20466
20733
  return [];
20467
20734
  const files = await Array.fromAsync(new Bun.Glob("**/*").scan({ cwd: root, onlyFiles: true }));
20468
- return files.filter((file) => extensions.has(extname8(file))).map((file) => join52(root, file));
20735
+ return files.filter((file) => extensions.has(extname8(file))).map((file) => join53(root, file));
20469
20736
  }, containsPattern = async (paths, pattern) => {
20470
- const sources = await Promise.all(paths.map((path) => readFile18(path, "utf8")));
20737
+ const sources = await Promise.all(paths.map((path) => readFile19(path, "utf8")));
20471
20738
  const index = sources.findIndex((source) => pattern.test(source));
20472
20739
  return index === NOT_FOUND3 ? undefined : paths[index];
20473
20740
  }, androidNativeSecurityCheck = async (androidRoot) => {
20474
- const manifestPath = join52(androidRoot, "app/src/main/AndroidManifest.xml");
20741
+ const manifestPath = join53(androidRoot, "app/src/main/AndroidManifest.xml");
20475
20742
  try {
20476
- const manifest = await readFile18(manifestPath, "utf8");
20743
+ const manifest = await readFile19(manifestPath, "utf8");
20477
20744
  if (/android:debuggable=["']true["']/u.test(manifest))
20478
20745
  throw new TypeError("Android release manifest explicitly enables application debugging.");
20479
- const sources = await sourceFiles(join52(androidRoot, "app/src/main"), new Set([".java", ".kt"]));
20746
+ const sources = await sourceFiles(join53(androidRoot, "app/src/main"), new Set([".java", ".kt"]));
20480
20747
  const debugSource = await containsPattern(sources, /setWebContentsDebuggingEnabled\s*\(\s*true\s*\)/u);
20481
20748
  if (debugSource)
20482
20749
  return fail5("android.native-debugging", "Android application source unconditionally enables WebView debugging.", debugSource, "Remove the unconditional WebView debugging call; use the platform debug-build behavior during development.");
@@ -20485,14 +20752,14 @@ var ABSOLUTE_MOBILE_COMPLIANCE_REPORT_FORMAT = 1, HMR_ASSET_PATTERN, RELEASE_ASS
20485
20752
  return fail5("android.native-debugging", error instanceof Error ? error.message : "Android native debugging configuration could not be validated.", manifestPath, "Remove explicit release debugging settings and rerun mobile sync.");
20486
20753
  }
20487
20754
  }, androidExportedComponentsCheck = async (manifestPath) => {
20488
- const source = await readFile18(manifestPath, "utf8").catch(() => "");
20755
+ const source = await readFile19(manifestPath, "utf8").catch(() => "");
20489
20756
  const exported = [
20490
20757
  ...source.matchAll(/<(?:activity|activity-alias|provider|receiver|service)\b[^>]*>/giu)
20491
20758
  ].map(([tag]) => tag).filter((tag) => /android:exported=["']true["']/iu.test(tag)).map((tag) => tag.match(/android:name=["']([^"']+)["']/iu)?.[1]).filter((name) => Boolean(name) && name !== ".MainActivity");
20492
20759
  return exported.length === 0 ? pass("android.exported-components", "No non-launcher Android component is explicitly exported.", manifestPath) : warn("android.exported-components", `${exported.length} non-launcher Android component(s) are exported and require manual authorization review.`, manifestPath, "Confirm each exported component is intentional, permission-protected where appropriate, and documented in the mobile threat model review.");
20493
20760
  }, androidDeepLinkProjectionCheck = async (config, manifestPath) => {
20494
20761
  try {
20495
- const source = await readFile18(manifestPath, "utf8");
20762
+ const source = await readFile19(manifestPath, "utf8");
20496
20763
  const hasWebHost = (host2) => [...source.matchAll(/<data\b[^>]*>/giu)].some(([tag]) => tag.includes('android:scheme="https"') && tag.includes(`android:host="${host2}"`));
20497
20764
  if (!source.includes('android:autoVerify="true"') || !source.includes("android.intent.category.BROWSABLE") || config.deepLinkHosts.some((host2) => !hasWebHost(host2)) || config.deepLinkScheme && !source.includes(`android:scheme="${config.deepLinkScheme}"`))
20498
20765
  throw new TypeError("Android App Link or custom-scheme projection does not match mobile config.");
@@ -20512,14 +20779,14 @@ var ABSOLUTE_MOBILE_COMPLIANCE_REPORT_FORMAT = 1, HMR_ASSET_PATTERN, RELEASE_ASS
20512
20779
  return fail5("ios.native-debugging", "iOS application source unconditionally enables WebView inspection.", debugSource, "Remove unconditional WebView inspection from release source.");
20513
20780
  return pass("ios.native-debugging", "iOS source does not enable release debugger attachment or WebView inspection.", entitlementPaths[0] ?? iosRoot);
20514
20781
  }, iosDeepLinkProjectionCheck = async (config, iosRoot) => {
20515
- const infoPath = join52(iosRoot, "App/App/Info.plist");
20516
- const entitlementsPath = join52(iosRoot, "App/AbsoluteJS.entitlements");
20517
- const projectPath = join52(iosRoot, "App/App.xcodeproj/project.pbxproj");
20782
+ const infoPath = join53(iosRoot, "App/App/Info.plist");
20783
+ const entitlementsPath = join53(iosRoot, "App/AbsoluteJS.entitlements");
20784
+ const projectPath = join53(iosRoot, "App/App.xcodeproj/project.pbxproj");
20518
20785
  try {
20519
20786
  const [info2, entitlements, project] = await Promise.all([
20520
- readFile18(infoPath, "utf8"),
20521
- readFile18(entitlementsPath, "utf8"),
20522
- readFile18(projectPath, "utf8")
20787
+ readFile19(infoPath, "utf8"),
20788
+ readFile19(entitlementsPath, "utf8"),
20789
+ readFile19(projectPath, "utf8")
20523
20790
  ]);
20524
20791
  if (config.deepLinkScheme && (!info2.includes("<key>CFBundleURLTypes</key>") || !info2.includes(`<string>${config.deepLinkScheme}</string>`)))
20525
20792
  throw new TypeError("iOS custom URL scheme does not match mobile config.");
@@ -20534,7 +20801,7 @@ var ABSOLUTE_MOBILE_COMPLIANCE_REPORT_FORMAT = 1, HMR_ASSET_PATTERN, RELEASE_ASS
20534
20801
  }, syncSchemaReleaseCheck = (projectRoot) => {
20535
20802
  if (!projectUsesAbsoluteSync(projectRoot))
20536
20803
  return;
20537
- const manifestPath = join52(projectRoot, "package.json");
20804
+ const manifestPath = join53(projectRoot, "package.json");
20538
20805
  try {
20539
20806
  const schema = discoverAbsoluteSyncSchema(projectRoot);
20540
20807
  const versions = schema.components.map((component2) => `${component2.id}@${component2.version}`).join(", ");
@@ -20556,8 +20823,8 @@ var ABSOLUTE_MOBILE_COMPLIANCE_REPORT_FORMAT = 1, HMR_ASSET_PATTERN, RELEASE_ASS
20556
20823
  }, IOS_USAGE_KEYS, androidDevicePermissionCheck = async (config, permissions) => {
20557
20824
  if (!config.platforms.includes("android") || permissions.length === 0)
20558
20825
  return;
20559
- const path = join52(config.nativeProjectDirectory, "android/app/src/main/AndroidManifest.xml");
20560
- const source = await readFile18(path, "utf8");
20826
+ const path = join53(config.nativeProjectDirectory, "android/app/src/main/AndroidManifest.xml");
20827
+ const source = await readFile19(path, "utf8");
20561
20828
  const missing = permissions.filter((permission) => !source.includes(`android:name="${permission}"`) && !source.includes(`android:name='${permission}'`));
20562
20829
  if (missing.length === 0)
20563
20830
  return;
@@ -20565,9 +20832,9 @@ var ABSOLUTE_MOBILE_COMPLIANCE_REPORT_FORMAT = 1, HMR_ASSET_PATTERN, RELEASE_ASS
20565
20832
  }, iosDevicePermissionCheck = async (config, purposes) => {
20566
20833
  if (!config.platforms.includes("ios") || purposes.length === 0)
20567
20834
  return;
20568
- const iosRoot = join52(config.nativeProjectDirectory, "ios");
20569
- const path = config.engine === "expo" ? await uniqueExpoIosFile(iosRoot, "**/Info.plist", "Info.plist") : join52(iosRoot, "App/App/Info.plist");
20570
- const source = await readFile18(path, "utf8");
20835
+ const iosRoot = join53(config.nativeProjectDirectory, "ios");
20836
+ const path = config.engine === "expo" ? await uniqueExpoIosFile(iosRoot, "**/Info.plist", "Info.plist") : join53(iosRoot, "App/App/Info.plist");
20837
+ const source = await readFile19(path, "utf8");
20571
20838
  const missing = purposes.filter((purpose) => !source.includes(`<key>${IOS_USAGE_KEYS[purpose]}</key>`));
20572
20839
  if (missing.length === 0)
20573
20840
  return;
@@ -20576,7 +20843,7 @@ var ABSOLUTE_MOBILE_COMPLIANCE_REPORT_FORMAT = 1, HMR_ASSET_PATTERN, RELEASE_ASS
20576
20843
  if (requirements.iosPrivacyAccessedApis.length === 0)
20577
20844
  return;
20578
20845
  const privacyPath = await uniqueExpoIosFile(iosRoot, "**/PrivacyInfo.xcprivacy", "privacy manifest");
20579
- const privacy = await readFile18(privacyPath, "utf8");
20846
+ const privacy = await readFile19(privacyPath, "utf8");
20580
20847
  const missing = requirements.iosPrivacyAccessedApis.some(({ api, reasons }) => !privacy.includes(`<string>${api}</string>`) || reasons.some((reason) => !privacy.includes(`<string>${reason}</string>`)));
20581
20848
  if (!missing && project.includes(privacyPath.split(/[\\/]/u).at(-1) ?? ""))
20582
20849
  return;
@@ -20585,14 +20852,14 @@ var ABSOLUTE_MOBILE_COMPLIANCE_REPORT_FORMAT = 1, HMR_ASSET_PATTERN, RELEASE_ASS
20585
20852
  if (!requirements.iosPushNotifications)
20586
20853
  return;
20587
20854
  const entitlementsPath = await uniqueExpoIosFile(iosRoot, "**/*.entitlements", "entitlements");
20588
- const entitlements = await readFile18(entitlementsPath, "utf8");
20855
+ const entitlements = await readFile19(entitlementsPath, "utf8");
20589
20856
  if (entitlements.includes("<key>aps-environment</key>"))
20590
20857
  return;
20591
20858
  return fail5("mobile.device-capabilities", "Expo iOS push entitlement does not match detected capabilities.", entitlementsPath, "Run `absolute mobile build ios` to regenerate native push integration.");
20592
20859
  }, expoIosCapabilityProjectionCheck = async (config, requirements) => {
20593
- const iosRoot = join52(config.nativeProjectDirectory, "ios");
20860
+ const iosRoot = join53(config.nativeProjectDirectory, "ios");
20594
20861
  const projectPath = await uniqueExpoIosFile(iosRoot, "**/*.xcodeproj/project.pbxproj", "Xcode project");
20595
- const project = await readFile18(projectPath, "utf8");
20862
+ const project = await readFile19(projectPath, "utf8");
20596
20863
  const privacy = await expoIosPrivacyCapabilityCheck(iosRoot, project, requirements);
20597
20864
  if (privacy)
20598
20865
  return privacy;
@@ -20602,41 +20869,41 @@ var ABSOLUTE_MOBILE_COMPLIANCE_REPORT_FORMAT = 1, HMR_ASSET_PATTERN, RELEASE_ASS
20602
20869
  return;
20603
20870
  if (config.engine === "expo")
20604
20871
  return expoIosCapabilityProjectionCheck(config, requirements);
20605
- const appRoot = join52(config.nativeProjectDirectory, "ios/App/App");
20606
- const infoPath = join52(appRoot, "Info.plist");
20607
- const info2 = await readFile18(infoPath, "utf8").catch(() => "");
20872
+ const appRoot = join53(config.nativeProjectDirectory, "ios/App/App");
20873
+ const infoPath = join53(appRoot, "Info.plist");
20874
+ const info2 = await readFile19(infoPath, "utf8").catch(() => "");
20608
20875
  if (requirements.iosSystemBars && !/<key>UIViewControllerBasedStatusBarAppearance<\/key>\s*<true\s*\/>/u.test(info2))
20609
20876
  return fail5("mobile.device-capabilities", "iOS system-bar capability is missing its required view-controller setting.", infoPath, "Run `absolute mobile sync ios` to regenerate native capability settings.");
20610
20877
  if (requirements.iosPrivacyAccessedApis.length > 0) {
20611
- const privacyPath = join52(appRoot, "PrivacyInfo.xcprivacy");
20612
- const projectPath = join52(config.nativeProjectDirectory, "ios/App/App.xcodeproj/project.pbxproj");
20878
+ const privacyPath = join53(appRoot, "PrivacyInfo.xcprivacy");
20879
+ const projectPath = join53(config.nativeProjectDirectory, "ios/App/App.xcodeproj/project.pbxproj");
20613
20880
  const [privacy, project] = await Promise.all([
20614
- readFile18(privacyPath, "utf8").catch(() => ""),
20615
- readFile18(projectPath, "utf8").catch(() => "")
20881
+ readFile19(privacyPath, "utf8").catch(() => ""),
20882
+ readFile19(projectPath, "utf8").catch(() => "")
20616
20883
  ]);
20617
20884
  const missing = requirements.iosPrivacyAccessedApis.some(({ api, reasons }) => !privacy.includes(`<string>${api}</string>`) || reasons.some((reason) => !privacy.includes(`<string>${reason}</string>`)));
20618
20885
  if (missing || !project.includes("PrivacyInfo.xcprivacy in Resources"))
20619
20886
  return fail5("mobile.device-capabilities", "iOS privacy manifest or target membership does not match detected native capabilities.", privacyPath, "Run `absolute mobile sync ios` to regenerate and target PrivacyInfo.xcprivacy.");
20620
20887
  }
20621
20888
  if (requirements.iosPushNotifications) {
20622
- const entitlementsPath = join52(config.nativeProjectDirectory, "ios/App/AbsoluteJS.entitlements");
20623
- const delegatePath = join52(appRoot, "AppDelegate.swift");
20889
+ const entitlementsPath = join53(config.nativeProjectDirectory, "ios/App/AbsoluteJS.entitlements");
20890
+ const delegatePath = join53(appRoot, "AppDelegate.swift");
20624
20891
  const [entitlements, delegate] = await Promise.all([
20625
- readFile18(entitlementsPath, "utf8").catch(() => ""),
20626
- readFile18(delegatePath, "utf8").catch(() => "")
20892
+ readFile19(entitlementsPath, "utf8").catch(() => ""),
20893
+ readFile19(delegatePath, "utf8").catch(() => "")
20627
20894
  ]);
20628
20895
  if (!entitlements.includes("<key>aps-environment</key>") || !delegate.includes("capacitorDidRegisterForRemoteNotifications") || !delegate.includes("capacitorDidFailToRegisterForRemoteNotifications"))
20629
20896
  return fail5("mobile.device-capabilities", "iOS push entitlement or AppDelegate forwarding does not match detected capabilities.", entitlementsPath, "Run `absolute mobile sync ios` to regenerate native push integration.");
20630
20897
  }
20631
20898
  return;
20632
20899
  }, deviceCapabilityReleaseCheck = async (config, projectRoot) => {
20633
- const manifestPath = join52(projectRoot, "package.json");
20900
+ const manifestPath = join53(projectRoot, "package.json");
20634
20901
  try {
20635
20902
  const plan = resolveAbsoluteDeviceCapabilityPlan(projectRoot, config.engine);
20636
20903
  const assertPackages = async () => {
20637
20904
  if (config.engine === "capacitor")
20638
20905
  return assertAbsoluteDeviceCapabilityPackages(projectRoot, plan);
20639
- const generated = await readJsonObject(join52(config.nativeProjectDirectory, "package.json"));
20906
+ const generated = await readJsonObject(join53(config.nativeProjectDirectory, "package.json"));
20640
20907
  const declarations = packageDeclarations(generated);
20641
20908
  const missing = plan.requiredPackages.filter((spec) => {
20642
20909
  const separator = spec.lastIndexOf("@");
@@ -20662,11 +20929,11 @@ var ABSOLUTE_MOBILE_COMPLIANCE_REPORT_FORMAT = 1, HMR_ASSET_PATTERN, RELEASE_ASS
20662
20929
  return fail5("mobile.device-capabilities", error instanceof Error ? error.message : "Native device capability provisioning is invalid.", manifestPath, "Run `absolute mobile sync` and approve the exact capability plugins before releasing.");
20663
20930
  }
20664
20931
  }, inspectAndroidRelease = async (config, projectRoot) => {
20665
- const androidRoot = join52(config.nativeProjectDirectory, "android");
20666
- const nativeConfigPath = join52(androidRoot, "app", "src", "main", "assets", "capacitor.config.json");
20667
- const manifestPath = join52(androidRoot, "app", "src", "main", "AndroidManifest.xml");
20668
- const publicRoot = join52(androidRoot, "app", "src", "main", "assets", "public");
20669
- const journalPath = join52(projectRoot, ".absolutejs", "mobile", "dev-session", "journal.json");
20932
+ const androidRoot = join53(config.nativeProjectDirectory, "android");
20933
+ const nativeConfigPath = join53(androidRoot, "app", "src", "main", "assets", "capacitor.config.json");
20934
+ const manifestPath = join53(androidRoot, "app", "src", "main", "AndroidManifest.xml");
20935
+ const publicRoot = join53(androidRoot, "app", "src", "main", "assets", "public");
20936
+ const journalPath = join53(projectRoot, ".absolutejs", "mobile", "dev-session", "journal.json");
20670
20937
  const checks = await Promise.all([
20671
20938
  journalReleaseCheck(journalPath, "android"),
20672
20939
  capacitorConfigReleaseCheck(nativeConfigPath),
@@ -20684,9 +20951,9 @@ var ABSOLUTE_MOBILE_COMPLIANCE_REPORT_FORMAT = 1, HMR_ASSET_PATTERN, RELEASE_ASS
20684
20951
  path: check2.path ? relative26(projectRoot, check2.path).replaceAll("\\", "/") || "." : undefined
20685
20952
  }));
20686
20953
  }, inspectExpoAndroidRelease = async (config, projectRoot) => {
20687
- const androidRoot = join52(config.nativeProjectDirectory, "android");
20688
- const manifestPath = join52(androidRoot, "app", "src", "main", "AndroidManifest.xml");
20689
- const journalPath = join52(projectRoot, ".absolutejs", "mobile", "expo-dev-session", "journal.json");
20954
+ const androidRoot = join53(config.nativeProjectDirectory, "android");
20955
+ const manifestPath = join53(androidRoot, "app", "src", "main", "AndroidManifest.xml");
20956
+ const journalPath = join53(projectRoot, ".absolutejs", "mobile", "expo-dev-session", "journal.json");
20690
20957
  const checks = await Promise.all([
20691
20958
  journalReleaseCheck(journalPath, "android"),
20692
20959
  expoApplicationConfigCheck(config),
@@ -20710,7 +20977,7 @@ var ABSOLUTE_MOBILE_COMPLIANCE_REPORT_FORMAT = 1, HMR_ASSET_PATTERN, RELEASE_ASS
20710
20977
  const [path] = paths;
20711
20978
  if (!path)
20712
20979
  throw new TypeError(`Generated Expo iOS project is missing its ${label}.`);
20713
- return join52(iosRoot, path);
20980
+ return join53(iosRoot, path);
20714
20981
  }, expoIosNativeProjectionCheck = async (config, iosRoot) => {
20715
20982
  try {
20716
20983
  const [infoPath, entitlementsPath, projectPath] = await Promise.all([
@@ -20719,9 +20986,9 @@ var ABSOLUTE_MOBILE_COMPLIANCE_REPORT_FORMAT = 1, HMR_ASSET_PATTERN, RELEASE_ASS
20719
20986
  uniqueExpoIosFile(iosRoot, "**/*.xcodeproj/project.pbxproj", "Xcode project")
20720
20987
  ]);
20721
20988
  const [info2, entitlements, project] = await Promise.all([
20722
- readFile18(infoPath, "utf8"),
20723
- readFile18(entitlementsPath, "utf8"),
20724
- readFile18(projectPath, "utf8")
20989
+ readFile19(infoPath, "utf8"),
20990
+ readFile19(entitlementsPath, "utf8"),
20991
+ readFile19(projectPath, "utf8")
20725
20992
  ]);
20726
20993
  if (/<key>NSAllowsArbitraryLoads<\/key>\s*<true\s*\/>/u.test(info2))
20727
20994
  throw new TypeError("Expo iOS App Transport Security permits arbitrary network loads.");
@@ -20736,8 +21003,8 @@ var ABSOLUTE_MOBILE_COMPLIANCE_REPORT_FORMAT = 1, HMR_ASSET_PATTERN, RELEASE_ASS
20736
21003
  return fail5("ios.expo-native-projection", error instanceof Error ? error.message : "Expo iOS native projection could not be validated.", iosRoot, "Run `absolute mobile build ios` to regenerate the production Expo CNG project.");
20737
21004
  }
20738
21005
  }, inspectExpoIosRelease = async (config, projectRoot) => {
20739
- const iosRoot = join52(config.nativeProjectDirectory, "ios");
20740
- const journalPath = join52(projectRoot, ".absolutejs", "mobile", "expo-dev-session", "journal.json");
21006
+ const iosRoot = join53(config.nativeProjectDirectory, "ios");
21007
+ const journalPath = join53(projectRoot, ".absolutejs", "mobile", "expo-dev-session", "journal.json");
20741
21008
  const checks = [
20742
21009
  await journalReleaseCheck(journalPath, "ios")
20743
21010
  ];
@@ -20756,11 +21023,11 @@ var ABSOLUTE_MOBILE_COMPLIANCE_REPORT_FORMAT = 1, HMR_ASSET_PATTERN, RELEASE_ASS
20756
21023
  path: check2.path ? relative26(projectRoot, check2.path).replaceAll("\\", "/") || "." : undefined
20757
21024
  }));
20758
21025
  }, inspectIosRelease = async (config, projectRoot) => {
20759
- const iosAppRoot = join52(config.nativeProjectDirectory, "ios", "App", "App");
20760
- const nativeConfigPath = join52(iosAppRoot, "capacitor.config.json");
20761
- const infoPath = join52(iosAppRoot, "Info.plist");
20762
- const publicRoot = join52(iosAppRoot, "public");
20763
- const journalPath = join52(projectRoot, ".absolutejs", "mobile", "ios-dev-session", "journal.json");
21026
+ const iosAppRoot = join53(config.nativeProjectDirectory, "ios", "App", "App");
21027
+ const nativeConfigPath = join53(iosAppRoot, "capacitor.config.json");
21028
+ const infoPath = join53(iosAppRoot, "Info.plist");
21029
+ const publicRoot = join53(iosAppRoot, "public");
21030
+ const journalPath = join53(projectRoot, ".absolutejs", "mobile", "ios-dev-session", "journal.json");
20764
21031
  const checks = [
20765
21032
  await journalReleaseCheck(journalPath, "ios")
20766
21033
  ];
@@ -20771,7 +21038,7 @@ var ABSOLUTE_MOBILE_COMPLIANCE_REPORT_FORMAT = 1, HMR_ASSET_PATTERN, RELEASE_ASS
20771
21038
  }
20772
21039
  if (!await pathExists6(nativeConfigPath)) {
20773
21040
  checks.push(fail5("ios.capacitor-config", "The generated iOS Capacitor config is missing.", nativeConfigPath, "Run `absolute mobile sync ios` before release validation."));
20774
- } else if (isUnsafeCapacitorConfig(await readFile18(nativeConfigPath, "utf8"))) {
21041
+ } else if (isUnsafeCapacitorConfig(await readFile19(nativeConfigPath, "utf8"))) {
20775
21042
  checks.push(fail5("ios.capacitor-config", "iOS Capacitor config contains a development server URL, cleartext transport, navigation allowlist, or invalid JSON.", nativeConfigPath, "Run `absolute mobile sync ios`; do not ship development transport overrides."));
20776
21043
  } else {
20777
21044
  checks.push(pass("ios.capacitor-config", "iOS Capacitor config contains no development transport overrides.", nativeConfigPath));
@@ -20780,12 +21047,12 @@ var ABSOLUTE_MOBILE_COMPLIANCE_REPORT_FORMAT = 1, HMR_ASSET_PATTERN, RELEASE_ASS
20780
21047
  if (!await pathExists6(infoPath)) {
20781
21048
  checks.push(fail5("ios.transport-security", "The iOS Info.plist is missing.", infoPath, "Run `absolute mobile sync ios` before release validation."));
20782
21049
  } else {
20783
- const info2 = await readFile18(infoPath, "utf8");
21050
+ const info2 = await readFile19(infoPath, "utf8");
20784
21051
  checks.push(/<key>NSAllowsArbitraryLoads<\/key>\s*<true\s*\/>/u.test(info2) ? fail5("ios.transport-security", "iOS App Transport Security permits arbitrary network loads.", infoPath, "Remove NSAllowsArbitraryLoads from the release Info.plist.") : pass("ios.transport-security", "iOS App Transport Security does not permit arbitrary loads.", infoPath));
20785
21052
  }
20786
21053
  const hmrAsset = await findHmrAsset(publicRoot);
20787
21054
  checks.push(hmrAsset ? fail5("ios.hmr-assets", "A packaged iOS asset contains the development HMR client.", hmrAsset, "Rebuild the production mobile bundle and run Capacitor sync again.") : pass("ios.hmr-assets", "Packaged iOS assets contain no development HMR markers.", publicRoot));
20788
- checks.push(await embeddedBundleReleaseCheck(config, projectRoot, "ios", publicRoot), await contentSecurityPolicyCheck(config, "ios", publicRoot), await iosNativeSecurityCheck(join52(config.nativeProjectDirectory, "ios")), await iosDeepLinkProjectionCheck(config, join52(config.nativeProjectDirectory, "ios")));
21055
+ checks.push(await embeddedBundleReleaseCheck(config, projectRoot, "ios", publicRoot), await contentSecurityPolicyCheck(config, "ios", publicRoot), await iosNativeSecurityCheck(join53(config.nativeProjectDirectory, "ios")), await iosDeepLinkProjectionCheck(config, join53(config.nativeProjectDirectory, "ios")));
20789
21056
  return checks.map((check2) => ({
20790
21057
  ...check2,
20791
21058
  path: check2.path ? relative26(projectRoot, check2.path).replaceAll("\\", "/") || "." : undefined
@@ -20873,20 +21140,20 @@ var init_releaseDoctor = __esm(() => {
20873
21140
  });
20874
21141
 
20875
21142
  // src/mobile/androidRelease.ts
20876
- import { createHash as createHash14 } from "crypto";
21143
+ import { createHash as createHash15 } from "crypto";
20877
21144
  import {
20878
21145
  access as access12,
20879
21146
  copyFile as copyFile5,
20880
21147
  mkdir as mkdir13,
20881
21148
  mkdtemp as mkdtemp7,
20882
- readFile as readFile19,
21149
+ readFile as readFile20,
20883
21150
  realpath as realpath2,
20884
- rename as rename13,
21151
+ rename as rename14,
20885
21152
  rm as rm10,
20886
21153
  stat as stat3,
20887
- writeFile as writeFile15
21154
+ writeFile as writeFile16
20888
21155
  } from "fs/promises";
20889
- import { dirname as dirname31, isAbsolute as isAbsolute7, join as join53, relative as relative27, resolve as resolve40, sep as sep7 } from "path";
21156
+ import { dirname as dirname31, isAbsolute as isAbsolute7, join as join54, relative as relative27, resolve as resolve40, sep as sep7 } from "path";
20890
21157
  var ABSOLUTE_ANDROID_RELEASE_FORMAT = 1, isRecord15 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), requireManifest2 = (value) => {
20891
21158
  if (!isRecord15(value) || typeof value.appBuild !== "string" || typeof value.appId !== "string" || typeof value.runtime !== "string") {
20892
21159
  throw new TypeError("Invalid embedded AbsoluteJS mobile manifest.");
@@ -20950,17 +21217,17 @@ var ABSOLUTE_ANDROID_RELEASE_FORMAT = 1, isRecord15 = (value) => typeof value ==
20950
21217
  ]);
20951
21218
  if (result.exitCode !== 0)
20952
21219
  throw new TypeError("jarsigner could not sign the Android App Bundle with the configured CI identity.");
20953
- }, sha256File2 = async (path) => createHash14("sha256").update(await readFile19(path)).digest("hex"), fingerprintExpoAndroidProject = async (nativeDirectory) => {
21220
+ }, sha256File2 = async (path) => createHash15("sha256").update(await readFile20(path)).digest("hex"), fingerprintExpoAndroidProject = async (nativeDirectory) => {
20954
21221
  const root = await realpath2(nativeDirectory);
20955
21222
  const files = await Array.fromAsync(new Bun.Glob("**/*").scan({ cwd: root, onlyFiles: true }));
20956
21223
  const records = await Promise.all(files.filter((path) => {
20957
21224
  const parts = path.replaceAll("\\", "/").split("/");
20958
21225
  return !parts.includes(".gradle") && !parts.includes("build");
20959
21226
  }).sort().map(async (path) => {
20960
- const contents = await readFile19(join53(root, path));
20961
- return `${path.replaceAll("\\", "/")}\x00${createHash14("sha256").update(contents).digest("hex")}\x00`;
21227
+ const contents = await readFile20(join54(root, path));
21228
+ return `${path.replaceAll("\\", "/")}\x00${createHash15("sha256").update(contents).digest("hex")}\x00`;
20962
21229
  }));
20963
- return createHash14("sha256").update(records.join("")).digest("hex");
21230
+ return createHash15("sha256").update(records.join("")).digest("hex");
20964
21231
  }, safeOutputDirectory2 = (projectRoot, requested) => {
20965
21232
  const root = resolve40(projectRoot);
20966
21233
  const output = resolve40(root, requested ?? ".absolutejs/mobile/releases/android");
@@ -20970,11 +21237,11 @@ var ABSOLUTE_ANDROID_RELEASE_FORMAT = 1, isRecord15 = (value) => typeof value ==
20970
21237
  }
20971
21238
  return output;
20972
21239
  }, installRelease2 = async (artifactPath, metadata, outputRoot) => {
20973
- const releaseRoot = join53(outputRoot, metadata.releaseId);
21240
+ const releaseRoot = join54(outputRoot, metadata.releaseId);
20974
21241
  const artifactName = "app-release.aab";
20975
- const destination = join53(releaseRoot, artifactName);
21242
+ const destination = join54(releaseRoot, artifactName);
20976
21243
  if (await pathExists7(releaseRoot)) {
20977
- const existing = requireManifestIdentity(JSON.parse(await readFile19(join53(releaseRoot, "release.json"), "utf8")), metadata);
21244
+ const existing = requireManifestIdentity(JSON.parse(await readFile20(join54(releaseRoot, "release.json"), "utf8")), metadata);
20978
21245
  const [installedBytes, installedSha256] = await Promise.all([
20979
21246
  stat3(destination).then(({ size }) => size),
20980
21247
  sha256File2(destination)
@@ -20985,16 +21252,16 @@ var ABSOLUTE_ANDROID_RELEASE_FORMAT = 1, isRecord15 = (value) => typeof value ==
20985
21252
  return { artifactPath: destination, metadata: existing, releaseRoot };
20986
21253
  }
20987
21254
  await mkdir13(dirname31(releaseRoot), { recursive: true });
20988
- const staging = await mkdtemp7(join53(dirname31(releaseRoot), ".android-stage-"));
21255
+ const staging = await mkdtemp7(join54(dirname31(releaseRoot), ".android-stage-"));
20989
21256
  try {
20990
- await copyFile5(artifactPath, join53(staging, artifactName));
21257
+ await copyFile5(artifactPath, join54(staging, artifactName));
20991
21258
  const complete = {
20992
21259
  ...metadata,
20993
21260
  artifact: artifactName
20994
21261
  };
20995
- await writeFile15(join53(staging, "release.json"), `${JSON.stringify(complete, null, "\t")}
21262
+ await writeFile16(join54(staging, "release.json"), `${JSON.stringify(complete, null, "\t")}
20996
21263
  `, { flag: "wx" });
20997
- await rename13(staging, releaseRoot);
21264
+ await rename14(staging, releaseRoot);
20998
21265
  return { artifactPath: destination, metadata: complete, releaseRoot };
20999
21266
  } finally {
21000
21267
  await rm10(staging, { force: true, recursive: true }).catch(() => {
@@ -21023,8 +21290,8 @@ var ABSOLUTE_ANDROID_RELEASE_FORMAT = 1, isRecord15 = (value) => typeof value ==
21023
21290
  throw new TypeError("Expo Android production builds from WSL are not available yet. Run the generated CI workflow on Linux or build from native Windows while the WSL projection is completed.");
21024
21291
  }
21025
21292
  const androidRoot = options.androidRoot ?? process.env.ANDROID_HOME ?? process.env.ANDROID_SDK_ROOT ?? absoluteManagedAndroidSdkRoot(host2);
21026
- const nativeDirectory = join53(options.config.nativeProjectDirectory, "android");
21027
- const manifest = requireManifest2(JSON.parse(await readFile19(join53(options.config.bundleDirectory, "absolute-mobile-manifest.json"), "utf8")));
21293
+ const nativeDirectory = join54(options.config.nativeProjectDirectory, "android");
21294
+ const manifest = requireManifest2(JSON.parse(await readFile20(join54(options.config.bundleDirectory, "absolute-mobile-manifest.json"), "utf8")));
21028
21295
  if (manifest.appId !== options.config.appId) {
21029
21296
  throw new TypeError("Embedded mobile manifest appId does not match mobile.appId.");
21030
21297
  }
@@ -21033,7 +21300,7 @@ var ABSOLUTE_ANDROID_RELEASE_FORMAT = 1, isRecord15 = (value) => typeof value ==
21033
21300
  const nativeFingerprint = options.config.engine === "expo" ? await fingerprintExpoAndroidProject(nativeDirectory) : await fingerprintAbsoluteAndroidNativeProject({
21034
21301
  nativeDirectory
21035
21302
  });
21036
- const buildIdentity = createHash14("sha256").update(`${manifest.appBuild}\x00${nativeFingerprint}`).digest("hex");
21303
+ const buildIdentity = createHash15("sha256").update(`${manifest.appBuild}\x00${nativeFingerprint}`).digest("hex");
21037
21304
  versionCode = await options.prepareVersionCode(buildIdentity);
21038
21305
  }
21039
21306
  if (versionCode !== undefined && (!Number.isSafeInteger(versionCode) || versionCode < 1 || versionCode > 2100000000)) {
@@ -21138,7 +21405,7 @@ var absoluteIosDeviceAcceptanceCommands = (options) => {
21138
21405
  };
21139
21406
 
21140
21407
  // src/mobile/iosConformance.ts
21141
- import { readFile as readFile20, stat as stat4 } from "fs/promises";
21408
+ import { readFile as readFile21, stat as stat4 } from "fs/promises";
21142
21409
  var HMR_LINE, parseAbsoluteIosHmrLog = (line) => {
21143
21410
  const match = HMR_LINE.exec(line);
21144
21411
  if (!match)
@@ -21173,7 +21440,7 @@ var HMR_LINE, parseAbsoluteIosHmrLog = (line) => {
21173
21440
  if (Date.now() > deadline)
21174
21441
  throw new Error(`No iOS native HMR acknowledgement was observed within ${timeoutMs}ms.`);
21175
21442
  options.signal?.throwIfAborted();
21176
- const contents = await readFile20(options.logPath).catch(() => Buffer.alloc(0));
21443
+ const contents = await readFile21(options.logPath).catch(() => Buffer.alloc(0));
21177
21444
  if (contents.byteLength < offset) {
21178
21445
  offset = 0;
21179
21446
  buffered = "";
@@ -21197,8 +21464,8 @@ var init_iosConformance = __esm(() => {
21197
21464
  });
21198
21465
 
21199
21466
  // src/mobile/nativeTestReport.ts
21200
- import { mkdir as mkdir14, readFile as readFile21, writeFile as writeFile16 } from "fs/promises";
21201
- import { join as join54 } from "path";
21467
+ import { mkdir as mkdir14, readFile as readFile22, writeFile as writeFile17 } from "fs/promises";
21468
+ import { join as join55 } from "path";
21202
21469
  var secretPattern, bearerPattern, coordinatePattern, nativeCredentialPattern, sanitizeNativeReportText = (value) => value.replace(nativeCredentialPattern, "[REDACTED]").replace(bearerPattern, "Bearer [REDACTED]").replace(secretPattern, "$1$2[REDACTED]").replace(coordinatePattern, "$1$2[REDACTED]").replace(/(https?:\/\/[^\s?#]+)[?#][^\s]*/giu, "$1?[REDACTED]"), markdownCell = (value) => sanitizeNativeReportText(value).replaceAll("|", "\\|").replaceAll(`
21203
21470
  `, "<br>"), createAbsoluteNativeAutomatedChecks = (run) => {
21204
21471
  const target = `${run.targetKind} ${run.targetId}`;
@@ -21300,7 +21567,7 @@ var secretPattern, bearerPattern, coordinatePattern, nativeCredentialPattern, sa
21300
21567
  reportVersion: 1,
21301
21568
  run: options.run
21302
21569
  }), readPackageVersionForNativeReport = async (packageJsonPath) => {
21303
- const manifest = JSON.parse(await readFile21(packageJsonPath, "utf8"));
21570
+ const manifest = JSON.parse(await readFile22(packageJsonPath, "utf8"));
21304
21571
  if (typeof manifest !== "object" || manifest === null)
21305
21572
  return "unknown";
21306
21573
  const version2 = Reflect.get(manifest, "version");
@@ -21337,12 +21604,12 @@ ${table(report.manualChecks)}
21337
21604
  `;
21338
21605
  }, writeAbsoluteNativeTestReport = async (directory, report) => {
21339
21606
  await mkdir14(directory, { recursive: true });
21340
- const jsonPath = join54(directory, "report.json");
21341
- const markdownPath = join54(directory, "report.md");
21607
+ const jsonPath = join55(directory, "report.json");
21608
+ const markdownPath = join55(directory, "report.md");
21342
21609
  await Promise.all([
21343
- writeFile16(jsonPath, `${JSON.stringify(report, null, 2)}
21610
+ writeFile17(jsonPath, `${JSON.stringify(report, null, 2)}
21344
21611
  `),
21345
- writeFile16(markdownPath, renderAbsoluteNativeTestReport(report))
21612
+ writeFile17(markdownPath, renderAbsoluteNativeTestReport(report))
21346
21613
  ]);
21347
21614
  return { directory, jsonPath, markdownPath };
21348
21615
  };
@@ -21697,8 +21964,8 @@ var prepareAbsoluteIosRelease = async (publisher, options) => {
21697
21964
  var init_releasePublisher = () => {};
21698
21965
 
21699
21966
  // src/mobile/mobileInspect.ts
21700
- import { access as access14, readFile as readFile22 } from "fs/promises";
21701
- import { join as join55, relative as relative29, resolve as resolve42 } from "path";
21967
+ import { access as access14, readFile as readFile23 } from "fs/promises";
21968
+ import { join as join56, relative as relative29, resolve as resolve42 } from "path";
21702
21969
  var ABSOLUTE_MOBILE_INSPECTION_FORMAT = 1, MOBILE_PACKAGE_NAMES, isObject3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), portablePath2 = (projectRoot, path) => {
21703
21970
  const value = relative29(resolve42(projectRoot), resolve42(path)).replaceAll("\\", "/");
21704
21971
  return value || ".";
@@ -21710,7 +21977,7 @@ var ABSOLUTE_MOBILE_INSPECTION_FORMAT = 1, MOBILE_PACKAGE_NAMES, isObject3 = (va
21710
21977
  return false;
21711
21978
  }
21712
21979
  }, readObject2 = async (path) => {
21713
- const value = JSON.parse(await readFile22(path, "utf8"));
21980
+ const value = JSON.parse(await readFile23(path, "utf8"));
21714
21981
  if (!isObject3(value))
21715
21982
  throw new TypeError("JSON root must be an object.");
21716
21983
  return value;
@@ -21720,13 +21987,13 @@ var ABSOLUTE_MOBILE_INSPECTION_FORMAT = 1, MOBILE_PACKAGE_NAMES, isObject3 = (va
21720
21987
  for (const [name, declared] of Object.entries(value).filter((entry) => typeof entry[1] === "string"))
21721
21988
  declarations.set(name, declared);
21722
21989
  }, packageInspections = async (projectRoot, additionalNames) => {
21723
- const project = await readObject2(join55(projectRoot, "package.json"));
21990
+ const project = await readObject2(join56(projectRoot, "package.json"));
21724
21991
  const declarations = new Map;
21725
21992
  for (const field of ["dependencies", "devDependencies"])
21726
21993
  addPackageDeclarations(declarations, project[field]);
21727
21994
  const names = [...new Set([...declarations.keys(), ...additionalNames])].filter((name) => MOBILE_PACKAGE_NAMES.has(name) || name.startsWith("@capacitor/") || additionalNames.includes(name)).sort();
21728
21995
  return Promise.all(names.map(async (name) => {
21729
- const installedManifest = await readObject2(join55(projectRoot, "node_modules", name, "package.json")).catch(() => {
21996
+ const installedManifest = await readObject2(join56(projectRoot, "node_modules", name, "package.json")).catch(() => {
21730
21997
  return;
21731
21998
  });
21732
21999
  const installed = installedManifest?.version;
@@ -21775,7 +22042,7 @@ var ABSOLUTE_MOBILE_INSPECTION_FORMAT = 1, MOBILE_PACKAGE_NAMES, isObject3 = (va
21775
22042
  },
21776
22043
  format: ABSOLUTE_MOBILE_INSPECTION_FORMAT,
21777
22044
  nativeProjects: await Promise.all(config.platforms.map(async (platform6) => {
21778
- const path = join55(config.nativeProjectDirectory, platform6);
22045
+ const path = join56(config.nativeProjectDirectory, platform6);
21779
22046
  return {
21780
22047
  initialized: await pathExists8(path),
21781
22048
  path: portablePath2(projectRoot, path),
@@ -21846,7 +22113,7 @@ var init_mobileInspect = __esm(() => {
21846
22113
 
21847
22114
  // src/mobile/ciWorkflow.ts
21848
22115
  import { existsSync as existsSync43 } from "fs";
21849
- import { access as access15, mkdir as mkdir15, readFile as readFile23, writeFile as writeFile17 } from "fs/promises";
22116
+ import { access as access15, mkdir as mkdir15, readFile as readFile24, writeFile as writeFile18 } from "fs/promises";
21850
22117
  import { dirname as dirname32, extname as extname9, relative as relative30, resolve as resolve43, sep as sep9 } from "path";
21851
22118
  var ABSOLUTE_MOBILE_CI_WORKFLOW_FORMAT = 1, SECRET_NAME_PATTERN, CI_ENV_INDENTATION = 6, RESERVED_SECRET_NAMES, exists4 = async (path) => {
21852
22119
  try {
@@ -22260,12 +22527,12 @@ ${bundleAuditSteps}${platforms.includes("android") ? androidJob({ customSecrets,
22260
22527
  }, writeAbsoluteMobileGithubWorkflow = async (options) => {
22261
22528
  const path = workflowOutputPath(options.projectRoot, options.outputPath);
22262
22529
  const generated = createAbsoluteMobileGithubWorkflow(options);
22263
- const previous = await exists4(path) ? await readFile23(path, "utf8") : undefined;
22530
+ const previous = await exists4(path) ? await readFile24(path, "utf8") : undefined;
22264
22531
  if (previous !== undefined && previous !== generated.workflow && !options.force)
22265
22532
  throw new TypeError(`${relative30(options.projectRoot, path)} already exists and differs. Rerun with --force to replace the generated workflow.`);
22266
22533
  if (previous !== generated.workflow) {
22267
22534
  await mkdir15(dirname32(path), { recursive: true });
22268
- await writeFile17(path, generated.workflow);
22535
+ await writeFile18(path, generated.workflow);
22269
22536
  }
22270
22537
  return {
22271
22538
  changed: previous !== generated.workflow,
@@ -22322,16 +22589,171 @@ var init_ciWorkflow = __esm(() => {
22322
22589
  run: exit 1`;
22323
22590
  });
22324
22591
 
22592
+ // src/mobile/updateSigning.ts
22593
+ import { createHash as createHash16, sign as sign2, verify as verify2 } from "crypto";
22594
+ import {
22595
+ cp as cp4,
22596
+ mkdir as mkdir16,
22597
+ mkdtemp as mkdtemp8,
22598
+ readdir as readdir6,
22599
+ readFile as readFile25,
22600
+ rename as rename15,
22601
+ rm as rm11,
22602
+ stat as stat5,
22603
+ writeFile as writeFile19
22604
+ } from "fs/promises";
22605
+ import { dirname as dirname33, join as join57, relative as relative31, resolve as resolve44 } from "path";
22606
+ var UPDATE_MANIFEST_FILE = "update.json", UPDATE_FILES_DIRECTORY = "files", sha2562 = (value) => createHash16("sha256").update(value).digest("hex"), listFiles = async (root, directory = root) => {
22607
+ const entries = await readdir6(directory, { withFileTypes: true });
22608
+ const paths = await Promise.all(entries.map(async (entry) => {
22609
+ const path = join57(directory, entry.name);
22610
+ if (entry.isDirectory())
22611
+ return listFiles(root, path);
22612
+ if (!entry.isFile())
22613
+ throw new TypeError("Mobile updates cannot contain links or special files.");
22614
+ return [relative31(root, path).replaceAll("\\", "/")];
22615
+ }));
22616
+ return paths.flat().sort();
22617
+ }, inspectFiles = async (root, paths) => Promise.all(paths.map(async (path) => {
22618
+ const source = join57(root, path);
22619
+ const [metadata, contents] = await Promise.all([
22620
+ stat5(source),
22621
+ readFile25(source)
22622
+ ]);
22623
+ return { bytes: metadata.size, path, sha256: sha2562(contents) };
22624
+ })), releaseIdFor = (value) => `amu_${sha2562(canonicalizeAbsoluteMobileUpdate(value))}`, buildAbsoluteMobileUpdate = async (options) => {
22625
+ const bundleDirectory = resolve44(options.bundleDirectory);
22626
+ const outputRoot = resolve44(options.outputDirectory);
22627
+ if (outputRoot === bundleDirectory || outputRoot.startsWith(`${bundleDirectory}/`))
22628
+ throw new TypeError("Mobile update output must be outside the embedded bundle.");
22629
+ const paths = await listFiles(bundleDirectory);
22630
+ const files = await inspectFiles(bundleDirectory, paths);
22631
+ const withoutId = {
22632
+ appId: options.appId,
22633
+ channel: options.channel,
22634
+ classification: options.classification,
22635
+ createdAt: (options.createdAt ?? new Date).toISOString(),
22636
+ files,
22637
+ format: ABSOLUTE_MOBILE_UPDATE_FORMAT,
22638
+ runtimeFingerprint: options.runtimeFingerprint,
22639
+ withinSubmittedPurpose: true
22640
+ };
22641
+ const unsigned = {
22642
+ ...withoutId,
22643
+ releaseId: releaseIdFor(withoutId)
22644
+ };
22645
+ const signature = sign2("sha256", absoluteMobileUpdateSigningPayload(unsigned), { dsaEncoding: "ieee-p1363", key: options.privateKey });
22646
+ const manifest = parseAbsoluteMobileUpdateManifest({
22647
+ ...unsigned,
22648
+ signature: {
22649
+ algorithm: "ecdsa-p256-sha256",
22650
+ keyId: options.keyId,
22651
+ value: signature.toString("base64")
22652
+ }
22653
+ });
22654
+ await mkdir16(outputRoot, { recursive: true });
22655
+ const outputDirectory = join57(outputRoot, manifest.releaseId);
22656
+ const staging = await mkdtemp8(join57(outputRoot, ".stage-"));
22657
+ try {
22658
+ await cp4(bundleDirectory, join57(staging, UPDATE_FILES_DIRECTORY), {
22659
+ force: true,
22660
+ recursive: true
22661
+ });
22662
+ await writeFile19(join57(staging, UPDATE_MANIFEST_FILE), `${JSON.stringify(manifest, null, "\t")}
22663
+ `);
22664
+ await rename15(staging, outputDirectory);
22665
+ } catch (error) {
22666
+ await rm11(staging, { force: true, recursive: true });
22667
+ if (typeof error === "object" && error !== null && Reflect.get(error, "code") === "EEXIST")
22668
+ throw new TypeError(`Mobile update ${manifest.releaseId} already exists.`, { cause: error });
22669
+ throw error;
22670
+ }
22671
+ return {
22672
+ manifest,
22673
+ manifestPath: join57(outputDirectory, UPDATE_MANIFEST_FILE),
22674
+ outputDirectory
22675
+ };
22676
+ }, readAbsoluteMobileUpdate = async (directory) => parseAbsoluteMobileUpdateManifest(JSON.parse(await readFile25(join57(resolve44(directory), UPDATE_MANIFEST_FILE), "utf8"))), verifyAbsoluteMobileUpdateSignature = (manifestValue, publicKey) => {
22677
+ const manifest = parseAbsoluteMobileUpdateManifest(manifestValue);
22678
+ const valid = verify2("sha256", absoluteMobileUpdateSigningPayload(unsignedAbsoluteMobileUpdate(manifest)), { dsaEncoding: "ieee-p1363", key: publicKey }, Buffer.from(manifest.signature.value, "base64"));
22679
+ if (!valid)
22680
+ throw new TypeError("Mobile update signature verification failed.");
22681
+ return manifest;
22682
+ };
22683
+ var init_updateSigning = __esm(() => {
22684
+ init_updateProtocol();
22685
+ });
22686
+
22687
+ // src/mobile/updatePublisher.ts
22688
+ import { access as access16 } from "fs/promises";
22689
+ import { isAbsolute as isAbsolute9, relative as relative32, resolve as resolve45, sep as sep10 } from "path";
22690
+ import { pathToFileURL as pathToFileURL3 } from "url";
22691
+ 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) => {
22692
+ const root = resolve45(projectRoot);
22693
+ const path = resolve45(root, requested);
22694
+ const projectRelative = relative32(root, path);
22695
+ if (projectRelative === ".." || projectRelative.startsWith(`..${sep10}`) || isAbsolute9(projectRelative))
22696
+ throw new TypeError(`${label} must remain inside the project.`);
22697
+ return path;
22698
+ }, loadAbsoluteMobileUpdatePublisher = async (projectRoot, requestedModulePath) => {
22699
+ const modulePath = projectPath2(projectRoot, requestedModulePath, "mobile update registry");
22700
+ await access16(modulePath).catch(() => {
22701
+ throw new TypeError(`Mobile update registry does not exist: ${modulePath}`);
22702
+ });
22703
+ const loaded = await import(pathToFileURL3(modulePath).href);
22704
+ const publisher = object4(loaded) ? loaded.default ?? loaded.registry : undefined;
22705
+ if (!isPublisher2(publisher))
22706
+ throw new TypeError("Mobile update registry must implement publishUpdate, promoteUpdate, and rollbackUpdate.");
22707
+ return publisher;
22708
+ }, promoteAbsoluteMobileUpdate = async (options) => {
22709
+ const result = await options.publisher.promoteUpdate({
22710
+ appId: options.appId,
22711
+ channel: options.channel,
22712
+ releaseId: options.releaseId,
22713
+ rollout: options.rollout,
22714
+ signal: options.signal
22715
+ });
22716
+ if (result.appId !== options.appId || result.channel !== options.channel || result.releaseId !== options.releaseId || result.rollout !== options.rollout || result.stage !== "promoted")
22717
+ throw new TypeError("Mobile update registry returned a different promotion identity.");
22718
+ return result;
22719
+ }, publishAbsoluteMobileUpdate = async (options) => {
22720
+ const releaseDirectory = projectPath2(options.projectRoot, options.releaseDirectory, "mobile update release directory");
22721
+ const manifest = await readAbsoluteMobileUpdate(releaseDirectory);
22722
+ const result = await options.publisher.publishUpdate({
22723
+ manifest,
22724
+ releaseDirectory,
22725
+ rollout: options.rollout,
22726
+ signal: options.signal
22727
+ });
22728
+ if (result.appId !== manifest.appId || result.channel !== manifest.channel || result.releaseId !== manifest.releaseId || result.rollout !== options.rollout || result.stage !== "published" || typeof result.reused !== "boolean")
22729
+ throw new TypeError("Mobile update registry returned a different publication identity.");
22730
+ return result;
22731
+ }, rollbackAbsoluteMobileUpdate = async (options) => {
22732
+ const result = await options.publisher.rollbackUpdate({
22733
+ appId: options.appId,
22734
+ channel: options.channel,
22735
+ ...options.releaseId ? { releaseId: options.releaseId } : {},
22736
+ signal: options.signal
22737
+ });
22738
+ if (result.appId !== options.appId || result.channel !== options.channel || result.releaseId !== options.releaseId || result.stage !== "rolled-back")
22739
+ throw new TypeError("Mobile update registry returned a different rollback identity.");
22740
+ return result;
22741
+ };
22742
+ var init_updatePublisher = __esm(() => {
22743
+ init_updateSigning();
22744
+ });
22745
+
22325
22746
  // src/cli/scripts/mobile.ts
22326
22747
  var exports_mobile = {};
22327
22748
  __export(exports_mobile, {
22328
22749
  runMobile: () => runMobile
22329
22750
  });
22330
- import { access as access16, mkdir as mkdir16, readFile as readFile24, writeFile as writeFile18 } from "fs/promises";
22331
- import { join as join56, relative as relative31, resolve as resolve44 } from "path";
22751
+ import { access as access17, mkdir as mkdir17, readFile as readFile26, writeFile as writeFile20 } from "fs/promises";
22752
+ import { createPublicKey as createPublicKey3 } from "crypto";
22753
+ import { join as join58, relative as relative33, resolve as resolve46 } from "path";
22332
22754
  import { createInterface } from "readline/promises";
22333
22755
  var NOT_FOUND4 = -1, isRecord17 = (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) => {
22334
- const manifest = JSON.parse(await readFile24(join56(projectRoot, "package.json"), "utf8"));
22756
+ const manifest = JSON.parse(await readFile26(join58(projectRoot, "package.json"), "utf8"));
22335
22757
  if (!isRecord17(manifest))
22336
22758
  throw new TypeError("Application package.json must contain an object.");
22337
22759
  const names = new Set;
@@ -22344,7 +22766,7 @@ var NOT_FOUND4 = -1, isRecord17 = (value) => typeof value === "object" && value
22344
22766
  return names;
22345
22767
  }, resolvedPackageVersion = async (projectRoot, packageName) => {
22346
22768
  try {
22347
- const manifest = JSON.parse(await readFile24(join56(projectRoot, "node_modules", packageName, "package.json"), "utf8"));
22769
+ const manifest = JSON.parse(await readFile26(join58(projectRoot, "node_modules", packageName, "package.json"), "utf8"));
22348
22770
  return isRecord17(manifest) && typeof manifest.version === "string" ? manifest.version : undefined;
22349
22771
  } catch {
22350
22772
  return;
@@ -22361,9 +22783,10 @@ var NOT_FOUND4 = -1, isRecord17 = (value) => typeof value === "object" && value
22361
22783
  const name = packageNameFromSpec(spec);
22362
22784
  const needsInstall = !installed.has(name) || exactPackages.has(name) && await resolvedPackageVersion(projectRoot, name) !== exactVersionFromSpec(spec);
22363
22785
  return needsInstall ? spec : undefined;
22364
- }))).filter((spec) => spec !== undefined), ensureCapacitorPackages = async (projectRoot, args) => {
22786
+ }))).filter((spec) => spec !== undefined), ensureCapacitorPackages = async (projectRoot, args, mobile) => {
22365
22787
  const specs = [
22366
22788
  ...CAPACITOR_PACKAGE_SPECS,
22789
+ ...mobile?.updates ? ["@capacitor/filesystem@8.1.3"] : [],
22367
22790
  ...projectUsesAbsoluteSync(projectRoot) ? CAPACITOR_SYNC_PACKAGE_SPECS : []
22368
22791
  ];
22369
22792
  const installed = await directProjectPackages(projectRoot);
@@ -22385,9 +22808,9 @@ var NOT_FOUND4 = -1, isRecord17 = (value) => typeof value === "object" && value
22385
22808
  }
22386
22809
  return value;
22387
22810
  }, capacitorExecutable = async (projectRoot) => {
22388
- const executable = join56(projectRoot, "node_modules", ".bin", "cap");
22811
+ const executable = join58(projectRoot, "node_modules", ".bin", "cap");
22389
22812
  try {
22390
- await access16(executable);
22813
+ await access17(executable);
22391
22814
  return executable;
22392
22815
  } catch {
22393
22816
  throw new TypeError(`Capacitor is not installed in this app. Run: bun add ${CAPACITOR_PACKAGES.join(" ")}`);
@@ -22405,9 +22828,9 @@ var NOT_FOUND4 = -1, isRecord17 = (value) => typeof value === "object" && value
22405
22828
  throw new TypeError(`Capacitor exited with status ${exitCode}.`);
22406
22829
  }
22407
22830
  }, runCapacitorForPlatforms = (projectRoot, command, platforms) => platforms.reduce((pending, platform6) => pending.then(() => runCapacitor(projectRoot, [command, platform6])), Promise.resolve()), expoExecutable = async (project) => {
22408
- const executable = join56(project, "node_modules", ".bin", "expo");
22831
+ const executable = join58(project, "node_modules", ".bin", "expo");
22409
22832
  try {
22410
- await access16(executable);
22833
+ await access17(executable);
22411
22834
  return executable;
22412
22835
  } catch {
22413
22836
  throw new TypeError("Expo dependencies are not installed in the generated shell. Run `absolute mobile init --yes`.");
@@ -22436,14 +22859,14 @@ var NOT_FOUND4 = -1, isRecord17 = (value) => typeof value === "object" && value
22436
22859
  throw new TypeError(`Expo exited with status ${exitCode}.`);
22437
22860
  }, ensureExpoPackages = async (project, args) => {
22438
22861
  try {
22439
- const manifest = JSON.parse(await readFile24(join56(project, "package.json"), "utf8"));
22862
+ const manifest = JSON.parse(await readFile26(join58(project, "package.json"), "utf8"));
22440
22863
  const dependencies = typeof manifest === "object" && manifest !== null ? Reflect.get(manifest, "dependencies") : undefined;
22441
22864
  if (typeof dependencies !== "object" || dependencies === null || Array.isArray(dependencies))
22442
22865
  throw new TypeError("Generated Expo dependencies are invalid.");
22443
22866
  await Promise.all(Object.entries(dependencies).map(async ([name, expected]) => {
22444
22867
  if (typeof expected !== "string")
22445
22868
  throw new TypeError("Generated Expo dependency version is invalid.");
22446
- const installed = JSON.parse(await readFile24(join56(project, "node_modules", name, "package.json"), "utf8"));
22869
+ const installed = JSON.parse(await readFile26(join58(project, "node_modules", name, "package.json"), "utf8"));
22447
22870
  const actual = typeof installed === "object" && installed !== null ? Reflect.get(installed, "version") : undefined;
22448
22871
  if (typeof actual !== "string")
22449
22872
  throw new TypeError("Installed Expo dependency version is invalid.");
@@ -22579,7 +23002,7 @@ var NOT_FOUND4 = -1, isRecord17 = (value) => typeof value === "object" && value
22579
23002
  ]);
22580
23003
  return;
22581
23004
  }
22582
- await ensureCapacitorPackages(projectRoot, args);
23005
+ await ensureCapacitorPackages(projectRoot, args, mobile);
22583
23006
  const generated = await writeAbsoluteCapacitorConfig(mobile, {
22584
23007
  force: args.includes("--force"),
22585
23008
  projectRoot
@@ -22591,6 +23014,7 @@ var NOT_FOUND4 = -1, isRecord17 = (value) => typeof value === "object" && value
22591
23014
  await applyAbsoluteNativeDeepLinks(mobile);
22592
23015
  await applyAbsoluteNativeDeviceCapabilities(projectRoot, mobile);
22593
23016
  await applyAbsoluteNativeBackgroundSync(projectRoot, mobile);
23017
+ await applyAbsoluteNativeUpdates(mobile);
22594
23018
  }, sync = async (args) => {
22595
23019
  const { mobile, projectRoot } = await loadMobile(valueAfter(args, "--config"));
22596
23020
  if (mobile.engine === "expo") {
@@ -22612,7 +23036,7 @@ var NOT_FOUND4 = -1, isRecord17 = (value) => typeof value === "object" && value
22612
23036
  ]);
22613
23037
  return;
22614
23038
  }
22615
- await ensureCapacitorPackages(projectRoot, args);
23039
+ await ensureCapacitorPackages(projectRoot, args, mobile);
22616
23040
  const platform6 = args.find((value) => value === "android" || value === "ios");
22617
23041
  const platforms = platform6 ? [platform6] : mobile.platforms;
22618
23042
  if (platforms.includes("android"))
@@ -22623,9 +23047,10 @@ var NOT_FOUND4 = -1, isRecord17 = (value) => typeof value === "object" && value
22623
23047
  await applyAbsoluteNativeDeepLinks(mobile, platforms);
22624
23048
  await applyAbsoluteNativeDeviceCapabilities(projectRoot, mobile, platforms);
22625
23049
  await applyAbsoluteNativeBackgroundSync(projectRoot, mobile, platforms);
23050
+ await applyAbsoluteNativeUpdates(mobile, platforms);
22626
23051
  }, associations = async (args) => {
22627
23052
  const { mobile, projectRoot } = await loadMobile(valueAfter(args, "--config"));
22628
- const outputDirectory = resolve44(projectRoot, valueAfter(args, "--outdir") ?? ".absolutejs/mobile/associations");
23053
+ const outputDirectory = resolve46(projectRoot, valueAfter(args, "--outdir") ?? ".absolutejs/mobile/associations");
22629
23054
  if (args.includes("--verify")) {
22630
23055
  const result2 = await verifyAbsoluteMobileAssociationFiles(mobile);
22631
23056
  console.log(`Verified ${result2.results.length} hosted association files`);
@@ -22675,7 +23100,7 @@ var NOT_FOUND4 = -1, isRecord17 = (value) => typeof value === "object" && value
22675
23100
  const publicResult = {
22676
23101
  changed: result.changed,
22677
23102
  format: result.format,
22678
- path: relative31(projectRoot, result.path).replaceAll("\\", "/"),
23103
+ path: relative33(projectRoot, result.path).replaceAll("\\", "/"),
22679
23104
  platforms: result.platforms,
22680
23105
  publishing: result.publishing,
22681
23106
  requiredSecrets: result.requiredSecrets
@@ -22742,6 +23167,7 @@ Mobile release security and compliance checks failed.`);
22742
23167
  }, mobileBuildServerEntry = (args) => {
22743
23168
  const valueFlags = new Set([
22744
23169
  "--channel",
23170
+ "--classification",
22745
23171
  "--config",
22746
23172
  "--outdir",
22747
23173
  "--play-name",
@@ -22751,6 +23177,8 @@ Mobile release security and compliance checks failed.`);
22751
23177
  "--play-track",
22752
23178
  "--play-update-priority",
22753
23179
  "--registry",
23180
+ "--key-id",
23181
+ "--signing-key",
22754
23182
  "--remote",
22755
23183
  "--testflight-group",
22756
23184
  "--testflight-notes",
@@ -22764,6 +23192,132 @@ Mobile release security and compliance checks failed.`);
22764
23192
  }
22765
23193
  });
22766
23194
  return args.find((value, index) => !skipped.has(index) && value !== "--unsigned" && !value.startsWith("-")) ?? DEFAULT_SERVER_ENTRY;
23195
+ }, requireUpdateClassification = (value) => {
23196
+ if (value === "bug-fix" || value === "content" || value === "security")
23197
+ return value;
23198
+ throw new TypeError("mobile update build requires --classification bug-fix|content|security.");
23199
+ }, buildMobileUpdate = async (args) => {
23200
+ const configPath2 = valueAfter(args, "--config");
23201
+ const { mobile, projectRoot } = await loadMobile(configPath2);
23202
+ requireCapacitorEngine(mobile, "mobile update build");
23203
+ if (!mobile.updates)
23204
+ throw new TypeError("mobile update build requires mobile.updates.publicKeys in absolute.config.ts.");
23205
+ if (!args.includes("--within-submitted-purpose"))
23206
+ throw new TypeError("mobile update build requires --within-submitted-purpose to attest that the update does not change the submitted app purpose.");
23207
+ const classification = requireUpdateClassification(valueAfter(args, "--classification"));
23208
+ const keyId = valueAfter(args, "--key-id");
23209
+ if (!keyId || !mobile.updates.publicKeys[keyId])
23210
+ throw new TypeError("mobile update build requires --key-id matching mobile.updates.publicKeys.");
23211
+ const signingKeyPath = valueAfter(args, "--signing-key");
23212
+ if (!signingKeyPath)
23213
+ throw new TypeError("mobile update build requires --signing-key <private-key.pem>.");
23214
+ const startedAt = performance.now();
23215
+ let success = false;
23216
+ try {
23217
+ await start(mobileBuildServerEntry(args), valueAfter(args, "--web-outdir"), configPath2, { prepareOnly: true });
23218
+ const embedded = JSON.parse(await readFile26(join58(mobile.bundleDirectory, "absolute-mobile-manifest.json"), "utf8"));
23219
+ const runtimeFingerprint = isRecord17(embedded) ? embedded.nativeRuntime : undefined;
23220
+ if (typeof runtimeFingerprint !== "string" || !/^[a-f0-9]{64}$/u.test(runtimeFingerprint))
23221
+ throw new TypeError("Prepared mobile bundle is missing its native runtime fingerprint.");
23222
+ const privateKey = await readFile26(resolve46(projectRoot, signingKeyPath));
23223
+ const configuredPublicKey = Buffer.from(mobile.updates.publicKeys[keyId], "base64");
23224
+ const derivedPublicKey = createPublicKey3(privateKey).export({
23225
+ format: "der",
23226
+ type: "spki"
23227
+ });
23228
+ if (!configuredPublicKey.equals(derivedPublicKey))
23229
+ throw new TypeError(`The private key does not match mobile.updates.publicKeys.${keyId}.`);
23230
+ const result = await buildAbsoluteMobileUpdate({
23231
+ appId: mobile.appId,
23232
+ bundleDirectory: mobile.bundleDirectory,
23233
+ channel: mobile.updates.channel,
23234
+ classification,
23235
+ keyId,
23236
+ outputDirectory: valueAfter(args, "--outdir") ?? join58(projectRoot, ".absolutejs", "mobile", "updates"),
23237
+ privateKey,
23238
+ runtimeFingerprint
23239
+ });
23240
+ verifyAbsoluteMobileUpdateSignature(result.manifest, derivedPublicKey);
23241
+ success = true;
23242
+ console.log(`Built signed mobile update ${result.manifest.releaseId}.`);
23243
+ console.log(`Release: ${result.outputDirectory}`);
23244
+ console.log(`Manifest: ${result.manifestPath}`);
23245
+ return result;
23246
+ } finally {
23247
+ sendTelemetryEvent("mobile:update-build", {
23248
+ classification: classification ?? "invalid",
23249
+ durationMs: Math.round(performance.now() - startedAt),
23250
+ engine: mobile.engine,
23251
+ success
23252
+ });
23253
+ }
23254
+ }, updateRollout = (args, fallback) => {
23255
+ const value = valueAfter(args, "--rollout");
23256
+ if (value === undefined) {
23257
+ if (fallback !== undefined)
23258
+ return fallback;
23259
+ throw new TypeError("This command requires --rollout <fraction>.");
23260
+ }
23261
+ const rollout = Number(value);
23262
+ if (!Number.isFinite(rollout) || rollout <= 0 || rollout > 1)
23263
+ throw new TypeError("--rollout must be greater than 0 and at most 1.");
23264
+ return rollout;
23265
+ }, mobileUpdatePublisher = async (args) => {
23266
+ const { projectRoot } = await loadMobile(valueAfter(args, "--config"));
23267
+ const modulePath = valueAfter(args, "--registry") ?? "mobile.release.ts";
23268
+ return {
23269
+ projectRoot,
23270
+ publisher: await loadAbsoluteMobileUpdatePublisher(projectRoot, modulePath)
23271
+ };
23272
+ }, publishMobileUpdate = async (args) => {
23273
+ const releaseDirectory = args.find((value, index) => {
23274
+ if (value.startsWith("-"))
23275
+ return false;
23276
+ const previous = args[index - 1];
23277
+ return !["--config", "--registry", "--rollout"].includes(previous ?? "");
23278
+ });
23279
+ if (!releaseDirectory)
23280
+ throw new TypeError("mobile update publish requires a release directory.");
23281
+ const { projectRoot, publisher } = await mobileUpdatePublisher(args);
23282
+ const result = await publishAbsoluteMobileUpdate({
23283
+ projectRoot,
23284
+ publisher,
23285
+ releaseDirectory,
23286
+ rollout: updateRollout(args, 0.05)
23287
+ });
23288
+ console.log(`${result.reused ? "Reused" : "Published"} mobile update ${result.releaseId} to ${result.channel} at ${Math.round(result.rollout * 100)}%.`);
23289
+ return result;
23290
+ }, promoteMobileUpdate = async (args) => {
23291
+ const { mobile } = await loadMobile(valueAfter(args, "--config"));
23292
+ if (!mobile.updates)
23293
+ throw new TypeError("mobile update promote requires mobile.updates config.");
23294
+ const releaseId = valueAfter(args, "--release");
23295
+ if (!releaseId)
23296
+ throw new TypeError("mobile update promote requires --release <release-id>.");
23297
+ const { publisher } = await mobileUpdatePublisher(args);
23298
+ const result = await promoteAbsoluteMobileUpdate({
23299
+ appId: mobile.appId,
23300
+ channel: mobile.updates.channel,
23301
+ publisher,
23302
+ releaseId,
23303
+ rollout: updateRollout(args)
23304
+ });
23305
+ console.log(`Promoted mobile update ${result.releaseId} to ${Math.round(result.rollout * 100)}%.`);
23306
+ return result;
23307
+ }, rollbackMobileUpdate = async (args) => {
23308
+ const { mobile } = await loadMobile(valueAfter(args, "--config"));
23309
+ if (!mobile.updates)
23310
+ throw new TypeError("mobile update rollback requires mobile.updates config.");
23311
+ const { publisher } = await mobileUpdatePublisher(args);
23312
+ const releaseId = valueAfter(args, "--release");
23313
+ const result = await rollbackAbsoluteMobileUpdate({
23314
+ appId: mobile.appId,
23315
+ channel: mobile.updates.channel,
23316
+ publisher,
23317
+ ...releaseId ? { releaseId } : {}
23318
+ });
23319
+ console.log(result.releaseId ? `Rolled ${result.channel} back to ${result.releaseId}.` : `Rolled ${result.channel} back to the embedded store build.`);
23320
+ return result;
22767
23321
  }, requireValueAfter = (args, flag) => {
22768
23322
  const value = valueAfter(args, flag);
22769
23323
  if (!value || value.startsWith("-")) {
@@ -22919,6 +23473,7 @@ Mobile release security and compliance checks failed.`);
22919
23473
  await applyAbsoluteNativeDeepLinks(mobile, ["ios"]);
22920
23474
  await applyAbsoluteNativeDeviceCapabilities(projectRoot, mobile, ["ios"]);
22921
23475
  await applyAbsoluteNativeBackgroundSync(projectRoot, mobile, ["ios"]);
23476
+ await applyAbsoluteNativeUpdates(mobile, ["ios"]);
22922
23477
  }, prepareIosReleaseProject = (mobile, projectRoot, args) => mobile.engine === "expo" ? prepareExpoIosReleaseProject(mobile, projectRoot, args) : prepareCapacitorIosReleaseProject(mobile, projectRoot), prepareCapacitorAndroidReleaseProject = async (mobile, projectRoot) => {
22923
23478
  await writeAbsoluteCapacitorConfig(mobile, { projectRoot });
22924
23479
  await runCapacitorForPlatforms(projectRoot, "sync", ["android"]);
@@ -22954,7 +23509,7 @@ Mobile release security and compliance checks failed.`);
22954
23509
  const durationMs = Math.round(performance.now() - startedAt);
22955
23510
  console.log(`Built ${release.metadata.signed ? "signed" : "unsigned"} Android App Bundle in ${getDurationString(durationMs)}.`);
22956
23511
  console.log(`Artifact: ${release.artifactPath}`);
22957
- console.log(`Metadata: ${join56(release.releaseRoot, "release.json")}`);
23512
+ console.log(`Metadata: ${join58(release.releaseRoot, "release.json")}`);
22958
23513
  return release;
22959
23514
  } finally {
22960
23515
  sendTelemetryEvent("mobile:android-release-build", {
@@ -23102,7 +23657,7 @@ Mobile release security and compliance checks failed.`);
23102
23657
  const durationMs = Math.round(performance.now() - startedAt);
23103
23658
  console.log(`Built ${release.metadata.signed ? "signed" : "unsigned"} iOS IPA ${release.metadata.marketingVersion}${release.metadata.buildNumber ? ` (${release.metadata.buildNumber})` : ""} in ${getDurationString(durationMs)}.`);
23104
23659
  console.log(`Artifact: ${release.artifactPath}`);
23105
- console.log(`Metadata: ${join56(release.releaseRoot, "release.json")}`);
23660
+ console.log(`Metadata: ${join58(release.releaseRoot, "release.json")}`);
23106
23661
  return release;
23107
23662
  } finally {
23108
23663
  stopListeningForCancellation();
@@ -23211,7 +23766,7 @@ Mobile release security and compliance checks failed.`);
23211
23766
  checks.push({
23212
23767
  id: "sync.storage-schema",
23213
23768
  label: `Offline schema ${schema.components.map((component2) => `${component2.id}@${component2.version}`).join(", ")}`,
23214
- path: join56(projectRoot, "package.json"),
23769
+ path: join58(projectRoot, "package.json"),
23215
23770
  platform: "host",
23216
23771
  status: "pass"
23217
23772
  });
@@ -23219,7 +23774,7 @@ Mobile release security and compliance checks failed.`);
23219
23774
  checks.push({
23220
23775
  id: "sync.storage-schema",
23221
23776
  label: "Offline schema metadata is invalid",
23222
- path: join56(projectRoot, "package.json"),
23777
+ path: join58(projectRoot, "package.json"),
23223
23778
  platform: "host",
23224
23779
  remediation: error instanceof Error ? error.message : String(error),
23225
23780
  status: "fail"
@@ -23304,7 +23859,7 @@ Emulator setup verification:`);
23304
23859
  }
23305
23860
  return { https: args.includes("--https"), port };
23306
23861
  }
23307
- const instances = listLiveInstances().filter((instance2) => resolve44(instance2.cwd) === resolve44(projectRoot) && instance2.source === "dev" && instance2.port !== null);
23862
+ const instances = listLiveInstances().filter((instance2) => resolve46(instance2.cwd) === resolve46(projectRoot) && instance2.source === "dev" && instance2.port !== null);
23308
23863
  if (instances.length !== 1) {
23309
23864
  throw new TypeError(instances.length === 0 ? "No running AbsoluteJS dev server was found for this project. Start `bun dev`, wait for Android to report ready, then run `absolute mobile test android`." : "Multiple dev servers are running for this project. Select one with mobile test android --port <port>.");
23310
23865
  }
@@ -23347,8 +23902,8 @@ Emulator setup verification:`);
23347
23902
  }
23348
23903
  return selected;
23349
23904
  }, safeArtifactRoot = (projectRoot, value) => {
23350
- const root = resolve44(projectRoot, value ?? ".absolutejs/mobile/test-artifacts");
23351
- if (root !== projectRoot && !root.startsWith(`${resolve44(projectRoot)}/`)) {
23905
+ const root = resolve46(projectRoot, value ?? ".absolutejs/mobile/test-artifacts");
23906
+ if (root !== projectRoot && !root.startsWith(`${resolve46(projectRoot)}/`)) {
23352
23907
  throw new TypeError("mobile test --artifacts must remain inside the project.");
23353
23908
  }
23354
23909
  return root;
@@ -23372,12 +23927,12 @@ Emulator setup verification:`);
23372
23927
  timeoutMs
23373
23928
  });
23374
23929
  }, writeAndroidFailureArtifacts = async (options) => {
23375
- await mkdir16(options.artifactRoot, { recursive: true });
23376
- const screenshot = options.session ? await options.session.screenshot(join56(options.artifactRoot, "android-failure.png")).catch(() => {
23930
+ await mkdir17(options.artifactRoot, { recursive: true });
23931
+ const screenshot = options.session ? await options.session.screenshot(join58(options.artifactRoot, "android-failure.png")).catch(() => {
23377
23932
  return;
23378
23933
  }) : undefined;
23379
- const diagnosticPath = join56(options.artifactRoot, "android-failure.json");
23380
- await writeFile18(diagnosticPath, `${JSON.stringify({
23934
+ const diagnosticPath = join58(options.artifactRoot, "android-failure.json");
23935
+ await writeFile20(diagnosticPath, `${JSON.stringify({
23381
23936
  diagnostics: options.session?.diagnostics ?? [],
23382
23937
  error: options.error instanceof Error ? options.error.message : String(options.error),
23383
23938
  platform: "android",
@@ -23441,7 +23996,7 @@ Emulator setup verification:`);
23441
23996
  console.log(JSON.stringify(report, null, 2));
23442
23997
  else
23443
23998
  printAndroidTestReport(report);
23444
- const screenshot = reportRoot ? await session.screenshot(join56(artifactRoot, "android-emulator.png")) : undefined;
23999
+ const screenshot = reportRoot ? await session.screenshot(join58(artifactRoot, "android-emulator.png")) : undefined;
23445
24000
  await writeRequestedAndroidReport({
23446
24001
  adb,
23447
24002
  args,
@@ -23509,14 +24064,14 @@ Emulator setup verification:`);
23509
24064
  const port = Number(explicit);
23510
24065
  if (!Number.isInteger(port) || port < 1 || port > 65535)
23511
24066
  throw new TypeError("mobile test --port must be a valid TCP port.");
23512
- const instance2 = listLiveInstances().find((candidate) => resolve44(candidate.cwd) === resolve44(projectRoot) && candidate.source === "dev" && candidate.port === port);
24067
+ const instance2 = listLiveInstances().find((candidate) => resolve46(candidate.cwd) === resolve46(projectRoot) && candidate.source === "dev" && candidate.port === port);
23513
24068
  return {
23514
24069
  https: instance2?.https ?? args.includes("--https"),
23515
24070
  instance: instance2,
23516
24071
  port
23517
24072
  };
23518
24073
  }
23519
- const instances = listLiveInstances().filter((instance2) => resolve44(instance2.cwd) === resolve44(projectRoot) && instance2.source === "dev" && instance2.port !== null);
24074
+ const instances = listLiveInstances().filter((instance2) => resolve46(instance2.cwd) === resolve46(projectRoot) && instance2.source === "dev" && instance2.port !== null);
23520
24075
  if (instances.length !== 1)
23521
24076
  throw new TypeError(instances.length === 0 ? "No running AbsoluteJS dev server was found for this project. Start `bun dev`, wait for iOS to report ready, then run `absolute mobile test ios`." : "Multiple dev servers are running for this project. Select one with mobile test ios --port <port>.");
23522
24077
  const [instance] = instances;
@@ -23630,8 +24185,8 @@ Emulator setup verification:`);
23630
24185
  throw new Error(`${label} failed: ${result.stderr.trim() || result.stdout.trim() || `status ${result.exitCode}`}`);
23631
24186
  return result;
23632
24187
  }, writeIosFailureArtifacts = async (options) => {
23633
- await mkdir16(options.artifactRoot, { recursive: true });
23634
- const screenshot = join56(options.artifactRoot, "ios-failure.png");
24188
+ await mkdir17(options.artifactRoot, { recursive: true });
24189
+ const screenshot = join58(options.artifactRoot, "ios-failure.png");
23635
24190
  const screenshotResult = captureCommand4([
23636
24191
  options.xcrun,
23637
24192
  "simctl",
@@ -23640,8 +24195,8 @@ Emulator setup verification:`);
23640
24195
  "screenshot",
23641
24196
  screenshot
23642
24197
  ]);
23643
- const diagnosticPath = join56(options.artifactRoot, "ios-failure.json");
23644
- await writeFile18(diagnosticPath, `${JSON.stringify({
24198
+ const diagnosticPath = join58(options.artifactRoot, "ios-failure.json");
24199
+ await writeFile20(diagnosticPath, `${JSON.stringify({
23645
24200
  appId: options.appId,
23646
24201
  error: options.error instanceof Error ? options.error.message : String(options.error),
23647
24202
  platform: "ios",
@@ -23666,8 +24221,8 @@ Emulator setup verification:`);
23666
24221
  }, absolutejsVersionForReport = async () => {
23667
24222
  let absolutejsVersion = process.env.ABSOLUTE_VERSION ?? "unknown";
23668
24223
  const versions = await Promise.all([
23669
- resolve44(import.meta.dir, "..", "..", "package.json"),
23670
- resolve44(import.meta.dir, "..", "..", "..", "package.json")
24224
+ resolve46(import.meta.dir, "..", "..", "package.json"),
24225
+ resolve46(import.meta.dir, "..", "..", "..", "package.json")
23671
24226
  ].map((candidate) => readPackageVersionForIosReport(candidate).catch(() => "unknown")));
23672
24227
  for (const version2 of versions) {
23673
24228
  if (version2 === "unknown")
@@ -23886,8 +24441,8 @@ Emulator setup verification:`);
23886
24441
  mobile.appId
23887
24442
  ], "iOS app launch");
23888
24443
  await waitForIosHmrClient({ https, port, timeoutMs });
23889
- await mkdir16(artifactRoot, { recursive: true });
23890
- const screenshot = join56(artifactRoot, "ios-simulator.png");
24444
+ await mkdir17(artifactRoot, { recursive: true });
24445
+ const screenshot = join58(artifactRoot, "ios-simulator.png");
23891
24446
  requireCapturedCommand([xcrun, "simctl", "io", simulator.udid, "screenshot", screenshot], "iOS simulator screenshot");
23892
24447
  const hmrApply = await waitForRequestedIosHmr(args, instance, timeoutMs);
23893
24448
  const report = {
@@ -24024,6 +24579,22 @@ Emulator setup verification:`);
24024
24579
  await buildIos(args.slice(2));
24025
24580
  return;
24026
24581
  }
24582
+ if (command === "update" && args[1] === "build") {
24583
+ await buildMobileUpdate(args.slice(2));
24584
+ return;
24585
+ }
24586
+ if (command === "update" && args[1] === "publish") {
24587
+ await publishMobileUpdate(args.slice(2));
24588
+ return;
24589
+ }
24590
+ if (command === "update" && args[1] === "promote") {
24591
+ await promoteMobileUpdate(args.slice(2));
24592
+ return;
24593
+ }
24594
+ if (command === "update" && args[1] === "rollback") {
24595
+ await rollbackMobileUpdate(args.slice(2));
24596
+ return;
24597
+ }
24027
24598
  if (command === "publish" && args[1] === "android") {
24028
24599
  await publishAndroid(args.slice(2));
24029
24600
  return;
@@ -24032,7 +24603,7 @@ Emulator setup verification:`);
24032
24603
  await publishIos(args.slice(2));
24033
24604
  return;
24034
24605
  }
24035
- throw new TypeError("Usage: absolute mobile <pair mac <name> <user@host> [--port n] [--workspace path] | remotes [inspect [name] [--json] | clean [name] --yes | --json] | unpair mac <name> | init [--no-native] [--force] | sync [ios|android] | inspect [--json] [--require-bundle] | associations [--outdir dir] [--verify] | ci github [server-entry] [--publish] [--registry module] [--secret-env NAME] [--output path] [--force] [--json] | doctor [ios|android|release [ios|android]] [--remote name] [--json|--fix [--yes]] | build <android|ios> [server-entry] [--remote name] [--outdir dir] [--web-outdir dir] [--unsigned] | publish android [server-entry] [--registry module] [--channel name] [--play-track track] [--play-status completed|draft|halted|in-progress] [--play-rollout fraction] [--play-name name] [--play-notes language=text] [--play-update-priority 0..5] [--play-hold-review] [--play-cancel-existing-review] [--outdir dir] [--web-outdir dir] [--unsigned] | publish ios [server-entry] [--remote name] [--registry module] [--channel name] [--testflight-group name-or-id] [--testflight-notes locale=text] [--testflight-submit-review] [--outdir dir] [--web-outdir dir] [--unsigned] | test android [--route path] [--wait-for-hmr] [--report [dir]] [--timeout ms] [--port n] [--serial id] [--artifacts dir] [--json] | test ios [--device identifier [--remote name] | --udid id] [--wait-for-hmr] [--report [dir]] [--timeout ms] [--port n] [--artifacts dir] [--json]> [--config path]");
24606
+ throw new TypeError("Usage: absolute mobile <pair mac <name> <user@host> [--port n] [--workspace path] | remotes [inspect [name] [--json] | clean [name] --yes | --json] | unpair mac <name> | init [--no-native] [--force] | sync [ios|android] | inspect [--json] [--require-bundle] | associations [--outdir dir] [--verify] | ci github [server-entry] [--publish] [--registry module] [--secret-env NAME] [--output path] [--force] [--json] | doctor [ios|android|release [ios|android]] [--remote name] [--json|--fix [--yes]] | build <android|ios> [server-entry] [--remote name] [--outdir dir] [--web-outdir dir] [--unsigned] | update build [server-entry] --classification bug-fix|content|security --key-id id --signing-key path --within-submitted-purpose [--outdir dir] [--web-outdir dir] | update publish <release-directory> [--rollout fraction] [--registry module] | update promote --release id --rollout fraction [--registry module] | update rollback [--release id] [--registry module] | publish android [server-entry] [--registry module] [--channel name] [--play-track track] [--play-status completed|draft|halted|in-progress] [--play-rollout fraction] [--play-name name] [--play-notes language=text] [--play-update-priority 0..5] [--play-hold-review] [--play-cancel-existing-review] [--outdir dir] [--web-outdir dir] [--unsigned] | publish ios [server-entry] [--remote name] [--registry module] [--channel name] [--testflight-group name-or-id] [--testflight-notes locale=text] [--testflight-submit-review] [--outdir dir] [--web-outdir dir] [--unsigned] | test android [--route path] [--wait-for-hmr] [--report [dir]] [--timeout ms] [--port n] [--serial id] [--artifacts dir] [--json] | test ios [--device identifier [--remote name] | --udid id] [--wait-for-hmr] [--report [dir]] [--timeout ms] [--port n] [--artifacts dir] [--json]> [--config path]");
24036
24607
  };
24037
24608
  var init_mobile = __esm(() => {
24038
24609
  init_dependencies();
@@ -24042,6 +24613,7 @@ var init_mobile = __esm(() => {
24042
24613
  init_nativeDeepLinks();
24043
24614
  init_nativeDeviceCapabilities();
24044
24615
  init_nativeBackgroundSync();
24616
+ init_nativeUpdates();
24045
24617
  init_emulatorDoctor();
24046
24618
  init_emulatorInstaller();
24047
24619
  init_associationFiles();
@@ -24069,6 +24641,8 @@ var init_mobile = __esm(() => {
24069
24641
  init_deviceCapabilities();
24070
24642
  init_mobileInspect();
24071
24643
  init_ciWorkflow();
24644
+ init_updateSigning();
24645
+ init_updatePublisher();
24072
24646
  CAPACITOR_PACKAGES = [
24073
24647
  "@capacitor/core",
24074
24648
  "@capacitor/app",
@@ -24104,10 +24678,10 @@ var exports_typecheck = {};
24104
24678
  __export(exports_typecheck, {
24105
24679
  typecheck: () => typecheck
24106
24680
  });
24107
- import { resolve as resolve45, join as join57 } from "path";
24681
+ import { resolve as resolve47, join as join59 } from "path";
24108
24682
  import { existsSync as existsSync44, readFileSync as readFileSync40 } from "fs";
24109
- import { mkdir as mkdir17, writeFile as writeFile19 } from "fs/promises";
24110
- var isCommandService3 = (service) => service.kind === "command" || Array.isArray(service.command), resolveConfigPath = (configPath2) => resolve45(configPath2 ?? process.env.ABSOLUTE_CONFIG ?? "absolute.config.ts"), getTypecheckTargets = async (configPath2) => {
24683
+ import { mkdir as mkdir18, writeFile as writeFile21 } from "fs/promises";
24684
+ var isCommandService3 = (service) => service.kind === "command" || Array.isArray(service.command), resolveConfigPath = (configPath2) => resolve47(configPath2 ?? process.env.ABSOLUTE_CONFIG ?? "absolute.config.ts"), getTypecheckTargets = async (configPath2) => {
24111
24685
  if (!existsSync44(resolveConfigPath(configPath2))) {
24112
24686
  const defaultService = {};
24113
24687
  return [defaultService];
@@ -24129,7 +24703,7 @@ var isCommandService3 = (service) => service.kind === "command" || Array.isArray
24129
24703
  const exitCode = await proc.exited;
24130
24704
  return { exitCode, name, output: (stdout + stderr).trim() };
24131
24705
  }, shellEscape = (value) => `'${value.replaceAll("'", "'\\''")}'`, runShell = async (name, command) => run(name, ["/bin/bash", "-lc", command]), findBin = (name) => {
24132
- const local = resolve45("node_modules", ".bin", name);
24706
+ const local = resolve47("node_modules", ".bin", name);
24133
24707
  return existsSync44(local) ? local : null;
24134
24708
  }, ANSI_COLOR_REGEX, ANSI_PURPLE_REGEX, ANSI_CYAN_REGEX, ANSI_TOKEN_END_REGEX, stripAnsi4 = (str) => str.replace(ANSI_COLOR_REGEX, ""), formatSvelteOutput = (output) => {
24135
24709
  const cwd = `${process.cwd()}/`;
@@ -24177,15 +24751,15 @@ Found ${errorCount} error${suffix}.`;
24177
24751
  return formatted;
24178
24752
  }, ABSOLUTE_INTERNAL_EXCLUDES, resolveAbsoluteTypeFile = (fileName) => {
24179
24753
  const candidates = [
24180
- resolve45("node_modules/@absolutejs/absolute/dist/types", fileName),
24181
- resolve45(import.meta.dir, "../types", fileName),
24182
- resolve45(import.meta.dir, "../../types", fileName),
24183
- resolve45(import.meta.dir, "../../../types", fileName)
24754
+ resolve47("node_modules/@absolutejs/absolute/dist/types", fileName),
24755
+ resolve47(import.meta.dir, "../types", fileName),
24756
+ resolve47(import.meta.dir, "../../types", fileName),
24757
+ resolve47(import.meta.dir, "../../../types", fileName)
24184
24758
  ];
24185
24759
  return candidates.find((candidate) => existsSync44(candidate)) ?? candidates[0];
24186
24760
  }, ABSOLUTE_TYPECHECK_FILES, readProjectTsconfig = () => {
24187
24761
  try {
24188
- return JSON.parse(readFileSync40(resolve45("tsconfig.json"), "utf-8"));
24762
+ return JSON.parse(readFileSync40(resolve47("tsconfig.json"), "utf-8"));
24189
24763
  } catch {
24190
24764
  return {};
24191
24765
  }
@@ -24213,27 +24787,27 @@ Found ${errorCount} error${suffix}.`;
24213
24787
  console.error("\x1B[31m\u2717\x1B[0m vue-tsc is required for Vue type checking. Install it: bun add -d vue-tsc");
24214
24788
  process.exit(1);
24215
24789
  }
24216
- const vueTsconfigPath = join57(cacheDir, "tsconfig.vue-check.json");
24217
- await writeFile19(vueTsconfigPath, JSON.stringify({
24790
+ const vueTsconfigPath = join59(cacheDir, "tsconfig.vue-check.json");
24791
+ await writeFile21(vueTsconfigPath, JSON.stringify({
24218
24792
  compilerOptions: {
24219
24793
  rootDir: ".."
24220
24794
  },
24221
24795
  exclude: getProjectTypecheckExcludes(),
24222
- extends: resolve45("tsconfig.json"),
24796
+ extends: resolve47("tsconfig.json"),
24223
24797
  include: getProjectTypecheckIncludes()
24224
24798
  }, null, "\t"));
24225
24799
  const base = [
24226
24800
  vueTscBin,
24227
24801
  "--noEmit",
24228
24802
  "--project",
24229
- resolve45(vueTsconfigPath),
24803
+ resolve47(vueTsconfigPath),
24230
24804
  "--pretty"
24231
24805
  ];
24232
24806
  const cached = await run("vue-tsc", [
24233
24807
  ...base,
24234
24808
  "--incremental",
24235
24809
  "--tsBuildInfoFile",
24236
- join57(cacheDir, "vue-tsc.tsbuildinfo")
24810
+ join59(cacheDir, "vue-tsc.tsbuildinfo")
24237
24811
  ]);
24238
24812
  if (cached.exitCode === 0 || cached.output.length > 0)
24239
24813
  return cached;
@@ -24244,8 +24818,8 @@ Found ${errorCount} error${suffix}.`;
24244
24818
  console.error("\x1B[31m\u2717\x1B[0m @angular/compiler-cli is required for Angular type checking. Install it: bun add -d @angular/compiler-cli");
24245
24819
  process.exit(1);
24246
24820
  }
24247
- const angularTsconfigPath = join57(cacheDir, "tsconfig.angular-check.json");
24248
- await writeFile19(angularTsconfigPath, JSON.stringify({
24821
+ const angularTsconfigPath = join59(cacheDir, "tsconfig.angular-check.json");
24822
+ await writeFile21(angularTsconfigPath, JSON.stringify({
24249
24823
  angularCompilerOptions: {
24250
24824
  strictTemplates: true
24251
24825
  },
@@ -24254,32 +24828,32 @@ Found ${errorCount} error${suffix}.`;
24254
24828
  rootDir: ".."
24255
24829
  },
24256
24830
  exclude: ABSOLUTE_INTERNAL_EXCLUDES.map(toGeneratedConfigPath),
24257
- extends: resolve45("tsconfig.json"),
24831
+ extends: resolve47("tsconfig.json"),
24258
24832
  include: [`../${angularDir}/**/*`]
24259
24833
  }, null, "\t"));
24260
- return runShell("ngc", `${shellEscape(ngcBin)} -p ${shellEscape(resolve45(angularTsconfigPath))}`);
24834
+ return runShell("ngc", `${shellEscape(ngcBin)} -p ${shellEscape(resolve47(angularTsconfigPath))}`);
24261
24835
  }, buildTscCheck = (cacheDir) => {
24262
24836
  const tscBin = findBin("tsc");
24263
24837
  if (!tscBin) {
24264
24838
  console.error("\x1B[31m\u2717\x1B[0m typescript is required for type checking. Install it: bun add -d typescript");
24265
24839
  process.exit(1);
24266
24840
  }
24267
- const tscConfigPath = join57(cacheDir, "tsconfig.typecheck.json");
24268
- return writeFile19(tscConfigPath, JSON.stringify({
24841
+ const tscConfigPath = join59(cacheDir, "tsconfig.typecheck.json");
24842
+ return writeFile21(tscConfigPath, JSON.stringify({
24269
24843
  compilerOptions: {
24270
24844
  rootDir: ".."
24271
24845
  },
24272
24846
  exclude: getProjectTypecheckExcludes(),
24273
- extends: resolve45("tsconfig.json"),
24847
+ extends: resolve47("tsconfig.json"),
24274
24848
  include: getProjectTypecheckIncludes()
24275
24849
  }, null, "\t")).then(() => run("tsc", [
24276
24850
  tscBin,
24277
24851
  "--noEmit",
24278
24852
  "--project",
24279
- resolve45(tscConfigPath),
24853
+ resolve47(tscConfigPath),
24280
24854
  "--incremental",
24281
24855
  "--tsBuildInfoFile",
24282
- join57(cacheDir, "tsc.tsbuildinfo"),
24856
+ join59(cacheDir, "tsc.tsbuildinfo"),
24283
24857
  "--pretty"
24284
24858
  ]));
24285
24859
  }, buildSvelteCheck = async (cacheDir, svelteDir) => {
@@ -24288,16 +24862,16 @@ Found ${errorCount} error${suffix}.`;
24288
24862
  console.error("\x1B[31m\u2717\x1B[0m svelte-check is required for Svelte type checking. Install it: bun add -d svelte-check");
24289
24863
  process.exit(1);
24290
24864
  }
24291
- const svelteTsconfigPath = join57(cacheDir, "tsconfig.svelte-check.json");
24292
- await writeFile19(svelteTsconfigPath, JSON.stringify({
24293
- extends: resolve45("tsconfig.json"),
24865
+ const svelteTsconfigPath = join59(cacheDir, "tsconfig.svelte-check.json");
24866
+ await writeFile21(svelteTsconfigPath, JSON.stringify({
24867
+ extends: resolve47("tsconfig.json"),
24294
24868
  files: ABSOLUTE_TYPECHECK_FILES,
24295
24869
  include: [`../${svelteDir}/**/*`]
24296
24870
  }, null, "\t"));
24297
24871
  return run("svelte-check", [
24298
24872
  svelteBin,
24299
24873
  "--tsconfig",
24300
- resolve45(svelteTsconfigPath),
24874
+ resolve47(svelteTsconfigPath),
24301
24875
  "--threshold",
24302
24876
  "error",
24303
24877
  "--compiler-warnings",
@@ -24318,7 +24892,7 @@ Found ${errorCount} error${suffix}.`;
24318
24892
  ...new Set(targets.map((config) => config.angularDirectory).filter((dir) => typeof dir === "string" && dir.length > 0))
24319
24893
  ];
24320
24894
  const cacheDir = ".absolutejs";
24321
- await mkdir17(cacheDir, { recursive: true });
24895
+ await mkdir18(cacheDir, { recursive: true });
24322
24896
  const checks = [];
24323
24897
  checks.push(hasVue ? buildVueTscCheck(cacheDir) : buildTscCheck(cacheDir));
24324
24898
  for (const svelteDir of hasSvelte ? svelteDirs : []) {
@@ -24491,11 +25065,11 @@ var DEFAULT_RELAY_PORT = 8787, DEFAULT_REQUEST_TIMEOUT_MS = 30000, headersToObje
24491
25065
  url: url.pathname + url.search,
24492
25066
  ...bodyBytes && bodyBytes.length > 0 ? { bodyBase64: Buffer.from(bodyBytes).toString("base64") } : {}
24493
25067
  };
24494
- const responsePromise = new Promise((resolve46) => {
24495
- pending.set(id, resolve46);
25068
+ const responsePromise = new Promise((resolve48) => {
25069
+ pending.set(id, resolve48);
24496
25070
  });
24497
25071
  client.send(encodeTunnelMessage(message));
24498
- const timeout = new Promise((resolve46) => setTimeout(() => resolve46({ id, message: "timeout", type: "error" }), requestTimeoutMs));
25072
+ const timeout = new Promise((resolve48) => setTimeout(() => resolve48({ id, message: "timeout", type: "error" }), requestTimeoutMs));
24499
25073
  const result = await Promise.race([responsePromise, timeout]);
24500
25074
  pending.delete(id);
24501
25075
  if (result.type === "error") {