@absolutejs/absolute 0.20.0-beta.37 → 0.20.0-beta.39

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
@@ -2589,7 +2589,14 @@ import {
2589
2589
  writeFile as writeFile3
2590
2590
  } from "fs/promises";
2591
2591
  import { dirname as dirname4, isAbsolute as isAbsolute2, join as join9, relative as relative3, resolve as resolve6, sep as sep2 } from "path";
2592
- var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), requireManifest = (value) => {
2592
+ var developmentTeamArgument = (value) => {
2593
+ if (value === undefined)
2594
+ return;
2595
+ const team = value.trim().toUpperCase();
2596
+ if (!/^[A-Z0-9]{10}$/u.test(team))
2597
+ throw new TypeError("iOS development team must contain ten letters or digits.");
2598
+ return `DEVELOPMENT_TEAM=${team}`;
2599
+ }, isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), requireManifest = (value) => {
2593
2600
  if (!isRecord2(value) || typeof value.appBuild !== "string" || typeof value.appId !== "string" || typeof value.runtime !== "string") {
2594
2601
  throw new TypeError("Invalid embedded AbsoluteJS mobile manifest.");
2595
2602
  }
@@ -2762,8 +2769,10 @@ var isRecord2 = (value) => typeof value === "object" && value !== null && !Array
2762
2769
  await writeFile3(exportPlist, exportOptions());
2763
2770
  const run = options.run ?? defaultRun2;
2764
2771
  try {
2772
+ const developmentTeam = developmentTeamArgument(options.developmentTeam);
2765
2773
  const versionArguments = [
2766
2774
  `MARKETING_VERSION=${marketingVersion}`,
2775
+ ...developmentTeam ? [developmentTeam] : [],
2767
2776
  ...buildNumber === undefined ? [] : [`CURRENT_PROJECT_VERSION=${buildNumber}`]
2768
2777
  ];
2769
2778
  const archiveExit = await run([
@@ -6411,12 +6420,25 @@ var MANIFEST_FILE = "absolute-mobile-manifest.json", BOOTSTRAP_FILE = "absolute-
6411
6420
  if (candidate)
6412
6421
  return candidate;
6413
6422
  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>
6423
+ }, escapeHtml = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;"), contentSecurityPolicy = (productionOrigin) => {
6424
+ const backend = new URL(productionOrigin);
6425
+ const socketOrigin = `${backend.protocol === "https:" ? "wss:" : "ws:"}//${backend.host}`;
6426
+ return [
6427
+ "default-src 'self' data: blob: https:",
6428
+ "base-uri 'none'",
6429
+ "object-src 'none'",
6430
+ "script-src 'self'",
6431
+ "style-src 'self' 'unsafe-inline'",
6432
+ `connect-src 'self' ${backend.origin} ${socketOrigin}`,
6433
+ "form-action 'none'"
6434
+ ].join("; ");
6435
+ }, indexHtml = (appName, productionOrigin) => `<!doctype html>
6415
6436
  <html>
6416
6437
  <head>
6417
6438
  <meta charset="utf-8">
6418
6439
  <meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
6419
6440
  <meta name="color-scheme" content="light dark">
6441
+ <meta http-equiv="Content-Security-Policy" content="${escapeHtml(contentSecurityPolicy(productionOrigin))}">
6420
6442
  <title>${escapeHtml(appName)}</title>
6421
6443
  </head>
6422
6444
  <body>
@@ -6635,7 +6657,7 @@ void startAbsoluteMobileShell(${push ? `{ createAuth: (config, options) => creat
6635
6657
  await Promise.all([
6636
6658
  writeFile7(join17(staging, MANIFEST_FILE), `${JSON.stringify(manifest, null, "\t")}
6637
6659
  `),
6638
- writeFile7(join17(staging, INDEX_FILE), indexHtml(options.config.appName)),
6660
+ writeFile7(join17(staging, INDEX_FILE), indexHtml(options.config.appName, options.config.productionOrigin)),
6639
6661
  buildShellBootstrap(staging, options.auth !== undefined, options.auth !== undefined && options.sync === true, `absolutejs.${options.auth?.clientId ?? options.config.appId}.`, options.deviceCapabilities, options.projectRoot)
6640
6662
  ]);
6641
6663
  await installBundle(staging, destination);
@@ -18048,28 +18070,173 @@ var DEFAULT_ROUTE_TIMEOUT_MS = 30000, DEFAULT_HMR_TIMEOUT_MS = 30000, routeExpre
18048
18070
  return apply;
18049
18071
  };
18050
18072
 
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) => {
18073
+ // src/mobile/mobileBundleInspection.ts
18074
+ import { createHash as createHash12 } from "crypto";
18075
+ import { access as access8, readFile as readFile16, stat as stat2 } from "fs/promises";
18076
+ import { join as join49, relative as relative24, resolve as resolve39 } from "path";
18077
+ var MOBILE_FRAMEWORKS, SHA256_PATTERN, isObject2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), portablePath = (projectRoot, path) => {
18078
+ const value = relative24(resolve39(projectRoot), resolve39(path)).replaceAll("\\", "/");
18079
+ return value || ".";
18080
+ }, pathExists5 = async (path) => {
18055
18081
  try {
18056
18082
  await access8(path);
18057
18083
  return true;
18058
18084
  } catch {
18059
18085
  return false;
18060
18086
  }
18087
+ }, readObject = async (path) => {
18088
+ const value = JSON.parse(await readFile16(path, "utf8"));
18089
+ if (!isObject2(value))
18090
+ throw new TypeError("JSON root must be an object.");
18091
+ return value;
18092
+ }, requireString = (value, field) => {
18093
+ if (typeof value !== "string" || value.length === 0)
18094
+ throw new TypeError(`${field} must be a non-empty string.`);
18095
+ return value;
18096
+ }, requireStringArray = (value, field) => {
18097
+ if (!Array.isArray(value) || !value.every((item) => typeof item === "string"))
18098
+ throw new TypeError(`${field} must be a string array.`);
18099
+ return value;
18100
+ }, requireBundleFile = async (root, value, field, expectedHash) => {
18101
+ const portable = requireString(value, field);
18102
+ const path = resolve39(root, portable);
18103
+ const normalizedRoot = resolve39(root);
18104
+ if (path === normalizedRoot || !path.startsWith(`${normalizedRoot}/`))
18105
+ throw new TypeError(`${field} must remain inside the mobile bundle.`);
18106
+ if (!(await stat2(path).catch(() => {
18107
+ return;
18108
+ }))?.isFile())
18109
+ throw new TypeError(`${field} does not exist in the mobile bundle.`);
18110
+ if (expectedHash !== undefined) {
18111
+ if (!SHA256_PATTERN.test(expectedHash))
18112
+ throw new TypeError(`${field} has an invalid SHA-256 digest.`);
18113
+ const actual = createHash12("sha256").update(await readFile16(path)).digest("hex");
18114
+ if (actual !== expectedHash)
18115
+ throw new TypeError(`${field} failed its SHA-256 integrity check.`);
18116
+ }
18117
+ return portable;
18118
+ }, inspectAbsoluteMobileBundle = async (config, projectRoot) => {
18119
+ const manifestPath = join49(config.bundleDirectory, "absolute-mobile-manifest.json");
18120
+ const manifest = portablePath(projectRoot, manifestPath);
18121
+ if (!await pathExists5(manifestPath))
18122
+ return { manifest, status: "missing" };
18123
+ try {
18124
+ const value = await readObject(manifestPath);
18125
+ if (value.format !== ABSOLUTE_MOBILE_CLIENT_MANIFEST_FORMAT)
18126
+ throw new TypeError("format is not supported by this runtime.");
18127
+ if (requireString(value.appId, "appId") !== config.appId)
18128
+ throw new TypeError("appId does not match the effective mobile config.");
18129
+ if (requireString(value.productionOrigin, "productionOrigin") !== config.productionOrigin)
18130
+ throw new TypeError("productionOrigin does not match the effective mobile config.");
18131
+ const appBuild = requireString(value.appBuild, "appBuild");
18132
+ const runtime = requireString(value.runtime, "runtime");
18133
+ if (runtime !== String(ABSOLUTE_MOBILE_PAGE_PROTOCOL_VERSION))
18134
+ throw new TypeError("runtime is not supported by this AbsoluteJS build.");
18135
+ const capabilities = requireStringArray(value.deviceCapabilities, "deviceCapabilities").sort();
18136
+ if (!Array.isArray(value.pages) || !Array.isArray(value.routes))
18137
+ throw new TypeError("pages and routes must be arrays.");
18138
+ const pageIds = new Set;
18139
+ const frameworks7 = new Set;
18140
+ await Promise.all(value.pages.map(async (candidate) => {
18141
+ if (!isObject2(candidate))
18142
+ throw new TypeError("pages contains an invalid entry.");
18143
+ const pageId = requireString(candidate.pageId, "page.pageId");
18144
+ if (pageIds.has(pageId))
18145
+ throw new TypeError("page.pageId values must be unique.");
18146
+ pageIds.add(pageId);
18147
+ const framework = requireString(candidate.framework, "page.framework");
18148
+ if (!MOBILE_FRAMEWORKS.has(framework))
18149
+ throw new TypeError("page.framework is unsupported.");
18150
+ frameworks7.add(framework);
18151
+ const bundleHash = requireString(candidate.bundleHash, "page.bundleHash");
18152
+ requireString(candidate.contract, "page.contract");
18153
+ requireString(candidate.propsSchemaHash, "page.propsSchemaHash");
18154
+ await requireBundleFile(config.bundleDirectory, candidate.localBundlePath, "page.localBundlePath", bundleHash);
18155
+ if (candidate.localStylePath !== undefined) {
18156
+ const styleHash = requireString(candidate.styleBundleHash, "page.styleBundleHash");
18157
+ await requireBundleFile(config.bundleDirectory, candidate.localStylePath, "page.localStylePath", styleHash);
18158
+ } else if (candidate.styleBundleHash !== undefined)
18159
+ throw new TypeError("page.styleBundleHash requires page.localStylePath.");
18160
+ }));
18161
+ const routes = value.routes.map((candidate) => {
18162
+ if (!isObject2(candidate))
18163
+ throw new TypeError("routes contains an invalid entry.");
18164
+ const { method } = candidate;
18165
+ if (method !== "GET" && method !== "HEAD")
18166
+ throw new TypeError("route.method must be GET or HEAD.");
18167
+ const pageId = requireString(candidate.pageId, "route.pageId");
18168
+ if (!pageIds.has(pageId))
18169
+ throw new TypeError("route.pageId references a missing page.");
18170
+ return {
18171
+ method,
18172
+ pageId,
18173
+ pattern: requireString(candidate.pattern, "route.pattern")
18174
+ };
18175
+ });
18176
+ await Promise.all(["index.html", "absolute-mobile-bootstrap.js"].map((file) => requireBundleFile(config.bundleDirectory, file, file)));
18177
+ const entryPath = new URL(config.entry, "https://absolute.invalid").pathname;
18178
+ const entryResolved = resolveAbsoluteMobileRoute(routes, entryPath) !== undefined;
18179
+ if (!entryResolved)
18180
+ throw new TypeError("entry is not owned by an embedded route.");
18181
+ return {
18182
+ appBuild,
18183
+ auth: isObject2(value.auth),
18184
+ capabilities,
18185
+ entryResolved,
18186
+ frameworks: [...frameworks7].sort(),
18187
+ manifest,
18188
+ pageCount: value.pages.length,
18189
+ routeCount: value.routes.length,
18190
+ runtime,
18191
+ status: "valid",
18192
+ sync: isObject2(value.sync)
18193
+ };
18194
+ } catch (error) {
18195
+ return {
18196
+ issue: error instanceof Error ? error.message : "The embedded mobile manifest is invalid.",
18197
+ manifest,
18198
+ status: "invalid"
18199
+ };
18200
+ }
18201
+ };
18202
+ var init_mobileBundleInspection = __esm(() => {
18203
+ init_pageProtocol();
18204
+ init_routeMatcher();
18205
+ init_transport();
18206
+ MOBILE_FRAMEWORKS = new Set([
18207
+ "angular",
18208
+ "ember",
18209
+ "html",
18210
+ "htmx",
18211
+ "react",
18212
+ "svelte",
18213
+ "vue"
18214
+ ]);
18215
+ SHA256_PATTERN = /^[a-f0-9]{64}$/u;
18216
+ });
18217
+
18218
+ // src/mobile/releaseDoctor.ts
18219
+ import { access as access9, readFile as readFile17, readdir as readdir4 } from "fs/promises";
18220
+ import { dirname as dirname29, extname as extname8, join as join50, relative as relative25 } from "path";
18221
+ 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) => {
18222
+ try {
18223
+ await access9(path);
18224
+ return true;
18225
+ } catch {
18226
+ return false;
18227
+ }
18061
18228
  }, inspectReleaseAsset = async (path, isDirectory, isFile2) => {
18062
18229
  if (isDirectory)
18063
18230
  return findHmrAsset(path);
18064
18231
  if (!isFile2 || !RELEASE_ASSET_EXTENSIONS.has(extname8(path)))
18065
18232
  return;
18066
- const source = await readFile16(path, "utf8");
18233
+ const source = await readFile17(path, "utf8");
18067
18234
  return HMR_ASSET_PATTERN.test(source) ? path : undefined;
18068
18235
  }, findHmrAsset = async (root) => {
18069
- if (!await pathExists5(root))
18236
+ if (!await pathExists6(root))
18070
18237
  return;
18071
18238
  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())));
18239
+ const matches = await Promise.all(entries.map((entry) => inspectReleaseAsset(join50(root, entry.name), entry.isDirectory(), entry.isFile())));
18073
18240
  return matches.find((match) => match !== undefined);
18074
18241
  }, pass = (id, detail, path) => ({ detail, id, path, status: "pass" }), fail5 = (id, detail, path, remediation) => ({
18075
18242
  detail,
@@ -18077,7 +18244,70 @@ var HMR_ASSET_PATTERN, RELEASE_ASSET_EXTENSIONS, pathExists5 = async (path) => {
18077
18244
  path,
18078
18245
  remediation,
18079
18246
  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) => {
18247
+ }), warn = (id, detail, path, remediation) => ({
18248
+ detail,
18249
+ id,
18250
+ path,
18251
+ remediation,
18252
+ status: "warn"
18253
+ }), readJsonObject = async (path) => {
18254
+ const value = JSON.parse(await readFile17(path, "utf8"));
18255
+ if (typeof value !== "object" || value === null || Array.isArray(value))
18256
+ throw new TypeError("JSON root must be an object.");
18257
+ return Object.fromEntries(Object.entries(value));
18258
+ }, packageDeclarations = (manifest) => {
18259
+ const declarations = new Map;
18260
+ for (const field of ["dependencies", "devDependencies"]) {
18261
+ const value = manifest[field];
18262
+ if (typeof value !== "object" || value === null || Array.isArray(value))
18263
+ continue;
18264
+ for (const [name, version2] of Object.entries(value))
18265
+ if (typeof version2 === "string")
18266
+ declarations.set(name, version2);
18267
+ }
18268
+ return declarations;
18269
+ }, versionCore = (version2) => version2.split("-")[0]?.split(".").slice(0, 2).join("."), capacitorVersionCheck = async (config, projectRoot) => {
18270
+ const manifestPath = join50(projectRoot, "package.json");
18271
+ try {
18272
+ const manifest = await readJsonObject(manifestPath);
18273
+ const declarations = packageDeclarations(manifest);
18274
+ const names = [
18275
+ "@capacitor/core",
18276
+ "@capacitor/cli",
18277
+ ...config.platforms.map((platform6) => `@capacitor/${platform6}`)
18278
+ ];
18279
+ const versions = await Promise.all(names.map(async (name) => {
18280
+ const declared = declarations.get(name);
18281
+ if (!declared || !EXACT_VERSION_PATTERN.test(declared))
18282
+ throw new TypeError(`${name} must be a direct exact dependency.`);
18283
+ const installed = await readJsonObject(join50(projectRoot, "node_modules", name, "package.json"));
18284
+ if (installed.version !== declared)
18285
+ throw new TypeError(`${name} does not match its installed version.`);
18286
+ return declared;
18287
+ }));
18288
+ const lines = new Set(versions.map(versionCore));
18289
+ if (lines.size !== 1)
18290
+ throw new TypeError("Capacitor core, CLI, and platform packages must use one major/minor line.");
18291
+ return pass("mobile.capacitor-versions", `Capacitor core, CLI, and configured platforms are pinned and aligned on ${versions[0]}.`, manifestPath);
18292
+ } catch (error) {
18293
+ 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.");
18294
+ }
18295
+ }, dependencyLockCheck = async (projectRoot) => {
18296
+ const present = (await Promise.all(LOCK_FILES.map(async (name) => ({
18297
+ exists: await pathExists6(join50(projectRoot, name)),
18298
+ name
18299
+ })))).find(({ exists: exists3 }) => exists3);
18300
+ 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.");
18301
+ }, productionOriginCheck = (config, projectRoot) => {
18302
+ const origin = new URL(config.productionOrigin);
18303
+ 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.");
18304
+ }, associationIdentityCheck = (config, projectRoot) => {
18305
+ const missing = [
18306
+ ...config.platforms.includes("ios") && !config.appleAppIdPrefix ? ["mobile.deepLinks.apple.appIdPrefix"] : [],
18307
+ ...config.platforms.includes("android") && config.androidCertificateFingerprints.length === 0 ? ["mobile.deepLinks.android.sha256CertificateFingerprints"] : []
18308
+ ];
18309
+ 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.");
18310
+ }, 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
18311
  try {
18082
18312
  const parsed = JSON.parse(source);
18083
18313
  return parsed;
@@ -18099,30 +18329,138 @@ var HMR_ASSET_PATTERN, RELEASE_ASSET_EXTENSIONS, pathExists5 = async (path) => {
18099
18329
  const allowNavigation = Reflect.get(server, "allowNavigation");
18100
18330
  return typeof Reflect.get(server, "url") === "string" || Reflect.get(server, "cleartext") === true || Array.isArray(allowNavigation) && allowNavigation.length > 0;
18101
18331
  }, capacitorConfigReleaseCheck = async (nativeConfigPath) => {
18102
- if (!await pathExists5(nativeConfigPath)) {
18332
+ if (!await pathExists6(nativeConfigPath)) {
18103
18333
  return fail5("android.capacitor-config", "The generated Android Capacitor config is missing.", nativeConfigPath, "Run `absolute mobile sync android` before release validation.");
18104
18334
  }
18105
- const unsafe = isUnsafeCapacitorConfig(await readFile16(nativeConfigPath, "utf8"));
18335
+ const unsafe = isUnsafeCapacitorConfig(await readFile17(nativeConfigPath, "utf8"));
18106
18336
  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);
18337
+ }, capacitorIdentityCheck = async (config, platform6, nativeConfigPath) => {
18338
+ try {
18339
+ const parsed = await readJsonObject(nativeConfigPath);
18340
+ if (parsed.appId !== config.appId || parsed.appName !== config.appName)
18341
+ throw new TypeError(`${platform6} packaged application identity does not match mobile config.`);
18342
+ return pass(`${platform6}.app-identity`, `Packaged ${platform6} application identity matches mobile config.`, nativeConfigPath);
18343
+ } catch (error) {
18344
+ 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.`);
18345
+ }
18107
18346
  }, manifestReleaseCheck = async (manifestPath) => {
18108
- if (!await pathExists5(manifestPath)) {
18347
+ if (!await pathExists6(manifestPath)) {
18109
18348
  return fail5("android.cleartext", "The Android manifest is missing.", manifestPath, "Run `absolute mobile sync android` before release validation.");
18110
18349
  }
18111
- const source = await readFile16(manifestPath, "utf8");
18350
+ const source = await readFile17(manifestPath, "utf8");
18112
18351
  const cleartext = /android:usesCleartextTraffic=["']true["']/u.test(source);
18113
18352
  const networkConfigName = source.match(/android:networkSecurityConfig=["']@xml\/([a-z0-9_]+)["']/u)?.[1];
18114
- const networkConfigPath = networkConfigName ? join49(dirname29(manifestPath), "res", "xml", `${networkConfigName}.xml`) : undefined;
18353
+ const networkConfigPath = networkConfigName ? join50(dirname29(manifestPath), "res", "xml", `${networkConfigName}.xml`) : undefined;
18115
18354
  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;
18355
+ const developmentTrustContents = networkConfigPath ? await readFile17(networkConfigPath, "utf8").then((value) => value.includes("@raw/absolutejs_dev_ca")).catch(() => false) : false;
18117
18356
  const developmentTrust = developmentTrustReference || developmentTrustContents;
18118
18357
  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
18358
  }, hmrAssetsReleaseCheck = async (publicRoot) => {
18120
18359
  const hmrAsset = await findHmrAsset(publicRoot);
18121
18360
  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);
18361
+ }, embeddedBundleReleaseCheck = async (config, projectRoot, platform6, publicRoot) => {
18362
+ const inspection = await inspectAbsoluteMobileBundle({ ...config, bundleDirectory: publicRoot }, projectRoot);
18363
+ if (inspection.status === "valid")
18364
+ 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"));
18365
+ 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.");
18366
+ }, contentSecurityPolicyCheck = async (config, platform6, publicRoot) => {
18367
+ const path = join50(publicRoot, "index.html");
18368
+ try {
18369
+ const source = await readFile17(path, "utf8");
18370
+ const requirements = [
18371
+ "Content-Security-Policy",
18372
+ "default-src 'self'",
18373
+ "base-uri 'none'",
18374
+ "object-src 'none'",
18375
+ "script-src 'self'",
18376
+ "form-action 'none'",
18377
+ config.productionOrigin
18378
+ ];
18379
+ const missing = requirements.filter((value) => !source.includes(value));
18380
+ if (missing.length > 0)
18381
+ throw new TypeError("Packaged shell CSP is missing a required AbsoluteJS directive or backend origin.");
18382
+ return pass(`${platform6}.content-security-policy`, "Packaged shell CSP restricts scripts, objects, base URLs, forms, and backend connections.", path);
18383
+ } catch (error) {
18384
+ 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.");
18385
+ }
18386
+ }, sourceFiles = async (root, extensions) => {
18387
+ if (!await pathExists6(root))
18388
+ return [];
18389
+ const files = await Array.fromAsync(new Bun.Glob("**/*").scan({ cwd: root, onlyFiles: true }));
18390
+ return files.filter((file) => extensions.has(extname8(file))).map((file) => join50(root, file));
18391
+ }, containsPattern = async (paths, pattern) => {
18392
+ const sources = await Promise.all(paths.map((path) => readFile17(path, "utf8")));
18393
+ const index = sources.findIndex((source) => pattern.test(source));
18394
+ return index === NOT_FOUND3 ? undefined : paths[index];
18395
+ }, androidNativeSecurityCheck = async (androidRoot) => {
18396
+ const manifestPath = join50(androidRoot, "app/src/main/AndroidManifest.xml");
18397
+ try {
18398
+ const manifest = await readFile17(manifestPath, "utf8");
18399
+ if (/android:debuggable=["']true["']/u.test(manifest))
18400
+ throw new TypeError("Android release manifest explicitly enables application debugging.");
18401
+ const sources = await sourceFiles(join50(androidRoot, "app/src/main"), new Set([".java", ".kt"]));
18402
+ const debugSource = await containsPattern(sources, /setWebContentsDebuggingEnabled\s*\(\s*true\s*\)/u);
18403
+ if (debugSource)
18404
+ 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.");
18405
+ return pass("android.native-debugging", "Android does not explicitly enable app or WebView debugging in release source.", manifestPath);
18406
+ } catch (error) {
18407
+ 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.");
18408
+ }
18409
+ }, androidExportedComponentsCheck = async (manifestPath) => {
18410
+ const source = await readFile17(manifestPath, "utf8").catch(() => "");
18411
+ const exported = [
18412
+ ...source.matchAll(/<(?:activity|activity-alias|provider|receiver|service)\b[^>]*>/giu)
18413
+ ].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");
18414
+ 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.");
18415
+ }, androidDeepLinkProjectionCheck = async (config, manifestPath) => {
18416
+ try {
18417
+ const source = await readFile17(manifestPath, "utf8");
18418
+ const required = [
18419
+ 'android:autoVerify="true"',
18420
+ "android.intent.category.BROWSABLE",
18421
+ ...config.deepLinkHosts.map((host2) => `android:scheme="https" android:host="${host2}"`),
18422
+ ...config.deepLinkScheme ? [`android:scheme="${config.deepLinkScheme}"`] : []
18423
+ ];
18424
+ if (required.some((value) => !source.includes(value)))
18425
+ throw new TypeError("Android App Link or custom-scheme projection does not match mobile config.");
18426
+ return pass("android.deep-links", "Android verified links and custom scheme match the effective mobile config.", manifestPath);
18427
+ } catch (error) {
18428
+ 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.");
18429
+ }
18430
+ }, iosNativeSecurityCheck = async (iosRoot) => {
18431
+ const entitlementsPath = join50(iosRoot, "App/AbsoluteJS.entitlements");
18432
+ const entitlements = await readFile17(entitlementsPath, "utf8").catch(() => "");
18433
+ 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))
18434
+ 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.");
18435
+ const sources = await sourceFiles(join50(iosRoot, "App"), new Set([".m", ".mm", ".swift"]));
18436
+ const debugSource = await containsPattern(sources, /\.isInspectable\s*=\s*true|setInspectable\s*\(\s*true\s*\)/u);
18437
+ if (debugSource)
18438
+ return fail5("ios.native-debugging", "iOS application source unconditionally enables WebView inspection.", debugSource, "Remove unconditional WebView inspection from release source.");
18439
+ return pass("ios.native-debugging", "iOS source does not enable release debugger attachment or WebView inspection.", entitlementsPath);
18440
+ }, iosDeepLinkProjectionCheck = async (config, iosRoot) => {
18441
+ const infoPath = join50(iosRoot, "App/App/Info.plist");
18442
+ const entitlementsPath = join50(iosRoot, "App/AbsoluteJS.entitlements");
18443
+ const projectPath = join50(iosRoot, "App/App.xcodeproj/project.pbxproj");
18444
+ try {
18445
+ const [info2, entitlements, project] = await Promise.all([
18446
+ readFile17(infoPath, "utf8"),
18447
+ readFile17(entitlementsPath, "utf8"),
18448
+ readFile17(projectPath, "utf8")
18449
+ ]);
18450
+ if (config.deepLinkScheme && (!info2.includes("<key>CFBundleURLTypes</key>") || !info2.includes(`<string>${config.deepLinkScheme}</string>`)))
18451
+ throw new TypeError("iOS custom URL scheme does not match mobile config.");
18452
+ if (config.deepLinkHosts.some((host2) => !entitlements.includes(`<string>applinks:${host2}</string>`)))
18453
+ throw new TypeError("iOS associated domains do not match mobile config.");
18454
+ if (!project.includes("CODE_SIGN_ENTITLEMENTS = App/AbsoluteJS.entitlements;"))
18455
+ throw new TypeError("iOS target does not sign the AbsoluteJS entitlements file.");
18456
+ return pass("ios.deep-links", "iOS universal links, custom scheme, and signed entitlements match mobile config.", entitlementsPath);
18457
+ } catch (error) {
18458
+ 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.");
18459
+ }
18122
18460
  }, syncSchemaReleaseCheck = (projectRoot) => {
18123
18461
  if (!projectUsesAbsoluteSync(projectRoot))
18124
18462
  return;
18125
- const manifestPath = join49(projectRoot, "package.json");
18463
+ const manifestPath = join50(projectRoot, "package.json");
18126
18464
  try {
18127
18465
  const schema = discoverAbsoluteSyncSchema(projectRoot);
18128
18466
  const versions = schema.components.map((component2) => `${component2.id}@${component2.version}`).join(", ");
@@ -18144,8 +18482,8 @@ var HMR_ASSET_PATTERN, RELEASE_ASSET_EXTENSIONS, pathExists5 = async (path) => {
18144
18482
  }, IOS_USAGE_KEYS, androidDevicePermissionCheck = async (config, permissions) => {
18145
18483
  if (!config.platforms.includes("android") || permissions.length === 0)
18146
18484
  return;
18147
- const path = join49(config.nativeProjectDirectory, "android/app/src/main/AndroidManifest.xml");
18148
- const source = await readFile16(path, "utf8");
18485
+ const path = join50(config.nativeProjectDirectory, "android/app/src/main/AndroidManifest.xml");
18486
+ const source = await readFile17(path, "utf8");
18149
18487
  const missing = permissions.filter((permission) => !source.includes(`android:name="${permission}"`) && !source.includes(`android:name='${permission}'`));
18150
18488
  if (missing.length === 0)
18151
18489
  return;
@@ -18153,14 +18491,44 @@ var HMR_ASSET_PATTERN, RELEASE_ASSET_EXTENSIONS, pathExists5 = async (path) => {
18153
18491
  }, iosDevicePermissionCheck = async (config, purposes) => {
18154
18492
  if (!config.platforms.includes("ios") || purposes.length === 0)
18155
18493
  return;
18156
- const path = join49(config.nativeProjectDirectory, "ios/App/App/Info.plist");
18157
- const source = await readFile16(path, "utf8");
18494
+ const path = join50(config.nativeProjectDirectory, "ios/App/App/Info.plist");
18495
+ const source = await readFile17(path, "utf8");
18158
18496
  const missing = purposes.filter((purpose) => !source.includes(`<key>${IOS_USAGE_KEYS[purpose]}</key>`));
18159
18497
  if (missing.length === 0)
18160
18498
  return;
18161
18499
  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.");
18500
+ }, iosCapabilityProjectionCheck = async (config, requirements) => {
18501
+ if (!config.platforms.includes("ios"))
18502
+ return;
18503
+ const appRoot = join50(config.nativeProjectDirectory, "ios/App/App");
18504
+ const infoPath = join50(appRoot, "Info.plist");
18505
+ const info2 = await readFile17(infoPath, "utf8").catch(() => "");
18506
+ if (requirements.iosSystemBars && !/<key>UIViewControllerBasedStatusBarAppearance<\/key>\s*<true\s*\/>/u.test(info2))
18507
+ 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.");
18508
+ if (requirements.iosPrivacyAccessedApis.length > 0) {
18509
+ const privacyPath = join50(appRoot, "PrivacyInfo.xcprivacy");
18510
+ const projectPath = join50(config.nativeProjectDirectory, "ios/App/App.xcodeproj/project.pbxproj");
18511
+ const [privacy, project] = await Promise.all([
18512
+ readFile17(privacyPath, "utf8").catch(() => ""),
18513
+ readFile17(projectPath, "utf8").catch(() => "")
18514
+ ]);
18515
+ const missing = requirements.iosPrivacyAccessedApis.some(({ api, reasons }) => !privacy.includes(`<string>${api}</string>`) || reasons.some((reason) => !privacy.includes(`<string>${reason}</string>`)));
18516
+ if (missing || !project.includes("PrivacyInfo.xcprivacy in Resources"))
18517
+ 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.");
18518
+ }
18519
+ if (requirements.iosPushNotifications) {
18520
+ const entitlementsPath = join50(config.nativeProjectDirectory, "ios/App/AbsoluteJS.entitlements");
18521
+ const delegatePath = join50(appRoot, "AppDelegate.swift");
18522
+ const [entitlements, delegate] = await Promise.all([
18523
+ readFile17(entitlementsPath, "utf8").catch(() => ""),
18524
+ readFile17(delegatePath, "utf8").catch(() => "")
18525
+ ]);
18526
+ if (!entitlements.includes("<key>aps-environment</key>") || !delegate.includes("capacitorDidRegisterForRemoteNotifications") || !delegate.includes("capacitorDidFailToRegisterForRemoteNotifications"))
18527
+ 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.");
18528
+ }
18529
+ return;
18162
18530
  }, deviceCapabilityReleaseCheck = async (config, projectRoot) => {
18163
- const manifestPath = join49(projectRoot, "package.json");
18531
+ const manifestPath = join50(projectRoot, "package.json");
18164
18532
  try {
18165
18533
  const plan = resolveAbsoluteDeviceCapabilityPlan(projectRoot);
18166
18534
  assertAbsoluteDeviceCapabilityPackages(projectRoot, plan);
@@ -18171,32 +18539,41 @@ var HMR_ASSET_PATTERN, RELEASE_ASSET_EXTENSIONS, pathExists5 = async (path) => {
18171
18539
  const iosCheck = await iosDevicePermissionCheck(config, requirements.iosUsageDescriptions);
18172
18540
  if (iosCheck)
18173
18541
  return iosCheck;
18542
+ const iosProjectionCheck = await iosCapabilityProjectionCheck(config, requirements);
18543
+ if (iosProjectionCheck)
18544
+ return iosProjectionCheck;
18174
18545
  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
18546
  } catch (error) {
18176
18547
  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
18548
  }
18178
18549
  }, 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");
18550
+ const androidRoot = join50(config.nativeProjectDirectory, "android");
18551
+ const nativeConfigPath = join50(androidRoot, "app", "src", "main", "assets", "capacitor.config.json");
18552
+ const manifestPath = join50(androidRoot, "app", "src", "main", "AndroidManifest.xml");
18553
+ const publicRoot = join50(androidRoot, "app", "src", "main", "assets", "public");
18554
+ const journalPath = join50(projectRoot, ".absolutejs", "mobile", "dev-session", "journal.json");
18184
18555
  const checks = await Promise.all([
18185
18556
  journalReleaseCheck(journalPath, "android"),
18186
18557
  capacitorConfigReleaseCheck(nativeConfigPath),
18558
+ capacitorIdentityCheck(config, "android", nativeConfigPath),
18187
18559
  manifestReleaseCheck(manifestPath),
18188
- hmrAssetsReleaseCheck(publicRoot)
18560
+ hmrAssetsReleaseCheck(publicRoot),
18561
+ embeddedBundleReleaseCheck(config, projectRoot, "android", publicRoot),
18562
+ contentSecurityPolicyCheck(config, "android", publicRoot),
18563
+ androidNativeSecurityCheck(androidRoot),
18564
+ androidExportedComponentsCheck(manifestPath),
18565
+ androidDeepLinkProjectionCheck(config, manifestPath)
18189
18566
  ]);
18190
18567
  return checks.map((check2) => ({
18191
18568
  ...check2,
18192
- path: check2.path ? relative24(projectRoot, check2.path).replaceAll("\\", "/") || "." : undefined
18569
+ path: check2.path ? relative25(projectRoot, check2.path).replaceAll("\\", "/") || "." : undefined
18193
18570
  }));
18194
18571
  }, 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");
18572
+ const iosAppRoot = join50(config.nativeProjectDirectory, "ios", "App", "App");
18573
+ const nativeConfigPath = join50(iosAppRoot, "capacitor.config.json");
18574
+ const infoPath = join50(iosAppRoot, "Info.plist");
18575
+ const publicRoot = join50(iosAppRoot, "public");
18576
+ const journalPath = join50(projectRoot, ".absolutejs", "mobile", "ios-dev-session", "journal.json");
18200
18577
  const checks = [
18201
18578
  await journalReleaseCheck(journalPath, "ios")
18202
18579
  ];
@@ -18205,27 +18582,59 @@ var HMR_ASSET_PATTERN, RELEASE_ASSET_EXTENSIONS, pathExists5 = async (path) => {
18205
18582
  } else {
18206
18583
  checks.push(pass("ios.marketing-version", `The iOS marketing version is ${config.iosVersion}.`));
18207
18584
  }
18208
- if (!await pathExists5(nativeConfigPath)) {
18585
+ if (!await pathExists6(nativeConfigPath)) {
18209
18586
  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"))) {
18587
+ } else if (isUnsafeCapacitorConfig(await readFile17(nativeConfigPath, "utf8"))) {
18211
18588
  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
18589
  } else {
18213
18590
  checks.push(pass("ios.capacitor-config", "iOS Capacitor config contains no development transport overrides.", nativeConfigPath));
18214
18591
  }
18215
- if (!await pathExists5(infoPath)) {
18592
+ checks.push(await capacitorIdentityCheck(config, "ios", nativeConfigPath));
18593
+ if (!await pathExists6(infoPath)) {
18216
18594
  checks.push(fail5("ios.transport-security", "The iOS Info.plist is missing.", infoPath, "Run `absolute mobile sync ios` before release validation."));
18217
18595
  } else {
18218
- const info2 = await readFile16(infoPath, "utf8");
18596
+ const info2 = await readFile17(infoPath, "utf8");
18219
18597
  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
18598
  }
18221
18599
  const hmrAsset = await findHmrAsset(publicRoot);
18222
18600
  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));
18601
+ 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
18602
  return checks.map((check2) => ({
18224
18603
  ...check2,
18225
- path: check2.path ? relative24(projectRoot, check2.path).replaceAll("\\", "/") || "." : undefined
18604
+ path: check2.path ? relative25(projectRoot, check2.path).replaceAll("\\", "/") || "." : undefined
18226
18605
  }));
18606
+ }, createAbsoluteMobileComplianceReport = (config, result) => {
18607
+ const summary = {
18608
+ failed: result.checks.filter(({ status: status2 }) => status2 === "fail").length,
18609
+ passed: result.checks.filter(({ status: status2 }) => status2 === "pass").length,
18610
+ warnings: result.checks.filter(({ status: status2 }) => status2 === "warn").length
18611
+ };
18612
+ return {
18613
+ app: {
18614
+ appId: config.appId,
18615
+ engine: config.engine,
18616
+ platforms: [...config.platforms],
18617
+ productionOrigin: config.productionOrigin
18618
+ },
18619
+ checks: result.checks.map(({ id, status: status2 }) => ({ id, status: status2 })),
18620
+ format: ABSOLUTE_MOBILE_COMPLIANCE_REPORT_FORMAT,
18621
+ manualReview: [...MANUAL_REVIEW],
18622
+ ready: result.ready,
18623
+ summary
18624
+ };
18227
18625
  }, inspectAbsoluteMobileRelease = async (config, projectRoot) => {
18228
- const checks = config.platforms.includes("android") ? await inspectAndroidRelease(config, projectRoot) : [];
18626
+ const globalChecks = await Promise.all([
18627
+ Promise.resolve(productionOriginCheck(config, projectRoot)),
18628
+ Promise.resolve(associationIdentityCheck(config, projectRoot)),
18629
+ dependencyLockCheck(projectRoot),
18630
+ capacitorVersionCheck(config, projectRoot)
18631
+ ]);
18632
+ const checks = globalChecks.map((check2) => ({
18633
+ ...check2,
18634
+ path: check2.path ? relative25(projectRoot, check2.path).replaceAll("\\", "/") || "." : undefined
18635
+ }));
18636
+ if (config.platforms.includes("android"))
18637
+ checks.push(...await inspectAndroidRelease(config, projectRoot));
18229
18638
  if (config.platforms.includes("ios")) {
18230
18639
  checks.push(...await inspectIosRelease(config, projectRoot));
18231
18640
  }
@@ -18233,25 +18642,41 @@ var HMR_ASSET_PATTERN, RELEASE_ASSET_EXTENSIONS, pathExists5 = async (path) => {
18233
18642
  if (syncSchema) {
18234
18643
  checks.push({
18235
18644
  ...syncSchema,
18236
- path: syncSchema.path ? relative24(projectRoot, syncSchema.path).replaceAll("\\", "/") || "." : undefined
18645
+ path: syncSchema.path ? relative25(projectRoot, syncSchema.path).replaceAll("\\", "/") || "." : undefined
18237
18646
  });
18238
18647
  }
18239
18648
  const deviceCapabilities = await deviceCapabilityReleaseCheck(config, projectRoot);
18240
18649
  checks.push({
18241
18650
  ...deviceCapabilities,
18242
- path: deviceCapabilities.path ? relative24(projectRoot, deviceCapabilities.path).replaceAll("\\", "/") || "." : undefined
18651
+ path: deviceCapabilities.path ? relative25(projectRoot, deviceCapabilities.path).replaceAll("\\", "/") || "." : undefined
18243
18652
  });
18244
18653
  return {
18245
18654
  checks,
18246
- ready: checks.length > 0 && checks.every((check2) => check2.status === "pass")
18655
+ ready: checks.length > 0 && checks.every((check2) => check2.status !== "fail")
18247
18656
  };
18248
18657
  };
18249
18658
  var init_releaseDoctor = __esm(() => {
18659
+ init_mobileBundleInspection();
18250
18660
  init_nativeAuth();
18251
18661
  init_syncSchema();
18252
18662
  init_deviceCapabilities();
18253
18663
  HMR_ASSET_PATTERN = /(?:__HMR_WS__|hmr-timing|__absolute_target|absolutejs-error-overlay)/u;
18254
18664
  RELEASE_ASSET_EXTENSIONS = new Set([".html", ".js", ".mjs"]);
18665
+ EXACT_VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u;
18666
+ LOCK_FILES = [
18667
+ "bun.lock",
18668
+ "bun.lockb",
18669
+ "package-lock.json",
18670
+ "pnpm-lock.yaml",
18671
+ "yarn.lock"
18672
+ ];
18673
+ MANUAL_REVIEW = [
18674
+ "physical-device",
18675
+ "store-privacy-questionnaire",
18676
+ "privacy-policy",
18677
+ "signing-key-custody",
18678
+ "native-sdk-data-practices"
18679
+ ];
18255
18680
  IOS_USAGE_KEYS = {
18256
18681
  camera: "NSCameraUsageDescription",
18257
18682
  "location-always": "NSLocationAlwaysAndWhenInUseUsageDescription",
@@ -18262,19 +18687,19 @@ var init_releaseDoctor = __esm(() => {
18262
18687
  });
18263
18688
 
18264
18689
  // src/mobile/androidRelease.ts
18265
- import { createHash as createHash12 } from "crypto";
18690
+ import { createHash as createHash13 } from "crypto";
18266
18691
  import {
18267
- access as access9,
18692
+ access as access10,
18268
18693
  copyFile as copyFile5,
18269
18694
  mkdir as mkdir12,
18270
18695
  mkdtemp as mkdtemp5,
18271
- readFile as readFile17,
18696
+ readFile as readFile18,
18272
18697
  rename as rename12,
18273
18698
  rm as rm8,
18274
- stat as stat2,
18699
+ stat as stat3,
18275
18700
  writeFile as writeFile14
18276
18701
  } 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";
18702
+ import { dirname as dirname30, isAbsolute as isAbsolute7, join as join51, relative as relative26, resolve as resolve40, sep as sep6 } from "path";
18278
18703
  var ABSOLUTE_ANDROID_RELEASE_FORMAT = 1, isRecord13 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), requireManifest2 = (value) => {
18279
18704
  if (!isRecord13(value) || typeof value.appBuild !== "string" || typeof value.appId !== "string" || typeof value.runtime !== "string") {
18280
18705
  throw new TypeError("Invalid embedded AbsoluteJS mobile manifest.");
@@ -18284,9 +18709,9 @@ var ABSOLUTE_ANDROID_RELEASE_FORMAT = 1, isRecord13 = (value) => typeof value ==
18284
18709
  appId: value.appId,
18285
18710
  runtime: value.runtime
18286
18711
  };
18287
- }, pathExists6 = async (path) => {
18712
+ }, pathExists7 = async (path) => {
18288
18713
  try {
18289
- await access9(path);
18714
+ await access10(path);
18290
18715
  return true;
18291
18716
  } catch {
18292
18717
  return false;
@@ -18324,22 +18749,36 @@ var ABSOLUTE_ANDROID_RELEASE_FORMAT = 1, isRecord13 = (value) => typeof value ==
18324
18749
  artifactPath
18325
18750
  ]);
18326
18751
  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);
18752
+ }, signAab = (artifactPath, capture, jarsigner, signing) => {
18753
+ const result = capture([
18754
+ jarsigner,
18755
+ "-keystore",
18756
+ signing.keystorePath,
18757
+ "-storepass:env",
18758
+ signing.storePasswordEnvironment,
18759
+ "-keypass:env",
18760
+ signing.keyPasswordEnvironment,
18761
+ artifactPath,
18762
+ signing.keyAlias
18763
+ ]);
18764
+ if (result.exitCode !== 0)
18765
+ throw new TypeError("jarsigner could not sign the Android App Bundle with the configured CI identity.");
18766
+ }, sha256File2 = async (path) => createHash13("sha256").update(await readFile18(path)).digest("hex"), safeOutputDirectory2 = (projectRoot, requested) => {
18767
+ const root = resolve40(projectRoot);
18768
+ const output = resolve40(root, requested ?? ".absolutejs/mobile/releases/android");
18769
+ const projectRelative = relative26(root, output);
18331
18770
  if (projectRelative === ".." || projectRelative.startsWith(`..${sep6}`) || isAbsolute7(projectRelative)) {
18332
18771
  throw new TypeError("mobile build --outdir must remain inside the project.");
18333
18772
  }
18334
18773
  return output;
18335
18774
  }, installRelease2 = async (artifactPath, metadata, outputRoot) => {
18336
- const releaseRoot = join50(outputRoot, metadata.releaseId);
18775
+ const releaseRoot = join51(outputRoot, metadata.releaseId);
18337
18776
  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);
18777
+ const destination = join51(releaseRoot, artifactName);
18778
+ if (await pathExists7(releaseRoot)) {
18779
+ const existing = requireManifestIdentity(JSON.parse(await readFile18(join51(releaseRoot, "release.json"), "utf8")), metadata);
18341
18780
  const [installedBytes, installedSha256] = await Promise.all([
18342
- stat2(destination).then(({ size }) => size),
18781
+ stat3(destination).then(({ size }) => size),
18343
18782
  sha256File2(destination)
18344
18783
  ]);
18345
18784
  if (installedBytes !== metadata.bytes || installedSha256 !== metadata.sha256) {
@@ -18348,14 +18787,14 @@ var ABSOLUTE_ANDROID_RELEASE_FORMAT = 1, isRecord13 = (value) => typeof value ==
18348
18787
  return { artifactPath: destination, metadata: existing, releaseRoot };
18349
18788
  }
18350
18789
  await mkdir12(dirname30(releaseRoot), { recursive: true });
18351
- const staging = await mkdtemp5(join50(dirname30(releaseRoot), ".android-stage-"));
18790
+ const staging = await mkdtemp5(join51(dirname30(releaseRoot), ".android-stage-"));
18352
18791
  try {
18353
- await copyFile5(artifactPath, join50(staging, artifactName));
18792
+ await copyFile5(artifactPath, join51(staging, artifactName));
18354
18793
  const complete = {
18355
18794
  ...metadata,
18356
18795
  artifact: artifactName
18357
18796
  };
18358
- await writeFile14(join50(staging, "release.json"), `${JSON.stringify(complete, null, "\t")}
18797
+ await writeFile14(join51(staging, "release.json"), `${JSON.stringify(complete, null, "\t")}
18359
18798
  `, { flag: "wx" });
18360
18799
  await rename12(staging, releaseRoot);
18361
18800
  return { artifactPath: destination, metadata: complete, releaseRoot };
@@ -18380,18 +18819,18 @@ var ABSOLUTE_ANDROID_RELEASE_FORMAT = 1, isRecord13 = (value) => typeof value ==
18380
18819
  if (options.versionCode !== undefined && (!Number.isSafeInteger(options.versionCode) || options.versionCode < 1 || options.versionCode > 2100000000)) {
18381
18820
  throw new TypeError("Android versionCode must be an integer from 1 through 2100000000.");
18382
18821
  }
18383
- const projectRoot = resolve39(options.projectRoot);
18822
+ const projectRoot = resolve40(options.projectRoot);
18384
18823
  const host2 = options.host ?? detectAbsoluteMobileHost();
18385
18824
  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")));
18825
+ const nativeDirectory = join51(options.config.nativeProjectDirectory, "android");
18826
+ const manifest = requireManifest2(JSON.parse(await readFile18(join51(options.config.bundleDirectory, "absolute-mobile-manifest.json"), "utf8")));
18388
18827
  if (manifest.appId !== options.config.appId) {
18389
18828
  throw new TypeError("Embedded mobile manifest appId does not match mobile.appId.");
18390
18829
  }
18391
18830
  let { versionCode } = options;
18392
18831
  if (options.prepareVersionCode) {
18393
18832
  const nativeFingerprint = await fingerprintAbsoluteAndroidNativeProject({ nativeDirectory });
18394
- const buildIdentity = createHash12("sha256").update(`${manifest.appBuild}\x00${nativeFingerprint}`).digest("hex");
18833
+ const buildIdentity = createHash13("sha256").update(`${manifest.appBuild}\x00${nativeFingerprint}`).digest("hex");
18395
18834
  versionCode = await options.prepareVersionCode(buildIdentity);
18396
18835
  }
18397
18836
  if (versionCode !== undefined && (!Number.isSafeInteger(versionCode) || versionCode < 1 || versionCode > 2100000000)) {
@@ -18410,11 +18849,20 @@ var ABSOLUTE_ANDROID_RELEASE_FORMAT = 1, isRecord13 = (value) => typeof value ==
18410
18849
  run: options.run,
18411
18850
  task: "bundleRelease"
18412
18851
  });
18413
- if (!await pathExists6(artifactPath)) {
18852
+ if (!await pathExists7(artifactPath)) {
18414
18853
  throw new TypeError(`Android Gradle did not produce the expected App Bundle: ${artifactPath}`);
18415
18854
  }
18416
18855
  const capture = options.capture ?? defaultCapture4;
18417
- const signed2 = verifyAabSignature(artifactPath, capture, options.jarsigner);
18856
+ const jarsigner = options.jarsigner === undefined ? Bun.which("jarsigner") : options.jarsigner;
18857
+ let signed2 = verifyAabSignature(artifactPath, capture, jarsigner);
18858
+ if (signed2 === false && options.signing) {
18859
+ if (!jarsigner)
18860
+ throw new TypeError("Could not sign the Android App Bundle because jarsigner is unavailable.");
18861
+ signAab(artifactPath, capture, jarsigner, options.signing);
18862
+ signed2 = verifyAabSignature(artifactPath, capture, jarsigner);
18863
+ if (!signed2)
18864
+ throw new TypeError("Android App Bundle signature verification failed after CI signing.");
18865
+ }
18418
18866
  if (signed2 === null && !options.allowUnsigned) {
18419
18867
  throw new TypeError("Could not verify the Android App Bundle signature because jarsigner is unavailable. Install a JDK, or use --unsigned only for a non-publishable build.");
18420
18868
  }
@@ -18422,7 +18870,7 @@ var ABSOLUTE_ANDROID_RELEASE_FORMAT = 1, isRecord13 = (value) => typeof value ==
18422
18870
  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
18871
  }
18424
18872
  const [bytes, sha2562] = await Promise.all([
18425
- stat2(artifactPath).then(({ size }) => size),
18873
+ stat3(artifactPath).then(({ size }) => size),
18426
18874
  sha256File2(artifactPath)
18427
18875
  ]);
18428
18876
  const releaseId = `amobile_android_${sha2562}`;
@@ -18486,7 +18934,7 @@ var absoluteIosDeviceAcceptanceCommands = (options) => {
18486
18934
  };
18487
18935
 
18488
18936
  // src/mobile/iosConformance.ts
18489
- import { readFile as readFile18, stat as stat3 } from "fs/promises";
18937
+ import { readFile as readFile19, stat as stat4 } from "fs/promises";
18490
18938
  var HMR_LINE, parseAbsoluteIosHmrLog = (line) => {
18491
18939
  const match = HMR_LINE.exec(line);
18492
18940
  if (!match)
@@ -18515,13 +18963,13 @@ var HMR_LINE, parseAbsoluteIosHmrLog = (line) => {
18515
18963
  const sleep2 = options.sleep ?? Bun.sleep;
18516
18964
  const timeoutMs = options.timeoutMs ?? 30000;
18517
18965
  const deadline = Date.now() + timeoutMs;
18518
- let offset = options.startOffset ?? await stat3(options.logPath).then(({ size }) => size).catch(() => 0);
18966
+ let offset = options.startOffset ?? await stat4(options.logPath).then(({ size }) => size).catch(() => 0);
18519
18967
  let buffered = "";
18520
18968
  const poll = async () => {
18521
18969
  if (Date.now() > deadline)
18522
18970
  throw new Error(`No iOS native HMR acknowledgement was observed within ${timeoutMs}ms.`);
18523
18971
  options.signal?.throwIfAborted();
18524
- const contents = await readFile18(options.logPath).catch(() => Buffer.alloc(0));
18972
+ const contents = await readFile19(options.logPath).catch(() => Buffer.alloc(0));
18525
18973
  if (contents.byteLength < offset) {
18526
18974
  offset = 0;
18527
18975
  buffered = "";
@@ -18545,8 +18993,8 @@ var init_iosConformance = __esm(() => {
18545
18993
  });
18546
18994
 
18547
18995
  // 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";
18996
+ import { mkdir as mkdir13, readFile as readFile20, writeFile as writeFile15 } from "fs/promises";
18997
+ import { join as join52 } from "path";
18550
18998
  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
18999
  `, "<br>"), createAbsoluteNativeAutomatedChecks = (run) => {
18552
19000
  const target = `${run.targetKind} ${run.targetId}`;
@@ -18648,7 +19096,7 @@ var secretPattern, bearerPattern, coordinatePattern, nativeCredentialPattern, sa
18648
19096
  reportVersion: 1,
18649
19097
  run: options.run
18650
19098
  }), readPackageVersionForNativeReport = async (packageJsonPath) => {
18651
- const manifest = JSON.parse(await readFile19(packageJsonPath, "utf8"));
19099
+ const manifest = JSON.parse(await readFile20(packageJsonPath, "utf8"));
18652
19100
  if (typeof manifest !== "object" || manifest === null)
18653
19101
  return "unknown";
18654
19102
  const version2 = Reflect.get(manifest, "version");
@@ -18685,8 +19133,8 @@ ${table(report.manualChecks)}
18685
19133
  `;
18686
19134
  }, writeAbsoluteNativeTestReport = async (directory, report) => {
18687
19135
  await mkdir13(directory, { recursive: true });
18688
- const jsonPath = join51(directory, "report.json");
18689
- const markdownPath = join51(directory, "report.md");
19136
+ const jsonPath = join52(directory, "report.json");
19137
+ const markdownPath = join52(directory, "report.md");
18690
19138
  await Promise.all([
18691
19139
  writeFile15(jsonPath, `${JSON.stringify(report, null, 2)}
18692
19140
  `),
@@ -18904,8 +19352,8 @@ var init_androidTestReport = __esm(() => {
18904
19352
  });
18905
19353
 
18906
19354
  // 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";
19355
+ import { access as access11 } from "fs/promises";
19356
+ import { isAbsolute as isAbsolute8, relative as relative27, resolve as resolve41, sep as sep7 } from "path";
18909
19357
  import { pathToFileURL as pathToFileURL2 } from "url";
18910
19358
  var prepareAbsoluteIosRelease = async (publisher, options) => {
18911
19359
  if (typeof publisher.prepareIosRelease !== "function") {
@@ -18927,16 +19375,16 @@ var prepareAbsoluteIosRelease = async (publisher, options) => {
18927
19375
  }
18928
19376
  return versionCode;
18929
19377
  }, 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);
19378
+ const root = resolve41(projectRoot);
19379
+ const path = resolve41(root, requested);
19380
+ const projectRelative = relative27(root, path);
18933
19381
  if (projectRelative === ".." || projectRelative.startsWith(`..${sep7}`) || isAbsolute8(projectRelative)) {
18934
19382
  throw new TypeError("mobile publish --registry must remain inside the project.");
18935
19383
  }
18936
19384
  return path;
18937
19385
  }, loadAbsoluteNativeReleasePublisher = async (projectRoot, requestedModulePath) => {
18938
19386
  const modulePath = publisherModulePath(projectRoot, requestedModulePath);
18939
- await access10(modulePath).catch(() => {
19387
+ await access11(modulePath).catch(() => {
18940
19388
  throw new TypeError(`Native release registry module does not exist: ${modulePath}`);
18941
19389
  });
18942
19390
  const loaded = await import(pathToFileURL2(modulePath).href);
@@ -18997,133 +19445,36 @@ var prepareAbsoluteIosRelease = async (publisher, options) => {
18997
19445
  var init_releasePublisher = () => {};
18998
19446
 
18999
19447
  // src/mobile/mobileInspect.ts
19000
- import { access as access11, readFile as readFile20, stat as stat4 } from "fs/promises";
19001
- import { join as join52, relative as relative27, resolve as resolve41 } from "path";
19002
- var ABSOLUTE_MOBILE_INSPECTION_FORMAT = 1, MOBILE_PACKAGE_NAMES, MOBILE_FRAMEWORKS, isObject2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), portablePath = (projectRoot, path) => {
19003
- const value = relative27(resolve41(projectRoot), resolve41(path)).replaceAll("\\", "/");
19448
+ import { access as access12, readFile as readFile21 } from "fs/promises";
19449
+ import { join as join53, relative as relative28, resolve as resolve42 } from "path";
19450
+ var ABSOLUTE_MOBILE_INSPECTION_FORMAT = 1, MOBILE_PACKAGE_NAMES, isObject3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), portablePath2 = (projectRoot, path) => {
19451
+ const value = relative28(resolve42(projectRoot), resolve42(path)).replaceAll("\\", "/");
19004
19452
  return value || ".";
19005
- }, pathExists7 = async (path) => {
19453
+ }, pathExists8 = async (path) => {
19006
19454
  try {
19007
- await access11(path);
19455
+ await access12(path);
19008
19456
  return true;
19009
19457
  } catch {
19010
19458
  return false;
19011
19459
  }
19012
- }, readObject = async (path) => {
19013
- const value = JSON.parse(await readFile20(path, "utf8"));
19014
- if (!isObject2(value))
19460
+ }, readObject2 = async (path) => {
19461
+ const value = JSON.parse(await readFile21(path, "utf8"));
19462
+ if (!isObject3(value))
19015
19463
  throw new TypeError("JSON root must be an object.");
19016
19464
  return value;
19017
- }, requireString = (value, field) => {
19018
- if (typeof value !== "string" || value.length === 0)
19019
- throw new TypeError(`${field} must be a non-empty string.`);
19020
- return value;
19021
- }, requireStringArray = (value, field) => {
19022
- if (!Array.isArray(value) || !value.every((item) => typeof item === "string"))
19023
- throw new TypeError(`${field} must be a string array.`);
19024
- return value;
19025
- }, requireBundleFile = async (root, value, field) => {
19026
- const portable = requireString(value, field);
19027
- const path = resolve41(root, portable);
19028
- const normalizedRoot = resolve41(root);
19029
- if (path === normalizedRoot || !path.startsWith(`${normalizedRoot}/`))
19030
- throw new TypeError(`${field} must remain inside the mobile bundle.`);
19031
- if (!(await stat4(path).catch(() => {
19032
- return;
19033
- }))?.isFile())
19034
- throw new TypeError(`${field} does not exist in the mobile bundle.`);
19035
- return portable;
19036
- }, inspectBundle = async (config, projectRoot) => {
19037
- const manifestPath = join52(config.bundleDirectory, "absolute-mobile-manifest.json");
19038
- const manifest = portablePath(projectRoot, manifestPath);
19039
- if (!await pathExists7(manifestPath))
19040
- return { manifest, status: "missing" };
19041
- try {
19042
- const value = await readObject(manifestPath);
19043
- if (value.format !== 1)
19044
- throw new TypeError("format is not supported by this runtime.");
19045
- if (requireString(value.appId, "appId") !== config.appId)
19046
- throw new TypeError("appId does not match the effective mobile config.");
19047
- if (requireString(value.productionOrigin, "productionOrigin") !== config.productionOrigin)
19048
- throw new TypeError("productionOrigin does not match the effective mobile config.");
19049
- const appBuild = requireString(value.appBuild, "appBuild");
19050
- const runtime = requireString(value.runtime, "runtime");
19051
- const capabilities = requireStringArray(value.deviceCapabilities, "deviceCapabilities").sort();
19052
- if (!Array.isArray(value.pages) || !Array.isArray(value.routes))
19053
- throw new TypeError("pages and routes must be arrays.");
19054
- const pageIds = new Set;
19055
- const frameworks7 = new Set;
19056
- await Promise.all(value.pages.map(async (candidate) => {
19057
- if (!isObject2(candidate))
19058
- throw new TypeError("pages contains an invalid entry.");
19059
- const pageId = requireString(candidate.pageId, "page.pageId");
19060
- if (pageIds.has(pageId))
19061
- throw new TypeError("page.pageId values must be unique.");
19062
- pageIds.add(pageId);
19063
- const framework = requireString(candidate.framework, "page.framework");
19064
- if (!MOBILE_FRAMEWORKS.has(framework))
19065
- throw new TypeError("page.framework is unsupported.");
19066
- frameworks7.add(framework);
19067
- requireString(candidate.bundleHash, "page.bundleHash");
19068
- requireString(candidate.contract, "page.contract");
19069
- requireString(candidate.propsSchemaHash, "page.propsSchemaHash");
19070
- await requireBundleFile(config.bundleDirectory, candidate.localBundlePath, "page.localBundlePath");
19071
- if (candidate.localStylePath !== undefined)
19072
- await requireBundleFile(config.bundleDirectory, candidate.localStylePath, "page.localStylePath");
19073
- }));
19074
- const routes = value.routes.map((candidate) => {
19075
- if (!isObject2(candidate))
19076
- throw new TypeError("routes contains an invalid entry.");
19077
- const { method } = candidate;
19078
- if (method !== "GET" && method !== "HEAD")
19079
- throw new TypeError("route.method must be GET or HEAD.");
19080
- const pageId = requireString(candidate.pageId, "route.pageId");
19081
- if (!pageIds.has(pageId))
19082
- throw new TypeError("route.pageId references a missing page.");
19083
- return {
19084
- method,
19085
- pageId,
19086
- pattern: requireString(candidate.pattern, "route.pattern")
19087
- };
19088
- });
19089
- await Promise.all(["index.html", "absolute-mobile-bootstrap.js"].map((file) => requireBundleFile(config.bundleDirectory, file, file)));
19090
- const entryPath = new URL(config.entry, "https://absolute.invalid").pathname;
19091
- const entryResolved = resolveAbsoluteMobileRoute(routes, entryPath) !== undefined;
19092
- if (!entryResolved)
19093
- throw new TypeError("entry is not owned by an embedded route.");
19094
- return {
19095
- appBuild,
19096
- auth: isObject2(value.auth),
19097
- capabilities,
19098
- entryResolved,
19099
- frameworks: [...frameworks7].sort(),
19100
- manifest,
19101
- pageCount: value.pages.length,
19102
- routeCount: value.routes.length,
19103
- runtime,
19104
- status: "valid",
19105
- sync: isObject2(value.sync)
19106
- };
19107
- } catch (error) {
19108
- return {
19109
- issue: error instanceof Error ? error.message : "The embedded mobile manifest is invalid.",
19110
- manifest,
19111
- status: "invalid"
19112
- };
19113
- }
19114
19465
  }, addPackageDeclarations = (declarations, value) => {
19115
- if (!isObject2(value))
19466
+ if (!isObject3(value))
19116
19467
  return;
19117
19468
  for (const [name, declared] of Object.entries(value).filter((entry) => typeof entry[1] === "string"))
19118
19469
  declarations.set(name, declared);
19119
19470
  }, packageInspections = async (projectRoot, additionalNames) => {
19120
- const project = await readObject(join52(projectRoot, "package.json"));
19471
+ const project = await readObject2(join53(projectRoot, "package.json"));
19121
19472
  const declarations = new Map;
19122
19473
  for (const field of ["dependencies", "devDependencies"])
19123
19474
  addPackageDeclarations(declarations, project[field]);
19124
19475
  const names = [...new Set([...declarations.keys(), ...additionalNames])].filter((name) => MOBILE_PACKAGE_NAMES.has(name) || name.startsWith("@capacitor/") || additionalNames.includes(name)).sort();
19125
19476
  return Promise.all(names.map(async (name) => {
19126
- const installedManifest = await readObject(join52(projectRoot, "node_modules", name, "package.json")).catch(() => {
19477
+ const installedManifest = await readObject2(join53(projectRoot, "node_modules", name, "package.json")).catch(() => {
19127
19478
  return;
19128
19479
  });
19129
19480
  const installed = installedManifest?.version;
@@ -19134,7 +19485,7 @@ var ABSOLUTE_MOBILE_INSPECTION_FORMAT = 1, MOBILE_PACKAGE_NAMES, MOBILE_FRAMEWOR
19134
19485
  };
19135
19486
  }));
19136
19487
  }, inspectAbsoluteMobileProject = async (config, projectRoot, options = {}) => {
19137
- const bundle = await inspectBundle(config, projectRoot);
19488
+ const bundle = await inspectAbsoluteMobileBundle(config, projectRoot);
19138
19489
  let currentCapabilities = [];
19139
19490
  let capabilityIssue;
19140
19491
  let plugins = [];
@@ -19160,22 +19511,22 @@ var ABSOLUTE_MOBILE_INSPECTION_FORMAT = 1, MOBILE_PACKAGE_NAMES, MOBILE_FRAMEWOR
19160
19511
  config: {
19161
19512
  appId: config.appId,
19162
19513
  appName: config.appName,
19163
- bundleDirectory: portablePath(projectRoot, config.bundleDirectory),
19514
+ bundleDirectory: portablePath2(projectRoot, config.bundleDirectory),
19164
19515
  deepLinkHosts: config.deepLinkHosts,
19165
19516
  deepLinkScheme: config.deepLinkScheme,
19166
19517
  engine: config.engine,
19167
19518
  entry: config.entry,
19168
19519
  iosVersion: config.iosVersion,
19169
- nativeProjectDirectory: portablePath(projectRoot, config.nativeProjectDirectory),
19520
+ nativeProjectDirectory: portablePath2(projectRoot, config.nativeProjectDirectory),
19170
19521
  platforms: config.platforms,
19171
19522
  productionOrigin: config.productionOrigin
19172
19523
  },
19173
19524
  format: ABSOLUTE_MOBILE_INSPECTION_FORMAT,
19174
19525
  nativeProjects: await Promise.all(config.platforms.map(async (platform6) => {
19175
- const path = join52(config.nativeProjectDirectory, platform6);
19526
+ const path = join53(config.nativeProjectDirectory, platform6);
19176
19527
  return {
19177
- initialized: await pathExists7(path),
19178
- path: portablePath(projectRoot, path),
19528
+ initialized: await pathExists8(path),
19529
+ path: portablePath2(projectRoot, path),
19179
19530
  platform: platform6
19180
19531
  };
19181
19532
  })),
@@ -19227,7 +19578,7 @@ var ABSOLUTE_MOBILE_INSPECTION_FORMAT = 1, MOBILE_PACKAGE_NAMES, MOBILE_FRAMEWOR
19227
19578
  var init_mobileInspect = __esm(() => {
19228
19579
  init_deviceCapabilities();
19229
19580
  init_releaseDoctor();
19230
- init_routeMatcher();
19581
+ init_mobileBundleInspection();
19231
19582
  MOBILE_PACKAGE_NAMES = new Set([
19232
19583
  "@absolutejs/absolute",
19233
19584
  "@absolutejs/auth",
@@ -19239,15 +19590,484 @@ var init_mobileInspect = __esm(() => {
19239
19590
  "@absolutejs/sync-capacitor",
19240
19591
  "@capacitor-community/sqlite"
19241
19592
  ]);
19242
- MOBILE_FRAMEWORKS = new Set([
19243
- "angular",
19244
- "ember",
19245
- "html",
19246
- "htmx",
19247
- "react",
19248
- "svelte",
19249
- "vue"
19593
+ });
19594
+
19595
+ // src/mobile/ciWorkflow.ts
19596
+ import { existsSync as existsSync42 } from "fs";
19597
+ import { access as access13, mkdir as mkdir14, readFile as readFile22, writeFile as writeFile16 } from "fs/promises";
19598
+ import { dirname as dirname31, extname as extname9, relative as relative29, resolve as resolve43, sep as sep8 } from "path";
19599
+ var ABSOLUTE_MOBILE_CI_WORKFLOW_FORMAT = 1, SECRET_NAME_PATTERN, CI_ENV_INDENTATION = 6, RESERVED_SECRET_NAMES, exists3 = async (path) => {
19600
+ try {
19601
+ await access13(path);
19602
+ return true;
19603
+ } catch {
19604
+ return false;
19605
+ }
19606
+ }, yamlString = (value) => `'${value.replaceAll("'", "''")}'`, projectPath = (projectRoot, value, field, options = {}) => {
19607
+ const root = resolve43(projectRoot);
19608
+ const path = resolve43(root, value);
19609
+ const portable = relative29(root, path).replaceAll("\\", "/");
19610
+ if (portable === ".." || portable.startsWith(`..${sep8}`) || portable.startsWith("../") || portable === "") {
19611
+ throw new TypeError(`${field} must remain inside the project root.`);
19612
+ }
19613
+ if (/\r|\n/u.test(portable) || portable.startsWith("-"))
19614
+ throw new TypeError(`${field} contains an unsafe path.`);
19615
+ if (!options.allowMissing && !existsSync42(path))
19616
+ throw new TypeError(`${field} does not exist inside the project.`);
19617
+ return portable;
19618
+ }, workflowOutputPath = (projectRoot, value) => {
19619
+ const root = resolve43(projectRoot);
19620
+ const workflows = resolve43(root, ".github/workflows");
19621
+ const path = resolve43(root, value ?? ".github/workflows/absolute-mobile.yml");
19622
+ const portable = relative29(workflows, path);
19623
+ if (portable === ".." || portable.startsWith(`..${sep8}`) || extname9(path) !== ".yml" && extname9(path) !== ".yaml") {
19624
+ throw new TypeError("mobile ci github --output must be a .yml or .yaml file inside .github/workflows.");
19625
+ }
19626
+ return path;
19627
+ }, normalizeSecretEnvironment = (values = []) => {
19628
+ const names = [...new Set(values)].sort();
19629
+ for (const name of names) {
19630
+ if (!SECRET_NAME_PATTERN.test(name))
19631
+ throw new TypeError("mobile ci github --secret-env values must be uppercase environment variable names.");
19632
+ if (name.startsWith("GITHUB_") || name.startsWith("RUNNER_") || name.startsWith("ACTIONS_") || RESERVED_SECRET_NAMES.has(name)) {
19633
+ throw new TypeError(`mobile ci github --secret-env cannot replace reserved variable ${name}.`);
19634
+ }
19635
+ }
19636
+ return names;
19637
+ }, customSecretEnvironment = (names, indentation = CI_ENV_INDENTATION) => names.map((name) => `${" ".repeat(indentation)}${name}: \${{ secrets.${name} }}`).join(`
19638
+ `), commandEnvironment = (options) => ` ABSOLUTE_CONFIG_PATH: ${yamlString(options.configPath ?? "")}
19639
+ ABSOLUTE_REGISTRY_MODULE: ${yamlString(options.registryModule)}
19640
+ ABSOLUTE_SERVER_ENTRY: ${yamlString(options.serverEntry)}`, appendConfigArgument = `if [[ -n "$ABSOLUTE_CONFIG_PATH" ]]; then
19641
+ args+=(--config "$ABSOLUTE_CONFIG_PATH")
19642
+ fi`, installSteps = ` - name: Check out source
19643
+ uses: actions/checkout@v6
19644
+ - name: Install Bun
19645
+ uses: oven-sh/setup-bun@v2
19646
+ - name: Install exact dependencies
19647
+ run: bun ci`, bundleAuditSteps, releaseAuditSteps = (platform6) => ` - name: Run redacted mobile release audit
19648
+ id: mobile-release-audit
19649
+ continue-on-error: true
19650
+ shell: bash
19651
+ run: |
19652
+ mkdir -p .absolutejs/mobile-ci
19653
+ args=(bunx absolute mobile doctor release ${platform6} --json)
19654
+ ${appendConfigArgument}
19655
+ "\${args[@]}" > .absolutejs/mobile-ci/compliance.json
19656
+ - name: Upload mobile compliance report
19657
+ if: always()
19658
+ uses: actions/upload-artifact@v7
19659
+ with:
19660
+ name: absolute-mobile-compliance-\${{ github.job }}
19661
+ path: .absolutejs/mobile-ci/compliance.json
19662
+ if-no-files-found: error
19663
+ retention-days: 30
19664
+ include-hidden-files: true
19665
+ - name: Enforce mobile release audit
19666
+ if: steps.mobile-release-audit.outcome != 'success'
19667
+ run: exit 1`, platformInput = (platforms) => {
19668
+ const choices = platforms.length === 2 ? ["all", ...platforms] : platforms;
19669
+ return ` platform:
19670
+ description: Native platform to build
19671
+ required: true
19672
+ type: choice
19673
+ default: ${choices[0]}
19674
+ options:
19675
+ ${choices.map((value) => ` - ${value}`).join(`
19676
+ `)}`;
19677
+ }, publishingInputs = (platforms, includePublishing) => {
19678
+ if (!includePublishing)
19679
+ return "";
19680
+ const fields = [
19681
+ ` publish:
19682
+ description: Publish through mobile.release.ts after the signed build
19683
+ required: true
19684
+ type: boolean
19685
+ default: false`,
19686
+ ` channel:
19687
+ description: Optional AbsoluteJS immutable release channel
19688
+ required: false
19689
+ type: string`
19690
+ ];
19691
+ if (platforms.includes("android"))
19692
+ fields.push(` play_track:
19693
+ description: Optional Google Play track
19694
+ required: true
19695
+ type: choice
19696
+ default: registry-only
19697
+ options:
19698
+ - registry-only
19699
+ - internal
19700
+ - alpha
19701
+ - beta
19702
+ - production`);
19703
+ if (platforms.includes("ios")) {
19704
+ fields.push(` testflight_group:
19705
+ description: Optional internal or external TestFlight group
19706
+ required: false
19707
+ type: string`);
19708
+ fields.push(` submit_testflight_review:
19709
+ description: Explicitly submit an external TestFlight build for review
19710
+ required: true
19711
+ type: boolean
19712
+ default: false`);
19713
+ }
19714
+ return `
19715
+ ${fields.join(`
19716
+ `)}`;
19717
+ }, jobCondition = (platform6) => `github.event_name == 'workflow_dispatch' && (inputs.platform == 'all' || inputs.platform == '${platform6}')`, androidJob = (options) => {
19718
+ const custom = customSecretEnvironment(options.customSecrets);
19719
+ const publishEnvironment = options.includePublishing ? `
19720
+ ABSOLUTE_PUBLISH: \${{ inputs.publish }}
19721
+ ABSOLUTE_RELEASE_CHANNEL: \${{ inputs.channel }}
19722
+ ABSOLUTE_PLAY_TRACK: \${{ inputs.play_track }}
19723
+ ABSOLUTE_GOOGLE_CREDENTIALS_BASE64: \${{ secrets.ABSOLUTE_GOOGLE_CREDENTIALS_BASE64 }}
19724
+ GOOGLE_APPLICATION_CREDENTIALS: \${{ runner.temp }}/absolute-google-credentials.json` : "";
19725
+ const publishCommand = options.includePublishing ? `if [[ "$ABSOLUTE_PUBLISH" == "true" ]]; then
19726
+ args=(bunx absolute mobile publish android "$ABSOLUTE_SERVER_ENTRY" --registry "$ABSOLUTE_REGISTRY_MODULE")
19727
+ if [[ -n "$ABSOLUTE_RELEASE_CHANNEL" ]]; then
19728
+ args+=(--channel "$ABSOLUTE_RELEASE_CHANNEL")
19729
+ fi
19730
+ if [[ "$ABSOLUTE_PLAY_TRACK" != "registry-only" ]]; then
19731
+ args+=(--play-track "$ABSOLUTE_PLAY_TRACK")
19732
+ fi
19733
+ else
19734
+ args=(bunx absolute mobile build android "$ABSOLUTE_SERVER_ENTRY")
19735
+ fi` : `args=(bunx absolute mobile build android "$ABSOLUTE_SERVER_ENTRY")`;
19736
+ const googleSetup = options.includePublishing ? `
19737
+ if [[ "$ABSOLUTE_PUBLISH" == "true" && "$ABSOLUTE_PLAY_TRACK" != "registry-only" ]]; then
19738
+ if [[ -z "$ABSOLUTE_GOOGLE_CREDENTIALS_BASE64" ]]; then
19739
+ echo "ABSOLUTE_GOOGLE_CREDENTIALS_BASE64 is required for Google Play publication." >&2
19740
+ exit 1
19741
+ fi
19742
+ printf '%s' "$ABSOLUTE_GOOGLE_CREDENTIALS_BASE64" | base64 --decode > "$GOOGLE_APPLICATION_CREDENTIALS"
19743
+ chmod 600 "$GOOGLE_APPLICATION_CREDENTIALS"
19744
+ fi` : "";
19745
+ return `
19746
+ android:
19747
+ name: Signed Android release
19748
+ needs: validate
19749
+ if: \${{ ${jobCondition("android")} }}
19750
+ runs-on: ubuntu-latest
19751
+ environment: absolute-mobile-release
19752
+ permissions:
19753
+ contents: read
19754
+ id-token: write
19755
+ attestations: write
19756
+ env:
19757
+ ABSOLUTE_ANDROID_KEYSTORE_BASE64: \${{ secrets.ABSOLUTE_ANDROID_KEYSTORE_BASE64 }}
19758
+ ABSOLUTE_ANDROID_KEYSTORE_PASSWORD: \${{ secrets.ABSOLUTE_ANDROID_KEYSTORE_PASSWORD }}
19759
+ ABSOLUTE_ANDROID_KEY_ALIAS: \${{ secrets.ABSOLUTE_ANDROID_KEY_ALIAS }}
19760
+ ABSOLUTE_ANDROID_KEY_PASSWORD: \${{ secrets.ABSOLUTE_ANDROID_KEY_PASSWORD }}
19761
+ ABSOLUTE_ANDROID_KEYSTORE_PATH: \${{ runner.temp }}/absolute-release.jks${publishEnvironment}${custom ? `
19762
+ ${custom}` : ""}
19763
+ ${commandEnvironment({ configPath: undefined, registryModule: "", serverEntry: "" })}
19764
+ steps:
19765
+ ${installSteps}
19766
+ - name: Provision Android signing
19767
+ shell: bash
19768
+ run: |
19769
+ required=(
19770
+ ABSOLUTE_ANDROID_KEYSTORE_BASE64
19771
+ ABSOLUTE_ANDROID_KEYSTORE_PASSWORD
19772
+ ABSOLUTE_ANDROID_KEY_ALIAS
19773
+ ABSOLUTE_ANDROID_KEY_PASSWORD
19774
+ )
19775
+ for name in "\${required[@]}"; do
19776
+ if [[ -z "\${!name}" ]]; then
19777
+ echo "$name is required in the absolute-mobile-release environment." >&2
19778
+ exit 1
19779
+ fi
19780
+ done
19781
+ printf '%s' "$ABSOLUTE_ANDROID_KEYSTORE_BASE64" | base64 --decode > "\${{ runner.temp }}/absolute-release.jks"
19782
+ chmod 600 "\${{ runner.temp }}/absolute-release.jks"${googleSetup}
19783
+ - name: Build or publish Android
19784
+ shell: bash
19785
+ run: |
19786
+ ${publishCommand}
19787
+ ${appendConfigArgument}
19788
+ "\${args[@]}"
19789
+ ${releaseAuditSteps("android")}
19790
+ - name: Attest Android App Bundle
19791
+ if: inputs.attest
19792
+ uses: actions/attest@v4
19793
+ with:
19794
+ subject-path: .absolutejs/mobile/releases/android/**/app-release.aab
19795
+ - name: Upload Android release
19796
+ uses: actions/upload-artifact@v7
19797
+ with:
19798
+ name: absolute-mobile-android
19799
+ path: .absolutejs/mobile/releases/android/
19800
+ if-no-files-found: error
19801
+ retention-days: 14
19802
+ include-hidden-files: true
19803
+ - name: Remove Android credentials
19804
+ if: always()
19805
+ shell: bash
19806
+ run: |
19807
+ rm -f "\${{ runner.temp }}/absolute-release.jks"
19808
+ rm -f "\${{ runner.temp }}/absolute-google-credentials.json"`;
19809
+ }, iosJob = (options) => {
19810
+ const custom = customSecretEnvironment(options.customSecrets);
19811
+ const publishEnvironment = options.includePublishing ? `
19812
+ ABSOLUTE_PUBLISH: \${{ inputs.publish }}
19813
+ ABSOLUTE_RELEASE_CHANNEL: \${{ inputs.channel }}
19814
+ ABSOLUTE_TESTFLIGHT_GROUP: \${{ inputs.testflight_group }}
19815
+ ABSOLUTE_TESTFLIGHT_SUBMIT_REVIEW: \${{ inputs.submit_testflight_review }}
19816
+ APP_STORE_CONNECT_ISSUER_ID: \${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
19817
+ APP_STORE_CONNECT_KEY_ID: \${{ secrets.APP_STORE_CONNECT_KEY_ID }}
19818
+ APP_STORE_CONNECT_PRIVATE_KEY_BASE64: \${{ secrets.APP_STORE_CONNECT_PRIVATE_KEY_BASE64 }}
19819
+ APP_STORE_CONNECT_PRIVATE_KEY_PATH: \${{ runner.temp }}/AuthKey_AbsoluteJS.p8` : "";
19820
+ const publishCommand = options.includePublishing ? `if [[ "$ABSOLUTE_PUBLISH" == "true" ]]; then
19821
+ args=(bunx absolute mobile publish ios "$ABSOLUTE_SERVER_ENTRY" --registry "$ABSOLUTE_REGISTRY_MODULE")
19822
+ if [[ -n "$ABSOLUTE_RELEASE_CHANNEL" ]]; then
19823
+ args+=(--channel "$ABSOLUTE_RELEASE_CHANNEL")
19824
+ fi
19825
+ if [[ -n "$ABSOLUTE_TESTFLIGHT_GROUP" ]]; then
19826
+ args+=(--testflight-group "$ABSOLUTE_TESTFLIGHT_GROUP")
19827
+ fi
19828
+ if [[ "$ABSOLUTE_TESTFLIGHT_SUBMIT_REVIEW" == "true" ]]; then
19829
+ args+=(--testflight-submit-review)
19830
+ fi
19831
+ else
19832
+ args=(bunx absolute mobile build ios "$ABSOLUTE_SERVER_ENTRY")
19833
+ fi` : `args=(bunx absolute mobile build ios "$ABSOLUTE_SERVER_ENTRY")`;
19834
+ const appStoreSetup = options.includePublishing ? `
19835
+ if [[ "$ABSOLUTE_PUBLISH" == "true" && -n "$ABSOLUTE_TESTFLIGHT_GROUP" ]]; then
19836
+ required+=(APP_STORE_CONNECT_ISSUER_ID APP_STORE_CONNECT_KEY_ID APP_STORE_CONNECT_PRIVATE_KEY_BASE64)
19837
+ fi` : "";
19838
+ const appStoreDecode = options.includePublishing ? `
19839
+ if [[ "$ABSOLUTE_PUBLISH" == "true" && -n "$ABSOLUTE_TESTFLIGHT_GROUP" ]]; then
19840
+ printf '%s' "$APP_STORE_CONNECT_PRIVATE_KEY_BASE64" | base64 --decode > "$APP_STORE_CONNECT_PRIVATE_KEY_PATH"
19841
+ chmod 600 "$APP_STORE_CONNECT_PRIVATE_KEY_PATH"
19842
+ fi` : "";
19843
+ return `
19844
+ ios:
19845
+ name: Signed iOS release
19846
+ needs: validate
19847
+ if: \${{ ${jobCondition("ios")} }}
19848
+ runs-on: macos-latest
19849
+ environment: absolute-mobile-release
19850
+ permissions:
19851
+ contents: read
19852
+ id-token: write
19853
+ attestations: write
19854
+ env:
19855
+ ABSOLUTE_IOS_CERTIFICATE_BASE64: \${{ secrets.ABSOLUTE_IOS_CERTIFICATE_BASE64 }}
19856
+ ABSOLUTE_IOS_CERTIFICATE_PASSWORD: \${{ secrets.ABSOLUTE_IOS_CERTIFICATE_PASSWORD }}
19857
+ ABSOLUTE_IOS_PROVISIONING_PROFILE_BASE64: \${{ secrets.ABSOLUTE_IOS_PROVISIONING_PROFILE_BASE64 }}
19858
+ ABSOLUTE_IOS_KEYCHAIN_PASSWORD: \${{ secrets.ABSOLUTE_IOS_KEYCHAIN_PASSWORD }}${publishEnvironment}${custom ? `
19859
+ ${custom}` : ""}
19860
+ ABSOLUTE_IOS_DEVELOPMENT_TEAM: \${{ secrets.ABSOLUTE_IOS_DEVELOPMENT_TEAM }}
19861
+ ${commandEnvironment({ configPath: undefined, registryModule: "", serverEntry: "" })}
19862
+ steps:
19863
+ ${installSteps}
19864
+ - name: Provision iOS signing
19865
+ shell: bash
19866
+ run: |
19867
+ required=(
19868
+ ABSOLUTE_IOS_CERTIFICATE_BASE64
19869
+ ABSOLUTE_IOS_CERTIFICATE_PASSWORD
19870
+ ABSOLUTE_IOS_PROVISIONING_PROFILE_BASE64
19871
+ ABSOLUTE_IOS_KEYCHAIN_PASSWORD
19872
+ ABSOLUTE_IOS_DEVELOPMENT_TEAM
19873
+ )${appStoreSetup}
19874
+ for name in "\${required[@]}"; do
19875
+ if [[ -z "\${!name}" ]]; then
19876
+ echo "$name is required in the absolute-mobile-release environment." >&2
19877
+ exit 1
19878
+ fi
19879
+ done
19880
+ CERTIFICATE_PATH="\${{ runner.temp }}/absolute-signing.p12"
19881
+ PROFILE_PATH="\${{ runner.temp }}/absolute.mobileprovision"
19882
+ KEYCHAIN_PATH="\${{ runner.temp }}/absolute-signing.keychain-db"
19883
+ PROFILE_DESTINATION="$HOME/Library/MobileDevice/Provisioning Profiles/absolute.mobileprovision"
19884
+ printf '%s' "$ABSOLUTE_IOS_CERTIFICATE_BASE64" | base64 --decode > "$CERTIFICATE_PATH"
19885
+ printf '%s' "$ABSOLUTE_IOS_PROVISIONING_PROFILE_BASE64" | base64 --decode > "$PROFILE_PATH"
19886
+ security create-keychain -p "$ABSOLUTE_IOS_KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
19887
+ security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
19888
+ security unlock-keychain -p "$ABSOLUTE_IOS_KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
19889
+ security import "$CERTIFICATE_PATH" -P "$ABSOLUTE_IOS_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH"
19890
+ security set-key-partition-list -S apple-tool:,apple: -k "$ABSOLUTE_IOS_KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
19891
+ security list-keychain -d user -s "$KEYCHAIN_PATH"
19892
+ mkdir -p "$(dirname "$PROFILE_DESTINATION")"
19893
+ cp "$PROFILE_PATH" "$PROFILE_DESTINATION"${appStoreDecode}
19894
+ - name: Build or publish iOS
19895
+ shell: bash
19896
+ run: |
19897
+ ${publishCommand}
19898
+ ${appendConfigArgument}
19899
+ "\${args[@]}"
19900
+ ${releaseAuditSteps("ios")}
19901
+ - name: Attest iOS IPA
19902
+ if: inputs.attest
19903
+ uses: actions/attest@v4
19904
+ with:
19905
+ subject-path: .absolutejs/mobile/releases/ios/**/App.ipa
19906
+ - name: Upload iOS release
19907
+ uses: actions/upload-artifact@v7
19908
+ with:
19909
+ name: absolute-mobile-ios
19910
+ path: .absolutejs/mobile/releases/ios/
19911
+ if-no-files-found: error
19912
+ retention-days: 14
19913
+ include-hidden-files: true
19914
+ - name: Remove iOS credentials
19915
+ if: always()
19916
+ shell: bash
19917
+ run: |
19918
+ security delete-keychain "\${{ runner.temp }}/absolute-signing.keychain-db" 2>/dev/null || true
19919
+ rm -f "$HOME/Library/MobileDevice/Provisioning Profiles/absolute.mobileprovision"
19920
+ rm -f "\${{ runner.temp }}/absolute-signing.p12"
19921
+ rm -f "\${{ runner.temp }}/absolute.mobileprovision"
19922
+ rm -f "\${{ runner.temp }}/AuthKey_AbsoluteJS.p8"`;
19923
+ }, requiredSecrets = (platforms, includePublishing, custom) => [
19924
+ ...platforms.includes("android") ? [
19925
+ "ABSOLUTE_ANDROID_KEYSTORE_BASE64",
19926
+ "ABSOLUTE_ANDROID_KEYSTORE_PASSWORD",
19927
+ "ABSOLUTE_ANDROID_KEY_ALIAS",
19928
+ "ABSOLUTE_ANDROID_KEY_PASSWORD",
19929
+ ...includePublishing ? ["ABSOLUTE_GOOGLE_CREDENTIALS_BASE64"] : []
19930
+ ] : [],
19931
+ ...platforms.includes("ios") ? [
19932
+ "ABSOLUTE_IOS_CERTIFICATE_BASE64",
19933
+ "ABSOLUTE_IOS_CERTIFICATE_PASSWORD",
19934
+ "ABSOLUTE_IOS_PROVISIONING_PROFILE_BASE64",
19935
+ "ABSOLUTE_IOS_KEYCHAIN_PASSWORD",
19936
+ "ABSOLUTE_IOS_DEVELOPMENT_TEAM",
19937
+ ...includePublishing ? [
19938
+ "APP_STORE_CONNECT_ISSUER_ID",
19939
+ "APP_STORE_CONNECT_KEY_ID",
19940
+ "APP_STORE_CONNECT_PRIVATE_KEY_BASE64"
19941
+ ] : []
19942
+ ] : [],
19943
+ ...custom
19944
+ ], createAbsoluteMobileGithubWorkflow = (options) => {
19945
+ const platforms = [
19946
+ ...options.config.platforms
19947
+ ].sort();
19948
+ const includePublishing = options.includePublishing === true;
19949
+ const customSecrets = normalizeSecretEnvironment(options.secretEnvironment);
19950
+ const serverEntry = projectPath(options.projectRoot, options.serverEntry ?? "server.ts", "mobile ci github server entry");
19951
+ const configPath2 = options.configPath ? projectPath(options.projectRoot, options.configPath, "mobile ci github --config") : undefined;
19952
+ const registryModule = projectPath(options.projectRoot, options.registryModule ?? "mobile.release.ts", "mobile ci github --registry", { allowMissing: !includePublishing });
19953
+ const environment = commandEnvironment({
19954
+ configPath: configPath2,
19955
+ registryModule,
19956
+ serverEntry
19957
+ });
19958
+ let workflow = `# Generated by AbsoluteJS. Regenerate with: absolute mobile ci github${includePublishing ? " --publish" : ""}
19959
+ name: AbsoluteJS Mobile
19960
+
19961
+ on:
19962
+ pull_request:
19963
+ workflow_dispatch:
19964
+ inputs:
19965
+ ${platformInput(platforms)}
19966
+ attest:
19967
+ description: Generate GitHub artifact provenance attestations
19968
+ required: true
19969
+ type: boolean
19970
+ default: false${publishingInputs(platforms, includePublishing)}
19971
+
19972
+ concurrency:
19973
+ group: absolute-mobile-\${{ github.repository }}
19974
+ cancel-in-progress: false
19975
+
19976
+ jobs:
19977
+ validate:
19978
+ name: Validate mobile release inputs
19979
+ runs-on: ubuntu-latest
19980
+ permissions:
19981
+ contents: read
19982
+ env:
19983
+ ${environment}
19984
+ steps:
19985
+ ${installSteps}
19986
+ ${bundleAuditSteps}${platforms.includes("android") ? androidJob({ customSecrets, includePublishing }) : ""}${platforms.includes("ios") ? iosJob({ customSecrets, includePublishing }) : ""}
19987
+ `;
19988
+ const replacements = new Map([
19989
+ [
19990
+ "ABSOLUTE_CONFIG_PATH: ''",
19991
+ `ABSOLUTE_CONFIG_PATH: ${yamlString(configPath2 ?? "")}`
19992
+ ],
19993
+ [
19994
+ "ABSOLUTE_REGISTRY_MODULE: ''",
19995
+ `ABSOLUTE_REGISTRY_MODULE: ${yamlString(registryModule)}`
19996
+ ],
19997
+ [
19998
+ "ABSOLUTE_SERVER_ENTRY: ''",
19999
+ `ABSOLUTE_SERVER_ENTRY: ${yamlString(serverEntry)}`
20000
+ ]
20001
+ ]);
20002
+ for (const [placeholder, replacement] of replacements)
20003
+ workflow = workflow.replaceAll(placeholder, replacement);
20004
+ return {
20005
+ requiredSecrets: requiredSecrets(platforms, includePublishing, customSecrets),
20006
+ workflow
20007
+ };
20008
+ }, writeAbsoluteMobileGithubWorkflow = async (options) => {
20009
+ const path = workflowOutputPath(options.projectRoot, options.outputPath);
20010
+ const generated = createAbsoluteMobileGithubWorkflow(options);
20011
+ const previous = await exists3(path) ? await readFile22(path, "utf8") : undefined;
20012
+ if (previous !== undefined && previous !== generated.workflow && !options.force)
20013
+ throw new TypeError(`${relative29(options.projectRoot, path)} already exists and differs. Rerun with --force to replace the generated workflow.`);
20014
+ if (previous !== generated.workflow) {
20015
+ await mkdir14(dirname31(path), { recursive: true });
20016
+ await writeFile16(path, generated.workflow);
20017
+ }
20018
+ return {
20019
+ changed: previous !== generated.workflow,
20020
+ format: ABSOLUTE_MOBILE_CI_WORKFLOW_FORMAT,
20021
+ path,
20022
+ platforms: [...options.config.platforms].sort(),
20023
+ publishing: options.includePublishing === true,
20024
+ requiredSecrets: generated.requiredSecrets
20025
+ };
20026
+ };
20027
+ var init_ciWorkflow = __esm(() => {
20028
+ SECRET_NAME_PATTERN = /^[A-Z][A-Z0-9_]*$/u;
20029
+ RESERVED_SECRET_NAMES = new Set([
20030
+ "ABSOLUTE_ANDROID_KEYSTORE_BASE64",
20031
+ "ABSOLUTE_ANDROID_KEYSTORE_PASSWORD",
20032
+ "ABSOLUTE_ANDROID_KEY_ALIAS",
20033
+ "ABSOLUTE_ANDROID_KEY_PASSWORD",
20034
+ "ABSOLUTE_GOOGLE_CREDENTIALS_BASE64",
20035
+ "ABSOLUTE_IOS_CERTIFICATE_BASE64",
20036
+ "ABSOLUTE_IOS_CERTIFICATE_PASSWORD",
20037
+ "ABSOLUTE_IOS_PROVISIONING_PROFILE_BASE64",
20038
+ "ABSOLUTE_IOS_KEYCHAIN_PASSWORD",
20039
+ "ABSOLUTE_IOS_DEVELOPMENT_TEAM",
20040
+ "APP_STORE_CONNECT_ISSUER_ID",
20041
+ "APP_STORE_CONNECT_KEY_ID",
20042
+ "APP_STORE_CONNECT_PRIVATE_KEY_BASE64"
19250
20043
  ]);
20044
+ bundleAuditSteps = ` - name: Prepare production mobile bundle
20045
+ shell: bash
20046
+ run: |
20047
+ args=(bunx absolute prepare "$ABSOLUTE_SERVER_ENTRY" --outdir .absolutejs/mobile-ci/server)
20048
+ ${appendConfigArgument}
20049
+ "\${args[@]}"
20050
+ - name: Run cryptographic mobile bundle audit
20051
+ id: mobile-bundle-audit
20052
+ continue-on-error: true
20053
+ shell: bash
20054
+ run: |
20055
+ mkdir -p .absolutejs/mobile-ci
20056
+ args=(bunx absolute mobile inspect --json --require-bundle)
20057
+ ${appendConfigArgument}
20058
+ "\${args[@]}" > .absolutejs/mobile-ci/inspection.json
20059
+ - name: Upload mobile inspection report
20060
+ if: always()
20061
+ uses: actions/upload-artifact@v7
20062
+ with:
20063
+ name: absolute-mobile-inspection
20064
+ path: .absolutejs/mobile-ci/inspection.json
20065
+ if-no-files-found: error
20066
+ retention-days: 30
20067
+ include-hidden-files: true
20068
+ - name: Enforce mobile bundle audit
20069
+ if: steps.mobile-bundle-audit.outcome != 'success'
20070
+ run: exit 1`;
19251
20071
  });
19252
20072
 
19253
20073
  // src/cli/scripts/mobile.ts
@@ -19255,11 +20075,11 @@ var exports_mobile = {};
19255
20075
  __export(exports_mobile, {
19256
20076
  runMobile: () => runMobile
19257
20077
  });
19258
- import { access as access12, mkdir as mkdir14, readFile as readFile21, writeFile as writeFile16 } from "fs/promises";
19259
- import { join as join53, resolve as resolve42 } from "path";
20078
+ import { access as access14, mkdir as mkdir15, readFile as readFile23, writeFile as writeFile17 } from "fs/promises";
20079
+ import { join as join54, relative as relative30, resolve as resolve44 } from "path";
19260
20080
  import { createInterface } from "readline/promises";
19261
- 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) => {
19262
- const manifest = JSON.parse(await readFile21(join53(projectRoot, "package.json"), "utf8"));
20081
+ 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) => {
20082
+ const manifest = JSON.parse(await readFile23(join54(projectRoot, "package.json"), "utf8"));
19263
20083
  if (!isRecord15(manifest))
19264
20084
  throw new TypeError("Application package.json must contain an object.");
19265
20085
  const names = new Set;
@@ -19272,7 +20092,7 @@ var NOT_FOUND3 = -1, isRecord15 = (value) => typeof value === "object" && value
19272
20092
  return names;
19273
20093
  }, resolvedPackageVersion = async (projectRoot, packageName) => {
19274
20094
  try {
19275
- const manifest = JSON.parse(await readFile21(join53(projectRoot, "node_modules", packageName, "package.json"), "utf8"));
20095
+ const manifest = JSON.parse(await readFile23(join54(projectRoot, "node_modules", packageName, "package.json"), "utf8"));
19276
20096
  return isRecord15(manifest) && typeof manifest.version === "string" ? manifest.version : undefined;
19277
20097
  } catch {
19278
20098
  return;
@@ -19303,7 +20123,7 @@ var NOT_FOUND3 = -1, isRecord15 = (value) => typeof value === "object" && value
19303
20123
  await installApprovedPackages(projectRoot, args, `AbsoluteJS detected native device capabilities (${capabilityPlan.capabilities.join(", ")}). Install only their required Capacitor plugins now?`, capabilityPackages);
19304
20124
  }, valueAfter = (args, flag) => {
19305
20125
  const index = args.indexOf(flag);
19306
- return index === NOT_FOUND3 ? undefined : args[index + 1];
20126
+ return index === NOT_FOUND4 ? undefined : args[index + 1];
19307
20127
  }, valuesAfter = (args, flag) => args.flatMap((value, index) => {
19308
20128
  const next = args[index + 1];
19309
20129
  return value === flag && next !== undefined ? [next] : [];
@@ -19313,9 +20133,9 @@ var NOT_FOUND3 = -1, isRecord15 = (value) => typeof value === "object" && value
19313
20133
  }
19314
20134
  return value;
19315
20135
  }, capacitorExecutable = async (projectRoot) => {
19316
- const executable = join53(projectRoot, "node_modules", ".bin", "cap");
20136
+ const executable = join54(projectRoot, "node_modules", ".bin", "cap");
19317
20137
  try {
19318
- await access12(executable);
20138
+ await access14(executable);
19319
20139
  return executable;
19320
20140
  } catch {
19321
20141
  throw new TypeError(`Capacitor is not installed in this app. Run: bun add ${CAPACITOR_PACKAGES.join(" ")}`);
@@ -19342,11 +20162,19 @@ var NOT_FOUND3 = -1, isRecord15 = (value) => typeof value === "object" && value
19342
20162
  const report = await inspectAbsoluteMobileProject(mobile, projectRoot, {
19343
20163
  absolutejsVersion: await absolutejsVersionForReport()
19344
20164
  });
20165
+ const requireBundle = () => {
20166
+ if (!args.includes("--require-bundle"))
20167
+ return;
20168
+ if (report.bundle.status !== "valid" || report.capabilities.issue || report.capabilities.embeddedMatchesCurrent !== true)
20169
+ throw new TypeError("Mobile bundle validation failed. Regenerate the production bundle and resolve capability drift.");
20170
+ };
19345
20171
  if (args.includes("--json")) {
19346
20172
  console.log(JSON.stringify(report, null, 2));
20173
+ requireBundle();
19347
20174
  return report;
19348
20175
  }
19349
20176
  console.log(renderAbsoluteMobileProjectInspection(report).trimEnd());
20177
+ requireBundle();
19350
20178
  return report;
19351
20179
  }, remoteProfilePath = () => process.env.ABSOLUTE_REMOTE_MAC_PROFILE_PATH || undefined, pairRemoteMac = async (args) => {
19352
20180
  if (args[0] !== "mac" || !args[1] || !args[2])
@@ -19412,7 +20240,7 @@ var NOT_FOUND3 = -1, isRecord15 = (value) => typeof value === "object" && value
19412
20240
  await applyAbsoluteNativeBackgroundSync(projectRoot, mobile, platforms);
19413
20241
  }, associations = async (args) => {
19414
20242
  const { mobile, projectRoot } = await loadMobile(valueAfter(args, "--config"));
19415
- const outputDirectory = resolve42(projectRoot, valueAfter(args, "--outdir") ?? ".absolutejs/mobile/associations");
20243
+ const outputDirectory = resolve44(projectRoot, valueAfter(args, "--outdir") ?? ".absolutejs/mobile/associations");
19416
20244
  if (args.includes("--verify")) {
19417
20245
  const result2 = await verifyAbsoluteMobileAssociationFiles(mobile);
19418
20246
  console.log(`Verified ${result2.results.length} hosted association files`);
@@ -19423,6 +20251,59 @@ var NOT_FOUND3 = -1, isRecord15 = (value) => typeof value === "object" && value
19423
20251
  }
19424
20252
  const result = await materializeAbsoluteMobileAssociationFiles(mobile, outputDirectory);
19425
20253
  console.log(`Generated ${result.written.length} association files in ${result.root}`);
20254
+ }, mobileCiServerEntry = (args) => {
20255
+ const valueFlags = new Set([
20256
+ "--config",
20257
+ "--output",
20258
+ "--registry",
20259
+ "--secret-env"
20260
+ ]);
20261
+ const skipped = new Set;
20262
+ args.forEach((value, index) => {
20263
+ if (!valueFlags.has(value))
20264
+ return;
20265
+ skipped.add(index);
20266
+ skipped.add(index + 1);
20267
+ });
20268
+ return args.find((value, index) => !skipped.has(index) && value !== "github" && !value.startsWith("-")) ?? DEFAULT_SERVER_ENTRY;
20269
+ }, generateGithubCi = async (args) => {
20270
+ if (args[0] !== "github")
20271
+ throw new TypeError("Usage: absolute mobile ci github [server-entry] [--publish] [--registry module] [--secret-env NAME] [--output path] [--force] [--json] [--config path]");
20272
+ const configPath2 = valueAfter(args, "--config");
20273
+ const { mobile, projectRoot } = await loadMobile(configPath2);
20274
+ const result = await writeAbsoluteMobileGithubWorkflow({
20275
+ config: mobile,
20276
+ configPath: configPath2,
20277
+ force: args.includes("--force"),
20278
+ includePublishing: args.includes("--publish"),
20279
+ outputPath: valueAfter(args, "--output"),
20280
+ projectRoot,
20281
+ registryModule: valueAfter(args, "--registry"),
20282
+ secretEnvironment: valuesAfter(args, "--secret-env"),
20283
+ serverEntry: mobileCiServerEntry(args)
20284
+ });
20285
+ sendTelemetryEvent("mobile:ci-generated", {
20286
+ platformCount: result.platforms.length,
20287
+ provider: "github-actions",
20288
+ publishing: result.publishing
20289
+ });
20290
+ const publicResult = {
20291
+ changed: result.changed,
20292
+ format: result.format,
20293
+ path: relative30(projectRoot, result.path).replaceAll("\\", "/"),
20294
+ platforms: result.platforms,
20295
+ publishing: result.publishing,
20296
+ requiredSecrets: result.requiredSecrets
20297
+ };
20298
+ if (args.includes("--json")) {
20299
+ console.log(JSON.stringify(publicResult, null, 2));
20300
+ return publicResult;
20301
+ }
20302
+ console.log(`${result.changed ? "Generated" : "Verified"} ${publicResult.path} for ${result.platforms.join(" and ")}.`);
20303
+ console.log("Create a protected GitHub environment named absolute-mobile-release, then add these secrets:");
20304
+ result.requiredSecrets.forEach((name) => console.log(` ${name}`));
20305
+ console.log("Pull requests run the secret-free release audit. Signed builds and optional publishing run only through manual workflow dispatch.");
20306
+ return publicResult;
19426
20307
  }, doctorMark = (status2) => {
19427
20308
  if (status2 === "pass")
19428
20309
  return "\x1B[32m\u2713\x1B[0m";
@@ -19455,9 +20336,11 @@ var NOT_FOUND3 = -1, isRecord15 = (value) => typeof value === "object" && value
19455
20336
  }
19456
20337
  }, runReleaseDoctor = async (args) => {
19457
20338
  const { mobile, projectRoot } = await loadMobile(valueAfter(args, "--config"));
19458
- const result = await inspectAbsoluteMobileRelease(mobile, projectRoot);
20339
+ const platform6 = args.find((value) => value === "android" || value === "ios");
20340
+ const effectiveMobile = platform6 ? { ...mobile, platforms: [platform6] } : mobile;
20341
+ const result = await inspectAbsoluteMobileRelease(effectiveMobile, projectRoot);
19459
20342
  if (args.includes("--json")) {
19460
- console.log(JSON.stringify(result, null, 2));
20343
+ console.log(JSON.stringify(createAbsoluteMobileComplianceReport(effectiveMobile, result), null, 2));
19461
20344
  } else {
19462
20345
  result.checks.forEach((check2) => {
19463
20346
  console.log(`${doctorMark(check2.status)} ${check2.detail}${check2.path ? ` (${check2.path})` : ""}`);
@@ -19465,8 +20348,8 @@ var NOT_FOUND3 = -1, isRecord15 = (value) => typeof value === "object" && value
19465
20348
  console.log(` ${check2.remediation}`);
19466
20349
  });
19467
20350
  console.log(result.ready ? `
19468
- Mobile release transport checks passed.` : `
19469
- Mobile release transport checks failed.`);
20351
+ Mobile release security and compliance checks passed.` : `
20352
+ Mobile release security and compliance checks failed.`);
19470
20353
  }
19471
20354
  if (!result.ready) {
19472
20355
  throw new TypeError("Mobile release validation failed. Resolve every failed check before signing or publishing the app.");
@@ -19613,6 +20496,27 @@ Mobile release transport checks failed.`);
19613
20496
  status: check2.status
19614
20497
  })));
19615
20498
  throw new TypeError("Android release validation failed before Gradle signing.");
20499
+ }, androidCiSigning = () => {
20500
+ const keyAlias = process.env.ABSOLUTE_ANDROID_KEY_ALIAS;
20501
+ const keyPassword = process.env.ABSOLUTE_ANDROID_KEY_PASSWORD;
20502
+ const keystorePath = process.env.ABSOLUTE_ANDROID_KEYSTORE_PATH;
20503
+ const storePassword = process.env.ABSOLUTE_ANDROID_KEYSTORE_PASSWORD;
20504
+ const supplied = [
20505
+ keyAlias,
20506
+ keyPassword,
20507
+ keystorePath,
20508
+ storePassword
20509
+ ].filter((value) => value !== undefined && value !== "").length;
20510
+ if (supplied === 0)
20511
+ return;
20512
+ if (!keyAlias || !keyPassword || !keystorePath || !storePassword)
20513
+ throw new TypeError("AbsoluteJS CI signing requires ABSOLUTE_ANDROID_KEYSTORE_PATH, ABSOLUTE_ANDROID_KEYSTORE_PASSWORD, ABSOLUTE_ANDROID_KEY_ALIAS, and ABSOLUTE_ANDROID_KEY_PASSWORD together.");
20514
+ return {
20515
+ keyAlias,
20516
+ keyPasswordEnvironment: "ABSOLUTE_ANDROID_KEY_PASSWORD",
20517
+ keystorePath,
20518
+ storePasswordEnvironment: "ABSOLUTE_ANDROID_KEYSTORE_PASSWORD"
20519
+ };
19616
20520
  }, buildAndroid = async (args, prepareVersionCode) => {
19617
20521
  const configPath2 = valueAfter(args, "--config");
19618
20522
  const { mobile, projectRoot } = await loadMobile(configPath2);
@@ -19639,13 +20543,14 @@ Mobile release transport checks failed.`);
19639
20543
  config: mobile,
19640
20544
  outputDirectory: valueAfter(args, "--outdir"),
19641
20545
  projectRoot,
20546
+ signing: androidCiSigning(),
19642
20547
  ...prepareVersionCode === undefined ? {} : { prepareVersionCode }
19643
20548
  });
19644
20549
  success = true;
19645
20550
  const durationMs = Math.round(performance.now() - startedAt);
19646
20551
  console.log(`Built ${release.metadata.signed ? "signed" : "unsigned"} Android App Bundle in ${getDurationString(durationMs)}.`);
19647
20552
  console.log(`Artifact: ${release.artifactPath}`);
19648
- console.log(`Metadata: ${join53(release.releaseRoot, "release.json")}`);
20553
+ console.log(`Metadata: ${join54(release.releaseRoot, "release.json")}`);
19649
20554
  return release;
19650
20555
  } finally {
19651
20556
  sendTelemetryEvent("mobile:android-release-build", {
@@ -19740,6 +20645,7 @@ Mobile release transport checks failed.`);
19740
20645
  const release = await buildAbsoluteIosRelease({
19741
20646
  allowUnsigned: args.includes("--unsigned"),
19742
20647
  config: mobile,
20648
+ developmentTeam: process.env.ABSOLUTE_IOS_DEVELOPMENT_TEAM,
19743
20649
  outputDirectory: valueAfter(args, "--outdir"),
19744
20650
  ...prepareBuildNumber === undefined ? {} : { prepareBuildNumber },
19745
20651
  projectRoot
@@ -19748,7 +20654,7 @@ Mobile release transport checks failed.`);
19748
20654
  const durationMs = Math.round(performance.now() - startedAt);
19749
20655
  console.log(`Built ${release.metadata.signed ? "signed" : "unsigned"} iOS IPA ${release.metadata.marketingVersion}${release.metadata.buildNumber ? ` (${release.metadata.buildNumber})` : ""} in ${getDurationString(durationMs)}.`);
19750
20656
  console.log(`Artifact: ${release.artifactPath}`);
19751
- console.log(`Metadata: ${join53(release.releaseRoot, "release.json")}`);
20657
+ console.log(`Metadata: ${join54(release.releaseRoot, "release.json")}`);
19752
20658
  return release;
19753
20659
  } finally {
19754
20660
  sendTelemetryEvent("mobile:ios-release-build", {
@@ -19855,7 +20761,7 @@ Mobile release transport checks failed.`);
19855
20761
  checks.push({
19856
20762
  id: "sync.storage-schema",
19857
20763
  label: `Offline schema ${schema.components.map((component2) => `${component2.id}@${component2.version}`).join(", ")}`,
19858
- path: join53(projectRoot, "package.json"),
20764
+ path: join54(projectRoot, "package.json"),
19859
20765
  platform: "host",
19860
20766
  status: "pass"
19861
20767
  });
@@ -19863,7 +20769,7 @@ Mobile release transport checks failed.`);
19863
20769
  checks.push({
19864
20770
  id: "sync.storage-schema",
19865
20771
  label: "Offline schema metadata is invalid",
19866
- path: join53(projectRoot, "package.json"),
20772
+ path: join54(projectRoot, "package.json"),
19867
20773
  platform: "host",
19868
20774
  remediation: error instanceof Error ? error.message : String(error),
19869
20775
  status: "fail"
@@ -19948,7 +20854,7 @@ Emulator setup verification:`);
19948
20854
  }
19949
20855
  return { https: args.includes("--https"), port };
19950
20856
  }
19951
- const instances = listLiveInstances().filter((instance2) => resolve42(instance2.cwd) === resolve42(projectRoot) && instance2.source === "dev" && instance2.port !== null);
20857
+ const instances = listLiveInstances().filter((instance2) => resolve44(instance2.cwd) === resolve44(projectRoot) && instance2.source === "dev" && instance2.port !== null);
19952
20858
  if (instances.length !== 1) {
19953
20859
  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>.");
19954
20860
  }
@@ -19991,8 +20897,8 @@ Emulator setup verification:`);
19991
20897
  }
19992
20898
  return selected;
19993
20899
  }, safeArtifactRoot = (projectRoot, value) => {
19994
- const root = resolve42(projectRoot, value ?? ".absolutejs/mobile/test-artifacts");
19995
- if (root !== projectRoot && !root.startsWith(`${resolve42(projectRoot)}/`)) {
20900
+ const root = resolve44(projectRoot, value ?? ".absolutejs/mobile/test-artifacts");
20901
+ if (root !== projectRoot && !root.startsWith(`${resolve44(projectRoot)}/`)) {
19996
20902
  throw new TypeError("mobile test --artifacts must remain inside the project.");
19997
20903
  }
19998
20904
  return root;
@@ -20016,12 +20922,12 @@ Emulator setup verification:`);
20016
20922
  timeoutMs
20017
20923
  });
20018
20924
  }, writeAndroidFailureArtifacts = async (options) => {
20019
- await mkdir14(options.artifactRoot, { recursive: true });
20020
- const screenshot = options.session ? await options.session.screenshot(join53(options.artifactRoot, "android-failure.png")).catch(() => {
20925
+ await mkdir15(options.artifactRoot, { recursive: true });
20926
+ const screenshot = options.session ? await options.session.screenshot(join54(options.artifactRoot, "android-failure.png")).catch(() => {
20021
20927
  return;
20022
20928
  }) : undefined;
20023
- const diagnosticPath = join53(options.artifactRoot, "android-failure.json");
20024
- await writeFile16(diagnosticPath, `${JSON.stringify({
20929
+ const diagnosticPath = join54(options.artifactRoot, "android-failure.json");
20930
+ await writeFile17(diagnosticPath, `${JSON.stringify({
20025
20931
  diagnostics: options.session?.diagnostics ?? [],
20026
20932
  error: options.error instanceof Error ? options.error.message : String(options.error),
20027
20933
  platform: "android",
@@ -20084,7 +20990,7 @@ Emulator setup verification:`);
20084
20990
  console.log(JSON.stringify(report, null, 2));
20085
20991
  else
20086
20992
  printAndroidTestReport(report);
20087
- const screenshot = reportRoot ? await session.screenshot(join53(artifactRoot, "android-emulator.png")) : undefined;
20993
+ const screenshot = reportRoot ? await session.screenshot(join54(artifactRoot, "android-emulator.png")) : undefined;
20088
20994
  await writeRequestedAndroidReport({
20089
20995
  adb,
20090
20996
  args,
@@ -20152,14 +21058,14 @@ Emulator setup verification:`);
20152
21058
  const port = Number(explicit);
20153
21059
  if (!Number.isInteger(port) || port < 1 || port > 65535)
20154
21060
  throw new TypeError("mobile test --port must be a valid TCP port.");
20155
- const instance2 = listLiveInstances().find((candidate) => resolve42(candidate.cwd) === resolve42(projectRoot) && candidate.source === "dev" && candidate.port === port);
21061
+ const instance2 = listLiveInstances().find((candidate) => resolve44(candidate.cwd) === resolve44(projectRoot) && candidate.source === "dev" && candidate.port === port);
20156
21062
  return {
20157
21063
  https: instance2?.https ?? args.includes("--https"),
20158
21064
  instance: instance2,
20159
21065
  port
20160
21066
  };
20161
21067
  }
20162
- const instances = listLiveInstances().filter((instance2) => resolve42(instance2.cwd) === resolve42(projectRoot) && instance2.source === "dev" && instance2.port !== null);
21068
+ const instances = listLiveInstances().filter((instance2) => resolve44(instance2.cwd) === resolve44(projectRoot) && instance2.source === "dev" && instance2.port !== null);
20163
21069
  if (instances.length !== 1)
20164
21070
  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>.");
20165
21071
  const [instance] = instances;
@@ -20214,7 +21120,7 @@ Emulator setup verification:`);
20214
21120
  if (!instance)
20215
21121
  return;
20216
21122
  const index = instance.command.indexOf("--ios-device");
20217
- return index === NOT_FOUND3 ? undefined : instance.command[index + 1];
21123
+ return index === NOT_FOUND4 ? undefined : instance.command[index + 1];
20218
21124
  }, physicalIosCapture = async (options) => {
20219
21125
  const remoteName = valueAfter(options.args, "--remote");
20220
21126
  if (remoteName && options.instance.iosRemoteMac && remoteName !== options.instance.iosRemoteMac)
@@ -20273,8 +21179,8 @@ Emulator setup verification:`);
20273
21179
  throw new Error(`${label} failed: ${result.stderr.trim() || result.stdout.trim() || `status ${result.exitCode}`}`);
20274
21180
  return result;
20275
21181
  }, writeIosFailureArtifacts = async (options) => {
20276
- await mkdir14(options.artifactRoot, { recursive: true });
20277
- const screenshot = join53(options.artifactRoot, "ios-failure.png");
21182
+ await mkdir15(options.artifactRoot, { recursive: true });
21183
+ const screenshot = join54(options.artifactRoot, "ios-failure.png");
20278
21184
  const screenshotResult = captureCommand4([
20279
21185
  options.xcrun,
20280
21186
  "simctl",
@@ -20283,8 +21189,8 @@ Emulator setup verification:`);
20283
21189
  "screenshot",
20284
21190
  screenshot
20285
21191
  ]);
20286
- const diagnosticPath = join53(options.artifactRoot, "ios-failure.json");
20287
- await writeFile16(diagnosticPath, `${JSON.stringify({
21192
+ const diagnosticPath = join54(options.artifactRoot, "ios-failure.json");
21193
+ await writeFile17(diagnosticPath, `${JSON.stringify({
20288
21194
  appId: options.appId,
20289
21195
  error: options.error instanceof Error ? options.error.message : String(options.error),
20290
21196
  platform: "ios",
@@ -20300,7 +21206,7 @@ Emulator setup verification:`);
20300
21206
  };
20301
21207
  }, nativeReportRoot = (args, projectRoot, platform6) => {
20302
21208
  const index = args.indexOf("--report");
20303
- if (index === NOT_FOUND3)
21209
+ if (index === NOT_FOUND4)
20304
21210
  return;
20305
21211
  const candidate = args[index + 1];
20306
21212
  const explicit = candidate?.startsWith("--") ? undefined : candidate;
@@ -20309,8 +21215,8 @@ Emulator setup verification:`);
20309
21215
  }, absolutejsVersionForReport = async () => {
20310
21216
  let absolutejsVersion = process.env.ABSOLUTE_VERSION ?? "unknown";
20311
21217
  const versions = await Promise.all([
20312
- resolve42(import.meta.dir, "..", "..", "package.json"),
20313
- resolve42(import.meta.dir, "..", "..", "..", "package.json")
21218
+ resolve44(import.meta.dir, "..", "..", "package.json"),
21219
+ resolve44(import.meta.dir, "..", "..", "..", "package.json")
20314
21220
  ].map((candidate) => readPackageVersionForIosReport(candidate).catch(() => "unknown")));
20315
21221
  for (const version2 of versions) {
20316
21222
  if (version2 === "unknown")
@@ -20528,8 +21434,8 @@ Emulator setup verification:`);
20528
21434
  mobile.appId
20529
21435
  ], "iOS app launch");
20530
21436
  await waitForIosHmrClient({ https, port, timeoutMs });
20531
- await mkdir14(artifactRoot, { recursive: true });
20532
- const screenshot = join53(artifactRoot, "ios-simulator.png");
21437
+ await mkdir15(artifactRoot, { recursive: true });
21438
+ const screenshot = join54(artifactRoot, "ios-simulator.png");
20533
21439
  requireCapturedCommand([xcrun, "simctl", "io", simulator.udid, "screenshot", screenshot], "iOS simulator screenshot");
20534
21440
  const hmrApply = await waitForRequestedIosHmr(args, instance, timeoutMs);
20535
21441
  const report = {
@@ -20638,6 +21544,10 @@ Emulator setup verification:`);
20638
21544
  await associations(args.slice(1));
20639
21545
  return;
20640
21546
  }
21547
+ if (command === "ci") {
21548
+ await generateGithubCi(args.slice(1));
21549
+ return;
21550
+ }
20641
21551
  if (command === "doctor") {
20642
21552
  await doctor(args.slice(1));
20643
21553
  return;
@@ -20670,7 +21580,7 @@ Emulator setup verification:`);
20670
21580
  await publishIos(args.slice(2));
20671
21581
  return;
20672
21582
  }
20673
- 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]");
21583
+ 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] [--require-bundle] | associations [--outdir dir] [--verify] | ci github [server-entry] [--publish] [--registry module] [--secret-env NAME] [--output path] [--force] [--json] | doctor [ios|android|release [ios|android]] [--remote name] [--json|--fix [--yes]] | build <android|ios> [server-entry] [--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]");
20674
21584
  };
20675
21585
  var init_mobile = __esm(() => {
20676
21586
  init_dependencies();
@@ -20705,6 +21615,7 @@ var init_mobile = __esm(() => {
20705
21615
  init_syncSchema();
20706
21616
  init_deviceCapabilities();
20707
21617
  init_mobileInspect();
21618
+ init_ciWorkflow();
20708
21619
  CAPACITOR_PACKAGES = [
20709
21620
  "@capacitor/core",
20710
21621
  "@capacitor/app",
@@ -20740,11 +21651,11 @@ var exports_typecheck = {};
20740
21651
  __export(exports_typecheck, {
20741
21652
  typecheck: () => typecheck
20742
21653
  });
20743
- import { resolve as resolve43, join as join54 } from "path";
20744
- import { existsSync as existsSync42, readFileSync as readFileSync40 } from "fs";
20745
- import { mkdir as mkdir15, writeFile as writeFile17 } from "fs/promises";
20746
- var isCommandService3 = (service) => service.kind === "command" || Array.isArray(service.command), resolveConfigPath = (configPath2) => resolve43(configPath2 ?? process.env.ABSOLUTE_CONFIG ?? "absolute.config.ts"), getTypecheckTargets = async (configPath2) => {
20747
- if (!existsSync42(resolveConfigPath(configPath2))) {
21654
+ import { resolve as resolve45, join as join55 } from "path";
21655
+ import { existsSync as existsSync43, readFileSync as readFileSync40 } from "fs";
21656
+ import { mkdir as mkdir16, writeFile as writeFile18 } from "fs/promises";
21657
+ var isCommandService3 = (service) => service.kind === "command" || Array.isArray(service.command), resolveConfigPath = (configPath2) => resolve45(configPath2 ?? process.env.ABSOLUTE_CONFIG ?? "absolute.config.ts"), getTypecheckTargets = async (configPath2) => {
21658
+ if (!existsSync43(resolveConfigPath(configPath2))) {
20748
21659
  const defaultService = {};
20749
21660
  return [defaultService];
20750
21661
  }
@@ -20765,8 +21676,8 @@ var isCommandService3 = (service) => service.kind === "command" || Array.isArray
20765
21676
  const exitCode = await proc.exited;
20766
21677
  return { exitCode, name, output: (stdout + stderr).trim() };
20767
21678
  }, shellEscape = (value) => `'${value.replaceAll("'", "'\\''")}'`, runShell = async (name, command) => run(name, ["/bin/bash", "-lc", command]), findBin = (name) => {
20768
- const local = resolve43("node_modules", ".bin", name);
20769
- return existsSync42(local) ? local : null;
21679
+ const local = resolve45("node_modules", ".bin", name);
21680
+ return existsSync43(local) ? local : null;
20770
21681
  }, ANSI_COLOR_REGEX, ANSI_PURPLE_REGEX, ANSI_CYAN_REGEX, ANSI_TOKEN_END_REGEX, stripAnsi4 = (str) => str.replace(ANSI_COLOR_REGEX, ""), formatSvelteOutput = (output) => {
20771
21682
  const cwd = `${process.cwd()}/`;
20772
21683
  const summaryMatch = stripAnsi4(output).match(/svelte-check found (\d+) error/);
@@ -20813,15 +21724,15 @@ Found ${errorCount} error${suffix}.`;
20813
21724
  return formatted;
20814
21725
  }, ABSOLUTE_INTERNAL_EXCLUDES, resolveAbsoluteTypeFile = (fileName) => {
20815
21726
  const candidates = [
20816
- resolve43("node_modules/@absolutejs/absolute/dist/types", fileName),
20817
- resolve43(import.meta.dir, "../types", fileName),
20818
- resolve43(import.meta.dir, "../../types", fileName),
20819
- resolve43(import.meta.dir, "../../../types", fileName)
21727
+ resolve45("node_modules/@absolutejs/absolute/dist/types", fileName),
21728
+ resolve45(import.meta.dir, "../types", fileName),
21729
+ resolve45(import.meta.dir, "../../types", fileName),
21730
+ resolve45(import.meta.dir, "../../../types", fileName)
20820
21731
  ];
20821
- return candidates.find((candidate) => existsSync42(candidate)) ?? candidates[0];
21732
+ return candidates.find((candidate) => existsSync43(candidate)) ?? candidates[0];
20822
21733
  }, ABSOLUTE_TYPECHECK_FILES, readProjectTsconfig = () => {
20823
21734
  try {
20824
- return JSON.parse(readFileSync40(resolve43("tsconfig.json"), "utf-8"));
21735
+ return JSON.parse(readFileSync40(resolve45("tsconfig.json"), "utf-8"));
20825
21736
  } catch {
20826
21737
  return {};
20827
21738
  }
@@ -20849,27 +21760,27 @@ Found ${errorCount} error${suffix}.`;
20849
21760
  console.error("\x1B[31m\u2717\x1B[0m vue-tsc is required for Vue type checking. Install it: bun add -d vue-tsc");
20850
21761
  process.exit(1);
20851
21762
  }
20852
- const vueTsconfigPath = join54(cacheDir, "tsconfig.vue-check.json");
20853
- await writeFile17(vueTsconfigPath, JSON.stringify({
21763
+ const vueTsconfigPath = join55(cacheDir, "tsconfig.vue-check.json");
21764
+ await writeFile18(vueTsconfigPath, JSON.stringify({
20854
21765
  compilerOptions: {
20855
21766
  rootDir: ".."
20856
21767
  },
20857
21768
  exclude: getProjectTypecheckExcludes(),
20858
- extends: resolve43("tsconfig.json"),
21769
+ extends: resolve45("tsconfig.json"),
20859
21770
  include: getProjectTypecheckIncludes()
20860
21771
  }, null, "\t"));
20861
21772
  const base = [
20862
21773
  vueTscBin,
20863
21774
  "--noEmit",
20864
21775
  "--project",
20865
- resolve43(vueTsconfigPath),
21776
+ resolve45(vueTsconfigPath),
20866
21777
  "--pretty"
20867
21778
  ];
20868
21779
  const cached = await run("vue-tsc", [
20869
21780
  ...base,
20870
21781
  "--incremental",
20871
21782
  "--tsBuildInfoFile",
20872
- join54(cacheDir, "vue-tsc.tsbuildinfo")
21783
+ join55(cacheDir, "vue-tsc.tsbuildinfo")
20873
21784
  ]);
20874
21785
  if (cached.exitCode === 0 || cached.output.length > 0)
20875
21786
  return cached;
@@ -20880,8 +21791,8 @@ Found ${errorCount} error${suffix}.`;
20880
21791
  console.error("\x1B[31m\u2717\x1B[0m @angular/compiler-cli is required for Angular type checking. Install it: bun add -d @angular/compiler-cli");
20881
21792
  process.exit(1);
20882
21793
  }
20883
- const angularTsconfigPath = join54(cacheDir, "tsconfig.angular-check.json");
20884
- await writeFile17(angularTsconfigPath, JSON.stringify({
21794
+ const angularTsconfigPath = join55(cacheDir, "tsconfig.angular-check.json");
21795
+ await writeFile18(angularTsconfigPath, JSON.stringify({
20885
21796
  angularCompilerOptions: {
20886
21797
  strictTemplates: true
20887
21798
  },
@@ -20890,32 +21801,32 @@ Found ${errorCount} error${suffix}.`;
20890
21801
  rootDir: ".."
20891
21802
  },
20892
21803
  exclude: ABSOLUTE_INTERNAL_EXCLUDES.map(toGeneratedConfigPath),
20893
- extends: resolve43("tsconfig.json"),
21804
+ extends: resolve45("tsconfig.json"),
20894
21805
  include: [`../${angularDir}/**/*`]
20895
21806
  }, null, "\t"));
20896
- return runShell("ngc", `${shellEscape(ngcBin)} -p ${shellEscape(resolve43(angularTsconfigPath))}`);
21807
+ return runShell("ngc", `${shellEscape(ngcBin)} -p ${shellEscape(resolve45(angularTsconfigPath))}`);
20897
21808
  }, buildTscCheck = (cacheDir) => {
20898
21809
  const tscBin = findBin("tsc");
20899
21810
  if (!tscBin) {
20900
21811
  console.error("\x1B[31m\u2717\x1B[0m typescript is required for type checking. Install it: bun add -d typescript");
20901
21812
  process.exit(1);
20902
21813
  }
20903
- const tscConfigPath = join54(cacheDir, "tsconfig.typecheck.json");
20904
- return writeFile17(tscConfigPath, JSON.stringify({
21814
+ const tscConfigPath = join55(cacheDir, "tsconfig.typecheck.json");
21815
+ return writeFile18(tscConfigPath, JSON.stringify({
20905
21816
  compilerOptions: {
20906
21817
  rootDir: ".."
20907
21818
  },
20908
21819
  exclude: getProjectTypecheckExcludes(),
20909
- extends: resolve43("tsconfig.json"),
21820
+ extends: resolve45("tsconfig.json"),
20910
21821
  include: getProjectTypecheckIncludes()
20911
21822
  }, null, "\t")).then(() => run("tsc", [
20912
21823
  tscBin,
20913
21824
  "--noEmit",
20914
21825
  "--project",
20915
- resolve43(tscConfigPath),
21826
+ resolve45(tscConfigPath),
20916
21827
  "--incremental",
20917
21828
  "--tsBuildInfoFile",
20918
- join54(cacheDir, "tsc.tsbuildinfo"),
21829
+ join55(cacheDir, "tsc.tsbuildinfo"),
20919
21830
  "--pretty"
20920
21831
  ]));
20921
21832
  }, buildSvelteCheck = async (cacheDir, svelteDir) => {
@@ -20924,16 +21835,16 @@ Found ${errorCount} error${suffix}.`;
20924
21835
  console.error("\x1B[31m\u2717\x1B[0m svelte-check is required for Svelte type checking. Install it: bun add -d svelte-check");
20925
21836
  process.exit(1);
20926
21837
  }
20927
- const svelteTsconfigPath = join54(cacheDir, "tsconfig.svelte-check.json");
20928
- await writeFile17(svelteTsconfigPath, JSON.stringify({
20929
- extends: resolve43("tsconfig.json"),
21838
+ const svelteTsconfigPath = join55(cacheDir, "tsconfig.svelte-check.json");
21839
+ await writeFile18(svelteTsconfigPath, JSON.stringify({
21840
+ extends: resolve45("tsconfig.json"),
20930
21841
  files: ABSOLUTE_TYPECHECK_FILES,
20931
21842
  include: [`../${svelteDir}/**/*`]
20932
21843
  }, null, "\t"));
20933
21844
  return run("svelte-check", [
20934
21845
  svelteBin,
20935
21846
  "--tsconfig",
20936
- resolve43(svelteTsconfigPath),
21847
+ resolve45(svelteTsconfigPath),
20937
21848
  "--threshold",
20938
21849
  "error",
20939
21850
  "--compiler-warnings",
@@ -20954,7 +21865,7 @@ Found ${errorCount} error${suffix}.`;
20954
21865
  ...new Set(targets.map((config) => config.angularDirectory).filter((dir) => typeof dir === "string" && dir.length > 0))
20955
21866
  ];
20956
21867
  const cacheDir = ".absolutejs";
20957
- await mkdir15(cacheDir, { recursive: true });
21868
+ await mkdir16(cacheDir, { recursive: true });
20958
21869
  const checks = [];
20959
21870
  checks.push(hasVue ? buildVueTscCheck(cacheDir) : buildTscCheck(cacheDir));
20960
21871
  for (const svelteDir of hasSvelte ? svelteDirs : []) {
@@ -21127,11 +22038,11 @@ var DEFAULT_RELAY_PORT = 8787, DEFAULT_REQUEST_TIMEOUT_MS = 30000, headersToObje
21127
22038
  url: url.pathname + url.search,
21128
22039
  ...bodyBytes && bodyBytes.length > 0 ? { bodyBase64: Buffer.from(bodyBytes).toString("base64") } : {}
21129
22040
  };
21130
- const responsePromise = new Promise((resolve44) => {
21131
- pending.set(id, resolve44);
22041
+ const responsePromise = new Promise((resolve46) => {
22042
+ pending.set(id, resolve46);
21132
22043
  });
21133
22044
  client.send(encodeTunnelMessage(message));
21134
- const timeout = new Promise((resolve44) => setTimeout(() => resolve44({ id, message: "timeout", type: "error" }), requestTimeoutMs));
22045
+ const timeout = new Promise((resolve46) => setTimeout(() => resolve46({ id, message: "timeout", type: "error" }), requestTimeoutMs));
21135
22046
  const result = await Promise.race([responsePromise, timeout]);
21136
22047
  pending.delete(id);
21137
22048
  if (result.type === "error") {
@@ -25044,7 +25955,7 @@ if (command === "dev") {
25044
25955
  console.error(" prepare [entry] [--outdir dir] Build production assets and server without launching");
25045
25956
  console.error(" start [entry] [--outdir dir] [--prebuilt] Start production server");
25046
25957
  console.error(" compile [entry] [--outdir dir] [--outfile path] Compile standalone executable");
25047
- console.error(" mobile <init|sync|inspect|pair|remotes|doctor|test> Manage Capacitor projects, simulators, physical devices, Remote Macs, guided setup, and deep links");
25958
+ console.error(" mobile <init|sync|inspect|ci|pair|remotes|doctor|test> Manage Capacitor projects, CI, simulators, physical devices, Remote Macs, guided setup, and deep links");
25048
25959
  console.error(" config [--port n] Open the unified config UI (ESLint, tsconfig, Prettier)");
25049
25960
  console.error(" db <backup|restore|seed> Backup/restore any Postgres DB (ORM-agnostic, upsert by PK) or run the seed script");
25050
25961
  console.error(" doctor [--fix] [--json] Diagnose the project (bun, type graph, config, framework dirs, env, port)");