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

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
@@ -719,7 +719,7 @@ var init_portScan = () => {};
719
719
  // src/mobile/config.ts
720
720
  import { resolve as resolve2 } from "path";
721
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
+ var APP_ID_PATTERN, SCHEME_PATTERN, APPLE_APP_ID_PREFIX_PATTERN, CERTIFICATE_FINGERPRINT_PATTERN, UPDATE_NAME_PATTERN, UPDATE_PUBLIC_KEY_PATTERN, DEFAULT_UPDATE_BOOT_TIMEOUT_MS = 20000, MINIMUM_UPDATE_BOOT_TIMEOUT_MS = 5000, MAXIMUM_UPDATE_BOOT_TIMEOUT_MS = 120000, HOSTNAME_PATTERN, EXPO_RESERVED_ROUTE_PREFIXES, resolveProjectPath = (projectRoot, value, field) => {
723
723
  const root = resolve2(projectRoot);
724
724
  const path = resolve2(root, value);
725
725
  if (path !== root && !path.startsWith(`${root}/`)) {
@@ -795,6 +795,9 @@ var APP_ID_PATTERN, SCHEME_PATTERN, APPLE_APP_ID_PREFIX_PATTERN, CERTIFICATE_FIN
795
795
  ].sort(), normalizeUpdates = (config, productionOrigin) => {
796
796
  if (!config.updates)
797
797
  return;
798
+ const bootTimeoutMs = config.updates.bootTimeoutMs ?? DEFAULT_UPDATE_BOOT_TIMEOUT_MS;
799
+ if (!Number.isSafeInteger(bootTimeoutMs) || bootTimeoutMs < MINIMUM_UPDATE_BOOT_TIMEOUT_MS || bootTimeoutMs > MAXIMUM_UPDATE_BOOT_TIMEOUT_MS)
800
+ throw new TypeError("mobile.updates.bootTimeoutMs must be an integer from 5000 through 120000.");
798
801
  const channel = requireText(config.updates.channel ?? "production", "mobile.updates.channel");
799
802
  if (!UPDATE_NAME_PATTERN.test(channel))
800
803
  throw new TypeError("mobile.updates.channel contains unsupported characters.");
@@ -836,7 +839,12 @@ var APP_ID_PATTERN, SCHEME_PATTERN, APPLE_APP_ID_PREFIX_PATTERN, CERTIFICATE_FIN
836
839
  throw new TypeError(`mobile.updates.publicKeys.${keyId} is not an ECDSA P-256 SPKI public key.`);
837
840
  return [keyId, normalized];
838
841
  }));
839
- return { channel, manifestUrl: manifestUrl.href, publicKeys };
842
+ return {
843
+ bootTimeoutMs,
844
+ channel,
845
+ manifestUrl: manifestUrl.href,
846
+ publicKeys
847
+ };
840
848
  }, validateExpoNativeRouteSegment = (path, segment, index, count, parameters) => {
841
849
  if (segment === "*" && (index !== count - 1 || count === 1)) {
842
850
  throw new TypeError(`mobile.routes.native route ${path} must use * once, as the final segment after a static or parameterized prefix.`);
@@ -9660,7 +9668,7 @@ var init_updateProtocol = __esm(() => {
9660
9668
 
9661
9669
  // src/mobile/updateRuntime.ts
9662
9670
  import { createHash as createHash12 } from "crypto";
9663
- var ABSOLUTE_MOBILE_SHELL_ABI = 1, ABSOLUTE_MOBILE_UPDATE_RUNTIME_FORMAT = 1, createAbsoluteMobileUpdateRuntimeDescriptor = (options) => ({
9671
+ var ABSOLUTE_MOBILE_SHELL_ABI = 2, ABSOLUTE_MOBILE_UPDATE_RUNTIME_FORMAT = 1, createAbsoluteMobileUpdateRuntimeDescriptor = (options) => ({
9664
9672
  appId: options.config.appId,
9665
9673
  auth: options.auth ?? null,
9666
9674
  deepLinks: {
@@ -19664,70 +19672,490 @@ var init_nativeBackgroundSync = __esm(() => {
19664
19672
  });
19665
19673
 
19666
19674
  // 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");
19675
+ import {
19676
+ mkdir as mkdir11,
19677
+ readFile as readFile16,
19678
+ readdir as readdir5,
19679
+ rename as rename12,
19680
+ rm as rm9,
19681
+ writeFile as writeFile13
19682
+ } from "fs/promises";
19683
+ import { dirname as dirname29, join as join51 } from "path";
19684
+ var START = "// absolutejs:mobile-updates:start", END = "// absolutejs:mobile-updates:end", PLUGIN_START = "// absolutejs:mobile-update-plugin:start", PLUGIN_END = "// absolutejs:mobile-update-plugin:end", ANDROID_PLUGIN = "AbsoluteMobileUpdateWatchdogPlugin.java", optionalSource2 = async (path) => {
19685
+ try {
19686
+ return await readFile16(path, "utf8");
19687
+ } catch (error) {
19688
+ if (typeof error === "object" && error !== null && Reflect.get(error, "code") === "ENOENT")
19689
+ return null;
19690
+ throw error;
19691
+ }
19692
+ }, isRecord14 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), writeChanged2 = async (path, source) => {
19693
+ const current = await optionalSource2(path);
19671
19694
  if (current === source)
19672
19695
  return false;
19696
+ await mkdir11(dirname29(path), { recursive: true });
19697
+ if (current === null) {
19698
+ await writeFile13(path, source, { flag: "wx" });
19699
+ return true;
19700
+ }
19673
19701
  const temporary = `${path}.${crypto.randomUUID()}.tmp`;
19674
19702
  await writeFile13(temporary, source, { flag: "wx" });
19675
19703
  await rename12(temporary, path);
19676
19704
  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)
19705
+ }, removeOwnedFile = async (path) => {
19706
+ if (await optionalSource2(path) === null)
19707
+ return false;
19708
+ await rm9(path);
19709
+ return true;
19710
+ }, replaceMarkedRegion = (source, start2, end, region, insertion, error) => {
19711
+ const existingStart = source.indexOf(start2);
19712
+ const existingEnd = source.indexOf(end);
19713
+ if (existingStart < 0 !== existingEnd < 0 || existingStart >= 0 && existingEnd < existingStart)
19681
19714
  throw new TypeError("AbsoluteJS mobile update markers are malformed.");
19682
- if (start2 >= 0) {
19715
+ if (existingStart >= 0) {
19683
19716
  const from = source.lastIndexOf(`
19684
- `, start2) + 1;
19717
+ `, existingStart) + 1;
19685
19718
  const newline = source.indexOf(`
19686
- `, end + END.length);
19719
+ `, existingEnd + end.length);
19687
19720
  return `${source.slice(0, from)}${region}${source.slice(newline < 0 ? source.length : newline + 1)}`;
19688
19721
  }
19689
19722
  if (!region)
19690
19723
  return source;
19724
+ if (insertion < 0)
19725
+ throw new TypeError(error);
19726
+ return `${source.slice(0, insertion)}${region}${source.slice(insertion)}`;
19727
+ }, iosPluginRegion = (timeoutMs) => `${PLUGIN_START}
19728
+ @objc(AbsoluteMobileUpdateWatchdogPlugin)
19729
+ public final class AbsoluteMobileUpdateWatchdogPlugin: CAPPlugin, CAPBridgedPlugin {
19730
+ public let identifier = "AbsoluteMobileUpdateWatchdogPlugin"
19731
+ public let jsName = "AbsoluteMobileUpdateWatchdog"
19732
+ public let pluginMethods: [CAPPluginMethod] = [
19733
+ CAPPluginMethod(name: "arm", returnType: CAPPluginReturnPromise),
19734
+ CAPPluginMethod(name: "confirm", returnType: CAPPluginReturnPromise)
19735
+ ]
19736
+
19737
+ private static let stateKey = "CapacitorStorage.absolute.mobile.update.state.v1"
19738
+ private static let releasePattern = try! NSRegularExpression(pattern: "^amu_[a-f0-9]{64}$")
19739
+ private var deadline: DispatchWorkItem?
19740
+
19741
+ private static func state() -> [String: Any] {
19742
+ guard let encoded = UserDefaults.standard.string(forKey: stateKey),
19743
+ let data = encoded.data(using: .utf8),
19744
+ let object = try? JSONSerialization.jsonObject(with: data),
19745
+ let value = object as? [String: Any] else { return [:] }
19746
+ return value
19747
+ }
19748
+
19749
+ private static func write(_ state: [String: Any]) {
19750
+ guard let data = try? JSONSerialization.data(withJSONObject: state),
19751
+ let encoded = String(data: data, encoding: .utf8) else { return }
19752
+ UserDefaults.standard.set(encoded, forKey: stateKey)
19753
+ }
19754
+
19755
+ private static func validRelease(_ value: String) -> Bool {
19756
+ releasePattern.firstMatch(in: value, range: NSRange(value.startIndex..., in: value)) != nil
19757
+ }
19758
+
19759
+ private static func snapshotRoot() -> URL? {
19760
+ FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first?
19761
+ .appendingPathComponent("NoCloud/ionic_built_snapshots", isDirectory: true)
19762
+ }
19763
+
19764
+ @discardableResult
19765
+ private static func recover(_ reason: String) -> (String, String?, Bool)? {
19766
+ var value = state()
19767
+ guard let release = value["pendingRelease"] as? String, validRelease(release) else { return nil }
19768
+ let previous = value["previousPath"] as? String
19769
+ let active = value["activeRelease"] as? String
19770
+ let hasActive = active.map(validRelease) ?? false
19771
+ let started = value["pendingStartedAt"] as? Double ?? Date().timeIntervalSince1970 * 1000
19772
+ let duration = max(0, Date().timeIntervalSince1970 * 1000 - started)
19773
+ value.removeValue(forKey: "pendingRelease")
19774
+ value.removeValue(forKey: "pendingStartedAt")
19775
+ value.removeValue(forKey: "previousPath")
19776
+ value.removeValue(forKey: "readyRelease")
19777
+ var quarantined = value["quarantinedReleases"] as? [String] ?? []
19778
+ quarantined.removeAll(where: { $0 == release })
19779
+ quarantined.append(release)
19780
+ value["quarantinedReleases"] = Array(quarantined.suffix(8))
19781
+ value["recovery"] = ["durationMs": duration, "reason": reason, "releaseId": release]
19782
+ write(value)
19783
+
19784
+ if hasActive, let previous, let root = snapshotRoot(),
19785
+ URL(fileURLWithPath: previous).standardizedFileURL.path.hasPrefix(root.standardizedFileURL.path + "/") {
19786
+ UserDefaults.standard.set(previous, forKey: "serverBasePath")
19787
+ } else {
19788
+ UserDefaults.standard.removeObject(forKey: "serverBasePath")
19789
+ }
19790
+ if let root = snapshotRoot() {
19791
+ try? FileManager.default.removeItem(at: root.appendingPathComponent(release, isDirectory: true))
19792
+ }
19793
+
19794
+ return (release, previous, hasActive)
19795
+ }
19796
+
19797
+ public static func recoverInterruptedBoot() {
19798
+ if recover("boot-interrupted") != nil { return }
19799
+ guard let persisted = UserDefaults.standard.string(forKey: "serverBasePath"),
19800
+ let root = snapshotRoot() else { return }
19801
+ let path = URL(fileURLWithPath: persisted).standardizedFileURL.path
19802
+ if path.hasPrefix(root.standardizedFileURL.path + "/") &&
19803
+ !FileManager.default.fileExists(atPath: path) {
19804
+ UserDefaults.standard.removeObject(forKey: "serverBasePath")
19805
+ }
19806
+ }
19807
+
19808
+ @objc public func arm(_ call: CAPPluginCall) {
19809
+ guard let requested = call.getString("releaseId"),
19810
+ Self.validRelease(requested),
19811
+ Self.state()["pendingRelease"] as? String == requested else {
19812
+ call.reject("Mobile update watchdog cannot arm an unknown release.")
19813
+ return
19814
+ }
19815
+ deadline?.cancel()
19816
+ let work = DispatchWorkItem { [weak self] in
19817
+ guard let self, let recovery = Self.recover("boot-timeout") else { return }
19818
+ if recovery.2, let previous = recovery.1,
19819
+ let root = Self.snapshotRoot(),
19820
+ URL(fileURLWithPath: previous).standardizedFileURL.path.hasPrefix(root.standardizedFileURL.path + "/") {
19821
+ (self.bridge?.viewController as? CAPBridgeViewController)?.setServerBasePath(path: previous)
19822
+ } else if let embedded = Bundle.main.url(forResource: "public", withExtension: nil)?.path {
19823
+ (self.bridge?.viewController as? CAPBridgeViewController)?.setServerBasePath(path: embedded)
19824
+ }
19825
+ }
19826
+ deadline = work
19827
+ DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(${timeoutMs}), execute: work)
19828
+ call.resolve()
19829
+ }
19830
+
19831
+ @objc public func confirm(_ call: CAPPluginCall) {
19832
+ deadline?.cancel()
19833
+ deadline = nil
19834
+ call.resolve()
19835
+ }
19836
+ }
19837
+ ${PLUGIN_END}
19838
+ `, configureIos3 = async (config, enabled) => {
19839
+ const path = join51(config.nativeProjectDirectory, "ios/App/App/AppDelegate.swift");
19840
+ const capacitorConfigPath = join51(config.nativeProjectDirectory, "ios/App/App/capacitor.config.json");
19841
+ let source = await readFile16(path, "utf8");
19691
19842
  const launch = source.indexOf("didFinishLaunchingWithOptions");
19692
19843
  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")
19844
+ const launchRegion = enabled ? ` ${START}
19845
+ AbsoluteMobileUpdateWatchdogPlugin.recoverInterruptedBoot()
19846
+ ${END}
19847
+ ` : "";
19848
+ source = replaceMarkedRegion(source, START, END, launchRegion, brace < 0 ? -1 : source.indexOf(`
19849
+ `, brace) + 1, "Could not find a safe iOS location for mobile update recovery.");
19850
+ const plugin = enabled ? iosPluginRegion(config.updates?.bootTimeoutMs ?? 20000) : "";
19851
+ source = replaceMarkedRegion(source, PLUGIN_START, PLUGIN_END, plugin, source.length, "Could not find a safe iOS location for the mobile update watchdog.");
19852
+ const capacitorConfigSource2 = await readFile16(capacitorConfigPath, "utf8");
19853
+ let capacitorConfig;
19854
+ try {
19855
+ const value = JSON.parse(capacitorConfigSource2);
19856
+ if (!isRecord14(value))
19857
+ throw new TypeError;
19858
+ capacitorConfig = value;
19859
+ } catch {
19860
+ throw new TypeError("iOS capacitor.config.json is invalid; run Capacitor sync before projecting mobile updates.");
19861
+ }
19862
+ const packageClassList = Reflect.get(capacitorConfig, "packageClassList");
19863
+ if (!Array.isArray(packageClassList))
19864
+ throw new TypeError("iOS capacitor.config.json has no packageClassList; run Capacitor sync before projecting mobile updates.");
19865
+ const classes = packageClassList.filter((value) => typeof value === "string");
19866
+ const nextClasses = enabled ? [...new Set([...classes, "AbsoluteMobileUpdateWatchdogPlugin"])] : classes.filter((value) => value !== "AbsoluteMobileUpdateWatchdogPlugin");
19867
+ const nextCapacitorConfig = `${JSON.stringify({ ...capacitorConfig, packageClassList: nextClasses }, null, "\t")}
19868
+ `;
19869
+ const changed = await Promise.all([
19870
+ writeChanged2(path, source),
19871
+ writeChanged2(capacitorConfigPath, nextCapacitorConfig)
19872
+ ]);
19873
+ return changed.some(Boolean);
19874
+ }, androidPluginSource = (packageName, timeoutMs) => `package ${packageName};
19875
+
19876
+ import android.app.Activity;
19877
+ import android.content.Context;
19878
+ import android.content.SharedPreferences;
19879
+ import android.os.Handler;
19880
+ import android.os.Looper;
19881
+ import com.getcapacitor.JSObject;
19882
+ import com.getcapacitor.Plugin;
19883
+ import com.getcapacitor.PluginCall;
19884
+ import com.getcapacitor.PluginMethod;
19885
+ import com.getcapacitor.annotation.CapacitorPlugin;
19886
+ import java.io.File;
19887
+ import org.json.JSONArray;
19888
+ import org.json.JSONObject;
19889
+
19890
+ @CapacitorPlugin(name = "AbsoluteMobileUpdateWatchdog")
19891
+ public final class AbsoluteMobileUpdateWatchdogPlugin extends Plugin {
19892
+ private static final String STATE_KEY = "absolute.mobile.update.state.v1";
19893
+ private static final String RELEASE_PATTERN = "^amu_[a-f0-9]{64}$";
19894
+ private final Handler handler = new Handler(Looper.getMainLooper());
19895
+ private Runnable deadline;
19896
+
19897
+ private static SharedPreferences statePreferences(Context context) {
19898
+ return context.getSharedPreferences("CapacitorStorage", Activity.MODE_PRIVATE);
19899
+ }
19900
+
19901
+ private static JSONObject state(Context context) {
19902
+ try {
19903
+ return new JSONObject(statePreferences(context).getString(STATE_KEY, "{}"));
19904
+ } catch (Exception ignored) {
19905
+ return new JSONObject();
19906
+ }
19907
+ }
19908
+
19909
+ private static void write(Context context, JSONObject state) {
19910
+ statePreferences(context).edit().putString(STATE_KEY, state.toString()).commit();
19911
+ }
19912
+
19913
+ private static File snapshotRoot(Context context) {
19914
+ return new File(context.getFilesDir(), "NoCloud/ionic_built_snapshots");
19915
+ }
19916
+
19917
+ private static boolean validRelease(String release) {
19918
+ return release != null && release.matches(RELEASE_PATTERN);
19919
+ }
19920
+
19921
+ private static boolean inside(File root, String path) {
19922
+ try {
19923
+ String base = root.getCanonicalPath() + File.separator;
19924
+ return new File(path).getCanonicalPath().startsWith(base);
19925
+ } catch (Exception ignored) {
19926
+ return false;
19927
+ }
19928
+ }
19929
+
19930
+ private static void deleteTree(File file) {
19931
+ File[] children = file.listFiles();
19932
+ if (children != null) for (File child : children) deleteTree(child);
19933
+ file.delete();
19934
+ }
19935
+
19936
+ private static Recovery recover(Context context, String reason) {
19937
+ JSONObject value = state(context);
19938
+ String release = value.optString("pendingRelease", "");
19939
+ if (!validRelease(release)) return null;
19940
+ String previous = value.optString("previousPath", "");
19941
+ boolean hasActive = validRelease(value.optString("activeRelease", ""));
19942
+ long started = value.optLong("pendingStartedAt", System.currentTimeMillis());
19943
+ long duration = Math.max(0, System.currentTimeMillis() - started);
19944
+ value.remove("pendingRelease");
19945
+ value.remove("pendingStartedAt");
19946
+ value.remove("previousPath");
19947
+ value.remove("readyRelease");
19948
+ try {
19949
+ JSONArray prior = value.optJSONArray("quarantinedReleases");
19950
+ JSONArray quarantined = new JSONArray();
19951
+ if (prior != null) {
19952
+ int start = Math.max(0, prior.length() - 7);
19953
+ for (int index = start; index < prior.length(); index++) {
19954
+ String candidate = prior.optString(index, "");
19955
+ if (validRelease(candidate) && !release.equals(candidate)) quarantined.put(candidate);
19956
+ }
19718
19957
  }
19958
+ quarantined.put(release);
19959
+ value.put("quarantinedReleases", quarantined);
19960
+ value.put("recovery", new JSONObject()
19961
+ .put("durationMs", duration)
19962
+ .put("reason", reason)
19963
+ .put("releaseId", release));
19964
+ } catch (Exception ignored) {}
19965
+ write(context, value);
19966
+
19967
+ SharedPreferences webView = context.getSharedPreferences("CapWebViewSettings", Activity.MODE_PRIVATE);
19968
+ if (hasActive && inside(snapshotRoot(context), previous)) {
19969
+ webView.edit().putString("serverBasePath", previous).commit();
19970
+ } else {
19971
+ webView.edit().remove("serverBasePath").commit();
19972
+ }
19973
+ deleteTree(new File(snapshotRoot(context), release));
19974
+ return new Recovery(previous, hasActive);
19975
+ }
19976
+
19977
+ public static void recoverInterruptedBoot(Context context) {
19978
+ recover(context, "boot-interrupted");
19979
+ }
19980
+
19981
+ @PluginMethod
19982
+ public void arm(PluginCall call) {
19983
+ String requested = call.getString("releaseId");
19984
+ if (!validRelease(requested) || !requested.equals(state(getContext()).optString("pendingRelease", ""))) {
19985
+ call.reject("Mobile update watchdog cannot arm an unknown release.");
19986
+ return;
19719
19987
  }
19988
+ if (deadline != null) handler.removeCallbacks(deadline);
19989
+ deadline = () -> {
19990
+ Recovery recovery = recover(getContext(), "boot-timeout");
19991
+ if (recovery == null || bridge == null) return;
19992
+ if (recovery.hasActive && inside(snapshotRoot(getContext()), recovery.previousPath)) {
19993
+ bridge.setServerBasePath(recovery.previousPath);
19994
+ } else {
19995
+ bridge.setServerAssetPath("public");
19996
+ }
19997
+ };
19998
+ handler.postDelayed(deadline, ${timeoutMs}L);
19999
+ call.resolve(new JSObject());
20000
+ }
20001
+
20002
+ @PluginMethod
20003
+ public void confirm(PluginCall call) {
20004
+ if (deadline != null) handler.removeCallbacks(deadline);
20005
+ deadline = null;
20006
+ call.resolve(new JSObject());
20007
+ }
20008
+
20009
+ private static final class Recovery {
20010
+ final String previousPath;
20011
+ final boolean hasActive;
20012
+ Recovery(String previousPath, boolean hasActive) {
20013
+ this.previousPath = previousPath;
20014
+ this.hasActive = hasActive;
20015
+ }
20016
+ }
20017
+ }
20018
+ `, androidActivityInRoot = async (root) => {
20019
+ let entries;
20020
+ try {
20021
+ entries = await readdir5(root, { recursive: true });
20022
+ } catch (error) {
20023
+ if (typeof error === "object" && error !== null && Reflect.get(error, "code") === "ENOENT")
20024
+ return;
20025
+ throw error;
20026
+ }
20027
+ const candidate = entries.find((entry) => entry.endsWith("/MainActivity.java") || entry.endsWith("/MainActivity.kt") || entry === "MainActivity.java" || entry === "MainActivity.kt");
20028
+ return candidate ? join51(root, candidate) : undefined;
20029
+ }, androidActivity = async (config) => {
20030
+ const roots = [
20031
+ join51(config.nativeProjectDirectory, "android/app/src/main/java"),
20032
+ join51(config.nativeProjectDirectory, "android/app/src/main/kotlin")
20033
+ ];
20034
+ const candidates = await Promise.all(roots.map(androidActivityInRoot));
20035
+ const candidate = candidates.find((value) => value !== undefined);
20036
+ if (candidate)
20037
+ return candidate;
20038
+ throw new TypeError("Android MainActivity.java or MainActivity.kt was not found.");
20039
+ }, androidManagedRegion = (kotlin, generatedMethod) => {
20040
+ if (generatedMethod)
20041
+ return kotlin ? ` ${START}
20042
+ override fun onCreate(savedInstanceState: android.os.Bundle?) {
20043
+ AbsoluteMobileUpdateWatchdogPlugin.recoverInterruptedBoot(this)
20044
+ registerPlugin(AbsoluteMobileUpdateWatchdogPlugin::class.java)
20045
+ super.onCreate(savedInstanceState)
20046
+ }
20047
+ ${END}
20048
+ ` : ` ${START}
20049
+ @Override
20050
+ public void onCreate(android.os.Bundle savedInstanceState) {
20051
+ AbsoluteMobileUpdateWatchdogPlugin.recoverInterruptedBoot(this);
20052
+ registerPlugin(AbsoluteMobileUpdateWatchdogPlugin.class);
20053
+ super.onCreate(savedInstanceState);
20054
+ }
20055
+ ${END}
20056
+ `;
20057
+ return kotlin ? ` ${START}
20058
+ AbsoluteMobileUpdateWatchdogPlugin.recoverInterruptedBoot(this)
20059
+ registerPlugin(AbsoluteMobileUpdateWatchdogPlugin::class.java)
20060
+ ${END}
20061
+ ` : ` ${START}
20062
+ AbsoluteMobileUpdateWatchdogPlugin.recoverInterruptedBoot(this);
20063
+ registerPlugin(AbsoluteMobileUpdateWatchdogPlugin.class);
19720
20064
  ${END}
19721
20065
  `;
19722
- });
20066
+ }, injectAndroidActivity = (source, enabled) => {
20067
+ const kotlin = /\bfun\s+onCreate\s*\(/u.test(source) || source.includes("BridgeActivity()");
20068
+ const existingStart = source.indexOf(START);
20069
+ const existingEnd = source.indexOf(END);
20070
+ if (existingStart < 0 !== existingEnd < 0)
20071
+ throw new TypeError("AbsoluteJS mobile update markers are malformed.");
20072
+ if (existingStart >= 0) {
20073
+ const from = source.lastIndexOf(`
20074
+ `, existingStart) + 1;
20075
+ const newline = source.indexOf(`
20076
+ `, existingEnd + END.length);
20077
+ const through = newline < 0 ? source.length : newline + 1;
20078
+ const owned = source.slice(from, through);
20079
+ const generatedMethod = owned.includes("onCreate(");
20080
+ if (!enabled)
20081
+ return `${source.slice(0, from)}${source.slice(through)}`;
20082
+ const region2 = androidManagedRegion(kotlin, generatedMethod);
20083
+ return `${source.slice(0, from)}${region2}${source.slice(through)}`;
20084
+ }
20085
+ if (!enabled)
20086
+ return source;
20087
+ const onCreate = kotlin ? source.search(/\boverride\s+fun\s+onCreate\s*\([^)]*\)\s*\{/u) : source.search(/\b(?:public|protected)\s+void\s+onCreate\s*\([^)]*\)\s*\{/u);
20088
+ if (onCreate >= 0) {
20089
+ const brace = source.indexOf("{", onCreate);
20090
+ const insert = source.indexOf(`
20091
+ `, brace) + 1;
20092
+ const region2 = kotlin ? ` ${START}
20093
+ AbsoluteMobileUpdateWatchdogPlugin.recoverInterruptedBoot(this)
20094
+ registerPlugin(AbsoluteMobileUpdateWatchdogPlugin::class.java)
20095
+ ${END}
20096
+ ` : ` ${START}
20097
+ AbsoluteMobileUpdateWatchdogPlugin.recoverInterruptedBoot(this);
20098
+ registerPlugin(AbsoluteMobileUpdateWatchdogPlugin.class);
20099
+ ${END}
20100
+ `;
20101
+ return `${source.slice(0, insert)}${region2}${source.slice(insert)}`;
20102
+ }
20103
+ const close = source.lastIndexOf("}");
20104
+ if (close < 0)
20105
+ throw new TypeError("Could not find the Android MainActivity class body.");
20106
+ const region = kotlin ? ` ${START}
20107
+ override fun onCreate(savedInstanceState: android.os.Bundle?) {
20108
+ AbsoluteMobileUpdateWatchdogPlugin.recoverInterruptedBoot(this)
20109
+ registerPlugin(AbsoluteMobileUpdateWatchdogPlugin::class.java)
20110
+ super.onCreate(savedInstanceState)
20111
+ }
20112
+ ${END}
20113
+ ` : ` ${START}
20114
+ @Override
20115
+ public void onCreate(android.os.Bundle savedInstanceState) {
20116
+ AbsoluteMobileUpdateWatchdogPlugin.recoverInterruptedBoot(this);
20117
+ registerPlugin(AbsoluteMobileUpdateWatchdogPlugin.class);
20118
+ super.onCreate(savedInstanceState);
20119
+ }
20120
+ ${END}
20121
+ `;
20122
+ const separator = source[close - 1] === `
20123
+ ` ? "" : `
20124
+ `;
20125
+ return `${source.slice(0, close)}${separator}${region}${source.slice(close)}`;
20126
+ }, configureAndroid3 = async (config, enabled) => {
20127
+ const activityPath = await androidActivity(config);
20128
+ const activity = await readFile16(activityPath, "utf8");
20129
+ const packageName = activity.match(/^\s*package\s+([A-Za-z0-9_.]+)\s*[;\n]/mu)?.[1];
20130
+ if (!packageName)
20131
+ throw new TypeError("Android MainActivity package declaration was not found.");
20132
+ const packagePath = packageName.replaceAll(".", "/");
20133
+ const pluginPath = join51(config.nativeProjectDirectory, "android/app/src/main/java", packagePath, ANDROID_PLUGIN);
20134
+ const changed = await Promise.all([
20135
+ writeChanged2(activityPath, injectAndroidActivity(activity, enabled)),
20136
+ enabled ? writeChanged2(pluginPath, androidPluginSource(packageName, config.updates?.bootTimeoutMs ?? 20000)) : removeOwnedFile(pluginPath)
20137
+ ]);
20138
+ return changed.some(Boolean);
20139
+ }, applyAbsoluteNativeUpdates = async (config, platforms = config.platforms) => {
20140
+ if (config.engine !== "capacitor")
20141
+ return { changed: false };
20142
+ const enabled = config.updates !== undefined;
20143
+ const changed = [];
20144
+ if (platforms.includes("ios"))
20145
+ changed.push(await configureIos3(config, enabled));
20146
+ if (platforms.includes("android"))
20147
+ changed.push(await configureAndroid3(config, enabled));
20148
+ return { changed: changed.some(Boolean) };
20149
+ };
20150
+ var init_nativeUpdates = () => {};
19723
20151
 
19724
20152
  // src/mobile/associationFiles.ts
19725
20153
  import {
19726
20154
  access as access9,
19727
- mkdir as mkdir11,
20155
+ mkdir as mkdir12,
19728
20156
  readFile as readFile17,
19729
20157
  rename as rename13,
19730
- rm as rm9,
20158
+ rm as rm10,
19731
20159
  writeFile as writeFile14
19732
20160
  } from "fs/promises";
19733
20161
  import { resolve as resolve37 } from "path";
@@ -19828,10 +20256,10 @@ var OWNERSHIP_FILE = ".absolutejs-mobile-associations.json", HTTP_OK = 200, VERI
19828
20256
  throw error;
19829
20257
  }
19830
20258
  if (hasCurrent)
19831
- await rm9(backup, { force: true, recursive: true });
20259
+ await rm10(backup, { force: true, recursive: true });
19832
20260
  }, materializeHost = async (root, host2, files) => {
19833
20261
  const directory = resolve37(root, host2, ".well-known");
19834
- await mkdir11(directory, { recursive: true });
20262
+ await mkdir12(directory, { recursive: true });
19835
20263
  return Promise.all(files.map(async ([name, document]) => {
19836
20264
  const path = resolve37(directory, name);
19837
20265
  await writeAtomic(path, `${JSON.stringify(document, null, 2)}
@@ -19867,7 +20295,7 @@ var OWNERSHIP_FILE = ".absolutejs-mobile-associations.json", HTTP_OK = 200, VERI
19867
20295
  if (documents.apple) {
19868
20296
  files.push(["apple-app-site-association", documents.apple]);
19869
20297
  }
19870
- await mkdir11(temporary, { recursive: true });
20298
+ await mkdir12(temporary, { recursive: true });
19871
20299
  try {
19872
20300
  const temporaryPaths = (await Promise.all(config.deepLinkHosts.map((host2) => materializeHost(temporary, host2, files)))).flat();
19873
20301
  await writeAtomic(resolve37(temporary, OWNERSHIP_FILE), `${JSON.stringify({ format: 1, hosts: config.deepLinkHosts }, null, 2)}
@@ -19876,7 +20304,7 @@ var OWNERSHIP_FILE = ".absolutejs-mobile-associations.json", HTTP_OK = 200, VERI
19876
20304
  const written = temporaryPaths.map((path) => resolve37(root, path.slice(temporary.length + 1)));
19877
20305
  return { root, written };
19878
20306
  } catch (error) {
19879
- await rm9(temporary, { force: true, recursive: true });
20307
+ await rm10(temporary, { force: true, recursive: true });
19880
20308
  throw error;
19881
20309
  }
19882
20310
  }, verifyAbsoluteMobileAssociationFiles = async (config, request = globalThis.fetch) => {
@@ -19914,8 +20342,8 @@ var init_associationFiles = __esm(() => {
19914
20342
  });
19915
20343
 
19916
20344
  // src/mobile/androidWebView.ts
19917
- import { mkdir as mkdir12, writeFile as writeFile15 } from "fs/promises";
19918
- import { dirname as dirname29, resolve as resolve38 } from "path";
20345
+ import { mkdir as mkdir13, writeFile as writeFile15 } from "fs/promises";
20346
+ import { dirname as dirname30, resolve as resolve38 } from "path";
19919
20347
 
19920
20348
  class CdpConnection {
19921
20349
  #closed;
@@ -20189,7 +20617,7 @@ var CDP_COMMAND_TIMEOUT_MS = 1e4, WEBVIEW_ATTACH_TIMEOUT_MS = 30000, WEBVIEW_POL
20189
20617
  throw new Error("Android WebView screenshot returned no image data.");
20190
20618
  }
20191
20619
  const absolutePath = resolve38(path);
20192
- await mkdir12(dirname29(absolutePath), { recursive: true });
20620
+ await mkdir13(dirname30(absolutePath), { recursive: true });
20193
20621
  await writeFile15(absolutePath, Buffer.from(data, "base64"));
20194
20622
  return absolutePath;
20195
20623
  },
@@ -20473,8 +20901,8 @@ var init_mobileBundleInspection = __esm(() => {
20473
20901
  });
20474
20902
 
20475
20903
  // src/mobile/releaseDoctor.ts
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";
20904
+ import { access as access11, readFile as readFile19, readdir as readdir6 } from "fs/promises";
20905
+ import { dirname as dirname31, extname as extname8, join as join53, relative as relative26 } from "path";
20478
20906
  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) => {
20479
20907
  try {
20480
20908
  await access11(path);
@@ -20492,7 +20920,7 @@ var ABSOLUTE_MOBILE_COMPLIANCE_REPORT_FORMAT = 1, HMR_ASSET_PATTERN, RELEASE_ASS
20492
20920
  }, findHmrAsset = async (root) => {
20493
20921
  if (!await pathExists6(root))
20494
20922
  return;
20495
- const entries = await readdir5(root, { withFileTypes: true });
20923
+ const entries = await readdir6(root, { withFileTypes: true });
20496
20924
  const matches = await Promise.all(entries.map((entry) => inspectReleaseAsset(join53(root, entry.name), entry.isDirectory(), entry.isFile())));
20497
20925
  return matches.find((match) => match !== undefined);
20498
20926
  }, pass = (id, detail, path) => ({ detail, id, path, status: "pass" }), fail5 = (id, detail, path, remediation) => ({
@@ -20507,7 +20935,7 @@ var ABSOLUTE_MOBILE_COMPLIANCE_REPORT_FORMAT = 1, HMR_ASSET_PATTERN, RELEASE_ASS
20507
20935
  path,
20508
20936
  remediation,
20509
20937
  status: "warn"
20510
- }), isRecord14 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), readJsonObject = async (path) => {
20938
+ }), isRecord15 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), readJsonObject = async (path) => {
20511
20939
  const value = JSON.parse(await readFile19(path, "utf8"));
20512
20940
  if (typeof value !== "object" || value === null || Array.isArray(value))
20513
20941
  throw new TypeError("JSON root must be an object.");
@@ -20638,7 +21066,7 @@ var ABSOLUTE_MOBILE_COMPLIANCE_REPORT_FORMAT = 1, HMR_ASSET_PATTERN, RELEASE_ASS
20638
21066
  const source = await readFile19(manifestPath, "utf8");
20639
21067
  const cleartext = /android:usesCleartextTraffic=["']true["']/u.test(source);
20640
21068
  const networkConfigName = source.match(/android:networkSecurityConfig=["']@xml\/([a-z0-9_]+)["']/u)?.[1];
20641
- const networkConfigPath = networkConfigName ? join53(dirname30(manifestPath), "res", "xml", `${networkConfigName}.xml`) : undefined;
21069
+ const networkConfigPath = networkConfigName ? join53(dirname31(manifestPath), "res", "xml", `${networkConfigName}.xml`) : undefined;
20642
21070
  const developmentTrustReference = /android:networkSecurityConfig=["']@xml\/absolutejs_dev_network_security["']/u.test(source);
20643
21071
  const developmentTrustContents = networkConfigPath ? await readFile19(networkConfigPath, "utf8").then((value) => value.includes("@raw/absolutejs_dev_ca")).catch(() => false) : false;
20644
21072
  const developmentTrust = developmentTrustReference || developmentTrustContents;
@@ -20675,13 +21103,13 @@ var ABSOLUTE_MOBILE_COMPLIANCE_REPORT_FORMAT = 1, HMR_ASSET_PATTERN, RELEASE_ASS
20675
21103
  const path = join53(config.nativeProjectDirectory, "app.json");
20676
21104
  try {
20677
21105
  const root = await readJsonObject(path);
20678
- if (!isRecord14(root.expo))
21106
+ if (!isRecord15(root.expo))
20679
21107
  throw new TypeError("Generated Expo application config is invalid.");
20680
21108
  const { expo } = root;
20681
- if (expo.name !== config.appName || config.platforms.includes("android") && (!isRecord14(expo.android) || expo.android.package !== config.appId) || config.platforms.includes("ios") && (!isRecord14(expo.ios) || expo.ios.bundleIdentifier !== config.appId)) {
21109
+ if (expo.name !== config.appName || config.platforms.includes("android") && (!isRecord15(expo.android) || expo.android.package !== config.appId) || config.platforms.includes("ios") && (!isRecord15(expo.ios) || expo.ios.bundleIdentifier !== config.appId)) {
20682
21110
  throw new TypeError("Generated Expo application identity does not match mobile config.");
20683
21111
  }
20684
- if (!isRecord14(expo.runtimeVersion) || expo.runtimeVersion.policy !== "appVersion") {
21112
+ if (!isRecord15(expo.runtimeVersion) || expo.runtimeVersion.policy !== "appVersion") {
20685
21113
  throw new TypeError("Generated Expo runtimeVersion must follow the native app version.");
20686
21114
  }
20687
21115
  if (Array.isArray(expo.plugins) && expo.plugins.some((plugin) => typeof plugin === "string" && plugin.includes("withAbsoluteDevelopmentCa"))) {
@@ -20767,6 +21195,26 @@ var ABSOLUTE_MOBILE_COMPLIANCE_REPORT_FORMAT = 1, HMR_ASSET_PATTERN, RELEASE_ASS
20767
21195
  } catch (error) {
20768
21196
  return fail5("android.deep-links", error instanceof Error ? error.message : "Android deep-link projection could not be validated.", manifestPath, "Run `absolute mobile sync android` and review the AbsoluteJS-owned deep-link region.");
20769
21197
  }
21198
+ }, nativeUpdateWatchdogCheck = async (config, platform6) => {
21199
+ if (!config.updates)
21200
+ return;
21201
+ const timeout = String(config.updates.bootTimeoutMs);
21202
+ if (platform6 === "ios") {
21203
+ const path = join53(config.nativeProjectDirectory, "ios/App/App/AppDelegate.swift");
21204
+ const configPath2 = join53(config.nativeProjectDirectory, "ios/App/App/capacitor.config.json");
21205
+ const [source, nativeConfig] = await Promise.all([
21206
+ readFile19(path, "utf8").catch(() => ""),
21207
+ readFile19(configPath2, "utf8").catch(() => "")
21208
+ ]);
21209
+ return source.includes("AbsoluteMobileUpdateWatchdogPlugin") && source.includes("recoverInterruptedBoot()") && source.includes(`.milliseconds(${timeout})`) && source.includes("quarantinedReleases") && nativeConfig.includes("AbsoluteMobileUpdateWatchdogPlugin") ? pass("ios.update-watchdog", `The native update boot watchdog is projected with a ${timeout}ms deadline.`, path) : fail5("ios.update-watchdog", "The iOS update boot watchdog does not match mobile config.", path, "Run `absolute mobile sync ios` before building the release.");
21210
+ }
21211
+ const sourceRoot = join53(config.nativeProjectDirectory, "android/app/src/main");
21212
+ const sources = await sourceFiles(sourceRoot, new Set([".java", ".kt"]));
21213
+ const [activity, plugin] = await Promise.all([
21214
+ containsPattern(sources, /recoverInterruptedBoot\s*\(\s*this\s*\)/u),
21215
+ containsPattern(sources, new RegExp(`handler\\.postDelayed\\(deadline,\\s*${timeout}L\\)`, "u"))
21216
+ ]);
21217
+ return activity && plugin ? pass("android.update-watchdog", `The native update boot watchdog is projected with a ${timeout}ms deadline.`, plugin) : fail5("android.update-watchdog", "The Android update boot watchdog does not match mobile config.", plugin ?? sourceRoot, "Run `absolute mobile sync android` before building the release.");
20770
21218
  }, iosNativeSecurityCheck = async (iosRoot) => {
20771
21219
  const applicationFiles = async (extensions) => (await sourceFiles(iosRoot, extensions)).filter((path) => !relative26(iosRoot, path).split(/[\\/]/u).some((part) => ["Pods", "DerivedData", "build"].includes(part)));
20772
21220
  const entitlementPaths = await applicationFiles(new Set([".entitlements"]));
@@ -20944,9 +21392,10 @@ var ABSOLUTE_MOBILE_COMPLIANCE_REPORT_FORMAT = 1, HMR_ASSET_PATTERN, RELEASE_ASS
20944
21392
  contentSecurityPolicyCheck(config, "android", publicRoot),
20945
21393
  androidNativeSecurityCheck(androidRoot),
20946
21394
  androidExportedComponentsCheck(manifestPath),
20947
- androidDeepLinkProjectionCheck(config, manifestPath)
21395
+ androidDeepLinkProjectionCheck(config, manifestPath),
21396
+ nativeUpdateWatchdogCheck(config, "android")
20948
21397
  ]);
20949
- return checks.map((check2) => ({
21398
+ return checks.filter((check2) => check2 !== undefined).map((check2) => ({
20950
21399
  ...check2,
20951
21400
  path: check2.path ? relative26(projectRoot, check2.path).replaceAll("\\", "/") || "." : undefined
20952
21401
  }));
@@ -21031,6 +21480,7 @@ var ABSOLUTE_MOBILE_COMPLIANCE_REPORT_FORMAT = 1, HMR_ASSET_PATTERN, RELEASE_ASS
21031
21480
  const checks = [
21032
21481
  await journalReleaseCheck(journalPath, "ios")
21033
21482
  ];
21483
+ const updateWatchdog = config.updates ? await nativeUpdateWatchdogCheck(config, "ios") : undefined;
21034
21484
  if (!config.iosVersion) {
21035
21485
  checks.push(fail5("ios.marketing-version", "iOS has no explicit App Store marketing version.", projectRoot, "Add mobile.ios.version to absolutejs.config.ts, for example 1.0.0."));
21036
21486
  } else {
@@ -21053,6 +21503,8 @@ var ABSOLUTE_MOBILE_COMPLIANCE_REPORT_FORMAT = 1, HMR_ASSET_PATTERN, RELEASE_ASS
21053
21503
  const hmrAsset = await findHmrAsset(publicRoot);
21054
21504
  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));
21055
21505
  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")));
21506
+ if (updateWatchdog)
21507
+ checks.push(updateWatchdog);
21056
21508
  return checks.map((check2) => ({
21057
21509
  ...check2,
21058
21510
  path: check2.path ? relative26(projectRoot, check2.path).replaceAll("\\", "/") || "." : undefined
@@ -21144,18 +21596,18 @@ import { createHash as createHash15 } from "crypto";
21144
21596
  import {
21145
21597
  access as access12,
21146
21598
  copyFile as copyFile5,
21147
- mkdir as mkdir13,
21599
+ mkdir as mkdir14,
21148
21600
  mkdtemp as mkdtemp7,
21149
21601
  readFile as readFile20,
21150
21602
  realpath as realpath2,
21151
21603
  rename as rename14,
21152
- rm as rm10,
21604
+ rm as rm11,
21153
21605
  stat as stat3,
21154
21606
  writeFile as writeFile16
21155
21607
  } from "fs/promises";
21156
- import { dirname as dirname31, isAbsolute as isAbsolute7, join as join54, relative as relative27, resolve as resolve40, sep as sep7 } from "path";
21157
- var ABSOLUTE_ANDROID_RELEASE_FORMAT = 1, isRecord15 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), requireManifest2 = (value) => {
21158
- if (!isRecord15(value) || typeof value.appBuild !== "string" || typeof value.appId !== "string" || typeof value.runtime !== "string") {
21608
+ import { dirname as dirname32, isAbsolute as isAbsolute7, join as join54, relative as relative27, resolve as resolve40, sep as sep7 } from "path";
21609
+ var ABSOLUTE_ANDROID_RELEASE_FORMAT = 1, isRecord16 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), requireManifest2 = (value) => {
21610
+ if (!isRecord16(value) || typeof value.appBuild !== "string" || typeof value.appId !== "string" || typeof value.runtime !== "string") {
21159
21611
  throw new TypeError("Invalid embedded AbsoluteJS mobile manifest.");
21160
21612
  }
21161
21613
  return {
@@ -21251,8 +21703,8 @@ var ABSOLUTE_ANDROID_RELEASE_FORMAT = 1, isRecord15 = (value) => typeof value ==
21251
21703
  }
21252
21704
  return { artifactPath: destination, metadata: existing, releaseRoot };
21253
21705
  }
21254
- await mkdir13(dirname31(releaseRoot), { recursive: true });
21255
- const staging = await mkdtemp7(join54(dirname31(releaseRoot), ".android-stage-"));
21706
+ await mkdir14(dirname32(releaseRoot), { recursive: true });
21707
+ const staging = await mkdtemp7(join54(dirname32(releaseRoot), ".android-stage-"));
21256
21708
  try {
21257
21709
  await copyFile5(artifactPath, join54(staging, artifactName));
21258
21710
  const complete = {
@@ -21264,12 +21716,12 @@ var ABSOLUTE_ANDROID_RELEASE_FORMAT = 1, isRecord15 = (value) => typeof value ==
21264
21716
  await rename14(staging, releaseRoot);
21265
21717
  return { artifactPath: destination, metadata: complete, releaseRoot };
21266
21718
  } finally {
21267
- await rm10(staging, { force: true, recursive: true }).catch(() => {
21719
+ await rm11(staging, { force: true, recursive: true }).catch(() => {
21268
21720
  return;
21269
21721
  });
21270
21722
  }
21271
21723
  }, requireManifestIdentity = (value, expected) => {
21272
- if (!isRecord15(value)) {
21724
+ if (!isRecord16(value)) {
21273
21725
  throw new TypeError("Existing Android release metadata is invalid.");
21274
21726
  }
21275
21727
  const { artifact } = value;
@@ -21464,7 +21916,7 @@ var init_iosConformance = __esm(() => {
21464
21916
  });
21465
21917
 
21466
21918
  // src/mobile/nativeTestReport.ts
21467
- import { mkdir as mkdir14, readFile as readFile22, writeFile as writeFile17 } from "fs/promises";
21919
+ import { mkdir as mkdir15, readFile as readFile22, writeFile as writeFile17 } from "fs/promises";
21468
21920
  import { join as join55 } from "path";
21469
21921
  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(`
21470
21922
  `, "<br>"), createAbsoluteNativeAutomatedChecks = (run) => {
@@ -21603,7 +22055,7 @@ Replace each \`NOT_RUN\` with \`PASS\`, \`FAIL\`, or \`SKIPPED\` after completin
21603
22055
  ${table(report.manualChecks)}
21604
22056
  `;
21605
22057
  }, writeAbsoluteNativeTestReport = async (directory, report) => {
21606
- await mkdir14(directory, { recursive: true });
22058
+ await mkdir15(directory, { recursive: true });
21607
22059
  const jsonPath = join55(directory, "report.json");
21608
22060
  const markdownPath = join55(directory, "report.md");
21609
22061
  await Promise.all([
@@ -21893,7 +22345,7 @@ var prepareAbsoluteIosRelease = async (publisher, options) => {
21893
22345
  throw new TypeError("Google Play publisher returned an invalid Android versionCode.");
21894
22346
  }
21895
22347
  return versionCode;
21896
- }, isRecord16 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), isPublisher = (value) => isRecord16(value) && typeof value.publish === "function", publisherModulePath = (projectRoot, requested) => {
22348
+ }, isRecord17 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), isPublisher = (value) => isRecord17(value) && typeof value.publish === "function", publisherModulePath = (projectRoot, requested) => {
21897
22349
  const root = resolve41(projectRoot);
21898
22350
  const path = resolve41(root, requested);
21899
22351
  const projectRelative = relative28(root, path);
@@ -21907,7 +22359,7 @@ var prepareAbsoluteIosRelease = async (publisher, options) => {
21907
22359
  throw new TypeError(`Native release registry module does not exist: ${modulePath}`);
21908
22360
  });
21909
22361
  const loaded = await import(pathToFileURL2(modulePath).href);
21910
- const publisher = isRecord16(loaded) ? loaded.default ?? loaded.registry : undefined;
22362
+ const publisher = isRecord17(loaded) ? loaded.default ?? loaded.registry : undefined;
21911
22363
  if (!isPublisher(publisher)) {
21912
22364
  throw new TypeError("Native release registry module must default-export a registry with publish(options).");
21913
22365
  }
@@ -22113,8 +22565,8 @@ var init_mobileInspect = __esm(() => {
22113
22565
 
22114
22566
  // src/mobile/ciWorkflow.ts
22115
22567
  import { existsSync as existsSync43 } from "fs";
22116
- import { access as access15, mkdir as mkdir15, readFile as readFile24, writeFile as writeFile18 } from "fs/promises";
22117
- import { dirname as dirname32, extname as extname9, relative as relative30, resolve as resolve43, sep as sep9 } from "path";
22568
+ import { access as access15, mkdir as mkdir16, readFile as readFile24, writeFile as writeFile18 } from "fs/promises";
22569
+ import { dirname as dirname33, extname as extname9, relative as relative30, resolve as resolve43, sep as sep9 } from "path";
22118
22570
  var ABSOLUTE_MOBILE_CI_WORKFLOW_FORMAT = 1, SECRET_NAME_PATTERN, CI_ENV_INDENTATION = 6, RESERVED_SECRET_NAMES, exists4 = async (path) => {
22119
22571
  try {
22120
22572
  await access15(path);
@@ -22531,7 +22983,7 @@ ${bundleAuditSteps}${platforms.includes("android") ? androidJob({ customSecrets,
22531
22983
  if (previous !== undefined && previous !== generated.workflow && !options.force)
22532
22984
  throw new TypeError(`${relative30(options.projectRoot, path)} already exists and differs. Rerun with --force to replace the generated workflow.`);
22533
22985
  if (previous !== generated.workflow) {
22534
- await mkdir15(dirname32(path), { recursive: true });
22986
+ await mkdir16(dirname33(path), { recursive: true });
22535
22987
  await writeFile18(path, generated.workflow);
22536
22988
  }
22537
22989
  return {
@@ -22593,18 +23045,18 @@ var init_ciWorkflow = __esm(() => {
22593
23045
  import { createHash as createHash16, sign as sign2, verify as verify2 } from "crypto";
22594
23046
  import {
22595
23047
  cp as cp4,
22596
- mkdir as mkdir16,
23048
+ mkdir as mkdir17,
22597
23049
  mkdtemp as mkdtemp8,
22598
- readdir as readdir6,
23050
+ readdir as readdir7,
22599
23051
  readFile as readFile25,
22600
23052
  rename as rename15,
22601
- rm as rm11,
23053
+ rm as rm12,
22602
23054
  stat as stat5,
22603
23055
  writeFile as writeFile19
22604
23056
  } from "fs/promises";
22605
- import { dirname as dirname33, join as join57, relative as relative31, resolve as resolve44 } from "path";
23057
+ import { dirname as dirname34, join as join57, relative as relative31, resolve as resolve44 } from "path";
22606
23058
  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 });
23059
+ const entries = await readdir7(directory, { withFileTypes: true });
22608
23060
  const paths = await Promise.all(entries.map(async (entry) => {
22609
23061
  const path = join57(directory, entry.name);
22610
23062
  if (entry.isDirectory())
@@ -22651,7 +23103,7 @@ var UPDATE_MANIFEST_FILE = "update.json", UPDATE_FILES_DIRECTORY = "files", sha2
22651
23103
  value: signature.toString("base64")
22652
23104
  }
22653
23105
  });
22654
- await mkdir16(outputRoot, { recursive: true });
23106
+ await mkdir17(outputRoot, { recursive: true });
22655
23107
  const outputDirectory = join57(outputRoot, manifest.releaseId);
22656
23108
  const staging = await mkdtemp8(join57(outputRoot, ".stage-"));
22657
23109
  try {
@@ -22663,7 +23115,7 @@ var UPDATE_MANIFEST_FILE = "update.json", UPDATE_FILES_DIRECTORY = "files", sha2
22663
23115
  `);
22664
23116
  await rename15(staging, outputDirectory);
22665
23117
  } catch (error) {
22666
- await rm11(staging, { force: true, recursive: true });
23118
+ await rm12(staging, { force: true, recursive: true });
22667
23119
  if (typeof error === "object" && error !== null && Reflect.get(error, "code") === "EEXIST")
22668
23120
  throw new TypeError(`Mobile update ${manifest.releaseId} already exists.`, { cause: error });
22669
23121
  throw error;
@@ -22748,18 +23200,18 @@ var exports_mobile = {};
22748
23200
  __export(exports_mobile, {
22749
23201
  runMobile: () => runMobile
22750
23202
  });
22751
- import { access as access17, mkdir as mkdir17, readFile as readFile26, writeFile as writeFile20 } from "fs/promises";
23203
+ import { access as access17, mkdir as mkdir18, readFile as readFile26, writeFile as writeFile20 } from "fs/promises";
22752
23204
  import { createPublicKey as createPublicKey3 } from "crypto";
22753
23205
  import { join as join58, relative as relative33, resolve as resolve46 } from "path";
22754
23206
  import { createInterface } from "readline/promises";
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) => {
23207
+ var NOT_FOUND4 = -1, isRecord18 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), CAPACITOR_PACKAGES, CAPACITOR_PACKAGE_SPECS, CAPACITOR_SYNC_PACKAGE_SPECS, packageNameFromSpec = (spec) => spec.slice(0, spec.lastIndexOf("@")), directProjectPackages = async (projectRoot) => {
22756
23208
  const manifest = JSON.parse(await readFile26(join58(projectRoot, "package.json"), "utf8"));
22757
- if (!isRecord17(manifest))
23209
+ if (!isRecord18(manifest))
22758
23210
  throw new TypeError("Application package.json must contain an object.");
22759
23211
  const names = new Set;
22760
23212
  for (const field of ["dependencies", "devDependencies"]) {
22761
23213
  const dependencies = Reflect.get(manifest, field);
22762
- if (isRecord17(dependencies))
23214
+ if (isRecord18(dependencies))
22763
23215
  for (const name of Object.keys(dependencies))
22764
23216
  names.add(name);
22765
23217
  }
@@ -22767,7 +23219,7 @@ var NOT_FOUND4 = -1, isRecord17 = (value) => typeof value === "object" && value
22767
23219
  }, resolvedPackageVersion = async (projectRoot, packageName) => {
22768
23220
  try {
22769
23221
  const manifest = JSON.parse(await readFile26(join58(projectRoot, "node_modules", packageName, "package.json"), "utf8"));
22770
- return isRecord17(manifest) && typeof manifest.version === "string" ? manifest.version : undefined;
23222
+ return isRecord18(manifest) && typeof manifest.version === "string" ? manifest.version : undefined;
22771
23223
  } catch {
22772
23224
  return;
22773
23225
  }
@@ -23216,7 +23668,7 @@ Mobile release security and compliance checks failed.`);
23216
23668
  try {
23217
23669
  await start(mobileBuildServerEntry(args), valueAfter(args, "--web-outdir"), configPath2, { prepareOnly: true });
23218
23670
  const embedded = JSON.parse(await readFile26(join58(mobile.bundleDirectory, "absolute-mobile-manifest.json"), "utf8"));
23219
- const runtimeFingerprint = isRecord17(embedded) ? embedded.nativeRuntime : undefined;
23671
+ const runtimeFingerprint = isRecord18(embedded) ? embedded.nativeRuntime : undefined;
23220
23672
  if (typeof runtimeFingerprint !== "string" || !/^[a-f0-9]{64}$/u.test(runtimeFingerprint))
23221
23673
  throw new TypeError("Prepared mobile bundle is missing its native runtime fingerprint.");
23222
23674
  const privateKey = await readFile26(resolve46(projectRoot, signingKeyPath));
@@ -23482,6 +23934,7 @@ Mobile release security and compliance checks failed.`);
23482
23934
  "android"
23483
23935
  ]);
23484
23936
  await applyAbsoluteNativeBackgroundSync(projectRoot, mobile, ["android"]);
23937
+ await applyAbsoluteNativeUpdates(mobile, ["android"]);
23485
23938
  }, prepareAndroidReleaseProject = (mobile, projectRoot, args) => mobile.engine === "expo" ? prepareExpoAndroidReleaseProject(mobile, projectRoot, args) : prepareCapacitorAndroidReleaseProject(mobile, projectRoot), buildAndroid = async (args, prepareVersionCode) => {
23486
23939
  const configPath2 = valueAfter(args, "--config");
23487
23940
  const { mobile, projectRoot } = await loadMobile(configPath2);
@@ -23927,7 +24380,7 @@ Emulator setup verification:`);
23927
24380
  timeoutMs
23928
24381
  });
23929
24382
  }, writeAndroidFailureArtifacts = async (options) => {
23930
- await mkdir17(options.artifactRoot, { recursive: true });
24383
+ await mkdir18(options.artifactRoot, { recursive: true });
23931
24384
  const screenshot = options.session ? await options.session.screenshot(join58(options.artifactRoot, "android-failure.png")).catch(() => {
23932
24385
  return;
23933
24386
  }) : undefined;
@@ -24088,7 +24541,7 @@ Emulator setup verification:`);
24088
24541
  return;
24089
24542
  });
24090
24543
  const status2 = response?.ok ? await response.json().catch(() => null) : null;
24091
- const targets = isRecord17(status2) && isRecord17(status2.connectedTargets) ? status2.connectedTargets : undefined;
24544
+ const targets = isRecord18(status2) && isRecord18(status2.connectedTargets) ? status2.connectedTargets : undefined;
24092
24545
  if (targets && typeof targets["capacitor-ios"] === "number" && targets["capacitor-ios"] > 0)
24093
24546
  return;
24094
24547
  await Bun.sleep(100);
@@ -24185,7 +24638,7 @@ Emulator setup verification:`);
24185
24638
  throw new Error(`${label} failed: ${result.stderr.trim() || result.stdout.trim() || `status ${result.exitCode}`}`);
24186
24639
  return result;
24187
24640
  }, writeIosFailureArtifacts = async (options) => {
24188
- await mkdir17(options.artifactRoot, { recursive: true });
24641
+ await mkdir18(options.artifactRoot, { recursive: true });
24189
24642
  const screenshot = join58(options.artifactRoot, "ios-failure.png");
24190
24643
  const screenshotResult = captureCommand4([
24191
24644
  options.xcrun,
@@ -24441,7 +24894,7 @@ Emulator setup verification:`);
24441
24894
  mobile.appId
24442
24895
  ], "iOS app launch");
24443
24896
  await waitForIosHmrClient({ https, port, timeoutMs });
24444
- await mkdir17(artifactRoot, { recursive: true });
24897
+ await mkdir18(artifactRoot, { recursive: true });
24445
24898
  const screenshot = join58(artifactRoot, "ios-simulator.png");
24446
24899
  requireCapturedCommand([xcrun, "simctl", "io", simulator.udid, "screenshot", screenshot], "iOS simulator screenshot");
24447
24900
  const hmrApply = await waitForRequestedIosHmr(args, instance, timeoutMs);
@@ -24680,7 +25133,7 @@ __export(exports_typecheck, {
24680
25133
  });
24681
25134
  import { resolve as resolve47, join as join59 } from "path";
24682
25135
  import { existsSync as existsSync44, readFileSync as readFileSync40 } from "fs";
24683
- import { mkdir as mkdir18, writeFile as writeFile21 } from "fs/promises";
25136
+ import { mkdir as mkdir19, writeFile as writeFile21 } from "fs/promises";
24684
25137
  var isCommandService3 = (service) => service.kind === "command" || Array.isArray(service.command), resolveConfigPath = (configPath2) => resolve47(configPath2 ?? process.env.ABSOLUTE_CONFIG ?? "absolute.config.ts"), getTypecheckTargets = async (configPath2) => {
24685
25138
  if (!existsSync44(resolveConfigPath(configPath2))) {
24686
25139
  const defaultService = {};
@@ -24892,7 +25345,7 @@ Found ${errorCount} error${suffix}.`;
24892
25345
  ...new Set(targets.map((config) => config.angularDirectory).filter((dir) => typeof dir === "string" && dir.length > 0))
24893
25346
  ];
24894
25347
  const cacheDir = ".absolutejs";
24895
- await mkdir18(cacheDir, { recursive: true });
25348
+ await mkdir19(cacheDir, { recursive: true });
24896
25349
  const checks = [];
24897
25350
  checks.push(hasVue ? buildVueTscCheck(cacheDir) : buildTscCheck(cacheDir));
24898
25351
  for (const svelteDir of hasSvelte ? svelteDirs : []) {