@absolutejs/absolute 0.20.0-beta.36 → 0.20.0-beta.38

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
@@ -6411,12 +6411,25 @@ var MANIFEST_FILE = "absolute-mobile-manifest.json", BOOTSTRAP_FILE = "absolute-
6411
6411
  if (candidate)
6412
6412
  return candidate;
6413
6413
  throw new TypeError("AbsoluteJS mobile push shell module is missing.");
6414
- }, escapeHtml = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;"), indexHtml = (appName) => `<!doctype html>
6414
+ }, escapeHtml = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;"), contentSecurityPolicy = (productionOrigin) => {
6415
+ const backend = new URL(productionOrigin);
6416
+ const socketOrigin = `${backend.protocol === "https:" ? "wss:" : "ws:"}//${backend.host}`;
6417
+ return [
6418
+ "default-src 'self' data: blob: https:",
6419
+ "base-uri 'none'",
6420
+ "object-src 'none'",
6421
+ "script-src 'self'",
6422
+ "style-src 'self' 'unsafe-inline'",
6423
+ `connect-src 'self' ${backend.origin} ${socketOrigin}`,
6424
+ "form-action 'none'"
6425
+ ].join("; ");
6426
+ }, indexHtml = (appName, productionOrigin) => `<!doctype html>
6415
6427
  <html>
6416
6428
  <head>
6417
6429
  <meta charset="utf-8">
6418
6430
  <meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
6419
6431
  <meta name="color-scheme" content="light dark">
6432
+ <meta http-equiv="Content-Security-Policy" content="${escapeHtml(contentSecurityPolicy(productionOrigin))}">
6420
6433
  <title>${escapeHtml(appName)}</title>
6421
6434
  </head>
6422
6435
  <body>
@@ -6635,7 +6648,7 @@ void startAbsoluteMobileShell(${push ? `{ createAuth: (config, options) => creat
6635
6648
  await Promise.all([
6636
6649
  writeFile7(join17(staging, MANIFEST_FILE), `${JSON.stringify(manifest, null, "\t")}
6637
6650
  `),
6638
- writeFile7(join17(staging, INDEX_FILE), indexHtml(options.config.appName)),
6651
+ writeFile7(join17(staging, INDEX_FILE), indexHtml(options.config.appName, options.config.productionOrigin)),
6639
6652
  buildShellBootstrap(staging, options.auth !== undefined, options.auth !== undefined && options.sync === true, `absolutejs.${options.auth?.clientId ?? options.config.appId}.`, options.deviceCapabilities, options.projectRoot)
6640
6653
  ]);
6641
6654
  await installBundle(staging, destination);
@@ -18048,28 +18061,173 @@ var DEFAULT_ROUTE_TIMEOUT_MS = 30000, DEFAULT_HMR_TIMEOUT_MS = 30000, routeExpre
18048
18061
  return apply;
18049
18062
  };
18050
18063
 
18051
- // src/mobile/releaseDoctor.ts
18052
- import { access as access8, readFile as readFile16, readdir as readdir4 } from "fs/promises";
18053
- import { dirname as dirname29, extname as extname8, join as join49, relative as relative24 } from "path";
18054
- var HMR_ASSET_PATTERN, RELEASE_ASSET_EXTENSIONS, pathExists5 = async (path) => {
18064
+ // src/mobile/mobileBundleInspection.ts
18065
+ import { createHash as createHash12 } from "crypto";
18066
+ import { access as access8, readFile as readFile16, stat as stat2 } from "fs/promises";
18067
+ import { join as join49, relative as relative24, resolve as resolve39 } from "path";
18068
+ var MOBILE_FRAMEWORKS, SHA256_PATTERN, isObject2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), portablePath = (projectRoot, path) => {
18069
+ const value = relative24(resolve39(projectRoot), resolve39(path)).replaceAll("\\", "/");
18070
+ return value || ".";
18071
+ }, pathExists5 = async (path) => {
18055
18072
  try {
18056
18073
  await access8(path);
18057
18074
  return true;
18058
18075
  } catch {
18059
18076
  return false;
18060
18077
  }
18078
+ }, readObject = async (path) => {
18079
+ const value = JSON.parse(await readFile16(path, "utf8"));
18080
+ if (!isObject2(value))
18081
+ throw new TypeError("JSON root must be an object.");
18082
+ return value;
18083
+ }, requireString = (value, field) => {
18084
+ if (typeof value !== "string" || value.length === 0)
18085
+ throw new TypeError(`${field} must be a non-empty string.`);
18086
+ return value;
18087
+ }, requireStringArray = (value, field) => {
18088
+ if (!Array.isArray(value) || !value.every((item) => typeof item === "string"))
18089
+ throw new TypeError(`${field} must be a string array.`);
18090
+ return value;
18091
+ }, requireBundleFile = async (root, value, field, expectedHash) => {
18092
+ const portable = requireString(value, field);
18093
+ const path = resolve39(root, portable);
18094
+ const normalizedRoot = resolve39(root);
18095
+ if (path === normalizedRoot || !path.startsWith(`${normalizedRoot}/`))
18096
+ throw new TypeError(`${field} must remain inside the mobile bundle.`);
18097
+ if (!(await stat2(path).catch(() => {
18098
+ return;
18099
+ }))?.isFile())
18100
+ throw new TypeError(`${field} does not exist in the mobile bundle.`);
18101
+ if (expectedHash !== undefined) {
18102
+ if (!SHA256_PATTERN.test(expectedHash))
18103
+ throw new TypeError(`${field} has an invalid SHA-256 digest.`);
18104
+ const actual = createHash12("sha256").update(await readFile16(path)).digest("hex");
18105
+ if (actual !== expectedHash)
18106
+ throw new TypeError(`${field} failed its SHA-256 integrity check.`);
18107
+ }
18108
+ return portable;
18109
+ }, inspectAbsoluteMobileBundle = async (config, projectRoot) => {
18110
+ const manifestPath = join49(config.bundleDirectory, "absolute-mobile-manifest.json");
18111
+ const manifest = portablePath(projectRoot, manifestPath);
18112
+ if (!await pathExists5(manifestPath))
18113
+ return { manifest, status: "missing" };
18114
+ try {
18115
+ const value = await readObject(manifestPath);
18116
+ if (value.format !== ABSOLUTE_MOBILE_CLIENT_MANIFEST_FORMAT)
18117
+ throw new TypeError("format is not supported by this runtime.");
18118
+ if (requireString(value.appId, "appId") !== config.appId)
18119
+ throw new TypeError("appId does not match the effective mobile config.");
18120
+ if (requireString(value.productionOrigin, "productionOrigin") !== config.productionOrigin)
18121
+ throw new TypeError("productionOrigin does not match the effective mobile config.");
18122
+ const appBuild = requireString(value.appBuild, "appBuild");
18123
+ const runtime = requireString(value.runtime, "runtime");
18124
+ if (runtime !== String(ABSOLUTE_MOBILE_PAGE_PROTOCOL_VERSION))
18125
+ throw new TypeError("runtime is not supported by this AbsoluteJS build.");
18126
+ const capabilities = requireStringArray(value.deviceCapabilities, "deviceCapabilities").sort();
18127
+ if (!Array.isArray(value.pages) || !Array.isArray(value.routes))
18128
+ throw new TypeError("pages and routes must be arrays.");
18129
+ const pageIds = new Set;
18130
+ const frameworks7 = new Set;
18131
+ await Promise.all(value.pages.map(async (candidate) => {
18132
+ if (!isObject2(candidate))
18133
+ throw new TypeError("pages contains an invalid entry.");
18134
+ const pageId = requireString(candidate.pageId, "page.pageId");
18135
+ if (pageIds.has(pageId))
18136
+ throw new TypeError("page.pageId values must be unique.");
18137
+ pageIds.add(pageId);
18138
+ const framework = requireString(candidate.framework, "page.framework");
18139
+ if (!MOBILE_FRAMEWORKS.has(framework))
18140
+ throw new TypeError("page.framework is unsupported.");
18141
+ frameworks7.add(framework);
18142
+ const bundleHash = requireString(candidate.bundleHash, "page.bundleHash");
18143
+ requireString(candidate.contract, "page.contract");
18144
+ requireString(candidate.propsSchemaHash, "page.propsSchemaHash");
18145
+ await requireBundleFile(config.bundleDirectory, candidate.localBundlePath, "page.localBundlePath", bundleHash);
18146
+ if (candidate.localStylePath !== undefined) {
18147
+ const styleHash = requireString(candidate.styleBundleHash, "page.styleBundleHash");
18148
+ await requireBundleFile(config.bundleDirectory, candidate.localStylePath, "page.localStylePath", styleHash);
18149
+ } else if (candidate.styleBundleHash !== undefined)
18150
+ throw new TypeError("page.styleBundleHash requires page.localStylePath.");
18151
+ }));
18152
+ const routes = value.routes.map((candidate) => {
18153
+ if (!isObject2(candidate))
18154
+ throw new TypeError("routes contains an invalid entry.");
18155
+ const { method } = candidate;
18156
+ if (method !== "GET" && method !== "HEAD")
18157
+ throw new TypeError("route.method must be GET or HEAD.");
18158
+ const pageId = requireString(candidate.pageId, "route.pageId");
18159
+ if (!pageIds.has(pageId))
18160
+ throw new TypeError("route.pageId references a missing page.");
18161
+ return {
18162
+ method,
18163
+ pageId,
18164
+ pattern: requireString(candidate.pattern, "route.pattern")
18165
+ };
18166
+ });
18167
+ await Promise.all(["index.html", "absolute-mobile-bootstrap.js"].map((file) => requireBundleFile(config.bundleDirectory, file, file)));
18168
+ const entryPath = new URL(config.entry, "https://absolute.invalid").pathname;
18169
+ const entryResolved = resolveAbsoluteMobileRoute(routes, entryPath) !== undefined;
18170
+ if (!entryResolved)
18171
+ throw new TypeError("entry is not owned by an embedded route.");
18172
+ return {
18173
+ appBuild,
18174
+ auth: isObject2(value.auth),
18175
+ capabilities,
18176
+ entryResolved,
18177
+ frameworks: [...frameworks7].sort(),
18178
+ manifest,
18179
+ pageCount: value.pages.length,
18180
+ routeCount: value.routes.length,
18181
+ runtime,
18182
+ status: "valid",
18183
+ sync: isObject2(value.sync)
18184
+ };
18185
+ } catch (error) {
18186
+ return {
18187
+ issue: error instanceof Error ? error.message : "The embedded mobile manifest is invalid.",
18188
+ manifest,
18189
+ status: "invalid"
18190
+ };
18191
+ }
18192
+ };
18193
+ var init_mobileBundleInspection = __esm(() => {
18194
+ init_pageProtocol();
18195
+ init_routeMatcher();
18196
+ init_transport();
18197
+ MOBILE_FRAMEWORKS = new Set([
18198
+ "angular",
18199
+ "ember",
18200
+ "html",
18201
+ "htmx",
18202
+ "react",
18203
+ "svelte",
18204
+ "vue"
18205
+ ]);
18206
+ SHA256_PATTERN = /^[a-f0-9]{64}$/u;
18207
+ });
18208
+
18209
+ // src/mobile/releaseDoctor.ts
18210
+ import { access as access9, readFile as readFile17, readdir as readdir4 } from "fs/promises";
18211
+ import { dirname as dirname29, extname as extname8, join as join50, relative as relative25 } from "path";
18212
+ 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) => {
18213
+ try {
18214
+ await access9(path);
18215
+ return true;
18216
+ } catch {
18217
+ return false;
18218
+ }
18061
18219
  }, inspectReleaseAsset = async (path, isDirectory, isFile2) => {
18062
18220
  if (isDirectory)
18063
18221
  return findHmrAsset(path);
18064
18222
  if (!isFile2 || !RELEASE_ASSET_EXTENSIONS.has(extname8(path)))
18065
18223
  return;
18066
- const source = await readFile16(path, "utf8");
18224
+ const source = await readFile17(path, "utf8");
18067
18225
  return HMR_ASSET_PATTERN.test(source) ? path : undefined;
18068
18226
  }, findHmrAsset = async (root) => {
18069
- if (!await pathExists5(root))
18227
+ if (!await pathExists6(root))
18070
18228
  return;
18071
18229
  const entries = await readdir4(root, { withFileTypes: true });
18072
- const matches = await Promise.all(entries.map((entry) => inspectReleaseAsset(join49(root, entry.name), entry.isDirectory(), entry.isFile())));
18230
+ const matches = await Promise.all(entries.map((entry) => inspectReleaseAsset(join50(root, entry.name), entry.isDirectory(), entry.isFile())));
18073
18231
  return matches.find((match) => match !== undefined);
18074
18232
  }, pass = (id, detail, path) => ({ detail, id, path, status: "pass" }), fail5 = (id, detail, path, remediation) => ({
18075
18233
  detail,
@@ -18077,7 +18235,70 @@ var HMR_ASSET_PATTERN, RELEASE_ASSET_EXTENSIONS, pathExists5 = async (path) => {
18077
18235
  path,
18078
18236
  remediation,
18079
18237
  status: "fail"
18080
- }), journalReleaseCheck = async (journalPath, platform6) => await pathExists5(journalPath) ? fail5(`${platform6}.dev-journal`, "A live-reload recovery journal is still active.", journalPath, "Close the development session or run mobile doctor again after stale-session repair.") : pass(`${platform6}.dev-journal`, "No live-reload recovery journal is active.", journalPath), parsedJson = (source) => {
18238
+ }), warn = (id, detail, path, remediation) => ({
18239
+ detail,
18240
+ id,
18241
+ path,
18242
+ remediation,
18243
+ status: "warn"
18244
+ }), readJsonObject = async (path) => {
18245
+ const value = JSON.parse(await readFile17(path, "utf8"));
18246
+ if (typeof value !== "object" || value === null || Array.isArray(value))
18247
+ throw new TypeError("JSON root must be an object.");
18248
+ return Object.fromEntries(Object.entries(value));
18249
+ }, packageDeclarations = (manifest) => {
18250
+ const declarations = new Map;
18251
+ for (const field of ["dependencies", "devDependencies"]) {
18252
+ const value = manifest[field];
18253
+ if (typeof value !== "object" || value === null || Array.isArray(value))
18254
+ continue;
18255
+ for (const [name, version2] of Object.entries(value))
18256
+ if (typeof version2 === "string")
18257
+ declarations.set(name, version2);
18258
+ }
18259
+ return declarations;
18260
+ }, versionCore = (version2) => version2.split("-")[0]?.split(".").slice(0, 2).join("."), capacitorVersionCheck = async (config, projectRoot) => {
18261
+ const manifestPath = join50(projectRoot, "package.json");
18262
+ try {
18263
+ const manifest = await readJsonObject(manifestPath);
18264
+ const declarations = packageDeclarations(manifest);
18265
+ const names = [
18266
+ "@capacitor/core",
18267
+ "@capacitor/cli",
18268
+ ...config.platforms.map((platform6) => `@capacitor/${platform6}`)
18269
+ ];
18270
+ const versions = await Promise.all(names.map(async (name) => {
18271
+ const declared = declarations.get(name);
18272
+ if (!declared || !EXACT_VERSION_PATTERN.test(declared))
18273
+ throw new TypeError(`${name} must be a direct exact dependency.`);
18274
+ const installed = await readJsonObject(join50(projectRoot, "node_modules", name, "package.json"));
18275
+ if (installed.version !== declared)
18276
+ throw new TypeError(`${name} does not match its installed version.`);
18277
+ return declared;
18278
+ }));
18279
+ const lines = new Set(versions.map(versionCore));
18280
+ if (lines.size !== 1)
18281
+ throw new TypeError("Capacitor core, CLI, and platform packages must use one major/minor line.");
18282
+ return pass("mobile.capacitor-versions", `Capacitor core, CLI, and configured platforms are pinned and aligned on ${versions[0]}.`, manifestPath);
18283
+ } catch (error) {
18284
+ return fail5("mobile.capacitor-versions", error instanceof Error ? error.message : "Capacitor package versions could not be validated.", manifestPath, "Pin @capacitor/core, @capacitor/cli, and each configured platform to exact versions on the same major/minor line, then reinstall.");
18285
+ }
18286
+ }, dependencyLockCheck = async (projectRoot) => {
18287
+ const present = (await Promise.all(LOCK_FILES.map(async (name) => ({
18288
+ exists: await pathExists6(join50(projectRoot, name)),
18289
+ name
18290
+ })))).find(({ exists: exists3 }) => exists3);
18291
+ return present ? pass("mobile.dependency-lock", `Dependency graph is locked by ${present.name}.`, join50(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.");
18292
+ }, productionOriginCheck = (config, projectRoot) => {
18293
+ const origin = new URL(config.productionOrigin);
18294
+ 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.");
18295
+ }, associationIdentityCheck = (config, projectRoot) => {
18296
+ const missing = [
18297
+ ...config.platforms.includes("ios") && !config.appleAppIdPrefix ? ["mobile.deepLinks.apple.appIdPrefix"] : [],
18298
+ ...config.platforms.includes("android") && config.androidCertificateFingerprints.length === 0 ? ["mobile.deepLinks.android.sha256CertificateFingerprints"] : []
18299
+ ];
18300
+ return missing.length === 0 ? pass("mobile.association-identities", "Deep-link association identities are configured for every release platform.") : fail5("mobile.association-identities", `Release association identity is missing: ${missing.join(", ")}.`, projectRoot, "Configure the Apple application prefix and every Android signing-certificate SHA-256 fingerprint used for release.");
18301
+ }, journalReleaseCheck = async (journalPath, platform6) => await pathExists6(journalPath) ? fail5(`${platform6}.dev-journal`, "A live-reload recovery journal is still active.", journalPath, "Close the development session or run mobile doctor again after stale-session repair.") : pass(`${platform6}.dev-journal`, "No live-reload recovery journal is active.", journalPath), parsedJson = (source) => {
18081
18302
  try {
18082
18303
  const parsed = JSON.parse(source);
18083
18304
  return parsed;
@@ -18099,30 +18320,138 @@ var HMR_ASSET_PATTERN, RELEASE_ASSET_EXTENSIONS, pathExists5 = async (path) => {
18099
18320
  const allowNavigation = Reflect.get(server, "allowNavigation");
18100
18321
  return typeof Reflect.get(server, "url") === "string" || Reflect.get(server, "cleartext") === true || Array.isArray(allowNavigation) && allowNavigation.length > 0;
18101
18322
  }, capacitorConfigReleaseCheck = async (nativeConfigPath) => {
18102
- if (!await pathExists5(nativeConfigPath)) {
18323
+ if (!await pathExists6(nativeConfigPath)) {
18103
18324
  return fail5("android.capacitor-config", "The generated Android Capacitor config is missing.", nativeConfigPath, "Run `absolute mobile sync android` before release validation.");
18104
18325
  }
18105
- const unsafe = isUnsafeCapacitorConfig(await readFile16(nativeConfigPath, "utf8"));
18326
+ const unsafe = isUnsafeCapacitorConfig(await readFile17(nativeConfigPath, "utf8"));
18106
18327
  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);
18328
+ }, capacitorIdentityCheck = async (config, platform6, nativeConfigPath) => {
18329
+ try {
18330
+ const parsed = await readJsonObject(nativeConfigPath);
18331
+ if (parsed.appId !== config.appId || parsed.appName !== config.appName)
18332
+ throw new TypeError(`${platform6} packaged application identity does not match mobile config.`);
18333
+ return pass(`${platform6}.app-identity`, `Packaged ${platform6} application identity matches mobile config.`, nativeConfigPath);
18334
+ } catch (error) {
18335
+ return fail5(`${platform6}.app-identity`, error instanceof Error ? error.message : `${platform6} application identity could not be validated.`, nativeConfigPath, `Run \`absolute mobile sync ${platform6}\` and review the generated Capacitor config.`);
18336
+ }
18107
18337
  }, manifestReleaseCheck = async (manifestPath) => {
18108
- if (!await pathExists5(manifestPath)) {
18338
+ if (!await pathExists6(manifestPath)) {
18109
18339
  return fail5("android.cleartext", "The Android manifest is missing.", manifestPath, "Run `absolute mobile sync android` before release validation.");
18110
18340
  }
18111
- const source = await readFile16(manifestPath, "utf8");
18341
+ const source = await readFile17(manifestPath, "utf8");
18112
18342
  const cleartext = /android:usesCleartextTraffic=["']true["']/u.test(source);
18113
18343
  const networkConfigName = source.match(/android:networkSecurityConfig=["']@xml\/([a-z0-9_]+)["']/u)?.[1];
18114
- const networkConfigPath = networkConfigName ? join49(dirname29(manifestPath), "res", "xml", `${networkConfigName}.xml`) : undefined;
18344
+ const networkConfigPath = networkConfigName ? join50(dirname29(manifestPath), "res", "xml", `${networkConfigName}.xml`) : undefined;
18115
18345
  const developmentTrustReference = /android:networkSecurityConfig=["']@xml\/absolutejs_dev_network_security["']/u.test(source);
18116
- const developmentTrustContents = networkConfigPath ? await readFile16(networkConfigPath, "utf8").then((value) => value.includes("@raw/absolutejs_dev_ca")).catch(() => false) : false;
18346
+ const developmentTrustContents = networkConfigPath ? await readFile17(networkConfigPath, "utf8").then((value) => value.includes("@raw/absolutejs_dev_ca")).catch(() => false) : false;
18117
18347
  const developmentTrust = developmentTrustReference || developmentTrustContents;
18118
18348
  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);
18119
18349
  }, hmrAssetsReleaseCheck = async (publicRoot) => {
18120
18350
  const hmrAsset = await findHmrAsset(publicRoot);
18121
18351
  return hmrAsset ? fail5("android.hmr-assets", "A packaged Android asset contains the development HMR client.", hmrAsset, "Rebuild the production mobile bundle and run Capacitor sync again.") : pass("android.hmr-assets", "Packaged Android assets contain no development HMR markers.", publicRoot);
18352
+ }, embeddedBundleReleaseCheck = async (config, projectRoot, platform6, publicRoot) => {
18353
+ const inspection = await inspectAbsoluteMobileBundle({ ...config, bundleDirectory: publicRoot }, projectRoot);
18354
+ if (inspection.status === "valid")
18355
+ return pass(`${platform6}.bundle-integrity`, `Packaged mobile manifest, runtime, routes, and ${inspection.pageCount ?? 0} page asset(s) passed structural and SHA-256 validation.`, join50(publicRoot, "absolute-mobile-manifest.json"));
18356
+ 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"}`, join50(publicRoot, "absolute-mobile-manifest.json"), "Rebuild the production mobile bundle and run Capacitor sync for this platform.");
18357
+ }, contentSecurityPolicyCheck = async (config, platform6, publicRoot) => {
18358
+ const path = join50(publicRoot, "index.html");
18359
+ try {
18360
+ const source = await readFile17(path, "utf8");
18361
+ const requirements = [
18362
+ "Content-Security-Policy",
18363
+ "default-src 'self'",
18364
+ "base-uri 'none'",
18365
+ "object-src 'none'",
18366
+ "script-src 'self'",
18367
+ "form-action 'none'",
18368
+ config.productionOrigin
18369
+ ];
18370
+ const missing = requirements.filter((value) => !source.includes(value));
18371
+ if (missing.length > 0)
18372
+ throw new TypeError("Packaged shell CSP is missing a required AbsoluteJS directive or backend origin.");
18373
+ return pass(`${platform6}.content-security-policy`, "Packaged shell CSP restricts scripts, objects, base URLs, forms, and backend connections.", path);
18374
+ } catch (error) {
18375
+ 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.");
18376
+ }
18377
+ }, sourceFiles = async (root, extensions) => {
18378
+ if (!await pathExists6(root))
18379
+ return [];
18380
+ const files = await Array.fromAsync(new Bun.Glob("**/*").scan({ cwd: root, onlyFiles: true }));
18381
+ return files.filter((file) => extensions.has(extname8(file))).map((file) => join50(root, file));
18382
+ }, containsPattern = async (paths, pattern) => {
18383
+ const sources = await Promise.all(paths.map((path) => readFile17(path, "utf8")));
18384
+ const index = sources.findIndex((source) => pattern.test(source));
18385
+ return index === NOT_FOUND3 ? undefined : paths[index];
18386
+ }, androidNativeSecurityCheck = async (androidRoot) => {
18387
+ const manifestPath = join50(androidRoot, "app/src/main/AndroidManifest.xml");
18388
+ try {
18389
+ const manifest = await readFile17(manifestPath, "utf8");
18390
+ if (/android:debuggable=["']true["']/u.test(manifest))
18391
+ throw new TypeError("Android release manifest explicitly enables application debugging.");
18392
+ const sources = await sourceFiles(join50(androidRoot, "app/src/main"), new Set([".java", ".kt"]));
18393
+ const debugSource = await containsPattern(sources, /setWebContentsDebuggingEnabled\s*\(\s*true\s*\)/u);
18394
+ if (debugSource)
18395
+ 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.");
18396
+ return pass("android.native-debugging", "Android does not explicitly enable app or WebView debugging in release source.", manifestPath);
18397
+ } catch (error) {
18398
+ 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.");
18399
+ }
18400
+ }, androidExportedComponentsCheck = async (manifestPath) => {
18401
+ const source = await readFile17(manifestPath, "utf8").catch(() => "");
18402
+ const exported = [
18403
+ ...source.matchAll(/<(?:activity|activity-alias|provider|receiver|service)\b[^>]*>/giu)
18404
+ ].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");
18405
+ 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.");
18406
+ }, androidDeepLinkProjectionCheck = async (config, manifestPath) => {
18407
+ try {
18408
+ const source = await readFile17(manifestPath, "utf8");
18409
+ const required = [
18410
+ 'android:autoVerify="true"',
18411
+ "android.intent.category.BROWSABLE",
18412
+ ...config.deepLinkHosts.map((host2) => `android:scheme="https" android:host="${host2}"`),
18413
+ ...config.deepLinkScheme ? [`android:scheme="${config.deepLinkScheme}"`] : []
18414
+ ];
18415
+ if (required.some((value) => !source.includes(value)))
18416
+ throw new TypeError("Android App Link or custom-scheme projection does not match mobile config.");
18417
+ return pass("android.deep-links", "Android verified links and custom scheme match the effective mobile config.", manifestPath);
18418
+ } catch (error) {
18419
+ 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.");
18420
+ }
18421
+ }, iosNativeSecurityCheck = async (iosRoot) => {
18422
+ const entitlementsPath = join50(iosRoot, "App/AbsoluteJS.entitlements");
18423
+ const entitlements = await readFile17(entitlementsPath, "utf8").catch(() => "");
18424
+ if (/<key>get-task-allow<\/key>\s*<true\s*\/>/u.test(entitlements) || /<key>com\.apple\.security\.get-task-allow<\/key>\s*<true\s*\/>/u.test(entitlements))
18425
+ return fail5("ios.native-debugging", "iOS source entitlements explicitly permit debugger attachment.", entitlementsPath, "Remove get-task-allow from source entitlements; Xcode supplies development entitlements only to debug builds.");
18426
+ const sources = await sourceFiles(join50(iosRoot, "App"), new Set([".m", ".mm", ".swift"]));
18427
+ const debugSource = await containsPattern(sources, /\.isInspectable\s*=\s*true|setInspectable\s*\(\s*true\s*\)/u);
18428
+ if (debugSource)
18429
+ return fail5("ios.native-debugging", "iOS application source unconditionally enables WebView inspection.", debugSource, "Remove unconditional WebView inspection from release source.");
18430
+ return pass("ios.native-debugging", "iOS source does not enable release debugger attachment or WebView inspection.", entitlementsPath);
18431
+ }, iosDeepLinkProjectionCheck = async (config, iosRoot) => {
18432
+ const infoPath = join50(iosRoot, "App/App/Info.plist");
18433
+ const entitlementsPath = join50(iosRoot, "App/AbsoluteJS.entitlements");
18434
+ const projectPath = join50(iosRoot, "App/App.xcodeproj/project.pbxproj");
18435
+ try {
18436
+ const [info2, entitlements, project] = await Promise.all([
18437
+ readFile17(infoPath, "utf8"),
18438
+ readFile17(entitlementsPath, "utf8"),
18439
+ readFile17(projectPath, "utf8")
18440
+ ]);
18441
+ if (config.deepLinkScheme && (!info2.includes("<key>CFBundleURLTypes</key>") || !info2.includes(`<string>${config.deepLinkScheme}</string>`)))
18442
+ throw new TypeError("iOS custom URL scheme does not match mobile config.");
18443
+ if (config.deepLinkHosts.some((host2) => !entitlements.includes(`<string>applinks:${host2}</string>`)))
18444
+ throw new TypeError("iOS associated domains do not match mobile config.");
18445
+ if (!project.includes("CODE_SIGN_ENTITLEMENTS = App/AbsoluteJS.entitlements;"))
18446
+ throw new TypeError("iOS target does not sign the AbsoluteJS entitlements file.");
18447
+ return pass("ios.deep-links", "iOS universal links, custom scheme, and signed entitlements match mobile config.", entitlementsPath);
18448
+ } catch (error) {
18449
+ return fail5("ios.deep-links", error instanceof Error ? error.message : "iOS deep-link projection could not be validated.", entitlementsPath, "Run `absolute mobile sync ios` and review the AbsoluteJS-owned deep-link and entitlement projection.");
18450
+ }
18122
18451
  }, syncSchemaReleaseCheck = (projectRoot) => {
18123
18452
  if (!projectUsesAbsoluteSync(projectRoot))
18124
18453
  return;
18125
- const manifestPath = join49(projectRoot, "package.json");
18454
+ const manifestPath = join50(projectRoot, "package.json");
18126
18455
  try {
18127
18456
  const schema = discoverAbsoluteSyncSchema(projectRoot);
18128
18457
  const versions = schema.components.map((component2) => `${component2.id}@${component2.version}`).join(", ");
@@ -18144,8 +18473,8 @@ var HMR_ASSET_PATTERN, RELEASE_ASSET_EXTENSIONS, pathExists5 = async (path) => {
18144
18473
  }, IOS_USAGE_KEYS, androidDevicePermissionCheck = async (config, permissions) => {
18145
18474
  if (!config.platforms.includes("android") || permissions.length === 0)
18146
18475
  return;
18147
- const path = join49(config.nativeProjectDirectory, "android/app/src/main/AndroidManifest.xml");
18148
- const source = await readFile16(path, "utf8");
18476
+ const path = join50(config.nativeProjectDirectory, "android/app/src/main/AndroidManifest.xml");
18477
+ const source = await readFile17(path, "utf8");
18149
18478
  const missing = permissions.filter((permission) => !source.includes(`android:name="${permission}"`) && !source.includes(`android:name='${permission}'`));
18150
18479
  if (missing.length === 0)
18151
18480
  return;
@@ -18153,14 +18482,44 @@ var HMR_ASSET_PATTERN, RELEASE_ASSET_EXTENSIONS, pathExists5 = async (path) => {
18153
18482
  }, iosDevicePermissionCheck = async (config, purposes) => {
18154
18483
  if (!config.platforms.includes("ios") || purposes.length === 0)
18155
18484
  return;
18156
- const path = join49(config.nativeProjectDirectory, "ios/App/App/Info.plist");
18157
- const source = await readFile16(path, "utf8");
18485
+ const path = join50(config.nativeProjectDirectory, "ios/App/App/Info.plist");
18486
+ const source = await readFile17(path, "utf8");
18158
18487
  const missing = purposes.filter((purpose) => !source.includes(`<key>${IOS_USAGE_KEYS[purpose]}</key>`));
18159
18488
  if (missing.length === 0)
18160
18489
  return;
18161
18490
  return fail5("mobile.device-capabilities", `iOS is missing usage descriptions for: ${missing.join(", ")}.`, path, "Run `absolute mobile sync ios` to regenerate detected device usage descriptions.");
18491
+ }, iosCapabilityProjectionCheck = async (config, requirements) => {
18492
+ if (!config.platforms.includes("ios"))
18493
+ return;
18494
+ const appRoot = join50(config.nativeProjectDirectory, "ios/App/App");
18495
+ const infoPath = join50(appRoot, "Info.plist");
18496
+ const info2 = await readFile17(infoPath, "utf8").catch(() => "");
18497
+ if (requirements.iosSystemBars && !/<key>UIViewControllerBasedStatusBarAppearance<\/key>\s*<true\s*\/>/u.test(info2))
18498
+ 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.");
18499
+ if (requirements.iosPrivacyAccessedApis.length > 0) {
18500
+ const privacyPath = join50(appRoot, "PrivacyInfo.xcprivacy");
18501
+ const projectPath = join50(config.nativeProjectDirectory, "ios/App/App.xcodeproj/project.pbxproj");
18502
+ const [privacy, project] = await Promise.all([
18503
+ readFile17(privacyPath, "utf8").catch(() => ""),
18504
+ readFile17(projectPath, "utf8").catch(() => "")
18505
+ ]);
18506
+ const missing = requirements.iosPrivacyAccessedApis.some(({ api, reasons }) => !privacy.includes(`<string>${api}</string>`) || reasons.some((reason) => !privacy.includes(`<string>${reason}</string>`)));
18507
+ if (missing || !project.includes("PrivacyInfo.xcprivacy in Resources"))
18508
+ 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.");
18509
+ }
18510
+ if (requirements.iosPushNotifications) {
18511
+ const entitlementsPath = join50(config.nativeProjectDirectory, "ios/App/AbsoluteJS.entitlements");
18512
+ const delegatePath = join50(appRoot, "AppDelegate.swift");
18513
+ const [entitlements, delegate] = await Promise.all([
18514
+ readFile17(entitlementsPath, "utf8").catch(() => ""),
18515
+ readFile17(delegatePath, "utf8").catch(() => "")
18516
+ ]);
18517
+ if (!entitlements.includes("<key>aps-environment</key>") || !delegate.includes("capacitorDidRegisterForRemoteNotifications") || !delegate.includes("capacitorDidFailToRegisterForRemoteNotifications"))
18518
+ 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.");
18519
+ }
18520
+ return;
18162
18521
  }, deviceCapabilityReleaseCheck = async (config, projectRoot) => {
18163
- const manifestPath = join49(projectRoot, "package.json");
18522
+ const manifestPath = join50(projectRoot, "package.json");
18164
18523
  try {
18165
18524
  const plan = resolveAbsoluteDeviceCapabilityPlan(projectRoot);
18166
18525
  assertAbsoluteDeviceCapabilityPackages(projectRoot, plan);
@@ -18171,32 +18530,41 @@ var HMR_ASSET_PATTERN, RELEASE_ASSET_EXTENSIONS, pathExists5 = async (path) => {
18171
18530
  const iosCheck = await iosDevicePermissionCheck(config, requirements.iosUsageDescriptions);
18172
18531
  if (iosCheck)
18173
18532
  return iosCheck;
18533
+ const iosProjectionCheck = await iosCapabilityProjectionCheck(config, requirements);
18534
+ if (iosProjectionCheck)
18535
+ return iosProjectionCheck;
18174
18536
  return pass("mobile.device-capabilities", plan.capabilities.length > 0 ? `Native provider packages and permission declarations match detected capabilities: ${plan.capabilities.join(", ")}.` : "No optional native device capabilities are used.", manifestPath);
18175
18537
  } catch (error) {
18176
18538
  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.");
18177
18539
  }
18178
18540
  }, inspectAndroidRelease = async (config, projectRoot) => {
18179
- const androidRoot = join49(config.nativeProjectDirectory, "android");
18180
- const nativeConfigPath = join49(androidRoot, "app", "src", "main", "assets", "capacitor.config.json");
18181
- const manifestPath = join49(androidRoot, "app", "src", "main", "AndroidManifest.xml");
18182
- const publicRoot = join49(androidRoot, "app", "src", "main", "assets", "public");
18183
- const journalPath = join49(projectRoot, ".absolutejs", "mobile", "dev-session", "journal.json");
18541
+ const androidRoot = join50(config.nativeProjectDirectory, "android");
18542
+ const nativeConfigPath = join50(androidRoot, "app", "src", "main", "assets", "capacitor.config.json");
18543
+ const manifestPath = join50(androidRoot, "app", "src", "main", "AndroidManifest.xml");
18544
+ const publicRoot = join50(androidRoot, "app", "src", "main", "assets", "public");
18545
+ const journalPath = join50(projectRoot, ".absolutejs", "mobile", "dev-session", "journal.json");
18184
18546
  const checks = await Promise.all([
18185
18547
  journalReleaseCheck(journalPath, "android"),
18186
18548
  capacitorConfigReleaseCheck(nativeConfigPath),
18549
+ capacitorIdentityCheck(config, "android", nativeConfigPath),
18187
18550
  manifestReleaseCheck(manifestPath),
18188
- hmrAssetsReleaseCheck(publicRoot)
18551
+ hmrAssetsReleaseCheck(publicRoot),
18552
+ embeddedBundleReleaseCheck(config, projectRoot, "android", publicRoot),
18553
+ contentSecurityPolicyCheck(config, "android", publicRoot),
18554
+ androidNativeSecurityCheck(androidRoot),
18555
+ androidExportedComponentsCheck(manifestPath),
18556
+ androidDeepLinkProjectionCheck(config, manifestPath)
18189
18557
  ]);
18190
18558
  return checks.map((check2) => ({
18191
18559
  ...check2,
18192
- path: check2.path ? relative24(projectRoot, check2.path).replaceAll("\\", "/") || "." : undefined
18560
+ path: check2.path ? relative25(projectRoot, check2.path).replaceAll("\\", "/") || "." : undefined
18193
18561
  }));
18194
18562
  }, inspectIosRelease = async (config, projectRoot) => {
18195
- const iosAppRoot = join49(config.nativeProjectDirectory, "ios", "App", "App");
18196
- const nativeConfigPath = join49(iosAppRoot, "capacitor.config.json");
18197
- const infoPath = join49(iosAppRoot, "Info.plist");
18198
- const publicRoot = join49(iosAppRoot, "public");
18199
- const journalPath = join49(projectRoot, ".absolutejs", "mobile", "ios-dev-session", "journal.json");
18563
+ const iosAppRoot = join50(config.nativeProjectDirectory, "ios", "App", "App");
18564
+ const nativeConfigPath = join50(iosAppRoot, "capacitor.config.json");
18565
+ const infoPath = join50(iosAppRoot, "Info.plist");
18566
+ const publicRoot = join50(iosAppRoot, "public");
18567
+ const journalPath = join50(projectRoot, ".absolutejs", "mobile", "ios-dev-session", "journal.json");
18200
18568
  const checks = [
18201
18569
  await journalReleaseCheck(journalPath, "ios")
18202
18570
  ];
@@ -18205,27 +18573,59 @@ var HMR_ASSET_PATTERN, RELEASE_ASSET_EXTENSIONS, pathExists5 = async (path) => {
18205
18573
  } else {
18206
18574
  checks.push(pass("ios.marketing-version", `The iOS marketing version is ${config.iosVersion}.`));
18207
18575
  }
18208
- if (!await pathExists5(nativeConfigPath)) {
18576
+ if (!await pathExists6(nativeConfigPath)) {
18209
18577
  checks.push(fail5("ios.capacitor-config", "The generated iOS Capacitor config is missing.", nativeConfigPath, "Run `absolute mobile sync ios` before release validation."));
18210
- } else if (isUnsafeCapacitorConfig(await readFile16(nativeConfigPath, "utf8"))) {
18578
+ } else if (isUnsafeCapacitorConfig(await readFile17(nativeConfigPath, "utf8"))) {
18211
18579
  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."));
18212
18580
  } else {
18213
18581
  checks.push(pass("ios.capacitor-config", "iOS Capacitor config contains no development transport overrides.", nativeConfigPath));
18214
18582
  }
18215
- if (!await pathExists5(infoPath)) {
18583
+ checks.push(await capacitorIdentityCheck(config, "ios", nativeConfigPath));
18584
+ if (!await pathExists6(infoPath)) {
18216
18585
  checks.push(fail5("ios.transport-security", "The iOS Info.plist is missing.", infoPath, "Run `absolute mobile sync ios` before release validation."));
18217
18586
  } else {
18218
- const info2 = await readFile16(infoPath, "utf8");
18587
+ const info2 = await readFile17(infoPath, "utf8");
18219
18588
  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));
18220
18589
  }
18221
18590
  const hmrAsset = await findHmrAsset(publicRoot);
18222
18591
  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));
18592
+ checks.push(await embeddedBundleReleaseCheck(config, projectRoot, "ios", publicRoot), await contentSecurityPolicyCheck(config, "ios", publicRoot), await iosNativeSecurityCheck(join50(config.nativeProjectDirectory, "ios")), await iosDeepLinkProjectionCheck(config, join50(config.nativeProjectDirectory, "ios")));
18223
18593
  return checks.map((check2) => ({
18224
18594
  ...check2,
18225
- path: check2.path ? relative24(projectRoot, check2.path).replaceAll("\\", "/") || "." : undefined
18595
+ path: check2.path ? relative25(projectRoot, check2.path).replaceAll("\\", "/") || "." : undefined
18226
18596
  }));
18597
+ }, createAbsoluteMobileComplianceReport = (config, result) => {
18598
+ const summary = {
18599
+ failed: result.checks.filter(({ status: status2 }) => status2 === "fail").length,
18600
+ passed: result.checks.filter(({ status: status2 }) => status2 === "pass").length,
18601
+ warnings: result.checks.filter(({ status: status2 }) => status2 === "warn").length
18602
+ };
18603
+ return {
18604
+ app: {
18605
+ appId: config.appId,
18606
+ engine: config.engine,
18607
+ platforms: [...config.platforms],
18608
+ productionOrigin: config.productionOrigin
18609
+ },
18610
+ checks: result.checks.map(({ id, status: status2 }) => ({ id, status: status2 })),
18611
+ format: ABSOLUTE_MOBILE_COMPLIANCE_REPORT_FORMAT,
18612
+ manualReview: [...MANUAL_REVIEW],
18613
+ ready: result.ready,
18614
+ summary
18615
+ };
18227
18616
  }, inspectAbsoluteMobileRelease = async (config, projectRoot) => {
18228
- const checks = config.platforms.includes("android") ? await inspectAndroidRelease(config, projectRoot) : [];
18617
+ const globalChecks = await Promise.all([
18618
+ Promise.resolve(productionOriginCheck(config, projectRoot)),
18619
+ Promise.resolve(associationIdentityCheck(config, projectRoot)),
18620
+ dependencyLockCheck(projectRoot),
18621
+ capacitorVersionCheck(config, projectRoot)
18622
+ ]);
18623
+ const checks = globalChecks.map((check2) => ({
18624
+ ...check2,
18625
+ path: check2.path ? relative25(projectRoot, check2.path).replaceAll("\\", "/") || "." : undefined
18626
+ }));
18627
+ if (config.platforms.includes("android"))
18628
+ checks.push(...await inspectAndroidRelease(config, projectRoot));
18229
18629
  if (config.platforms.includes("ios")) {
18230
18630
  checks.push(...await inspectIosRelease(config, projectRoot));
18231
18631
  }
@@ -18233,25 +18633,41 @@ var HMR_ASSET_PATTERN, RELEASE_ASSET_EXTENSIONS, pathExists5 = async (path) => {
18233
18633
  if (syncSchema) {
18234
18634
  checks.push({
18235
18635
  ...syncSchema,
18236
- path: syncSchema.path ? relative24(projectRoot, syncSchema.path).replaceAll("\\", "/") || "." : undefined
18636
+ path: syncSchema.path ? relative25(projectRoot, syncSchema.path).replaceAll("\\", "/") || "." : undefined
18237
18637
  });
18238
18638
  }
18239
18639
  const deviceCapabilities = await deviceCapabilityReleaseCheck(config, projectRoot);
18240
18640
  checks.push({
18241
18641
  ...deviceCapabilities,
18242
- path: deviceCapabilities.path ? relative24(projectRoot, deviceCapabilities.path).replaceAll("\\", "/") || "." : undefined
18642
+ path: deviceCapabilities.path ? relative25(projectRoot, deviceCapabilities.path).replaceAll("\\", "/") || "." : undefined
18243
18643
  });
18244
18644
  return {
18245
18645
  checks,
18246
- ready: checks.length > 0 && checks.every((check2) => check2.status === "pass")
18646
+ ready: checks.length > 0 && checks.every((check2) => check2.status !== "fail")
18247
18647
  };
18248
18648
  };
18249
18649
  var init_releaseDoctor = __esm(() => {
18650
+ init_mobileBundleInspection();
18250
18651
  init_nativeAuth();
18251
18652
  init_syncSchema();
18252
18653
  init_deviceCapabilities();
18253
18654
  HMR_ASSET_PATTERN = /(?:__HMR_WS__|hmr-timing|__absolute_target|absolutejs-error-overlay)/u;
18254
18655
  RELEASE_ASSET_EXTENSIONS = new Set([".html", ".js", ".mjs"]);
18656
+ EXACT_VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u;
18657
+ LOCK_FILES = [
18658
+ "bun.lock",
18659
+ "bun.lockb",
18660
+ "package-lock.json",
18661
+ "pnpm-lock.yaml",
18662
+ "yarn.lock"
18663
+ ];
18664
+ MANUAL_REVIEW = [
18665
+ "physical-device",
18666
+ "store-privacy-questionnaire",
18667
+ "privacy-policy",
18668
+ "signing-key-custody",
18669
+ "native-sdk-data-practices"
18670
+ ];
18255
18671
  IOS_USAGE_KEYS = {
18256
18672
  camera: "NSCameraUsageDescription",
18257
18673
  "location-always": "NSLocationAlwaysAndWhenInUseUsageDescription",
@@ -18262,19 +18678,19 @@ var init_releaseDoctor = __esm(() => {
18262
18678
  });
18263
18679
 
18264
18680
  // src/mobile/androidRelease.ts
18265
- import { createHash as createHash12 } from "crypto";
18681
+ import { createHash as createHash13 } from "crypto";
18266
18682
  import {
18267
- access as access9,
18683
+ access as access10,
18268
18684
  copyFile as copyFile5,
18269
18685
  mkdir as mkdir12,
18270
18686
  mkdtemp as mkdtemp5,
18271
- readFile as readFile17,
18687
+ readFile as readFile18,
18272
18688
  rename as rename12,
18273
18689
  rm as rm8,
18274
- stat as stat2,
18690
+ stat as stat3,
18275
18691
  writeFile as writeFile14
18276
18692
  } from "fs/promises";
18277
- import { dirname as dirname30, isAbsolute as isAbsolute7, join as join50, relative as relative25, resolve as resolve39, sep as sep6 } from "path";
18693
+ import { dirname as dirname30, isAbsolute as isAbsolute7, join as join51, relative as relative26, resolve as resolve40, sep as sep6 } from "path";
18278
18694
  var ABSOLUTE_ANDROID_RELEASE_FORMAT = 1, isRecord13 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), requireManifest2 = (value) => {
18279
18695
  if (!isRecord13(value) || typeof value.appBuild !== "string" || typeof value.appId !== "string" || typeof value.runtime !== "string") {
18280
18696
  throw new TypeError("Invalid embedded AbsoluteJS mobile manifest.");
@@ -18284,9 +18700,9 @@ var ABSOLUTE_ANDROID_RELEASE_FORMAT = 1, isRecord13 = (value) => typeof value ==
18284
18700
  appId: value.appId,
18285
18701
  runtime: value.runtime
18286
18702
  };
18287
- }, pathExists6 = async (path) => {
18703
+ }, pathExists7 = async (path) => {
18288
18704
  try {
18289
- await access9(path);
18705
+ await access10(path);
18290
18706
  return true;
18291
18707
  } catch {
18292
18708
  return false;
@@ -18324,22 +18740,22 @@ var ABSOLUTE_ANDROID_RELEASE_FORMAT = 1, isRecord13 = (value) => typeof value ==
18324
18740
  artifactPath
18325
18741
  ]);
18326
18742
  return result.exitCode === 0 && /jar verified/iu.test(result.stdout);
18327
- }, sha256File2 = async (path) => createHash12("sha256").update(await readFile17(path)).digest("hex"), safeOutputDirectory2 = (projectRoot, requested) => {
18328
- const root = resolve39(projectRoot);
18329
- const output = resolve39(root, requested ?? ".absolutejs/mobile/releases/android");
18330
- const projectRelative = relative25(root, output);
18743
+ }, sha256File2 = async (path) => createHash13("sha256").update(await readFile18(path)).digest("hex"), safeOutputDirectory2 = (projectRoot, requested) => {
18744
+ const root = resolve40(projectRoot);
18745
+ const output = resolve40(root, requested ?? ".absolutejs/mobile/releases/android");
18746
+ const projectRelative = relative26(root, output);
18331
18747
  if (projectRelative === ".." || projectRelative.startsWith(`..${sep6}`) || isAbsolute7(projectRelative)) {
18332
18748
  throw new TypeError("mobile build --outdir must remain inside the project.");
18333
18749
  }
18334
18750
  return output;
18335
18751
  }, installRelease2 = async (artifactPath, metadata, outputRoot) => {
18336
- const releaseRoot = join50(outputRoot, metadata.releaseId);
18752
+ const releaseRoot = join51(outputRoot, metadata.releaseId);
18337
18753
  const artifactName = "app-release.aab";
18338
- const destination = join50(releaseRoot, artifactName);
18339
- if (await pathExists6(releaseRoot)) {
18340
- const existing = requireManifestIdentity(JSON.parse(await readFile17(join50(releaseRoot, "release.json"), "utf8")), metadata);
18754
+ const destination = join51(releaseRoot, artifactName);
18755
+ if (await pathExists7(releaseRoot)) {
18756
+ const existing = requireManifestIdentity(JSON.parse(await readFile18(join51(releaseRoot, "release.json"), "utf8")), metadata);
18341
18757
  const [installedBytes, installedSha256] = await Promise.all([
18342
- stat2(destination).then(({ size }) => size),
18758
+ stat3(destination).then(({ size }) => size),
18343
18759
  sha256File2(destination)
18344
18760
  ]);
18345
18761
  if (installedBytes !== metadata.bytes || installedSha256 !== metadata.sha256) {
@@ -18348,14 +18764,14 @@ var ABSOLUTE_ANDROID_RELEASE_FORMAT = 1, isRecord13 = (value) => typeof value ==
18348
18764
  return { artifactPath: destination, metadata: existing, releaseRoot };
18349
18765
  }
18350
18766
  await mkdir12(dirname30(releaseRoot), { recursive: true });
18351
- const staging = await mkdtemp5(join50(dirname30(releaseRoot), ".android-stage-"));
18767
+ const staging = await mkdtemp5(join51(dirname30(releaseRoot), ".android-stage-"));
18352
18768
  try {
18353
- await copyFile5(artifactPath, join50(staging, artifactName));
18769
+ await copyFile5(artifactPath, join51(staging, artifactName));
18354
18770
  const complete = {
18355
18771
  ...metadata,
18356
18772
  artifact: artifactName
18357
18773
  };
18358
- await writeFile14(join50(staging, "release.json"), `${JSON.stringify(complete, null, "\t")}
18774
+ await writeFile14(join51(staging, "release.json"), `${JSON.stringify(complete, null, "\t")}
18359
18775
  `, { flag: "wx" });
18360
18776
  await rename12(staging, releaseRoot);
18361
18777
  return { artifactPath: destination, metadata: complete, releaseRoot };
@@ -18380,18 +18796,18 @@ var ABSOLUTE_ANDROID_RELEASE_FORMAT = 1, isRecord13 = (value) => typeof value ==
18380
18796
  if (options.versionCode !== undefined && (!Number.isSafeInteger(options.versionCode) || options.versionCode < 1 || options.versionCode > 2100000000)) {
18381
18797
  throw new TypeError("Android versionCode must be an integer from 1 through 2100000000.");
18382
18798
  }
18383
- const projectRoot = resolve39(options.projectRoot);
18799
+ const projectRoot = resolve40(options.projectRoot);
18384
18800
  const host2 = options.host ?? detectAbsoluteMobileHost();
18385
18801
  const androidRoot = options.androidRoot ?? process.env.ANDROID_HOME ?? process.env.ANDROID_SDK_ROOT ?? absoluteManagedAndroidSdkRoot(host2);
18386
- const nativeDirectory = join50(options.config.nativeProjectDirectory, "android");
18387
- const manifest = requireManifest2(JSON.parse(await readFile17(join50(options.config.bundleDirectory, "absolute-mobile-manifest.json"), "utf8")));
18802
+ const nativeDirectory = join51(options.config.nativeProjectDirectory, "android");
18803
+ const manifest = requireManifest2(JSON.parse(await readFile18(join51(options.config.bundleDirectory, "absolute-mobile-manifest.json"), "utf8")));
18388
18804
  if (manifest.appId !== options.config.appId) {
18389
18805
  throw new TypeError("Embedded mobile manifest appId does not match mobile.appId.");
18390
18806
  }
18391
18807
  let { versionCode } = options;
18392
18808
  if (options.prepareVersionCode) {
18393
18809
  const nativeFingerprint = await fingerprintAbsoluteAndroidNativeProject({ nativeDirectory });
18394
- const buildIdentity = createHash12("sha256").update(`${manifest.appBuild}\x00${nativeFingerprint}`).digest("hex");
18810
+ const buildIdentity = createHash13("sha256").update(`${manifest.appBuild}\x00${nativeFingerprint}`).digest("hex");
18395
18811
  versionCode = await options.prepareVersionCode(buildIdentity);
18396
18812
  }
18397
18813
  if (versionCode !== undefined && (!Number.isSafeInteger(versionCode) || versionCode < 1 || versionCode > 2100000000)) {
@@ -18410,7 +18826,7 @@ var ABSOLUTE_ANDROID_RELEASE_FORMAT = 1, isRecord13 = (value) => typeof value ==
18410
18826
  run: options.run,
18411
18827
  task: "bundleRelease"
18412
18828
  });
18413
- if (!await pathExists6(artifactPath)) {
18829
+ if (!await pathExists7(artifactPath)) {
18414
18830
  throw new TypeError(`Android Gradle did not produce the expected App Bundle: ${artifactPath}`);
18415
18831
  }
18416
18832
  const capture = options.capture ?? defaultCapture4;
@@ -18422,7 +18838,7 @@ var ABSOLUTE_ANDROID_RELEASE_FORMAT = 1, isRecord13 = (value) => typeof value ==
18422
18838
  throw new TypeError("Android Gradle produced an unsigned App Bundle. Configure the release signingConfig in the source-owned Android project (prefer external Gradle properties), or pass --unsigned only for a non-publishable build.");
18423
18839
  }
18424
18840
  const [bytes, sha2562] = await Promise.all([
18425
- stat2(artifactPath).then(({ size }) => size),
18841
+ stat3(artifactPath).then(({ size }) => size),
18426
18842
  sha256File2(artifactPath)
18427
18843
  ]);
18428
18844
  const releaseId = `amobile_android_${sha2562}`;
@@ -18486,7 +18902,7 @@ var absoluteIosDeviceAcceptanceCommands = (options) => {
18486
18902
  };
18487
18903
 
18488
18904
  // src/mobile/iosConformance.ts
18489
- import { readFile as readFile18, stat as stat3 } from "fs/promises";
18905
+ import { readFile as readFile19, stat as stat4 } from "fs/promises";
18490
18906
  var HMR_LINE, parseAbsoluteIosHmrLog = (line) => {
18491
18907
  const match = HMR_LINE.exec(line);
18492
18908
  if (!match)
@@ -18515,13 +18931,13 @@ var HMR_LINE, parseAbsoluteIosHmrLog = (line) => {
18515
18931
  const sleep2 = options.sleep ?? Bun.sleep;
18516
18932
  const timeoutMs = options.timeoutMs ?? 30000;
18517
18933
  const deadline = Date.now() + timeoutMs;
18518
- let offset = options.startOffset ?? await stat3(options.logPath).then(({ size }) => size).catch(() => 0);
18934
+ let offset = options.startOffset ?? await stat4(options.logPath).then(({ size }) => size).catch(() => 0);
18519
18935
  let buffered = "";
18520
18936
  const poll = async () => {
18521
18937
  if (Date.now() > deadline)
18522
18938
  throw new Error(`No iOS native HMR acknowledgement was observed within ${timeoutMs}ms.`);
18523
18939
  options.signal?.throwIfAborted();
18524
- const contents = await readFile18(options.logPath).catch(() => Buffer.alloc(0));
18940
+ const contents = await readFile19(options.logPath).catch(() => Buffer.alloc(0));
18525
18941
  if (contents.byteLength < offset) {
18526
18942
  offset = 0;
18527
18943
  buffered = "";
@@ -18545,8 +18961,8 @@ var init_iosConformance = __esm(() => {
18545
18961
  });
18546
18962
 
18547
18963
  // src/mobile/nativeTestReport.ts
18548
- import { mkdir as mkdir13, readFile as readFile19, writeFile as writeFile15 } from "fs/promises";
18549
- import { join as join51 } from "path";
18964
+ import { mkdir as mkdir13, readFile as readFile20, writeFile as writeFile15 } from "fs/promises";
18965
+ import { join as join52 } from "path";
18550
18966
  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(`
18551
18967
  `, "<br>"), createAbsoluteNativeAutomatedChecks = (run) => {
18552
18968
  const target = `${run.targetKind} ${run.targetId}`;
@@ -18648,7 +19064,7 @@ var secretPattern, bearerPattern, coordinatePattern, nativeCredentialPattern, sa
18648
19064
  reportVersion: 1,
18649
19065
  run: options.run
18650
19066
  }), readPackageVersionForNativeReport = async (packageJsonPath) => {
18651
- const manifest = JSON.parse(await readFile19(packageJsonPath, "utf8"));
19067
+ const manifest = JSON.parse(await readFile20(packageJsonPath, "utf8"));
18652
19068
  if (typeof manifest !== "object" || manifest === null)
18653
19069
  return "unknown";
18654
19070
  const version2 = Reflect.get(manifest, "version");
@@ -18685,8 +19101,8 @@ ${table(report.manualChecks)}
18685
19101
  `;
18686
19102
  }, writeAbsoluteNativeTestReport = async (directory, report) => {
18687
19103
  await mkdir13(directory, { recursive: true });
18688
- const jsonPath = join51(directory, "report.json");
18689
- const markdownPath = join51(directory, "report.md");
19104
+ const jsonPath = join52(directory, "report.json");
19105
+ const markdownPath = join52(directory, "report.md");
18690
19106
  await Promise.all([
18691
19107
  writeFile15(jsonPath, `${JSON.stringify(report, null, 2)}
18692
19108
  `),
@@ -18904,8 +19320,8 @@ var init_androidTestReport = __esm(() => {
18904
19320
  });
18905
19321
 
18906
19322
  // src/mobile/releasePublisher.ts
18907
- import { access as access10 } from "fs/promises";
18908
- import { isAbsolute as isAbsolute8, relative as relative26, resolve as resolve40, sep as sep7 } from "path";
19323
+ import { access as access11 } from "fs/promises";
19324
+ import { isAbsolute as isAbsolute8, relative as relative27, resolve as resolve41, sep as sep7 } from "path";
18909
19325
  import { pathToFileURL as pathToFileURL2 } from "url";
18910
19326
  var prepareAbsoluteIosRelease = async (publisher, options) => {
18911
19327
  if (typeof publisher.prepareIosRelease !== "function") {
@@ -18927,16 +19343,16 @@ var prepareAbsoluteIosRelease = async (publisher, options) => {
18927
19343
  }
18928
19344
  return versionCode;
18929
19345
  }, isRecord14 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), isPublisher = (value) => isRecord14(value) && typeof value.publish === "function", publisherModulePath = (projectRoot, requested) => {
18930
- const root = resolve40(projectRoot);
18931
- const path = resolve40(root, requested);
18932
- const projectRelative = relative26(root, path);
19346
+ const root = resolve41(projectRoot);
19347
+ const path = resolve41(root, requested);
19348
+ const projectRelative = relative27(root, path);
18933
19349
  if (projectRelative === ".." || projectRelative.startsWith(`..${sep7}`) || isAbsolute8(projectRelative)) {
18934
19350
  throw new TypeError("mobile publish --registry must remain inside the project.");
18935
19351
  }
18936
19352
  return path;
18937
19353
  }, loadAbsoluteNativeReleasePublisher = async (projectRoot, requestedModulePath) => {
18938
19354
  const modulePath = publisherModulePath(projectRoot, requestedModulePath);
18939
- await access10(modulePath).catch(() => {
19355
+ await access11(modulePath).catch(() => {
18940
19356
  throw new TypeError(`Native release registry module does not exist: ${modulePath}`);
18941
19357
  });
18942
19358
  const loaded = await import(pathToFileURL2(modulePath).href);
@@ -18996,16 +19412,270 @@ var prepareAbsoluteIosRelease = async (publisher, options) => {
18996
19412
  };
18997
19413
  var init_releasePublisher = () => {};
18998
19414
 
19415
+ // src/mobile/mobileInspect.ts
19416
+ import { access as access12, readFile as readFile21, stat as stat5 } from "fs/promises";
19417
+ import { join as join53, relative as relative28, resolve as resolve42 } from "path";
19418
+ var ABSOLUTE_MOBILE_INSPECTION_FORMAT = 1, MOBILE_PACKAGE_NAMES, MOBILE_FRAMEWORKS2, isObject3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), portablePath2 = (projectRoot, path) => {
19419
+ const value = relative28(resolve42(projectRoot), resolve42(path)).replaceAll("\\", "/");
19420
+ return value || ".";
19421
+ }, pathExists8 = async (path) => {
19422
+ try {
19423
+ await access12(path);
19424
+ return true;
19425
+ } catch {
19426
+ return false;
19427
+ }
19428
+ }, readObject2 = async (path) => {
19429
+ const value = JSON.parse(await readFile21(path, "utf8"));
19430
+ if (!isObject3(value))
19431
+ throw new TypeError("JSON root must be an object.");
19432
+ return value;
19433
+ }, requireString2 = (value, field) => {
19434
+ if (typeof value !== "string" || value.length === 0)
19435
+ throw new TypeError(`${field} must be a non-empty string.`);
19436
+ return value;
19437
+ }, requireStringArray2 = (value, field) => {
19438
+ if (!Array.isArray(value) || !value.every((item) => typeof item === "string"))
19439
+ throw new TypeError(`${field} must be a string array.`);
19440
+ return value;
19441
+ }, requireBundleFile2 = async (root, value, field) => {
19442
+ const portable = requireString2(value, field);
19443
+ const path = resolve42(root, portable);
19444
+ const normalizedRoot = resolve42(root);
19445
+ if (path === normalizedRoot || !path.startsWith(`${normalizedRoot}/`))
19446
+ throw new TypeError(`${field} must remain inside the mobile bundle.`);
19447
+ if (!(await stat5(path).catch(() => {
19448
+ return;
19449
+ }))?.isFile())
19450
+ throw new TypeError(`${field} does not exist in the mobile bundle.`);
19451
+ return portable;
19452
+ }, inspectBundle = async (config, projectRoot) => {
19453
+ const manifestPath = join53(config.bundleDirectory, "absolute-mobile-manifest.json");
19454
+ const manifest = portablePath2(projectRoot, manifestPath);
19455
+ if (!await pathExists8(manifestPath))
19456
+ return { manifest, status: "missing" };
19457
+ try {
19458
+ const value = await readObject2(manifestPath);
19459
+ if (value.format !== 1)
19460
+ throw new TypeError("format is not supported by this runtime.");
19461
+ if (requireString2(value.appId, "appId") !== config.appId)
19462
+ throw new TypeError("appId does not match the effective mobile config.");
19463
+ if (requireString2(value.productionOrigin, "productionOrigin") !== config.productionOrigin)
19464
+ throw new TypeError("productionOrigin does not match the effective mobile config.");
19465
+ const appBuild = requireString2(value.appBuild, "appBuild");
19466
+ const runtime = requireString2(value.runtime, "runtime");
19467
+ const capabilities = requireStringArray2(value.deviceCapabilities, "deviceCapabilities").sort();
19468
+ if (!Array.isArray(value.pages) || !Array.isArray(value.routes))
19469
+ throw new TypeError("pages and routes must be arrays.");
19470
+ const pageIds = new Set;
19471
+ const frameworks7 = new Set;
19472
+ await Promise.all(value.pages.map(async (candidate) => {
19473
+ if (!isObject3(candidate))
19474
+ throw new TypeError("pages contains an invalid entry.");
19475
+ const pageId = requireString2(candidate.pageId, "page.pageId");
19476
+ if (pageIds.has(pageId))
19477
+ throw new TypeError("page.pageId values must be unique.");
19478
+ pageIds.add(pageId);
19479
+ const framework = requireString2(candidate.framework, "page.framework");
19480
+ if (!MOBILE_FRAMEWORKS2.has(framework))
19481
+ throw new TypeError("page.framework is unsupported.");
19482
+ frameworks7.add(framework);
19483
+ requireString2(candidate.bundleHash, "page.bundleHash");
19484
+ requireString2(candidate.contract, "page.contract");
19485
+ requireString2(candidate.propsSchemaHash, "page.propsSchemaHash");
19486
+ await requireBundleFile2(config.bundleDirectory, candidate.localBundlePath, "page.localBundlePath");
19487
+ if (candidate.localStylePath !== undefined)
19488
+ await requireBundleFile2(config.bundleDirectory, candidate.localStylePath, "page.localStylePath");
19489
+ }));
19490
+ const routes = value.routes.map((candidate) => {
19491
+ if (!isObject3(candidate))
19492
+ throw new TypeError("routes contains an invalid entry.");
19493
+ const { method } = candidate;
19494
+ if (method !== "GET" && method !== "HEAD")
19495
+ throw new TypeError("route.method must be GET or HEAD.");
19496
+ const pageId = requireString2(candidate.pageId, "route.pageId");
19497
+ if (!pageIds.has(pageId))
19498
+ throw new TypeError("route.pageId references a missing page.");
19499
+ return {
19500
+ method,
19501
+ pageId,
19502
+ pattern: requireString2(candidate.pattern, "route.pattern")
19503
+ };
19504
+ });
19505
+ await Promise.all(["index.html", "absolute-mobile-bootstrap.js"].map((file) => requireBundleFile2(config.bundleDirectory, file, file)));
19506
+ const entryPath = new URL(config.entry, "https://absolute.invalid").pathname;
19507
+ const entryResolved = resolveAbsoluteMobileRoute(routes, entryPath) !== undefined;
19508
+ if (!entryResolved)
19509
+ throw new TypeError("entry is not owned by an embedded route.");
19510
+ return {
19511
+ appBuild,
19512
+ auth: isObject3(value.auth),
19513
+ capabilities,
19514
+ entryResolved,
19515
+ frameworks: [...frameworks7].sort(),
19516
+ manifest,
19517
+ pageCount: value.pages.length,
19518
+ routeCount: value.routes.length,
19519
+ runtime,
19520
+ status: "valid",
19521
+ sync: isObject3(value.sync)
19522
+ };
19523
+ } catch (error) {
19524
+ return {
19525
+ issue: error instanceof Error ? error.message : "The embedded mobile manifest is invalid.",
19526
+ manifest,
19527
+ status: "invalid"
19528
+ };
19529
+ }
19530
+ }, addPackageDeclarations = (declarations, value) => {
19531
+ if (!isObject3(value))
19532
+ return;
19533
+ for (const [name, declared] of Object.entries(value).filter((entry) => typeof entry[1] === "string"))
19534
+ declarations.set(name, declared);
19535
+ }, packageInspections = async (projectRoot, additionalNames) => {
19536
+ const project = await readObject2(join53(projectRoot, "package.json"));
19537
+ const declarations = new Map;
19538
+ for (const field of ["dependencies", "devDependencies"])
19539
+ addPackageDeclarations(declarations, project[field]);
19540
+ const names = [...new Set([...declarations.keys(), ...additionalNames])].filter((name) => MOBILE_PACKAGE_NAMES.has(name) || name.startsWith("@capacitor/") || additionalNames.includes(name)).sort();
19541
+ return Promise.all(names.map(async (name) => {
19542
+ const installedManifest = await readObject2(join53(projectRoot, "node_modules", name, "package.json")).catch(() => {
19543
+ return;
19544
+ });
19545
+ const installed = installedManifest?.version;
19546
+ return {
19547
+ declared: declarations.get(name) ?? "transitive",
19548
+ ...typeof installed === "string" ? { installed } : {},
19549
+ name
19550
+ };
19551
+ }));
19552
+ }, inspectAbsoluteMobileProject = async (config, projectRoot, options = {}) => {
19553
+ const bundle = await inspectBundle(config, projectRoot);
19554
+ let currentCapabilities = [];
19555
+ let capabilityIssue;
19556
+ let plugins = [];
19557
+ try {
19558
+ const plan = resolveAbsoluteDeviceCapabilityPlan(projectRoot);
19559
+ currentCapabilities = plan.capabilities;
19560
+ plugins = plan.requiredPackages;
19561
+ } catch {
19562
+ capabilityIssue = "Native capability metadata could not be resolved.";
19563
+ }
19564
+ const pluginNames = plugins.map((spec) => spec.slice(0, spec.lastIndexOf("@")));
19565
+ const releaseInspection = await (options.inspectRelease ?? inspectAbsoluteMobileRelease)(config, projectRoot).catch(() => ({ checks: [], ready: false }));
19566
+ return {
19567
+ bundle,
19568
+ capabilities: {
19569
+ current: currentCapabilities,
19570
+ ...bundle.capabilities ? {
19571
+ embeddedMatchesCurrent: JSON.stringify(bundle.capabilities) === JSON.stringify(currentCapabilities)
19572
+ } : {},
19573
+ ...capabilityIssue ? { issue: capabilityIssue } : {},
19574
+ plugins
19575
+ },
19576
+ config: {
19577
+ appId: config.appId,
19578
+ appName: config.appName,
19579
+ bundleDirectory: portablePath2(projectRoot, config.bundleDirectory),
19580
+ deepLinkHosts: config.deepLinkHosts,
19581
+ deepLinkScheme: config.deepLinkScheme,
19582
+ engine: config.engine,
19583
+ entry: config.entry,
19584
+ iosVersion: config.iosVersion,
19585
+ nativeProjectDirectory: portablePath2(projectRoot, config.nativeProjectDirectory),
19586
+ platforms: config.platforms,
19587
+ productionOrigin: config.productionOrigin
19588
+ },
19589
+ format: ABSOLUTE_MOBILE_INSPECTION_FORMAT,
19590
+ nativeProjects: await Promise.all(config.platforms.map(async (platform6) => {
19591
+ const path = join53(config.nativeProjectDirectory, platform6);
19592
+ return {
19593
+ initialized: await pathExists8(path),
19594
+ path: portablePath2(projectRoot, path),
19595
+ platform: platform6
19596
+ };
19597
+ })),
19598
+ packages: await packageInspections(projectRoot, pluginNames),
19599
+ release: {
19600
+ checks: releaseInspection.checks.map(({ id, status: status2 }) => ({
19601
+ id,
19602
+ status: status2
19603
+ })),
19604
+ ready: releaseInspection.ready
19605
+ },
19606
+ runtime: { absolutejs: options.absolutejsVersion ?? "unknown" }
19607
+ };
19608
+ }, yesNo = (value) => value ? "yes" : "no", renderAbsoluteMobileProjectInspection = (report) => {
19609
+ const lines = [
19610
+ `AbsoluteJS mobile inspection (format ${report.format})`,
19611
+ "",
19612
+ `App: ${report.config.appName} (${report.config.appId})`,
19613
+ `Engine: ${report.config.engine}`,
19614
+ `Platforms: ${report.config.platforms.join(", ")}`,
19615
+ `Entry: ${report.config.entry}`,
19616
+ `Production origin: ${report.config.productionOrigin}`,
19617
+ `AbsoluteJS: ${report.runtime.absolutejs}`,
19618
+ "",
19619
+ `Bundle: ${report.bundle.status} (${report.bundle.manifest})`
19620
+ ];
19621
+ if (report.bundle.status === "valid") {
19622
+ lines.push(` Build/runtime: ${report.bundle.appBuild} / ${report.bundle.runtime}`, ` Pages/routes: ${report.bundle.pageCount} / ${report.bundle.routeCount}`, ` Frameworks: ${report.bundle.frameworks?.join(", ") || "none"}`, ` Auth/Sync: ${yesNo(report.bundle.auth === true)} / ${yesNo(report.bundle.sync === true)}`);
19623
+ } else if (report.bundle.issue)
19624
+ lines.push(` Issue: ${report.bundle.issue}`);
19625
+ lines.push("", `Capabilities: ${report.capabilities.current.join(", ") || "none"}`, `Native plugins: ${report.capabilities.plugins.join(", ") || "none"}`);
19626
+ if (report.capabilities.embeddedMatchesCurrent !== undefined)
19627
+ lines.push(`Embedded capabilities current: ${yesNo(report.capabilities.embeddedMatchesCurrent)}`);
19628
+ if (report.capabilities.issue)
19629
+ lines.push(`Capability issue: ${report.capabilities.issue}`);
19630
+ lines.push("", "Native projects:");
19631
+ for (const project of report.nativeProjects)
19632
+ lines.push(` ${project.platform}: ${project.initialized ? "initialized" : "missing"} (${project.path})`);
19633
+ lines.push("", "Runtime packages:");
19634
+ for (const runtimePackage of report.packages)
19635
+ lines.push(` ${runtimePackage.name}: ${runtimePackage.installed ?? "not installed"} (declared ${runtimePackage.declared})`);
19636
+ const failed = report.release.checks.filter((check2) => check2.status === "fail").length;
19637
+ const warned = report.release.checks.filter((check2) => check2.status === "warn").length;
19638
+ lines.push("", `Release projection: ${report.release.ready ? "ready" : "not ready"} (${failed} failed, ${warned} warnings)`);
19639
+ return `${lines.join(`
19640
+ `)}
19641
+ `;
19642
+ };
19643
+ var init_mobileInspect = __esm(() => {
19644
+ init_deviceCapabilities();
19645
+ init_releaseDoctor();
19646
+ init_routeMatcher();
19647
+ MOBILE_PACKAGE_NAMES = new Set([
19648
+ "@absolutejs/absolute",
19649
+ "@absolutejs/auth",
19650
+ "@absolutejs/devices",
19651
+ "@absolutejs/devices-capacitor",
19652
+ "@absolutejs/http",
19653
+ "@absolutejs/pwa",
19654
+ "@absolutejs/sync",
19655
+ "@absolutejs/sync-capacitor",
19656
+ "@capacitor-community/sqlite"
19657
+ ]);
19658
+ MOBILE_FRAMEWORKS2 = new Set([
19659
+ "angular",
19660
+ "ember",
19661
+ "html",
19662
+ "htmx",
19663
+ "react",
19664
+ "svelte",
19665
+ "vue"
19666
+ ]);
19667
+ });
19668
+
18999
19669
  // src/cli/scripts/mobile.ts
19000
19670
  var exports_mobile = {};
19001
19671
  __export(exports_mobile, {
19002
19672
  runMobile: () => runMobile
19003
19673
  });
19004
- import { access as access11, mkdir as mkdir14, readFile as readFile20, writeFile as writeFile16 } from "fs/promises";
19005
- import { join as join52, resolve as resolve41 } from "path";
19674
+ import { access as access13, mkdir as mkdir14, readFile as readFile22, writeFile as writeFile16 } from "fs/promises";
19675
+ import { join as join54, resolve as resolve43 } from "path";
19006
19676
  import { createInterface } from "readline/promises";
19007
- var NOT_FOUND3 = -1, isRecord15 = (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) => {
19008
- const manifest = JSON.parse(await readFile20(join52(projectRoot, "package.json"), "utf8"));
19677
+ var NOT_FOUND4 = -1, isRecord15 = (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) => {
19678
+ const manifest = JSON.parse(await readFile22(join54(projectRoot, "package.json"), "utf8"));
19009
19679
  if (!isRecord15(manifest))
19010
19680
  throw new TypeError("Application package.json must contain an object.");
19011
19681
  const names = new Set;
@@ -19018,7 +19688,7 @@ var NOT_FOUND3 = -1, isRecord15 = (value) => typeof value === "object" && value
19018
19688
  return names;
19019
19689
  }, resolvedPackageVersion = async (projectRoot, packageName) => {
19020
19690
  try {
19021
- const manifest = JSON.parse(await readFile20(join52(projectRoot, "node_modules", packageName, "package.json"), "utf8"));
19691
+ const manifest = JSON.parse(await readFile22(join54(projectRoot, "node_modules", packageName, "package.json"), "utf8"));
19022
19692
  return isRecord15(manifest) && typeof manifest.version === "string" ? manifest.version : undefined;
19023
19693
  } catch {
19024
19694
  return;
@@ -19049,7 +19719,7 @@ var NOT_FOUND3 = -1, isRecord15 = (value) => typeof value === "object" && value
19049
19719
  await installApprovedPackages(projectRoot, args, `AbsoluteJS detected native device capabilities (${capabilityPlan.capabilities.join(", ")}). Install only their required Capacitor plugins now?`, capabilityPackages);
19050
19720
  }, valueAfter = (args, flag) => {
19051
19721
  const index = args.indexOf(flag);
19052
- return index === NOT_FOUND3 ? undefined : args[index + 1];
19722
+ return index === NOT_FOUND4 ? undefined : args[index + 1];
19053
19723
  }, valuesAfter = (args, flag) => args.flatMap((value, index) => {
19054
19724
  const next = args[index + 1];
19055
19725
  return value === flag && next !== undefined ? [next] : [];
@@ -19059,9 +19729,9 @@ var NOT_FOUND3 = -1, isRecord15 = (value) => typeof value === "object" && value
19059
19729
  }
19060
19730
  return value;
19061
19731
  }, capacitorExecutable = async (projectRoot) => {
19062
- const executable = join52(projectRoot, "node_modules", ".bin", "cap");
19732
+ const executable = join54(projectRoot, "node_modules", ".bin", "cap");
19063
19733
  try {
19064
- await access11(executable);
19734
+ await access13(executable);
19065
19735
  return executable;
19066
19736
  } catch {
19067
19737
  throw new TypeError(`Capacitor is not installed in this app. Run: bun add ${CAPACITOR_PACKAGES.join(" ")}`);
@@ -19083,6 +19753,17 @@ var NOT_FOUND3 = -1, isRecord15 = (value) => typeof value === "object" && value
19083
19753
  const config = await loadConfig(configPath2);
19084
19754
  const mobile = normalizeAbsoluteMobileConfig(requireMobileConfig(config.mobile), projectRoot);
19085
19755
  return { mobile, projectRoot };
19756
+ }, inspectMobile = async (args) => {
19757
+ const { mobile, projectRoot } = await loadMobile(valueAfter(args, "--config"));
19758
+ const report = await inspectAbsoluteMobileProject(mobile, projectRoot, {
19759
+ absolutejsVersion: await absolutejsVersionForReport()
19760
+ });
19761
+ if (args.includes("--json")) {
19762
+ console.log(JSON.stringify(report, null, 2));
19763
+ return report;
19764
+ }
19765
+ console.log(renderAbsoluteMobileProjectInspection(report).trimEnd());
19766
+ return report;
19086
19767
  }, remoteProfilePath = () => process.env.ABSOLUTE_REMOTE_MAC_PROFILE_PATH || undefined, pairRemoteMac = async (args) => {
19087
19768
  if (args[0] !== "mac" || !args[1] || !args[2])
19088
19769
  throw new TypeError("Usage: absolute mobile pair mac <name> <user@host> [--port n] [--workspace path]");
@@ -19147,7 +19828,7 @@ var NOT_FOUND3 = -1, isRecord15 = (value) => typeof value === "object" && value
19147
19828
  await applyAbsoluteNativeBackgroundSync(projectRoot, mobile, platforms);
19148
19829
  }, associations = async (args) => {
19149
19830
  const { mobile, projectRoot } = await loadMobile(valueAfter(args, "--config"));
19150
- const outputDirectory = resolve41(projectRoot, valueAfter(args, "--outdir") ?? ".absolutejs/mobile/associations");
19831
+ const outputDirectory = resolve43(projectRoot, valueAfter(args, "--outdir") ?? ".absolutejs/mobile/associations");
19151
19832
  if (args.includes("--verify")) {
19152
19833
  const result2 = await verifyAbsoluteMobileAssociationFiles(mobile);
19153
19834
  console.log(`Verified ${result2.results.length} hosted association files`);
@@ -19192,7 +19873,7 @@ var NOT_FOUND3 = -1, isRecord15 = (value) => typeof value === "object" && value
19192
19873
  const { mobile, projectRoot } = await loadMobile(valueAfter(args, "--config"));
19193
19874
  const result = await inspectAbsoluteMobileRelease(mobile, projectRoot);
19194
19875
  if (args.includes("--json")) {
19195
- console.log(JSON.stringify(result, null, 2));
19876
+ console.log(JSON.stringify(createAbsoluteMobileComplianceReport(mobile, result), null, 2));
19196
19877
  } else {
19197
19878
  result.checks.forEach((check2) => {
19198
19879
  console.log(`${doctorMark(check2.status)} ${check2.detail}${check2.path ? ` (${check2.path})` : ""}`);
@@ -19200,8 +19881,8 @@ var NOT_FOUND3 = -1, isRecord15 = (value) => typeof value === "object" && value
19200
19881
  console.log(` ${check2.remediation}`);
19201
19882
  });
19202
19883
  console.log(result.ready ? `
19203
- Mobile release transport checks passed.` : `
19204
- Mobile release transport checks failed.`);
19884
+ Mobile release security and compliance checks passed.` : `
19885
+ Mobile release security and compliance checks failed.`);
19205
19886
  }
19206
19887
  if (!result.ready) {
19207
19888
  throw new TypeError("Mobile release validation failed. Resolve every failed check before signing or publishing the app.");
@@ -19380,7 +20061,7 @@ Mobile release transport checks failed.`);
19380
20061
  const durationMs = Math.round(performance.now() - startedAt);
19381
20062
  console.log(`Built ${release.metadata.signed ? "signed" : "unsigned"} Android App Bundle in ${getDurationString(durationMs)}.`);
19382
20063
  console.log(`Artifact: ${release.artifactPath}`);
19383
- console.log(`Metadata: ${join52(release.releaseRoot, "release.json")}`);
20064
+ console.log(`Metadata: ${join54(release.releaseRoot, "release.json")}`);
19384
20065
  return release;
19385
20066
  } finally {
19386
20067
  sendTelemetryEvent("mobile:android-release-build", {
@@ -19483,7 +20164,7 @@ Mobile release transport checks failed.`);
19483
20164
  const durationMs = Math.round(performance.now() - startedAt);
19484
20165
  console.log(`Built ${release.metadata.signed ? "signed" : "unsigned"} iOS IPA ${release.metadata.marketingVersion}${release.metadata.buildNumber ? ` (${release.metadata.buildNumber})` : ""} in ${getDurationString(durationMs)}.`);
19485
20166
  console.log(`Artifact: ${release.artifactPath}`);
19486
- console.log(`Metadata: ${join52(release.releaseRoot, "release.json")}`);
20167
+ console.log(`Metadata: ${join54(release.releaseRoot, "release.json")}`);
19487
20168
  return release;
19488
20169
  } finally {
19489
20170
  sendTelemetryEvent("mobile:ios-release-build", {
@@ -19590,7 +20271,7 @@ Mobile release transport checks failed.`);
19590
20271
  checks.push({
19591
20272
  id: "sync.storage-schema",
19592
20273
  label: `Offline schema ${schema.components.map((component2) => `${component2.id}@${component2.version}`).join(", ")}`,
19593
- path: join52(projectRoot, "package.json"),
20274
+ path: join54(projectRoot, "package.json"),
19594
20275
  platform: "host",
19595
20276
  status: "pass"
19596
20277
  });
@@ -19598,7 +20279,7 @@ Mobile release transport checks failed.`);
19598
20279
  checks.push({
19599
20280
  id: "sync.storage-schema",
19600
20281
  label: "Offline schema metadata is invalid",
19601
- path: join52(projectRoot, "package.json"),
20282
+ path: join54(projectRoot, "package.json"),
19602
20283
  platform: "host",
19603
20284
  remediation: error instanceof Error ? error.message : String(error),
19604
20285
  status: "fail"
@@ -19683,7 +20364,7 @@ Emulator setup verification:`);
19683
20364
  }
19684
20365
  return { https: args.includes("--https"), port };
19685
20366
  }
19686
- const instances = listLiveInstances().filter((instance2) => resolve41(instance2.cwd) === resolve41(projectRoot) && instance2.source === "dev" && instance2.port !== null);
20367
+ const instances = listLiveInstances().filter((instance2) => resolve43(instance2.cwd) === resolve43(projectRoot) && instance2.source === "dev" && instance2.port !== null);
19687
20368
  if (instances.length !== 1) {
19688
20369
  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>.");
19689
20370
  }
@@ -19726,8 +20407,8 @@ Emulator setup verification:`);
19726
20407
  }
19727
20408
  return selected;
19728
20409
  }, safeArtifactRoot = (projectRoot, value) => {
19729
- const root = resolve41(projectRoot, value ?? ".absolutejs/mobile/test-artifacts");
19730
- if (root !== projectRoot && !root.startsWith(`${resolve41(projectRoot)}/`)) {
20410
+ const root = resolve43(projectRoot, value ?? ".absolutejs/mobile/test-artifacts");
20411
+ if (root !== projectRoot && !root.startsWith(`${resolve43(projectRoot)}/`)) {
19731
20412
  throw new TypeError("mobile test --artifacts must remain inside the project.");
19732
20413
  }
19733
20414
  return root;
@@ -19752,10 +20433,10 @@ Emulator setup verification:`);
19752
20433
  });
19753
20434
  }, writeAndroidFailureArtifacts = async (options) => {
19754
20435
  await mkdir14(options.artifactRoot, { recursive: true });
19755
- const screenshot = options.session ? await options.session.screenshot(join52(options.artifactRoot, "android-failure.png")).catch(() => {
20436
+ const screenshot = options.session ? await options.session.screenshot(join54(options.artifactRoot, "android-failure.png")).catch(() => {
19756
20437
  return;
19757
20438
  }) : undefined;
19758
- const diagnosticPath = join52(options.artifactRoot, "android-failure.json");
20439
+ const diagnosticPath = join54(options.artifactRoot, "android-failure.json");
19759
20440
  await writeFile16(diagnosticPath, `${JSON.stringify({
19760
20441
  diagnostics: options.session?.diagnostics ?? [],
19761
20442
  error: options.error instanceof Error ? options.error.message : String(options.error),
@@ -19819,7 +20500,7 @@ Emulator setup verification:`);
19819
20500
  console.log(JSON.stringify(report, null, 2));
19820
20501
  else
19821
20502
  printAndroidTestReport(report);
19822
- const screenshot = reportRoot ? await session.screenshot(join52(artifactRoot, "android-emulator.png")) : undefined;
20503
+ const screenshot = reportRoot ? await session.screenshot(join54(artifactRoot, "android-emulator.png")) : undefined;
19823
20504
  await writeRequestedAndroidReport({
19824
20505
  adb,
19825
20506
  args,
@@ -19887,14 +20568,14 @@ Emulator setup verification:`);
19887
20568
  const port = Number(explicit);
19888
20569
  if (!Number.isInteger(port) || port < 1 || port > 65535)
19889
20570
  throw new TypeError("mobile test --port must be a valid TCP port.");
19890
- const instance2 = listLiveInstances().find((candidate) => resolve41(candidate.cwd) === resolve41(projectRoot) && candidate.source === "dev" && candidate.port === port);
20571
+ const instance2 = listLiveInstances().find((candidate) => resolve43(candidate.cwd) === resolve43(projectRoot) && candidate.source === "dev" && candidate.port === port);
19891
20572
  return {
19892
20573
  https: instance2?.https ?? args.includes("--https"),
19893
20574
  instance: instance2,
19894
20575
  port
19895
20576
  };
19896
20577
  }
19897
- const instances = listLiveInstances().filter((instance2) => resolve41(instance2.cwd) === resolve41(projectRoot) && instance2.source === "dev" && instance2.port !== null);
20578
+ const instances = listLiveInstances().filter((instance2) => resolve43(instance2.cwd) === resolve43(projectRoot) && instance2.source === "dev" && instance2.port !== null);
19898
20579
  if (instances.length !== 1)
19899
20580
  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>.");
19900
20581
  const [instance] = instances;
@@ -19949,7 +20630,7 @@ Emulator setup verification:`);
19949
20630
  if (!instance)
19950
20631
  return;
19951
20632
  const index = instance.command.indexOf("--ios-device");
19952
- return index === NOT_FOUND3 ? undefined : instance.command[index + 1];
20633
+ return index === NOT_FOUND4 ? undefined : instance.command[index + 1];
19953
20634
  }, physicalIosCapture = async (options) => {
19954
20635
  const remoteName = valueAfter(options.args, "--remote");
19955
20636
  if (remoteName && options.instance.iosRemoteMac && remoteName !== options.instance.iosRemoteMac)
@@ -20009,7 +20690,7 @@ Emulator setup verification:`);
20009
20690
  return result;
20010
20691
  }, writeIosFailureArtifacts = async (options) => {
20011
20692
  await mkdir14(options.artifactRoot, { recursive: true });
20012
- const screenshot = join52(options.artifactRoot, "ios-failure.png");
20693
+ const screenshot = join54(options.artifactRoot, "ios-failure.png");
20013
20694
  const screenshotResult = captureCommand4([
20014
20695
  options.xcrun,
20015
20696
  "simctl",
@@ -20018,7 +20699,7 @@ Emulator setup verification:`);
20018
20699
  "screenshot",
20019
20700
  screenshot
20020
20701
  ]);
20021
- const diagnosticPath = join52(options.artifactRoot, "ios-failure.json");
20702
+ const diagnosticPath = join54(options.artifactRoot, "ios-failure.json");
20022
20703
  await writeFile16(diagnosticPath, `${JSON.stringify({
20023
20704
  appId: options.appId,
20024
20705
  error: options.error instanceof Error ? options.error.message : String(options.error),
@@ -20035,7 +20716,7 @@ Emulator setup verification:`);
20035
20716
  };
20036
20717
  }, nativeReportRoot = (args, projectRoot, platform6) => {
20037
20718
  const index = args.indexOf("--report");
20038
- if (index === NOT_FOUND3)
20719
+ if (index === NOT_FOUND4)
20039
20720
  return;
20040
20721
  const candidate = args[index + 1];
20041
20722
  const explicit = candidate?.startsWith("--") ? undefined : candidate;
@@ -20044,8 +20725,8 @@ Emulator setup verification:`);
20044
20725
  }, absolutejsVersionForReport = async () => {
20045
20726
  let absolutejsVersion = process.env.ABSOLUTE_VERSION ?? "unknown";
20046
20727
  const versions = await Promise.all([
20047
- resolve41(import.meta.dir, "..", "..", "package.json"),
20048
- resolve41(import.meta.dir, "..", "..", "..", "package.json")
20728
+ resolve43(import.meta.dir, "..", "..", "package.json"),
20729
+ resolve43(import.meta.dir, "..", "..", "..", "package.json")
20049
20730
  ].map((candidate) => readPackageVersionForIosReport(candidate).catch(() => "unknown")));
20050
20731
  for (const version2 of versions) {
20051
20732
  if (version2 === "unknown")
@@ -20264,7 +20945,7 @@ Emulator setup verification:`);
20264
20945
  ], "iOS app launch");
20265
20946
  await waitForIosHmrClient({ https, port, timeoutMs });
20266
20947
  await mkdir14(artifactRoot, { recursive: true });
20267
- const screenshot = join52(artifactRoot, "ios-simulator.png");
20948
+ const screenshot = join54(artifactRoot, "ios-simulator.png");
20268
20949
  requireCapturedCommand([xcrun, "simctl", "io", simulator.udid, "screenshot", screenshot], "iOS simulator screenshot");
20269
20950
  const hmrApply = await waitForRequestedIosHmr(args, instance, timeoutMs);
20270
20951
  const report = {
@@ -20377,6 +21058,10 @@ Emulator setup verification:`);
20377
21058
  await doctor(args.slice(1));
20378
21059
  return;
20379
21060
  }
21061
+ if (command === "inspect") {
21062
+ await inspectMobile(args.slice(1));
21063
+ return;
21064
+ }
20380
21065
  if (command === "test" && args[1] === "android") {
20381
21066
  await testAndroid(args.slice(2));
20382
21067
  return;
@@ -20401,7 +21086,7 @@ Emulator setup verification:`);
20401
21086
  await publishIos(args.slice(2));
20402
21087
  return;
20403
21088
  }
20404
- throw new TypeError("Usage: absolute mobile <pair mac <name> <user@host> [--port n] [--workspace path] | remotes [--json] | unpair mac <name> | init [--no-native] [--force] | sync [ios|android] | associations [--outdir dir] [--verify] | doctor [ios|android|release] [--remote name] [--json|--fix [--yes]] | build <android|ios> [server-entry] [--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] [--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]");
21089
+ throw new TypeError("Usage: absolute mobile <pair mac <name> <user@host> [--port n] [--workspace path] | remotes [--json] | unpair mac <name> | init [--no-native] [--force] | sync [ios|android] | inspect [--json] | associations [--outdir dir] [--verify] | doctor [ios|android|release] [--remote name] [--json|--fix [--yes]] | build <android|ios> [server-entry] [--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] [--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]");
20405
21090
  };
20406
21091
  var init_mobile = __esm(() => {
20407
21092
  init_dependencies();
@@ -20435,6 +21120,7 @@ var init_mobile = __esm(() => {
20435
21120
  init_nativeAuth();
20436
21121
  init_syncSchema();
20437
21122
  init_deviceCapabilities();
21123
+ init_mobileInspect();
20438
21124
  CAPACITOR_PACKAGES = [
20439
21125
  "@capacitor/core",
20440
21126
  "@capacitor/app",
@@ -20470,10 +21156,10 @@ var exports_typecheck = {};
20470
21156
  __export(exports_typecheck, {
20471
21157
  typecheck: () => typecheck
20472
21158
  });
20473
- import { resolve as resolve42, join as join53 } from "path";
21159
+ import { resolve as resolve44, join as join55 } from "path";
20474
21160
  import { existsSync as existsSync42, readFileSync as readFileSync40 } from "fs";
20475
21161
  import { mkdir as mkdir15, writeFile as writeFile17 } from "fs/promises";
20476
- var isCommandService3 = (service) => service.kind === "command" || Array.isArray(service.command), resolveConfigPath = (configPath2) => resolve42(configPath2 ?? process.env.ABSOLUTE_CONFIG ?? "absolute.config.ts"), getTypecheckTargets = async (configPath2) => {
21162
+ var isCommandService3 = (service) => service.kind === "command" || Array.isArray(service.command), resolveConfigPath = (configPath2) => resolve44(configPath2 ?? process.env.ABSOLUTE_CONFIG ?? "absolute.config.ts"), getTypecheckTargets = async (configPath2) => {
20477
21163
  if (!existsSync42(resolveConfigPath(configPath2))) {
20478
21164
  const defaultService = {};
20479
21165
  return [defaultService];
@@ -20495,7 +21181,7 @@ var isCommandService3 = (service) => service.kind === "command" || Array.isArray
20495
21181
  const exitCode = await proc.exited;
20496
21182
  return { exitCode, name, output: (stdout + stderr).trim() };
20497
21183
  }, shellEscape = (value) => `'${value.replaceAll("'", "'\\''")}'`, runShell = async (name, command) => run(name, ["/bin/bash", "-lc", command]), findBin = (name) => {
20498
- const local = resolve42("node_modules", ".bin", name);
21184
+ const local = resolve44("node_modules", ".bin", name);
20499
21185
  return existsSync42(local) ? local : null;
20500
21186
  }, ANSI_COLOR_REGEX, ANSI_PURPLE_REGEX, ANSI_CYAN_REGEX, ANSI_TOKEN_END_REGEX, stripAnsi4 = (str) => str.replace(ANSI_COLOR_REGEX, ""), formatSvelteOutput = (output) => {
20501
21187
  const cwd = `${process.cwd()}/`;
@@ -20543,15 +21229,15 @@ Found ${errorCount} error${suffix}.`;
20543
21229
  return formatted;
20544
21230
  }, ABSOLUTE_INTERNAL_EXCLUDES, resolveAbsoluteTypeFile = (fileName) => {
20545
21231
  const candidates = [
20546
- resolve42("node_modules/@absolutejs/absolute/dist/types", fileName),
20547
- resolve42(import.meta.dir, "../types", fileName),
20548
- resolve42(import.meta.dir, "../../types", fileName),
20549
- resolve42(import.meta.dir, "../../../types", fileName)
21232
+ resolve44("node_modules/@absolutejs/absolute/dist/types", fileName),
21233
+ resolve44(import.meta.dir, "../types", fileName),
21234
+ resolve44(import.meta.dir, "../../types", fileName),
21235
+ resolve44(import.meta.dir, "../../../types", fileName)
20550
21236
  ];
20551
21237
  return candidates.find((candidate) => existsSync42(candidate)) ?? candidates[0];
20552
21238
  }, ABSOLUTE_TYPECHECK_FILES, readProjectTsconfig = () => {
20553
21239
  try {
20554
- return JSON.parse(readFileSync40(resolve42("tsconfig.json"), "utf-8"));
21240
+ return JSON.parse(readFileSync40(resolve44("tsconfig.json"), "utf-8"));
20555
21241
  } catch {
20556
21242
  return {};
20557
21243
  }
@@ -20579,27 +21265,27 @@ Found ${errorCount} error${suffix}.`;
20579
21265
  console.error("\x1B[31m\u2717\x1B[0m vue-tsc is required for Vue type checking. Install it: bun add -d vue-tsc");
20580
21266
  process.exit(1);
20581
21267
  }
20582
- const vueTsconfigPath = join53(cacheDir, "tsconfig.vue-check.json");
21268
+ const vueTsconfigPath = join55(cacheDir, "tsconfig.vue-check.json");
20583
21269
  await writeFile17(vueTsconfigPath, JSON.stringify({
20584
21270
  compilerOptions: {
20585
21271
  rootDir: ".."
20586
21272
  },
20587
21273
  exclude: getProjectTypecheckExcludes(),
20588
- extends: resolve42("tsconfig.json"),
21274
+ extends: resolve44("tsconfig.json"),
20589
21275
  include: getProjectTypecheckIncludes()
20590
21276
  }, null, "\t"));
20591
21277
  const base = [
20592
21278
  vueTscBin,
20593
21279
  "--noEmit",
20594
21280
  "--project",
20595
- resolve42(vueTsconfigPath),
21281
+ resolve44(vueTsconfigPath),
20596
21282
  "--pretty"
20597
21283
  ];
20598
21284
  const cached = await run("vue-tsc", [
20599
21285
  ...base,
20600
21286
  "--incremental",
20601
21287
  "--tsBuildInfoFile",
20602
- join53(cacheDir, "vue-tsc.tsbuildinfo")
21288
+ join55(cacheDir, "vue-tsc.tsbuildinfo")
20603
21289
  ]);
20604
21290
  if (cached.exitCode === 0 || cached.output.length > 0)
20605
21291
  return cached;
@@ -20610,7 +21296,7 @@ Found ${errorCount} error${suffix}.`;
20610
21296
  console.error("\x1B[31m\u2717\x1B[0m @angular/compiler-cli is required for Angular type checking. Install it: bun add -d @angular/compiler-cli");
20611
21297
  process.exit(1);
20612
21298
  }
20613
- const angularTsconfigPath = join53(cacheDir, "tsconfig.angular-check.json");
21299
+ const angularTsconfigPath = join55(cacheDir, "tsconfig.angular-check.json");
20614
21300
  await writeFile17(angularTsconfigPath, JSON.stringify({
20615
21301
  angularCompilerOptions: {
20616
21302
  strictTemplates: true
@@ -20620,32 +21306,32 @@ Found ${errorCount} error${suffix}.`;
20620
21306
  rootDir: ".."
20621
21307
  },
20622
21308
  exclude: ABSOLUTE_INTERNAL_EXCLUDES.map(toGeneratedConfigPath),
20623
- extends: resolve42("tsconfig.json"),
21309
+ extends: resolve44("tsconfig.json"),
20624
21310
  include: [`../${angularDir}/**/*`]
20625
21311
  }, null, "\t"));
20626
- return runShell("ngc", `${shellEscape(ngcBin)} -p ${shellEscape(resolve42(angularTsconfigPath))}`);
21312
+ return runShell("ngc", `${shellEscape(ngcBin)} -p ${shellEscape(resolve44(angularTsconfigPath))}`);
20627
21313
  }, buildTscCheck = (cacheDir) => {
20628
21314
  const tscBin = findBin("tsc");
20629
21315
  if (!tscBin) {
20630
21316
  console.error("\x1B[31m\u2717\x1B[0m typescript is required for type checking. Install it: bun add -d typescript");
20631
21317
  process.exit(1);
20632
21318
  }
20633
- const tscConfigPath = join53(cacheDir, "tsconfig.typecheck.json");
21319
+ const tscConfigPath = join55(cacheDir, "tsconfig.typecheck.json");
20634
21320
  return writeFile17(tscConfigPath, JSON.stringify({
20635
21321
  compilerOptions: {
20636
21322
  rootDir: ".."
20637
21323
  },
20638
21324
  exclude: getProjectTypecheckExcludes(),
20639
- extends: resolve42("tsconfig.json"),
21325
+ extends: resolve44("tsconfig.json"),
20640
21326
  include: getProjectTypecheckIncludes()
20641
21327
  }, null, "\t")).then(() => run("tsc", [
20642
21328
  tscBin,
20643
21329
  "--noEmit",
20644
21330
  "--project",
20645
- resolve42(tscConfigPath),
21331
+ resolve44(tscConfigPath),
20646
21332
  "--incremental",
20647
21333
  "--tsBuildInfoFile",
20648
- join53(cacheDir, "tsc.tsbuildinfo"),
21334
+ join55(cacheDir, "tsc.tsbuildinfo"),
20649
21335
  "--pretty"
20650
21336
  ]));
20651
21337
  }, buildSvelteCheck = async (cacheDir, svelteDir) => {
@@ -20654,16 +21340,16 @@ Found ${errorCount} error${suffix}.`;
20654
21340
  console.error("\x1B[31m\u2717\x1B[0m svelte-check is required for Svelte type checking. Install it: bun add -d svelte-check");
20655
21341
  process.exit(1);
20656
21342
  }
20657
- const svelteTsconfigPath = join53(cacheDir, "tsconfig.svelte-check.json");
21343
+ const svelteTsconfigPath = join55(cacheDir, "tsconfig.svelte-check.json");
20658
21344
  await writeFile17(svelteTsconfigPath, JSON.stringify({
20659
- extends: resolve42("tsconfig.json"),
21345
+ extends: resolve44("tsconfig.json"),
20660
21346
  files: ABSOLUTE_TYPECHECK_FILES,
20661
21347
  include: [`../${svelteDir}/**/*`]
20662
21348
  }, null, "\t"));
20663
21349
  return run("svelte-check", [
20664
21350
  svelteBin,
20665
21351
  "--tsconfig",
20666
- resolve42(svelteTsconfigPath),
21352
+ resolve44(svelteTsconfigPath),
20667
21353
  "--threshold",
20668
21354
  "error",
20669
21355
  "--compiler-warnings",
@@ -20857,11 +21543,11 @@ var DEFAULT_RELAY_PORT = 8787, DEFAULT_REQUEST_TIMEOUT_MS = 30000, headersToObje
20857
21543
  url: url.pathname + url.search,
20858
21544
  ...bodyBytes && bodyBytes.length > 0 ? { bodyBase64: Buffer.from(bodyBytes).toString("base64") } : {}
20859
21545
  };
20860
- const responsePromise = new Promise((resolve43) => {
20861
- pending.set(id, resolve43);
21546
+ const responsePromise = new Promise((resolve45) => {
21547
+ pending.set(id, resolve45);
20862
21548
  });
20863
21549
  client.send(encodeTunnelMessage(message));
20864
- const timeout = new Promise((resolve43) => setTimeout(() => resolve43({ id, message: "timeout", type: "error" }), requestTimeoutMs));
21550
+ const timeout = new Promise((resolve45) => setTimeout(() => resolve45({ id, message: "timeout", type: "error" }), requestTimeoutMs));
20865
21551
  const result = await Promise.race([responsePromise, timeout]);
20866
21552
  pending.delete(id);
20867
21553
  if (result.type === "error") {
@@ -22472,6 +23158,7 @@ var dev = async (serverEntry, configPath2, options = {}) => {
22472
23158
  const serverEntryBasename = absServerEntry.slice(absServerEntry.lastIndexOf("/") + 1);
22473
23159
  const configBasename = configPath2 ? configPath2.slice(configPath2.lastIndexOf("/") + 1) : "absolute.config.ts";
22474
23160
  const ATOMIC_RECOVERY_WINDOW_MS = 1000;
23161
+ const ATOMIC_RECOVERY_DELAY_MS = 25;
22475
23162
  const recentlyHandled = new Map;
22476
23163
  const handleCandidate = (filename) => {
22477
23164
  if (filename.includes("/") || filename.includes("\\"))
@@ -22515,18 +23202,28 @@ var dev = async (serverEntry, configPath2, options = {}) => {
22515
23202
  handleCandidate(entry.name);
22516
23203
  }
22517
23204
  };
23205
+ let atomicRecoveryTimer;
23206
+ const scheduleAtomicRecovery = () => {
23207
+ if (atomicRecoveryTimer)
23208
+ clearTimeout(atomicRecoveryTimer);
23209
+ atomicRecoveryTimer = setTimeout(() => {
23210
+ atomicRecoveryTimer = undefined;
23211
+ recoveryScan();
23212
+ }, ATOMIC_RECOVERY_DELAY_MS);
23213
+ };
22518
23214
  const watcher = watch3(serverEntryDir, { recursive: false }, (event, filename) => {
22519
23215
  if (!filename)
22520
23216
  return;
22521
23217
  if (isAtomicWriteTemp(filename)) {
22522
- if (event === "rename") {
22523
- recoveryScan();
22524
- }
23218
+ if (event === "rename")
23219
+ scheduleAtomicRecovery();
22525
23220
  return;
22526
23221
  }
22527
23222
  handleCandidate(filename);
22528
23223
  });
22529
23224
  const closeWatcher = () => {
23225
+ if (atomicRecoveryTimer)
23226
+ clearTimeout(atomicRecoveryTimer);
22530
23227
  try {
22531
23228
  watcher.close();
22532
23229
  } catch {}
@@ -24763,7 +25460,7 @@ if (command === "dev") {
24763
25460
  console.error(" prepare [entry] [--outdir dir] Build production assets and server without launching");
24764
25461
  console.error(" start [entry] [--outdir dir] [--prebuilt] Start production server");
24765
25462
  console.error(" compile [entry] [--outdir dir] [--outfile path] Compile standalone executable");
24766
- console.error(" mobile <init|sync|pair|remotes|doctor|test> Manage Capacitor projects, simulators, physical devices, Remote Macs, guided setup, and deep links");
25463
+ console.error(" mobile <init|sync|inspect|pair|remotes|doctor|test> Manage Capacitor projects, simulators, physical devices, Remote Macs, guided setup, and deep links");
24767
25464
  console.error(" config [--port n] Open the unified config UI (ESLint, tsconfig, Prettier)");
24768
25465
  console.error(" db <backup|restore|seed> Backup/restore any Postgres DB (ORM-agnostic, upsert by PK) or run the seed script");
24769
25466
  console.error(" doctor [--fix] [--json] Diagnose the project (bun, type graph, config, framework dirs, env, port)");