@absolutejs/absolute 0.20.0-beta.109 → 0.20.0-beta.110

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.
@@ -16779,15 +16779,15 @@ var require_main = __commonJS(function(exports) {
16779
16779
 
16780
16780
  // src/cli/scripts/mobile.ts
16781
16781
  import {
16782
- access as access15,
16783
- mkdir as mkdir16,
16782
+ access as access16,
16783
+ mkdir as mkdir17,
16784
16784
  mkdtemp as mkdtemp6,
16785
- readFile as readFile20,
16785
+ readFile as readFile21,
16786
16786
  rm as rm9,
16787
- writeFile as writeFile17
16787
+ writeFile as writeFile18
16788
16788
  } from "fs/promises";
16789
16789
  import { createPublicKey as createPublicKey2 } from "crypto";
16790
- import { join as join17, relative as relative14, resolve as resolve15 } from "path";
16790
+ import { join as join17, relative as relative15, resolve as resolve16 } from "path";
16791
16791
  import { createInterface } from "readline/promises";
16792
16792
 
16793
16793
  // src/mobile/nativeDeepLinks.ts
@@ -22521,6 +22521,8 @@ This certification is local and contains no signing credentials, device identifi
22521
22521
  ${evidence}
22522
22522
  `;
22523
22523
  };
22524
+ var serializeAbsoluteMobileReleaseCertification = (certification) => `${JSON.stringify(certification, null, 2)}
22525
+ `;
22524
22526
  var verifyAbsoluteMobileReleaseCertification = (certification, release, requirement) => {
22525
22527
  const parsed = parseCertification(certification);
22526
22528
  const expected = releaseIdentity(release);
@@ -22545,8 +22547,7 @@ var writeAbsoluteMobileReleaseCertification = async (projectRoot, certification,
22545
22547
  jsonPath: join13(destination, "certification.json"),
22546
22548
  markdownPath: join13(destination, "certification.md")
22547
22549
  };
22548
- const json = `${JSON.stringify(certification, null, 2)}
22549
- `;
22550
+ const json = serializeAbsoluteMobileReleaseCertification(certification);
22550
22551
  const markdown = renderAbsoluteMobileReleaseCertification(certification);
22551
22552
  const verifyExisting = async () => {
22552
22553
  const [existingJson, existingMarkdown] = await Promise.all([
@@ -22582,9 +22583,111 @@ var writeAbsoluteMobileReleaseCertification = async (projectRoot, certification,
22582
22583
  return paths;
22583
22584
  };
22584
22585
 
22586
+ // src/mobile/certificationVerification.ts
22587
+ import { access as access9, mkdir as mkdir12, readFile as readFile16, stat as stat6, writeFile as writeFile13 } from "fs/promises";
22588
+ import { dirname as dirname10, isAbsolute as isAbsolute6, relative as relative8, resolve as resolve9, sep as sep6 } from "path";
22589
+ var ABSOLUTE_MOBILE_CERTIFICATION_VERIFICATION_FORMAT = 1;
22590
+ var MAX_VERIFICATION_BYTES = 1048576;
22591
+ var SHA_PATTERN = /^[a-f0-9]{40}$/u;
22592
+ var REPOSITORY_PATTERN = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u;
22593
+ var REF_PATTERN = /^refs\/(?:heads|tags)\/[^@\s]+$/u;
22594
+ var WORKFLOW_PATTERN = /^\.github\/workflows\/[^/@]+\.ya?ml$/u;
22595
+ var isRecord7 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
22596
+ var required = (value, name) => {
22597
+ if (typeof value !== "string" || value.trim() === "")
22598
+ throw new TypeError(`${name} is required.`);
22599
+ return value.trim();
22600
+ };
22601
+ var requireMatch = (value, pattern, name) => {
22602
+ if (!pattern.test(value))
22603
+ throw new TypeError(`${name} is invalid.`);
22604
+ return value;
22605
+ };
22606
+ var createAbsoluteMobileCertificationVerification = (bundle, environment) => {
22607
+ const repository = required(environment.GITHUB_REPOSITORY, "GITHUB_REPOSITORY");
22608
+ const workflowReference = required(environment.GITHUB_WORKFLOW_REF, "GITHUB_WORKFLOW_REF");
22609
+ const prefix = `${repository}/`;
22610
+ const separator = workflowReference.lastIndexOf("@");
22611
+ if (!workflowReference.startsWith(prefix) || separator <= prefix.length)
22612
+ throw new TypeError("GITHUB_WORKFLOW_REF is invalid.");
22613
+ return parseAbsoluteMobileCertificationVerification({
22614
+ bundle,
22615
+ format: ABSOLUTE_MOBILE_CERTIFICATION_VERIFICATION_FORMAT,
22616
+ identity: {
22617
+ issuer: "https://token.actions.githubusercontent.com",
22618
+ ref: environment.GITHUB_REF,
22619
+ repository,
22620
+ sha: environment.GITHUB_SHA,
22621
+ workflowPath: workflowReference.slice(prefix.length, separator)
22622
+ },
22623
+ kind: "sigstore-bundle"
22624
+ });
22625
+ };
22626
+ var parseAbsoluteMobileCertificationVerification = (value) => {
22627
+ if (!isRecord7(value) || value.format !== ABSOLUTE_MOBILE_CERTIFICATION_VERIFICATION_FORMAT || value.kind !== "sigstore-bundle" || !isRecord7(value.bundle) || !isRecord7(value.identity))
22628
+ throw new TypeError("Mobile certification verification contract is invalid.");
22629
+ const issuer = required(value.identity.issuer, "verification issuer");
22630
+ if (issuer !== "https://token.actions.githubusercontent.com")
22631
+ throw new TypeError("verification issuer is invalid.");
22632
+ const verification = {
22633
+ bundle: value.bundle,
22634
+ format: ABSOLUTE_MOBILE_CERTIFICATION_VERIFICATION_FORMAT,
22635
+ identity: {
22636
+ issuer,
22637
+ ref: requireMatch(required(value.identity.ref, "verification ref"), REF_PATTERN, "verification ref"),
22638
+ repository: requireMatch(required(value.identity.repository, "verification repository"), REPOSITORY_PATTERN, "verification repository"),
22639
+ sha: requireMatch(required(value.identity.sha, "verification sha"), SHA_PATTERN, "verification sha"),
22640
+ workflowPath: requireMatch(required(value.identity.workflowPath, "verification workflowPath"), WORKFLOW_PATTERN, "verification workflowPath")
22641
+ },
22642
+ kind: "sigstore-bundle"
22643
+ };
22644
+ const bytes = new TextEncoder().encode(JSON.stringify(verification));
22645
+ if (bytes.byteLength > MAX_VERIFICATION_BYTES)
22646
+ throw new TypeError("Mobile certification verification exceeds the 1 MiB limit.");
22647
+ return verification;
22648
+ };
22649
+ var projectFile = (projectRoot, requested) => {
22650
+ const root = resolve9(projectRoot);
22651
+ const path = resolve9(root, requested);
22652
+ const portable = relative8(root, path);
22653
+ if (portable === ".." || portable.startsWith(`..${sep6}`) || isAbsolute6(portable))
22654
+ throw new TypeError("Mobile certification verification path must remain inside the project.");
22655
+ return path;
22656
+ };
22657
+ var readAbsoluteMobileCertificationVerification = async (projectRoot, requested) => {
22658
+ const path = projectFile(projectRoot, requested);
22659
+ await access9(path).catch(() => {
22660
+ throw new TypeError(`Mobile certification verification does not exist: ${path}`);
22661
+ });
22662
+ const metadata = await stat6(path);
22663
+ if (!metadata.isFile() || metadata.size > MAX_VERIFICATION_BYTES)
22664
+ throw new TypeError("Mobile certification verification file is invalid.");
22665
+ return parseAbsoluteMobileCertificationVerification(JSON.parse(await readFile16(path, "utf8")));
22666
+ };
22667
+ var readAbsoluteSigstoreBundle = async (projectRoot, requested) => {
22668
+ const path = projectFile(projectRoot, requested);
22669
+ const metadata = await stat6(path).catch(() => null);
22670
+ if (!metadata?.isFile() || metadata.size > MAX_VERIFICATION_BYTES)
22671
+ throw new TypeError("Sigstore bundle file is invalid.");
22672
+ const value = JSON.parse(await readFile16(path, "utf8"));
22673
+ if (!isRecord7(value))
22674
+ throw new TypeError("Sigstore bundle is invalid.");
22675
+ return value;
22676
+ };
22677
+ var writeAbsoluteMobileCertificationVerification = async (projectRoot, requested, verification) => {
22678
+ const path = projectFile(projectRoot, requested);
22679
+ const parsed = parseAbsoluteMobileCertificationVerification(verification);
22680
+ await mkdir12(dirname10(path), { recursive: true });
22681
+ await writeFile13(path, `${JSON.stringify(parsed, null, 2)}
22682
+ `, {
22683
+ mode: 384
22684
+ });
22685
+ return path;
22686
+ };
22687
+
22585
22688
  // src/mobile/releasePublisher.ts
22586
- import { access as access9 } from "fs/promises";
22587
- import { isAbsolute as isAbsolute6, relative as relative8, resolve as resolve9, sep as sep6 } from "path";
22689
+ import { access as access10 } from "fs/promises";
22690
+ import { isAbsolute as isAbsolute7, relative as relative9, resolve as resolve10, sep as sep7 } from "path";
22588
22691
  import { pathToFileURL as pathToFileURL2 } from "url";
22589
22692
  var prepareAbsoluteIosRelease = async (publisher, options) => {
22590
22693
  if (typeof publisher.prepareIosRelease !== "function") {
@@ -22607,33 +22710,35 @@ var prepareAbsoluteAndroidRelease = async (publisher, options) => {
22607
22710
  }
22608
22711
  return versionCode;
22609
22712
  };
22610
- var requireCertificationReceipt = (publication, certification, requirement, releaseId) => {
22611
- if (!certification && !requirement)
22713
+ var requireCertificationReceipt = (publication, certification, requirement, releaseId, verification) => {
22714
+ if (!certification && !requirement && !verification)
22612
22715
  return;
22613
22716
  if (!certification || !requirement)
22614
22717
  throw new TypeError("Native release publication certification contract is incomplete.");
22615
22718
  const receipt = publication.certification;
22616
22719
  if (!receipt || receipt.certificationId !== certification.certificationId || receipt.releaseId !== releaseId || receipt.requirement !== requirement || receipt.strength !== certification.strength)
22617
22720
  throw new TypeError("Native release registry did not retain the required release certification.");
22721
+ if (verification && (!receipt.provenance || receipt.provenance.subject !== releaseId))
22722
+ throw new TypeError("Native release registry did not retain trusted certification provenance.");
22618
22723
  };
22619
- var isRecord7 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
22620
- var isPublisher = (value) => isRecord7(value) && typeof value.publish === "function";
22724
+ var isRecord8 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
22725
+ var isPublisher = (value) => isRecord8(value) && typeof value.publish === "function";
22621
22726
  var publisherModulePath = (projectRoot, requested) => {
22622
- const root = resolve9(projectRoot);
22623
- const path = resolve9(root, requested);
22624
- const projectRelative = relative8(root, path);
22625
- if (projectRelative === ".." || projectRelative.startsWith(`..${sep6}`) || isAbsolute6(projectRelative)) {
22727
+ const root = resolve10(projectRoot);
22728
+ const path = resolve10(root, requested);
22729
+ const projectRelative = relative9(root, path);
22730
+ if (projectRelative === ".." || projectRelative.startsWith(`..${sep7}`) || isAbsolute7(projectRelative)) {
22626
22731
  throw new TypeError("mobile publish --registry must remain inside the project.");
22627
22732
  }
22628
22733
  return path;
22629
22734
  };
22630
22735
  var loadAbsoluteNativeReleasePublisher = async (projectRoot, requestedModulePath) => {
22631
22736
  const modulePath = publisherModulePath(projectRoot, requestedModulePath);
22632
- await access9(modulePath).catch(() => {
22737
+ await access10(modulePath).catch(() => {
22633
22738
  throw new TypeError(`Native release registry module does not exist: ${modulePath}`);
22634
22739
  });
22635
22740
  const loaded = await import(pathToFileURL2(modulePath).href);
22636
- const publisher = isRecord7(loaded) ? loaded.default ?? loaded.registry : undefined;
22741
+ const publisher = isRecord8(loaded) ? loaded.default ?? loaded.registry : undefined;
22637
22742
  if (!isPublisher(publisher)) {
22638
22743
  throw new TypeError("Native release registry module must default-export a registry with publish(options).");
22639
22744
  }
@@ -22641,6 +22746,8 @@ var loadAbsoluteNativeReleasePublisher = async (projectRoot, requestedModulePath
22641
22746
  };
22642
22747
  var publishAbsoluteAndroidRelease = async (options) => {
22643
22748
  const certificationRequirement = options.certificationRequirement ?? options.certification?.requirement;
22749
+ if (options.certificationVerification && !options.certification)
22750
+ throw new TypeError("Android certification verification requires certification.");
22644
22751
  if (options.certification)
22645
22752
  verifyAbsoluteMobileReleaseCertification(options.certification, options.release, certificationRequirement);
22646
22753
  else if (certificationRequirement)
@@ -22650,6 +22757,7 @@ var publishAbsoluteAndroidRelease = async (options) => {
22650
22757
  allowUnsigned: options.allowUnsigned,
22651
22758
  certification: options.certification,
22652
22759
  certificationRequirement,
22760
+ certificationVerification: options.certificationVerification,
22653
22761
  channel: options.channel,
22654
22762
  googlePlay: options.googlePlay,
22655
22763
  releaseRoot: options.release.releaseRoot,
@@ -22660,7 +22768,7 @@ var publishAbsoluteAndroidRelease = async (options) => {
22660
22768
  if (!actual || actual.appId !== expected.appId || actual.platform !== "android" || actual.releaseId !== expected.releaseId || actual.sha256 !== expected.sha256 || actual.signed !== expected.signed || actual.versionCode !== expected.versionCode || typeof publication.reused !== "boolean") {
22661
22769
  throw new TypeError("Native release registry returned a different Android release identity.");
22662
22770
  }
22663
- requireCertificationReceipt(publication, options.certification, certificationRequirement, expected.releaseId);
22771
+ requireCertificationReceipt(publication, options.certification, certificationRequirement, expected.releaseId, options.certificationVerification);
22664
22772
  if (options.channel !== undefined && (publication.channel?.channel !== options.channel || publication.channel.releaseId !== expected.releaseId)) {
22665
22773
  throw new TypeError("Native release registry did not promote the requested channel.");
22666
22774
  }
@@ -22674,6 +22782,8 @@ var publishAbsoluteAndroidRelease = async (options) => {
22674
22782
  };
22675
22783
  var publishAbsoluteIosRelease = async (options) => {
22676
22784
  const certificationRequirement = options.certificationRequirement ?? options.certification?.requirement;
22785
+ if (options.certificationVerification && !options.certification)
22786
+ throw new TypeError("iOS certification verification requires certification.");
22677
22787
  if (options.certification)
22678
22788
  verifyAbsoluteMobileReleaseCertification(options.certification, options.release, certificationRequirement);
22679
22789
  else if (certificationRequirement)
@@ -22684,6 +22794,7 @@ var publishAbsoluteIosRelease = async (options) => {
22684
22794
  appStoreConnect: options.appStoreConnect,
22685
22795
  certification: options.certification,
22686
22796
  certificationRequirement,
22797
+ certificationVerification: options.certificationVerification,
22687
22798
  channel: options.channel,
22688
22799
  releaseRoot: options.release.releaseRoot,
22689
22800
  signal: options.signal
@@ -22693,7 +22804,7 @@ var publishAbsoluteIosRelease = async (options) => {
22693
22804
  if (!actual || actual.appId !== expected.appId || actual.platform !== "ios" || actual.releaseId !== expected.releaseId || actual.sha256 !== expected.sha256 || actual.signed !== expected.signed || actual.buildNumber !== expected.buildNumber || actual.marketingVersion !== expected.marketingVersion || typeof publication.reused !== "boolean") {
22694
22805
  throw new TypeError("Native release registry returned a different iOS release identity.");
22695
22806
  }
22696
- requireCertificationReceipt(publication, options.certification, certificationRequirement, expected.releaseId);
22807
+ requireCertificationReceipt(publication, options.certification, certificationRequirement, expected.releaseId, options.certificationVerification);
22697
22808
  if (options.channel !== undefined && (publication.channel?.channel !== options.channel || publication.channel.releaseId !== expected.releaseId)) {
22698
22809
  throw new TypeError("Native release registry did not promote the requested channel.");
22699
22810
  }
@@ -22707,8 +22818,8 @@ var publishAbsoluteIosRelease = async (options) => {
22707
22818
  };
22708
22819
 
22709
22820
  // src/mobile/mobileInspect.ts
22710
- import { access as access10, readFile as readFile16 } from "fs/promises";
22711
- import { join as join14, relative as relative9, resolve as resolve10 } from "path";
22821
+ import { access as access11, readFile as readFile17 } from "fs/promises";
22822
+ import { join as join14, relative as relative10, resolve as resolve11 } from "path";
22712
22823
  var ABSOLUTE_MOBILE_INSPECTION_FORMAT = 1;
22713
22824
  var MOBILE_PACKAGE_NAMES = new Set([
22714
22825
  "@absolutejs/absolute",
@@ -22723,19 +22834,19 @@ var MOBILE_PACKAGE_NAMES = new Set([
22723
22834
  ]);
22724
22835
  var isObject2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
22725
22836
  var portablePath2 = (projectRoot, path) => {
22726
- const value = relative9(resolve10(projectRoot), resolve10(path)).replaceAll("\\", "/");
22837
+ const value = relative10(resolve11(projectRoot), resolve11(path)).replaceAll("\\", "/");
22727
22838
  return value || ".";
22728
22839
  };
22729
22840
  var pathExists4 = async (path) => {
22730
22841
  try {
22731
- await access10(path);
22842
+ await access11(path);
22732
22843
  return true;
22733
22844
  } catch {
22734
22845
  return false;
22735
22846
  }
22736
22847
  };
22737
22848
  var readObject2 = async (path) => {
22738
- const value = JSON.parse(await readFile16(path, "utf8"));
22849
+ const value = JSON.parse(await readFile17(path, "utf8"));
22739
22850
  if (!isObject2(value))
22740
22851
  throw new TypeError("JSON root must be an object.");
22741
22852
  return value;
@@ -22869,9 +22980,9 @@ var renderAbsoluteMobileProjectInspection = (report) => {
22869
22980
 
22870
22981
  // src/mobile/ciWorkflow.ts
22871
22982
  import { existsSync } from "fs";
22872
- import { access as access11, mkdir as mkdir12, readFile as readFile17, writeFile as writeFile13 } from "fs/promises";
22873
- import { dirname as dirname10, extname as extname2, relative as relative10, resolve as resolve11, sep as sep7 } from "path";
22874
- var ABSOLUTE_MOBILE_CI_WORKFLOW_FORMAT = 1;
22983
+ import { access as access12, mkdir as mkdir13, readFile as readFile18, writeFile as writeFile14 } from "fs/promises";
22984
+ import { dirname as dirname11, extname as extname2, relative as relative11, resolve as resolve12, sep as sep8 } from "path";
22985
+ var ABSOLUTE_MOBILE_CI_WORKFLOW_FORMAT = 2;
22875
22986
  var SECRET_NAME_PATTERN = /^[A-Z][A-Z0-9_]*$/u;
22876
22987
  var CI_ENV_INDENTATION = 6;
22877
22988
  var RESERVED_SECRET_NAMES = new Set([
@@ -22891,7 +23002,7 @@ var RESERVED_SECRET_NAMES = new Set([
22891
23002
  ]);
22892
23003
  var exists5 = async (path) => {
22893
23004
  try {
22894
- await access11(path);
23005
+ await access12(path);
22895
23006
  return true;
22896
23007
  } catch {
22897
23008
  return false;
@@ -22899,10 +23010,10 @@ var exists5 = async (path) => {
22899
23010
  };
22900
23011
  var yamlString = (value) => `'${value.replaceAll("'", "''")}'`;
22901
23012
  var projectPath2 = (projectRoot, value, field2, options = {}) => {
22902
- const root = resolve11(projectRoot);
22903
- const path = resolve11(root, value);
22904
- const portable = relative10(root, path).replaceAll("\\", "/");
22905
- if (portable === ".." || portable.startsWith(`..${sep7}`) || portable.startsWith("../") || portable === "") {
23013
+ const root = resolve12(projectRoot);
23014
+ const path = resolve12(root, value);
23015
+ const portable = relative11(root, path).replaceAll("\\", "/");
23016
+ if (portable === ".." || portable.startsWith(`..${sep8}`) || portable.startsWith("../") || portable === "") {
22906
23017
  throw new TypeError(`${field2} must remain inside the project root.`);
22907
23018
  }
22908
23019
  if (/\r|\n/u.test(portable) || portable.startsWith("-"))
@@ -22912,11 +23023,11 @@ var projectPath2 = (projectRoot, value, field2, options = {}) => {
22912
23023
  return portable;
22913
23024
  };
22914
23025
  var workflowOutputPath = (projectRoot, value) => {
22915
- const root = resolve11(projectRoot);
22916
- const workflows = resolve11(root, ".github/workflows");
22917
- const path = resolve11(root, value ?? ".github/workflows/absolute-mobile.yml");
22918
- const portable = relative10(workflows, path);
22919
- if (portable === ".." || portable.startsWith(`..${sep7}`) || extname2(path) !== ".yml" && extname2(path) !== ".yaml") {
23026
+ const root = resolve12(projectRoot);
23027
+ const workflows = resolve12(root, ".github/workflows");
23028
+ const path = resolve12(root, value ?? ".github/workflows/absolute-mobile.yml");
23029
+ const portable = relative11(workflows, path);
23030
+ if (portable === ".." || portable.startsWith(`..${sep8}`) || extname2(path) !== ".yml" && extname2(path) !== ".yaml") {
22920
23031
  throw new TypeError("mobile ci github --output must be a .yml or .yaml file inside .github/workflows.");
22921
23032
  }
22922
23033
  return path;
@@ -23009,6 +23120,14 @@ var publishingInputs = (platforms, includePublishing) => {
23009
23120
  if (!includePublishing)
23010
23121
  return "";
23011
23122
  const fields = [
23123
+ ` operation:
23124
+ description: Build a release or promote an exact artifact from an earlier run
23125
+ required: true
23126
+ type: choice
23127
+ default: build
23128
+ options:
23129
+ - build
23130
+ - promote`,
23012
23131
  ` publish:
23013
23132
  description: Publish through mobile.release.ts after the signed build
23014
23133
  required: true
@@ -23017,6 +23136,14 @@ var publishingInputs = (platforms, includePublishing) => {
23017
23136
  ` channel:
23018
23137
  description: Optional AbsoluteJS immutable release channel
23019
23138
  required: false
23139
+ type: string`,
23140
+ ` source_run_id:
23141
+ description: Source workflow run ID for promotion without rebuilding
23142
+ required: false
23143
+ type: string`,
23144
+ ` certification_base64:
23145
+ description: AbsoluteJS certification supplied by the promotion CLI
23146
+ required: false
23020
23147
  type: string`
23021
23148
  ];
23022
23149
  if (platforms.includes("android"))
@@ -23046,7 +23173,7 @@ var publishingInputs = (platforms, includePublishing) => {
23046
23173
  ${fields.join(`
23047
23174
  `)}`;
23048
23175
  };
23049
- var jobCondition = (platform) => `github.event_name == 'workflow_dispatch' && (inputs.platform == 'all' || inputs.platform == '${platform}')`;
23176
+ var jobCondition = (platform, includePublishing) => `github.event_name == 'workflow_dispatch' && ${includePublishing ? "inputs.operation == 'build' && " : ""}(inputs.platform == 'all' || inputs.platform == '${platform}')`;
23050
23177
  var androidJob = (options) => {
23051
23178
  const custom = customSecretEnvironment(options.customSecrets);
23052
23179
  const publishEnvironment = options.includePublishing ? `
@@ -23073,6 +23200,11 @@ var androidJob = (options) => {
23073
23200
  chmod 600 "$GOOGLE_APPLICATION_CREDENTIALS"
23074
23201
  fi` : "";
23075
23202
  const certificationSteps = options.includePublishing ? `
23203
+ - name: Install pinned Cosign for Android certification
23204
+ if: inputs.publish
23205
+ uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6
23206
+ with:
23207
+ cosign-release: v3.1.2
23076
23208
  - name: Install-test and certify exact Android release
23077
23209
  if: inputs.publish
23078
23210
  shell: bash
@@ -23092,6 +23224,12 @@ var androidJob = (options) => {
23092
23224
  "\${args[@]}"
23093
23225
  args=(bunx absolute mobile certify "$RELEASE_DIRECTORY" --evidence .absolutejs/mobile-ci/acceptance/android --require installed --outdir .absolutejs/mobile-ci/certifications/android --json)
23094
23226
  "\${args[@]}" > .absolutejs/mobile-ci/android-certification.json
23227
+ shopt -s nullglob
23228
+ certifications=(.absolutejs/mobile-ci/certifications/android/*)
23229
+ if [[ "\${#certifications[@]}" -ne 1 ]]; then echo "Expected exactly one Android certification." >&2; exit 1; fi
23230
+ CERTIFICATION_PATH="\${certifications[0]}/certification.json"
23231
+ bunx @absolutejs/attest@0.2.0 sign-blobs "$CERTIFICATION_PATH"
23232
+ bunx absolute mobile ci verification --bundle "$CERTIFICATION_PATH.sigstore.json" --out "\${certifications[0]}/verification.json"
23095
23233
  - name: Publish certified Android release
23096
23234
  if: inputs.publish
23097
23235
  shell: bash
@@ -23103,7 +23241,7 @@ var androidJob = (options) => {
23103
23241
  echo "Expected exactly one Android release and certification." >&2
23104
23242
  exit 1
23105
23243
  fi
23106
- args=(bunx absolute mobile publish android --release "\${releases[0]}" --certification "\${certifications[0]}" --registry "$ABSOLUTE_REGISTRY_MODULE")
23244
+ args=(bunx absolute mobile publish android --release "\${releases[0]}" --certification "\${certifications[0]}" --certification-attestation "\${certifications[0]}/verification.json" --registry "$ABSOLUTE_REGISTRY_MODULE")
23107
23245
  if [[ -n "$ABSOLUTE_RELEASE_CHANNEL" ]]; then
23108
23246
  args+=(--channel "$ABSOLUTE_RELEASE_CHANNEL")
23109
23247
  fi
@@ -23128,7 +23266,7 @@ var androidJob = (options) => {
23128
23266
  android:
23129
23267
  name: Signed Android release
23130
23268
  needs: validate
23131
- if: \${{ ${jobCondition("android")} }}
23269
+ if: \${{ ${jobCondition("android", options.includePublishing)} }}
23132
23270
  runs-on: ubuntu-latest
23133
23271
  environment: absolute-mobile-release
23134
23272
  permissions:
@@ -23219,6 +23357,11 @@ var iosJob = (options) => {
23219
23357
  chmod 600 "$APP_STORE_CONNECT_PRIVATE_KEY_PATH"
23220
23358
  fi` : "";
23221
23359
  const certificationSteps = options.includePublishing ? `
23360
+ - name: Install pinned Cosign for iOS certification
23361
+ if: inputs.publish
23362
+ uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6
23363
+ with:
23364
+ cosign-release: v3.1.2
23222
23365
  - name: Simulator-test and certify exact iOS release
23223
23366
  if: inputs.publish
23224
23367
  shell: bash
@@ -23235,6 +23378,12 @@ var iosJob = (options) => {
23235
23378
  "\${args[@]}"
23236
23379
  args=(bunx absolute mobile certify "$RELEASE_DIRECTORY" --evidence .absolutejs/mobile-ci/acceptance/ios --require simulator --outdir .absolutejs/mobile-ci/certifications/ios --json)
23237
23380
  "\${args[@]}" > .absolutejs/mobile-ci/ios-certification.json
23381
+ shopt -s nullglob
23382
+ certifications=(.absolutejs/mobile-ci/certifications/ios/*)
23383
+ if [[ "\${#certifications[@]}" -ne 1 ]]; then echo "Expected exactly one iOS certification." >&2; exit 1; fi
23384
+ CERTIFICATION_PATH="\${certifications[0]}/certification.json"
23385
+ bunx @absolutejs/attest@0.2.0 sign-blobs "$CERTIFICATION_PATH"
23386
+ bunx absolute mobile ci verification --bundle "$CERTIFICATION_PATH.sigstore.json" --out "\${certifications[0]}/verification.json"
23238
23387
  - name: Publish certified iOS release
23239
23388
  if: inputs.publish
23240
23389
  shell: bash
@@ -23246,7 +23395,7 @@ var iosJob = (options) => {
23246
23395
  echo "Expected exactly one iOS release and certification." >&2
23247
23396
  exit 1
23248
23397
  fi
23249
- args=(bunx absolute mobile publish ios --release "\${releases[0]}" --certification "\${certifications[0]}" --registry "$ABSOLUTE_REGISTRY_MODULE")
23398
+ args=(bunx absolute mobile publish ios --release "\${releases[0]}" --certification "\${certifications[0]}" --certification-attestation "\${certifications[0]}/verification.json" --registry "$ABSOLUTE_REGISTRY_MODULE")
23250
23399
  if [[ -n "$ABSOLUTE_RELEASE_CHANNEL" ]]; then
23251
23400
  args+=(--channel "$ABSOLUTE_RELEASE_CHANNEL")
23252
23401
  fi
@@ -23274,7 +23423,7 @@ var iosJob = (options) => {
23274
23423
  ios:
23275
23424
  name: Signed iOS release
23276
23425
  needs: validate
23277
- if: \${{ ${jobCondition("ios")} }}
23426
+ if: \${{ ${jobCondition("ios", options.includePublishing)} }}
23278
23427
  runs-on: macos-latest
23279
23428
  environment: absolute-mobile-release
23280
23429
  permissions:
@@ -23352,6 +23501,179 @@ ${certificationSteps}
23352
23501
  rm -f "\${{ runner.temp }}/absolute.mobileprovision"
23353
23502
  rm -f "\${{ runner.temp }}/AuthKey_AbsoluteJS.p8"`;
23354
23503
  };
23504
+ var promotionValidationSteps = ` - name: Validate promotion handoff
23505
+ if: github.event_name == 'workflow_dispatch' && inputs.operation == 'promote'
23506
+ shell: bash
23507
+ run: |
23508
+ if [[ "$ABSOLUTE_PROMOTION_PLATFORM" == "all" ]]; then
23509
+ echo "Promotion requires one exact platform." >&2
23510
+ exit 1
23511
+ fi
23512
+ if [[ ! "$ABSOLUTE_SOURCE_RUN_ID" =~ ^[1-9][0-9]*$ ]]; then
23513
+ echo "Promotion requires a numeric source workflow run ID." >&2
23514
+ exit 1
23515
+ fi
23516
+ if [[ -z "$ABSOLUTE_CERTIFICATION_BASE64" ]]; then
23517
+ echo "Promotion requires certification supplied by absolute mobile ci promote." >&2
23518
+ exit 1
23519
+ fi`;
23520
+ var promotionSharedSteps = (platform) => ` - name: Restore exact ${platform === "android" ? "Android" : "iOS"} release from source run
23521
+ uses: actions/download-artifact@v8
23522
+ with:
23523
+ name: absolute-mobile-${platform}
23524
+ path: .absolutejs/mobile/releases/${platform}
23525
+ run-id: \${{ inputs.source_run_id }}
23526
+ github-token: \${{ github.token }}
23527
+ - name: Import and verify exact certification
23528
+ shell: bash
23529
+ run: |
23530
+ mkdir -p .absolutejs/mobile-ci/imported-certification
23531
+ printf '%s' "$ABSOLUTE_CERTIFICATION_BASE64" | base64 --decode > .absolutejs/mobile-ci/imported-certification/certification.json
23532
+ shopt -s nullglob
23533
+ releases=(.absolutejs/mobile/releases/${platform}/*)
23534
+ if [[ "\${#releases[@]}" -ne 1 || ! -d "\${releases[0]}" ]]; then
23535
+ echo "Expected exactly one immutable ${platform} release directory from the source run." >&2
23536
+ exit 1
23537
+ fi
23538
+ args=(bunx absolute mobile certify "\${releases[0]}" --verify .absolutejs/mobile-ci/imported-certification/certification.json --json)
23539
+ "\${args[@]}" > .absolutejs/mobile-ci/imported-certification/verified.json
23540
+ - name: Install pinned Cosign
23541
+ uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6
23542
+ with:
23543
+ cosign-release: v3.1.2
23544
+ - name: Sign certification with GitHub OIDC
23545
+ shell: bash
23546
+ run: |
23547
+ bunx @absolutejs/attest@0.2.0 sign-blobs .absolutejs/mobile-ci/imported-certification/certification.json
23548
+ bunx absolute mobile ci verification --bundle .absolutejs/mobile-ci/imported-certification/certification.json.sigstore.json --out .absolutejs/mobile-ci/imported-certification/verification.json`;
23549
+ var androidPromotionJob = (options) => {
23550
+ const custom = customSecretEnvironment(options.customSecrets);
23551
+ return `
23552
+ promote_android:
23553
+ name: Promote exact certified Android release
23554
+ needs: validate
23555
+ if: \${{ github.event_name == 'workflow_dispatch' && inputs.operation == 'promote' && inputs.platform == 'android' }}
23556
+ runs-on: ubuntu-latest
23557
+ environment: absolute-mobile-release
23558
+ permissions:
23559
+ actions: read
23560
+ contents: read
23561
+ id-token: write
23562
+ env:
23563
+ ABSOLUTE_CERTIFICATION_BASE64: \${{ inputs.certification_base64 }}
23564
+ ABSOLUTE_RELEASE_CHANNEL: \${{ inputs.channel }}
23565
+ ABSOLUTE_PLAY_TRACK: \${{ inputs.play_track }}
23566
+ ABSOLUTE_GOOGLE_CREDENTIALS_BASE64: \${{ secrets.ABSOLUTE_GOOGLE_CREDENTIALS_BASE64 }}
23567
+ GOOGLE_APPLICATION_CREDENTIALS: \${{ runner.temp }}/absolute-google-credentials.json${custom ? `
23568
+ ${custom}` : ""}
23569
+ ${commandEnvironment({ configPath: undefined, registryModule: "", serverEntry: "" })}
23570
+ steps:
23571
+ ${installSteps}
23572
+ ${promotionSharedSteps("android")}
23573
+ - name: Publish restored Android release
23574
+ shell: bash
23575
+ run: |
23576
+ if [[ -n "$ABSOLUTE_RELEASE_CHANNEL" || "$ABSOLUTE_PLAY_TRACK" != "registry-only" ]]; then
23577
+ true
23578
+ else
23579
+ echo "Promotion requires a release channel or Google Play track." >&2
23580
+ exit 1
23581
+ fi
23582
+ if [[ "$ABSOLUTE_PLAY_TRACK" != "registry-only" ]]; then
23583
+ if [[ -z "$ABSOLUTE_GOOGLE_CREDENTIALS_BASE64" ]]; then
23584
+ echo "ABSOLUTE_GOOGLE_CREDENTIALS_BASE64 is required for Google Play publication." >&2
23585
+ exit 1
23586
+ fi
23587
+ printf '%s' "$ABSOLUTE_GOOGLE_CREDENTIALS_BASE64" | base64 --decode > "$GOOGLE_APPLICATION_CREDENTIALS"
23588
+ chmod 600 "$GOOGLE_APPLICATION_CREDENTIALS"
23589
+ fi
23590
+ shopt -s nullglob
23591
+ releases=(.absolutejs/mobile/releases/android/*)
23592
+ args=(bunx absolute mobile publish android --release "\${releases[0]}" --certification .absolutejs/mobile-ci/imported-certification/certification.json --certification-attestation .absolutejs/mobile-ci/imported-certification/verification.json --registry "$ABSOLUTE_REGISTRY_MODULE" --json)
23593
+ if [[ -n "$ABSOLUTE_RELEASE_CHANNEL" ]]; then args+=(--channel "$ABSOLUTE_RELEASE_CHANNEL"); fi
23594
+ if [[ "$ABSOLUTE_PLAY_TRACK" != "registry-only" ]]; then args+=(--play-track "$ABSOLUTE_PLAY_TRACK"); fi
23595
+ ${appendConfigArgument}
23596
+ "\${args[@]}" > .absolutejs/mobile-ci/promotion-receipt.json
23597
+ - name: Upload Android promotion audit
23598
+ if: always()
23599
+ uses: actions/upload-artifact@v7
23600
+ with:
23601
+ name: absolute-mobile-android-promotion-\${{ github.run_id }}
23602
+ path: |
23603
+ .absolutejs/mobile-ci/imported-certification/
23604
+ .absolutejs/mobile-ci/promotion-receipt.json
23605
+ if-no-files-found: error
23606
+ retention-days: 90
23607
+ include-hidden-files: true
23608
+ - name: Remove Google credentials
23609
+ if: always()
23610
+ run: rm -f "\${{ runner.temp }}/absolute-google-credentials.json"`;
23611
+ };
23612
+ var iosPromotionJob = (options) => {
23613
+ const custom = customSecretEnvironment(options.customSecrets);
23614
+ return `
23615
+ promote_ios:
23616
+ name: Promote exact certified iOS release
23617
+ needs: validate
23618
+ if: \${{ github.event_name == 'workflow_dispatch' && inputs.operation == 'promote' && inputs.platform == 'ios' }}
23619
+ runs-on: ubuntu-latest
23620
+ environment: absolute-mobile-release
23621
+ permissions:
23622
+ actions: read
23623
+ contents: read
23624
+ id-token: write
23625
+ env:
23626
+ ABSOLUTE_CERTIFICATION_BASE64: \${{ inputs.certification_base64 }}
23627
+ ABSOLUTE_RELEASE_CHANNEL: \${{ inputs.channel }}
23628
+ ABSOLUTE_TESTFLIGHT_GROUP: \${{ inputs.testflight_group }}
23629
+ ABSOLUTE_TESTFLIGHT_SUBMIT_REVIEW: \${{ inputs.submit_testflight_review }}
23630
+ APP_STORE_CONNECT_ISSUER_ID: \${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
23631
+ APP_STORE_CONNECT_KEY_ID: \${{ secrets.APP_STORE_CONNECT_KEY_ID }}
23632
+ APP_STORE_CONNECT_PRIVATE_KEY_BASE64: \${{ secrets.APP_STORE_CONNECT_PRIVATE_KEY_BASE64 }}
23633
+ APP_STORE_CONNECT_PRIVATE_KEY_PATH: \${{ runner.temp }}/AuthKey_AbsoluteJS.p8${custom ? `
23634
+ ${custom}` : ""}
23635
+ ${commandEnvironment({ configPath: undefined, registryModule: "", serverEntry: "" })}
23636
+ steps:
23637
+ ${installSteps}
23638
+ ${promotionSharedSteps("ios")}
23639
+ - name: Publish restored iOS release
23640
+ shell: bash
23641
+ run: |
23642
+ if [[ -z "$ABSOLUTE_RELEASE_CHANNEL" && -z "$ABSOLUTE_TESTFLIGHT_GROUP" ]]; then
23643
+ echo "Promotion requires a release channel or TestFlight group." >&2
23644
+ exit 1
23645
+ fi
23646
+ if [[ -n "$ABSOLUTE_TESTFLIGHT_GROUP" ]]; then
23647
+ required=(APP_STORE_CONNECT_ISSUER_ID APP_STORE_CONNECT_KEY_ID APP_STORE_CONNECT_PRIVATE_KEY_BASE64)
23648
+ for name in "\${required[@]}"; do
23649
+ if [[ -z "\${!name}" ]]; then echo "$name is required for TestFlight publication." >&2; exit 1; fi
23650
+ done
23651
+ printf '%s' "$APP_STORE_CONNECT_PRIVATE_KEY_BASE64" | base64 --decode > "$APP_STORE_CONNECT_PRIVATE_KEY_PATH"
23652
+ chmod 600 "$APP_STORE_CONNECT_PRIVATE_KEY_PATH"
23653
+ fi
23654
+ shopt -s nullglob
23655
+ releases=(.absolutejs/mobile/releases/ios/*)
23656
+ args=(bunx absolute mobile publish ios --release "\${releases[0]}" --certification .absolutejs/mobile-ci/imported-certification/certification.json --certification-attestation .absolutejs/mobile-ci/imported-certification/verification.json --registry "$ABSOLUTE_REGISTRY_MODULE" --json)
23657
+ if [[ -n "$ABSOLUTE_RELEASE_CHANNEL" ]]; then args+=(--channel "$ABSOLUTE_RELEASE_CHANNEL"); fi
23658
+ if [[ -n "$ABSOLUTE_TESTFLIGHT_GROUP" ]]; then args+=(--testflight-group "$ABSOLUTE_TESTFLIGHT_GROUP"); fi
23659
+ if [[ "$ABSOLUTE_TESTFLIGHT_SUBMIT_REVIEW" == "true" ]]; then args+=(--testflight-submit-review); fi
23660
+ ${appendConfigArgument}
23661
+ "\${args[@]}" > .absolutejs/mobile-ci/promotion-receipt.json
23662
+ - name: Upload iOS promotion audit
23663
+ if: always()
23664
+ uses: actions/upload-artifact@v7
23665
+ with:
23666
+ name: absolute-mobile-ios-promotion-\${{ github.run_id }}
23667
+ path: |
23668
+ .absolutejs/mobile-ci/imported-certification/
23669
+ .absolutejs/mobile-ci/promotion-receipt.json
23670
+ if-no-files-found: error
23671
+ retention-days: 90
23672
+ include-hidden-files: true
23673
+ - name: Remove App Store Connect credentials
23674
+ if: always()
23675
+ run: rm -f "\${{ runner.temp }}/AuthKey_AbsoluteJS.p8"`;
23676
+ };
23355
23677
  var requiredSecrets = (platforms, includePublishing, custom) => [
23356
23678
  ...platforms.includes("android") ? [
23357
23679
  "ABSOLUTE_ANDROID_KEYSTORE_BASE64",
@@ -23413,10 +23735,14 @@ jobs:
23413
23735
  permissions:
23414
23736
  contents: read
23415
23737
  env:
23416
- ${environment}
23738
+ ${environment}${includePublishing ? `
23739
+ ABSOLUTE_PROMOTION_PLATFORM: \${{ inputs.platform }}
23740
+ ABSOLUTE_SOURCE_RUN_ID: \${{ inputs.source_run_id }}
23741
+ ABSOLUTE_CERTIFICATION_BASE64: \${{ inputs.certification_base64 }}` : ""}
23417
23742
  steps:
23418
- ${installSteps}
23419
- ${bundleAuditSteps}${platforms.includes("android") ? androidJob({ customSecrets, includePublishing }) : ""}${platforms.includes("ios") ? iosJob({ customSecrets, includePublishing }) : ""}
23743
+ ${installSteps}${includePublishing ? `
23744
+ ${promotionValidationSteps}` : ""}
23745
+ ${bundleAuditSteps}${platforms.includes("android") ? androidJob({ customSecrets, includePublishing }) : ""}${platforms.includes("ios") ? iosJob({ customSecrets, includePublishing }) : ""}${includePublishing && platforms.includes("android") ? androidPromotionJob({ customSecrets, includePublishing }) : ""}${includePublishing && platforms.includes("ios") ? iosPromotionJob({ customSecrets, includePublishing }) : ""}
23420
23746
  `;
23421
23747
  const replacements = new Map([
23422
23748
  [
@@ -23442,12 +23768,12 @@ ${bundleAuditSteps}${platforms.includes("android") ? androidJob({ customSecrets,
23442
23768
  var writeAbsoluteMobileGithubWorkflow = async (options) => {
23443
23769
  const path = workflowOutputPath(options.projectRoot, options.outputPath);
23444
23770
  const generated = createAbsoluteMobileGithubWorkflow(options);
23445
- const previous = await exists5(path) ? await readFile17(path, "utf8") : undefined;
23771
+ const previous = await exists5(path) ? await readFile18(path, "utf8") : undefined;
23446
23772
  if (previous !== undefined && previous !== generated.workflow && !options.force)
23447
- throw new TypeError(`${relative10(options.projectRoot, path)} already exists and differs. Rerun with --force to replace the generated workflow.`);
23773
+ throw new TypeError(`${relative11(options.projectRoot, path)} already exists and differs. Rerun with --force to replace the generated workflow.`);
23448
23774
  if (previous !== generated.workflow) {
23449
- await mkdir12(dirname10(path), { recursive: true });
23450
- await writeFile13(path, generated.workflow);
23775
+ await mkdir13(dirname11(path), { recursive: true });
23776
+ await writeFile14(path, generated.workflow);
23451
23777
  }
23452
23778
  return {
23453
23779
  changed: previous !== generated.workflow,
@@ -23463,16 +23789,16 @@ var writeAbsoluteMobileGithubWorkflow = async (options) => {
23463
23789
  import { createHash as createHash6, sign, verify } from "crypto";
23464
23790
  import {
23465
23791
  cp,
23466
- mkdir as mkdir13,
23792
+ mkdir as mkdir14,
23467
23793
  mkdtemp as mkdtemp5,
23468
23794
  readdir as readdir6,
23469
- readFile as readFile18,
23795
+ readFile as readFile19,
23470
23796
  rename as rename11,
23471
23797
  rm as rm8,
23472
- stat as stat6,
23473
- writeFile as writeFile14
23798
+ stat as stat7,
23799
+ writeFile as writeFile15
23474
23800
  } from "fs/promises";
23475
- import { dirname as dirname11, join as join15, relative as relative11, resolve as resolve12 } from "path";
23801
+ import { dirname as dirname12, join as join15, relative as relative12, resolve as resolve13 } from "path";
23476
23802
  var UPDATE_MANIFEST_FILE = "update.json";
23477
23803
  var UPDATE_FILES_DIRECTORY = "files";
23478
23804
  var sha2564 = (value) => createHash6("sha256").update(value).digest("hex");
@@ -23484,22 +23810,22 @@ var listFiles = async (root, directory = root) => {
23484
23810
  return listFiles(root, path);
23485
23811
  if (!entry.isFile())
23486
23812
  throw new TypeError("Mobile updates cannot contain links or special files.");
23487
- return [relative11(root, path).replaceAll("\\", "/")];
23813
+ return [relative12(root, path).replaceAll("\\", "/")];
23488
23814
  }));
23489
23815
  return paths.flat().sort((left, right) => left.localeCompare(right));
23490
23816
  };
23491
23817
  var inspectFiles = async (root, paths) => Promise.all(paths.map(async (path) => {
23492
23818
  const source = join15(root, path);
23493
23819
  const [metadata, contents] = await Promise.all([
23494
- stat6(source),
23495
- readFile18(source)
23820
+ stat7(source),
23821
+ readFile19(source)
23496
23822
  ]);
23497
23823
  return { bytes: metadata.size, path, sha256: sha2564(contents) };
23498
23824
  }));
23499
23825
  var releaseIdFor = (value) => `amu_${sha2564(canonicalizeAbsoluteMobileUpdate(value))}`;
23500
23826
  var buildAbsoluteMobileUpdate = async (options) => {
23501
- const bundleDirectory = resolve12(options.bundleDirectory);
23502
- const outputRoot = resolve12(options.outputDirectory);
23827
+ const bundleDirectory = resolve13(options.bundleDirectory);
23828
+ const outputRoot = resolve13(options.outputDirectory);
23503
23829
  if (outputRoot === bundleDirectory || outputRoot.startsWith(`${bundleDirectory}/`))
23504
23830
  throw new TypeError("Mobile update output must be outside the embedded bundle.");
23505
23831
  const paths = await listFiles(bundleDirectory);
@@ -23527,7 +23853,7 @@ var buildAbsoluteMobileUpdate = async (options) => {
23527
23853
  value: signature.toString("base64")
23528
23854
  }
23529
23855
  });
23530
- await mkdir13(outputRoot, { recursive: true });
23856
+ await mkdir14(outputRoot, { recursive: true });
23531
23857
  const outputDirectory = join15(outputRoot, manifest.releaseId);
23532
23858
  const staging = await mkdtemp5(join15(outputRoot, ".stage-"));
23533
23859
  try {
@@ -23535,7 +23861,7 @@ var buildAbsoluteMobileUpdate = async (options) => {
23535
23861
  force: true,
23536
23862
  recursive: true
23537
23863
  });
23538
- await writeFile14(join15(staging, UPDATE_MANIFEST_FILE), `${JSON.stringify(manifest, null, "\t")}
23864
+ await writeFile15(join15(staging, UPDATE_MANIFEST_FILE), `${JSON.stringify(manifest, null, "\t")}
23539
23865
  `);
23540
23866
  await rename11(staging, outputDirectory);
23541
23867
  } catch (error) {
@@ -23550,7 +23876,7 @@ var buildAbsoluteMobileUpdate = async (options) => {
23550
23876
  outputDirectory
23551
23877
  };
23552
23878
  };
23553
- var readAbsoluteMobileUpdate = async (directory) => parseAbsoluteMobileUpdateManifest(JSON.parse(await readFile18(join15(resolve12(directory), UPDATE_MANIFEST_FILE), "utf8")));
23879
+ var readAbsoluteMobileUpdate = async (directory) => parseAbsoluteMobileUpdateManifest(JSON.parse(await readFile19(join15(resolve13(directory), UPDATE_MANIFEST_FILE), "utf8")));
23554
23880
  var verifyAbsoluteMobileUpdateSignature = (manifestValue, publicKey) => {
23555
23881
  const manifest = parseAbsoluteMobileUpdateManifest(manifestValue);
23556
23882
  const valid = verify("sha256", absoluteMobileUpdateSigningPayload(unsignedAbsoluteMobileUpdate(manifest)), { dsaEncoding: "ieee-p1363", key: publicKey }, Buffer.from(manifest.signature.value, "base64"));
@@ -23560,8 +23886,8 @@ var verifyAbsoluteMobileUpdateSignature = (manifestValue, publicKey) => {
23560
23886
  };
23561
23887
 
23562
23888
  // src/mobile/expoUpdate.ts
23563
- import { access as access12, mkdir as mkdir14, readFile as readFile19, writeFile as writeFile15 } from "fs/promises";
23564
- import { dirname as dirname12, extname as extname3, join as join16 } from "path";
23889
+ import { access as access13, mkdir as mkdir15, readFile as readFile20, writeFile as writeFile16 } from "fs/promises";
23890
+ import { dirname as dirname13, extname as extname3, join as join16 } from "path";
23565
23891
  var ABSOLUTE_EXPO_UPDATE_DESCRIPTOR = "_absolute/expo-update.json";
23566
23892
  var ABSOLUTE_EXPO_UPDATE_FORMAT = 1;
23567
23893
  var object2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
@@ -23627,7 +23953,7 @@ var parseAbsoluteExpoUpdateDescriptor = (value) => {
23627
23953
  };
23628
23954
  };
23629
23955
  var finalizeAbsoluteExpoUpdateExport = async (options) => {
23630
- const metadata = JSON.parse(await readFile19(join16(options.exportDirectory, "metadata.json"), "utf8"));
23956
+ const metadata = JSON.parse(await readFile20(join16(options.exportDirectory, "metadata.json"), "utf8"));
23631
23957
  if (!object2(metadata) || !object2(metadata.fileMetadata))
23632
23958
  throw new TypeError("Expo export metadata.json is invalid.");
23633
23959
  const descriptor = parseAbsoluteExpoUpdateDescriptor({
@@ -23638,30 +23964,30 @@ var finalizeAbsoluteExpoUpdateExport = async (options) => {
23638
23964
  runtimeVersion: options.runtimeVersion
23639
23965
  });
23640
23966
  const referenced = Object.values(descriptor.platforms).flatMap((entry) => entry ? [entry.launchAsset, ...entry.assets] : []);
23641
- await Promise.all(referenced.map(({ path }) => access12(join16(options.exportDirectory, path)).catch(() => {
23967
+ await Promise.all(referenced.map(({ path }) => access13(join16(options.exportDirectory, path)).catch(() => {
23642
23968
  throw new TypeError(`Expo export metadata references missing asset ${path}.`);
23643
23969
  })));
23644
23970
  const destination = join16(options.exportDirectory, ABSOLUTE_EXPO_UPDATE_DESCRIPTOR);
23645
- await mkdir14(dirname12(destination), { recursive: true });
23646
- await writeFile15(destination, `${JSON.stringify(descriptor, null, "\t")}
23971
+ await mkdir15(dirname13(destination), { recursive: true });
23972
+ await writeFile16(destination, `${JSON.stringify(descriptor, null, "\t")}
23647
23973
  `);
23648
23974
  return { descriptor, path: destination };
23649
23975
  };
23650
23976
 
23651
23977
  // src/mobile/expoCodeSigning.ts
23652
23978
  var import_code_signing_certificates = __toESM(require_main(), 1);
23653
- import { access as access13, mkdir as mkdir15, writeFile as writeFile16 } from "fs/promises";
23654
- import { dirname as dirname13, isAbsolute as isAbsolute7, relative as relative12, resolve as resolve13, sep as sep8 } from "path";
23655
- var exists6 = async (path) => access13(path).then(() => true).catch(() => false);
23979
+ import { access as access14, mkdir as mkdir16, writeFile as writeFile17 } from "fs/promises";
23980
+ import { dirname as dirname14, isAbsolute as isAbsolute8, relative as relative13, resolve as resolve14, sep as sep9 } from "path";
23981
+ var exists6 = async (path) => access14(path).then(() => true).catch(() => false);
23656
23982
  var inside4 = (root, path) => {
23657
- const location = relative12(root, path);
23658
- return location === "" || location !== ".." && !location.startsWith(`..${sep8}`) && !isAbsolute7(location);
23983
+ const location = relative13(root, path);
23984
+ return location === "" || location !== ".." && !location.startsWith(`..${sep9}`) && !isAbsolute8(location);
23659
23985
  };
23660
23986
  var generateAbsoluteExpoCodeSigning = async (options) => {
23661
- const root = resolve13(options.projectRoot);
23662
- const certificatePath = resolve13(root, options.certificatePath);
23663
- const privateKeyPath = resolve13(root, options.privateKeyPath);
23664
- const publicKeyPath = resolve13(root, options.publicKeyPath ?? resolve13(dirname13(privateKeyPath), "public-key.pem"));
23987
+ const root = resolve14(options.projectRoot);
23988
+ const certificatePath = resolve14(root, options.certificatePath);
23989
+ const privateKeyPath = resolve14(root, options.privateKeyPath);
23990
+ const publicKeyPath = resolve14(root, options.publicKeyPath ?? resolve14(dirname14(privateKeyPath), "public-key.pem"));
23665
23991
  if (!inside4(root, certificatePath))
23666
23992
  throw new TypeError("Expo code-signing certificate must be written inside the project so store builds can embed it.");
23667
23993
  if (inside4(root, privateKeyPath) || inside4(root, publicKeyPath))
@@ -23691,11 +24017,11 @@ var generateAbsoluteExpoCodeSigning = async (options) => {
23691
24017
  import_code_signing_certificates.validateSelfSignedCertificate(certificate, keyPair);
23692
24018
  const { privateKeyPEM, publicKeyPEM } = import_code_signing_certificates.convertKeyPairToPEM(keyPair);
23693
24019
  const certificatePem = import_code_signing_certificates.convertCertificateToCertificatePEM(certificate);
23694
- await Promise.all([certificatePath, privateKeyPath, publicKeyPath].map((path) => mkdir15(dirname13(path), { recursive: true })));
24020
+ await Promise.all([certificatePath, privateKeyPath, publicKeyPath].map((path) => mkdir16(dirname14(path), { recursive: true })));
23695
24021
  await Promise.all([
23696
- writeFile16(certificatePath, certificatePem, { flag: "wx", mode: 420 }),
23697
- writeFile16(privateKeyPath, privateKeyPEM, { flag: "wx", mode: 384 }),
23698
- writeFile16(publicKeyPath, publicKeyPEM, { flag: "wx", mode: 420 })
24022
+ writeFile17(certificatePath, certificatePem, { flag: "wx", mode: 420 }),
24023
+ writeFile17(privateKeyPath, privateKeyPEM, { flag: "wx", mode: 384 }),
24024
+ writeFile17(publicKeyPath, publicKeyPEM, { flag: "wx", mode: 420 })
23699
24025
  ]);
23700
24026
  return {
23701
24027
  certificatePath,
@@ -23706,8 +24032,8 @@ var generateAbsoluteExpoCodeSigning = async (options) => {
23706
24032
  };
23707
24033
 
23708
24034
  // src/mobile/updatePublisher.ts
23709
- import { access as access14 } from "fs/promises";
23710
- import { isAbsolute as isAbsolute8, relative as relative13, resolve as resolve14, sep as sep9 } from "path";
24035
+ import { access as access15 } from "fs/promises";
24036
+ import { isAbsolute as isAbsolute9, relative as relative14, resolve as resolve15, sep as sep10 } from "path";
23711
24037
  import { pathToFileURL as pathToFileURL3 } from "url";
23712
24038
  var lifecycleMethod = (publisher, name) => {
23713
24039
  const method = publisher[name];
@@ -23816,16 +24142,16 @@ var pruneAbsoluteMobileUpdates = async (options) => {
23816
24142
  var object3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
23817
24143
  var isPublisher2 = (value) => object3(value) && typeof value.publishUpdate === "function" && typeof value.promoteUpdate === "function" && typeof value.rollbackUpdate === "function";
23818
24144
  var projectPath3 = (projectRoot, requested, label) => {
23819
- const root = resolve14(projectRoot);
23820
- const path = resolve14(root, requested);
23821
- const projectRelative = relative13(root, path);
23822
- if (projectRelative === ".." || projectRelative.startsWith(`..${sep9}`) || isAbsolute8(projectRelative))
24145
+ const root = resolve15(projectRoot);
24146
+ const path = resolve15(root, requested);
24147
+ const projectRelative = relative14(root, path);
24148
+ if (projectRelative === ".." || projectRelative.startsWith(`..${sep10}`) || isAbsolute9(projectRelative))
23823
24149
  throw new TypeError(`${label} must remain inside the project.`);
23824
24150
  return path;
23825
24151
  };
23826
24152
  var loadAbsoluteMobileUpdatePublisher = async (projectRoot, requestedModulePath) => {
23827
24153
  const modulePath = projectPath3(projectRoot, requestedModulePath, "mobile update registry");
23828
- await access14(modulePath).catch(() => {
24154
+ await access15(modulePath).catch(() => {
23829
24155
  throw new TypeError(`Mobile update registry does not exist: ${modulePath}`);
23830
24156
  });
23831
24157
  const loaded = await import(pathToFileURL3(modulePath).href);
@@ -23880,10 +24206,10 @@ var rollbackAbsoluteMobileUpdate = async (options) => {
23880
24206
  var NOT_FOUND4 = -1;
23881
24207
  var ANDROID_EMULATOR_BOOT_POLL_MS = 500;
23882
24208
  var ANDROID_EMULATOR_BOOT_TIMEOUT_MS = 180000;
23883
- var isRecord8 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
24209
+ var isRecord9 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
23884
24210
  var expoPublicConfig = (config) => {
23885
24211
  const result = { ...config };
23886
- if (!isRecord8(result.updates))
24212
+ if (!isRecord9(result.updates))
23887
24213
  return result;
23888
24214
  const {
23889
24215
  codeSigningCertificate: _codeSigningCertificate,
@@ -23923,13 +24249,13 @@ var CAPACITOR_SYNC_PACKAGE_SPECS = [
23923
24249
  ];
23924
24250
  var packageNameFromSpec = (spec) => spec.slice(0, spec.lastIndexOf("@"));
23925
24251
  var directProjectPackages = async (projectRoot) => {
23926
- const manifest = JSON.parse(await readFile20(join17(projectRoot, "package.json"), "utf8"));
23927
- if (!isRecord8(manifest))
24252
+ const manifest = JSON.parse(await readFile21(join17(projectRoot, "package.json"), "utf8"));
24253
+ if (!isRecord9(manifest))
23928
24254
  throw new TypeError("Application package.json must contain an object.");
23929
24255
  const names = new Set;
23930
24256
  for (const field2 of ["dependencies", "devDependencies"]) {
23931
24257
  const dependencies = Reflect.get(manifest, field2);
23932
- if (isRecord8(dependencies))
24258
+ if (isRecord9(dependencies))
23933
24259
  for (const name of Object.keys(dependencies))
23934
24260
  names.add(name);
23935
24261
  }
@@ -23937,8 +24263,8 @@ var directProjectPackages = async (projectRoot) => {
23937
24263
  };
23938
24264
  var resolvedPackageVersion = async (projectRoot, packageName) => {
23939
24265
  try {
23940
- const manifest = JSON.parse(await readFile20(join17(projectRoot, "node_modules", packageName, "package.json"), "utf8"));
23941
- return isRecord8(manifest) && typeof manifest.version === "string" ? manifest.version : undefined;
24266
+ const manifest = JSON.parse(await readFile21(join17(projectRoot, "node_modules", packageName, "package.json"), "utf8"));
24267
+ return isRecord9(manifest) && typeof manifest.version === "string" ? manifest.version : undefined;
23942
24268
  } catch {
23943
24269
  return;
23944
24270
  }
@@ -24003,7 +24329,7 @@ var runCapacitorForPlatforms = (projectRoot, command, platforms) => platforms.re
24003
24329
  var expoExecutable = async (project) => {
24004
24330
  const executable = join17(project, "node_modules", ".bin", "expo");
24005
24331
  try {
24006
- await access15(executable);
24332
+ await access16(executable);
24007
24333
  return executable;
24008
24334
  } catch {
24009
24335
  throw new TypeError("Expo dependencies are not installed in the generated shell. Run `absolute mobile init --yes`.");
@@ -24035,14 +24361,14 @@ var runExpo = async (project, args, options = {}) => {
24035
24361
  };
24036
24362
  var ensureExpoPackages = async (project, args) => {
24037
24363
  try {
24038
- const manifest = JSON.parse(await readFile20(join17(project, "package.json"), "utf8"));
24364
+ const manifest = JSON.parse(await readFile21(join17(project, "package.json"), "utf8"));
24039
24365
  const dependencies = typeof manifest === "object" && manifest !== null ? Reflect.get(manifest, "dependencies") : undefined;
24040
24366
  if (typeof dependencies !== "object" || dependencies === null || Array.isArray(dependencies))
24041
24367
  throw new TypeError("Generated Expo dependencies are invalid.");
24042
24368
  await Promise.all(Object.entries(dependencies).map(async ([name, expected]) => {
24043
24369
  if (typeof expected !== "string")
24044
24370
  throw new TypeError("Generated Expo dependency version is invalid.");
24045
- const installed = JSON.parse(await readFile20(join17(project, "node_modules", name, "package.json"), "utf8"));
24371
+ const installed = JSON.parse(await readFile21(join17(project, "node_modules", name, "package.json"), "utf8"));
24046
24372
  const actual = typeof installed === "object" && installed !== null ? Reflect.get(installed, "version") : undefined;
24047
24373
  if (typeof actual !== "string")
24048
24374
  throw new TypeError("Installed Expo dependency version is invalid.");
@@ -24240,7 +24566,7 @@ var sync = async (args) => {
24240
24566
  };
24241
24567
  var associations = async (args) => {
24242
24568
  const { mobile, projectRoot } = await loadMobile(valueAfter(args, "--config"));
24243
- const outputDirectory = resolve15(projectRoot, valueAfter(args, "--outdir") ?? ".absolutejs/mobile/associations");
24569
+ const outputDirectory = resolve16(projectRoot, valueAfter(args, "--outdir") ?? ".absolutejs/mobile/associations");
24244
24570
  if (args.includes("--verify")) {
24245
24571
  const result2 = await verifyAbsoluteMobileAssociationFiles(mobile);
24246
24572
  console.log(`Verified ${result2.results.length} hosted association files`);
@@ -24292,7 +24618,7 @@ var generateGithubCi = async (args) => {
24292
24618
  const publicResult = {
24293
24619
  changed: result.changed,
24294
24620
  format: result.format,
24295
- path: relative14(projectRoot, result.path).replaceAll("\\", "/"),
24621
+ path: relative15(projectRoot, result.path).replaceAll("\\", "/"),
24296
24622
  platforms: result.platforms,
24297
24623
  publishing: result.publishing,
24298
24624
  requiredSecrets: result.requiredSecrets
@@ -24305,8 +24631,91 @@ var generateGithubCi = async (args) => {
24305
24631
  console.log("Create a protected GitHub environment named absolute-mobile-release, then add these secrets:");
24306
24632
  result.requiredSecrets.forEach((name) => console.log(` ${name}`));
24307
24633
  console.log("Pull requests run the secret-free release audit. Signed builds and optional publishing run only through manual workflow dispatch.");
24634
+ if (result.publishing)
24635
+ console.log("After later installed-app certification, run `absolute mobile ci promote <platform> --run-id <source-run> --certification <path> ...` to publish the exact retained artifact without rebuilding.");
24308
24636
  return publicResult;
24309
24637
  };
24638
+ var createGithubCertificationVerification = async (args) => {
24639
+ const bundlePath = valueAfter(args, "--bundle");
24640
+ const outputPath = valueAfter(args, "--out");
24641
+ if (!bundlePath || bundlePath.startsWith("-") || !outputPath || outputPath.startsWith("-"))
24642
+ throw new TypeError("Usage: absolute mobile ci verification --bundle bundle.json --out verification.json");
24643
+ const projectRoot = process.cwd();
24644
+ const bundle = await readAbsoluteSigstoreBundle(projectRoot, bundlePath);
24645
+ const verification = createAbsoluteMobileCertificationVerification(bundle, {
24646
+ GITHUB_REF: process.env.GITHUB_REF,
24647
+ GITHUB_REPOSITORY: process.env.GITHUB_REPOSITORY,
24648
+ GITHUB_SHA: process.env.GITHUB_SHA,
24649
+ GITHUB_WORKFLOW_REF: process.env.GITHUB_WORKFLOW_REF
24650
+ });
24651
+ const path = await writeAbsoluteMobileCertificationVerification(projectRoot, outputPath, verification);
24652
+ console.log(relative15(projectRoot, path).replaceAll("\\", "/"));
24653
+ return verification;
24654
+ };
24655
+ var dispatchGithubMobilePromotion = async (args) => {
24656
+ const [platform2] = args;
24657
+ if (platform2 !== "android" && platform2 !== "ios")
24658
+ throw new TypeError("Usage: absolute mobile ci promote <android|ios> --run-id id --certification directory-or-json [--channel name] [store options]");
24659
+ const runId = valueAfter(args, "--run-id");
24660
+ if (!runId || !/^[1-9][0-9]*$/u.test(runId))
24661
+ throw new TypeError("mobile ci promote requires a numeric --run-id.");
24662
+ const requestedCertification = valueAfter(args, "--certification");
24663
+ if (!requestedCertification || requestedCertification.startsWith("-"))
24664
+ throw new TypeError("mobile ci promote requires --certification <directory-or-json>.");
24665
+ const configPath = valueAfter(args, "--config");
24666
+ const { mobile, projectRoot } = await loadMobile(configPath);
24667
+ const { certification } = await readAbsoluteMobileReleaseCertification(projectRoot, requestedCertification);
24668
+ if (certification.release.platform !== platform2 || certification.release.appId !== mobile.appId || certification.release.engine !== mobile.engine)
24669
+ throw new TypeError("The certification does not match this application platform, app ID, or engine.");
24670
+ const channel = valueAfter(args, "--channel") ?? "";
24671
+ const playTrack = valueAfter(args, "--play-track") ?? "registry-only";
24672
+ const testflightGroup = valueAfter(args, "--testflight-group") ?? "";
24673
+ if (channel === "" && (platform2 === "android" ? playTrack === "registry-only" : testflightGroup === ""))
24674
+ throw new TypeError("mobile ci promote requires --channel or a platform store target.");
24675
+ const certificationBase64 = Buffer.from(serializeAbsoluteMobileReleaseCertification(certification)).toString("base64");
24676
+ if (certificationBase64.length > 48000)
24677
+ throw new TypeError("Mobile certification is too large for protected GitHub dispatch.");
24678
+ const workflow = valueAfter(args, "--workflow") ?? ".github/workflows/absolute-mobile.yml";
24679
+ const command = [
24680
+ "workflow",
24681
+ "run",
24682
+ workflow,
24683
+ "-f",
24684
+ "operation=promote",
24685
+ "-f",
24686
+ `platform=${platform2}`,
24687
+ "-f",
24688
+ `source_run_id=${runId}`,
24689
+ "-f",
24690
+ `certification_base64=${certificationBase64}`,
24691
+ "-f",
24692
+ `channel=${channel}`
24693
+ ];
24694
+ if (platform2 === "android")
24695
+ command.push("-f", `play_track=${playTrack}`);
24696
+ else {
24697
+ command.push("-f", `testflight_group=${testflightGroup}`);
24698
+ command.push("-f", `submit_testflight_review=${args.includes("--testflight-submit-review")}`);
24699
+ }
24700
+ const ref = valueAfter(args, "--ref");
24701
+ if (ref)
24702
+ command.push("--ref", ref);
24703
+ const child = Bun.spawn(["gh", ...command], {
24704
+ cwd: projectRoot,
24705
+ stderr: "inherit",
24706
+ stdin: "inherit",
24707
+ stdout: "inherit"
24708
+ });
24709
+ const exitCode = await child.exited;
24710
+ if (exitCode !== 0)
24711
+ throw new TypeError(`GitHub promotion dispatch exited with status ${exitCode}.`);
24712
+ sendTelemetryEvent("mobile:ci-promotion-dispatched", {
24713
+ platform: platform2,
24714
+ provider: "github-actions",
24715
+ storeTarget: platform2 === "android" ? playTrack !== "registry-only" : testflightGroup !== ""
24716
+ });
24717
+ console.log(`Dispatched protected ${platform2} promotion for ${certification.release.releaseId} from workflow run ${runId}.`);
24718
+ };
24310
24719
  var doctorMark = (status) => {
24311
24720
  if (status === "pass")
24312
24721
  return "\x1B[32m\u2713\x1B[0m";
@@ -24404,7 +24813,7 @@ var prepareExpoMobileUpdateExport = async (options) => {
24404
24813
  };
24405
24814
  await ensureExpoPackages(mobile.nativeProjectDirectory, options.args);
24406
24815
  const temporaryParent = join17(options.projectRoot, ".absolutejs", "mobile", "expo-update-exports");
24407
- await mkdir16(temporaryParent, { recursive: true });
24816
+ await mkdir17(temporaryParent, { recursive: true });
24408
24817
  const temporaryDirectory = await mkdtemp6(join17(temporaryParent, ".stage-"));
24409
24818
  try {
24410
24819
  await runExpo(mobile.nativeProjectDirectory, [
@@ -24417,9 +24826,9 @@ var prepareExpoMobileUpdateExport = async (options) => {
24417
24826
  temporaryDirectory,
24418
24827
  "--clear"
24419
24828
  ], { production: true });
24420
- const generatedApp = JSON.parse(await readFile20(join17(mobile.nativeProjectDirectory, "app.json"), "utf8"));
24421
- const expoConfig = isRecord8(generatedApp) ? generatedApp.expo : undefined;
24422
- if (!isRecord8(expoConfig))
24829
+ const generatedApp = JSON.parse(await readFile21(join17(mobile.nativeProjectDirectory, "app.json"), "utf8"));
24830
+ const expoConfig = isRecord9(generatedApp) ? generatedApp.expo : undefined;
24831
+ if (!isRecord9(expoConfig))
24423
24832
  throw new TypeError("Generated Expo app configuration is invalid.");
24424
24833
  const publicExpoConfig = expoPublicConfig(expoConfig);
24425
24834
  const computedRuntime = resolveAbsoluteMobileUpdateRuntime(mobile, options.projectRoot).fingerprint;
@@ -24460,11 +24869,11 @@ var buildMobileUpdate = async (args) => {
24460
24869
  let expoExportDirectory;
24461
24870
  try {
24462
24871
  await start(mobileBuildServerEntry(args), valueAfter(args, "--web-outdir"), configPath, { prepareOnly: true });
24463
- const embedded = JSON.parse(await readFile20(join17(mobile.bundleDirectory, "absolute-mobile-manifest.json"), "utf8"));
24464
- const runtimeFingerprint = isRecord8(embedded) ? embedded.nativeRuntime : undefined;
24872
+ const embedded = JSON.parse(await readFile21(join17(mobile.bundleDirectory, "absolute-mobile-manifest.json"), "utf8"));
24873
+ const runtimeFingerprint = isRecord9(embedded) ? embedded.nativeRuntime : undefined;
24465
24874
  if (typeof runtimeFingerprint !== "string" || !/^[a-f0-9]{64}$/u.test(runtimeFingerprint))
24466
24875
  throw new TypeError("Prepared mobile bundle is missing its native runtime fingerprint.");
24467
- const privateKey = await readFile20(resolve15(projectRoot, signingKeyPath));
24876
+ const privateKey = await readFile21(resolve16(projectRoot, signingKeyPath));
24468
24877
  const configuredPublicKey = Buffer.from(mobile.updates.publicKeys[keyId], "base64");
24469
24878
  const derivedPublicKey = createPublicKey2(privateKey).export({
24470
24879
  format: "der",
@@ -24527,7 +24936,7 @@ var generateExpoUpdateSigning = async (args) => {
24527
24936
  ...valueAfter(args, "--public-key") ? { publicKeyPath: valueAfter(args, "--public-key") } : {},
24528
24937
  ...validityYears === undefined ? {} : { validityYears }
24529
24938
  });
24530
- const certificatePath = relative14(projectRoot, result.certificatePath).replaceAll("\\", "/");
24939
+ const certificatePath = relative15(projectRoot, result.certificatePath).replaceAll("\\", "/");
24531
24940
  sendTelemetryEvent("mobile:update-code-signing-generated", {
24532
24941
  engine: "expo",
24533
24942
  validityYears: validityYears ?? 10
@@ -24589,7 +24998,7 @@ var provisionMobileUpdate = async (args) => {
24589
24998
  publicKeys: mobile.updates.publicKeys,
24590
24999
  storage: requestedStorage
24591
25000
  });
24592
- console.log(`Provisioned ${requestedStorage === "local" ? "local development" : "durable S3-compatible"} mobile update storage in ${relative14(projectRoot, path).replaceAll("\\", "/")}.`);
25001
+ console.log(`Provisioned ${requestedStorage === "local" ? "local development" : "durable S3-compatible"} mobile update storage in ${relative15(projectRoot, path).replaceAll("\\", "/")}.`);
24593
25002
  if (requestedStorage === "local")
24594
25003
  console.log("Production release checks will require durable storage. Re-run with --storage s3 --force before deploying.");
24595
25004
  else
@@ -25116,6 +25525,16 @@ var loadPublicationCertification = async (options) => {
25116
25525
  const loaded = await readAbsoluteMobileReleaseCertification(options.projectRoot, requested);
25117
25526
  return verifyAbsoluteMobileReleaseCertification(loaded.certification, options.release, options.requirement);
25118
25527
  };
25528
+ var loadPublicationCertificationVerification = async (options) => {
25529
+ const requested = valueAfter(options.args, "--certification-attestation");
25530
+ if (options.args.includes("--certification-attestation") && (!requested || requested.startsWith("-")))
25531
+ throw new TypeError("mobile publish requires --certification-attestation <verification-json>.");
25532
+ if (!requested)
25533
+ return;
25534
+ if (!options.certificationLoaded)
25535
+ throw new TypeError("mobile publish --certification-attestation requires --certification.");
25536
+ return readAbsoluteMobileCertificationVerification(options.projectRoot, requested);
25537
+ };
25119
25538
  var publishAndroid = async (args) => {
25120
25539
  const registryModule = args.includes("--registry") ? requireValueAfter(args, "--registry") : "mobile.release.ts";
25121
25540
  const channel = args.includes("--channel") ? requireValueAfter(args, "--channel") : undefined;
@@ -25146,11 +25565,17 @@ var publishAndroid = async (args) => {
25146
25565
  release,
25147
25566
  ...certificationRequirement ? { requirement: certificationRequirement } : {}
25148
25567
  });
25568
+ const certificationVerification = await loadPublicationCertificationVerification({
25569
+ args,
25570
+ certificationLoaded: certification !== undefined,
25571
+ projectRoot
25572
+ });
25149
25573
  const publication = await publishAbsoluteAndroidRelease({
25150
25574
  allowUnsigned: args.includes("--unsigned"),
25151
25575
  channel,
25152
25576
  ...certification ? { certification } : {},
25153
25577
  ...certificationRequirement ? { certificationRequirement } : {},
25578
+ ...certificationVerification ? { certificationVerification } : {},
25154
25579
  googlePlay,
25155
25580
  modulePath: registryModule,
25156
25581
  projectRoot,
@@ -25159,6 +25584,8 @@ var publishAndroid = async (args) => {
25159
25584
  const { reused: publicationReused } = publication;
25160
25585
  reused = publicationReused;
25161
25586
  success = true;
25587
+ if (args.includes("--json"))
25588
+ return console.log(JSON.stringify(publication, null, 2)), publication;
25162
25589
  console.log(`${publication.reused ? "Reused" : "Published"} Android release ${release.metadata.releaseId}${publication.channel ? ` on ${publication.channel.channel}` : ""}.`);
25163
25590
  if (publication.googlePlay)
25164
25591
  printGooglePlayPublication(publication.googlePlay);
@@ -25335,12 +25762,18 @@ var publishIos = async (args) => {
25335
25762
  release,
25336
25763
  ...certificationRequirement ? { requirement: certificationRequirement } : {}
25337
25764
  });
25765
+ const certificationVerification = await loadPublicationCertificationVerification({
25766
+ args,
25767
+ certificationLoaded: certification !== undefined,
25768
+ projectRoot
25769
+ });
25338
25770
  const publication = await publishAbsoluteIosRelease({
25339
25771
  allowUnsigned: args.includes("--unsigned"),
25340
25772
  appStoreConnect,
25341
25773
  channel,
25342
25774
  ...certification ? { certification } : {},
25343
25775
  ...certificationRequirement ? { certificationRequirement } : {},
25776
+ ...certificationVerification ? { certificationVerification } : {},
25344
25777
  modulePath: registryModule,
25345
25778
  projectRoot,
25346
25779
  release
@@ -25351,6 +25784,8 @@ var publishIos = async (args) => {
25351
25784
  } = publication;
25352
25785
  reused = publicationReused;
25353
25786
  success = true;
25787
+ if (args.includes("--json"))
25788
+ return console.log(JSON.stringify(publication, null, 2)), publication;
25354
25789
  console.log(`${publication.reused ? "Reused" : "Published"} iOS release ${release.metadata.releaseId}${publication.channel ? ` on ${publication.channel.channel}` : ""}.`);
25355
25790
  if (appStoreConnectPublication)
25356
25791
  printAppStoreConnectPublication(appStoreConnectPublication);
@@ -25510,7 +25945,7 @@ var requireAndroidTestPort = (args, projectRoot) => {
25510
25945
  }
25511
25946
  return { https: args.includes("--https"), port };
25512
25947
  }
25513
- const instances = listLiveInstances().filter((instance2) => resolve15(instance2.cwd) === resolve15(projectRoot) && instance2.source === "dev" && instance2.port !== null);
25948
+ const instances = listLiveInstances().filter((instance2) => resolve16(instance2.cwd) === resolve16(projectRoot) && instance2.source === "dev" && instance2.port !== null);
25514
25949
  if (instances.length !== 1) {
25515
25950
  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>.");
25516
25951
  }
@@ -25604,8 +26039,8 @@ var selectOrStartAndroidReleaseEmulator = async (adb2, emulator, explicitSerial,
25604
26039
  return waitForBoot();
25605
26040
  };
25606
26041
  var safeArtifactRoot = (projectRoot, value) => {
25607
- const root = resolve15(projectRoot, value ?? ".absolutejs/mobile/test-artifacts");
25608
- if (root !== projectRoot && !root.startsWith(`${resolve15(projectRoot)}/`)) {
26042
+ const root = resolve16(projectRoot, value ?? ".absolutejs/mobile/test-artifacts");
26043
+ if (root !== projectRoot && !root.startsWith(`${resolve16(projectRoot)}/`)) {
25609
26044
  throw new TypeError("mobile test --artifacts must remain inside the project.");
25610
26045
  }
25611
26046
  return root;
@@ -25633,12 +26068,12 @@ var waitForRequestedAndroidHmr = async (args, session, checks, timeoutMs) => {
25633
26068
  });
25634
26069
  };
25635
26070
  var writeAndroidFailureArtifacts = async (options) => {
25636
- await mkdir16(options.artifactRoot, { recursive: true });
26071
+ await mkdir17(options.artifactRoot, { recursive: true });
25637
26072
  const screenshot = options.session ? await options.session.screenshot(join17(options.artifactRoot, "android-failure.png")).catch(() => {
25638
26073
  return;
25639
26074
  }) : undefined;
25640
26075
  const diagnosticPath = join17(options.artifactRoot, "android-failure.json");
25641
- await writeFile17(diagnosticPath, `${JSON.stringify({
26076
+ await writeFile18(diagnosticPath, `${JSON.stringify({
25642
26077
  diagnostics: options.session?.diagnostics ?? [],
25643
26078
  error: options.error instanceof Error ? options.error.message : String(options.error),
25644
26079
  platform: "android",
@@ -25659,7 +26094,7 @@ var captureAndroidReleaseScreenshot = async (adb2, serial, destination) => {
25659
26094
  ]);
25660
26095
  if (exitCode !== 0)
25661
26096
  throw new Error(`Android release screenshot failed: ${stderr.trim() || `status ${exitCode}`}`);
25662
- await writeFile17(destination, bytes);
26097
+ await writeFile18(destination, bytes);
25663
26098
  return destination;
25664
26099
  };
25665
26100
  var printAndroidReleaseAcceptance = (result, json) => {
@@ -25760,9 +26195,9 @@ var testAndroidRelease = async (args, mobile, projectRoot) => {
25760
26195
  platform: "android",
25761
26196
  success: false
25762
26197
  });
25763
- await mkdir16(artifactRoot, { recursive: true });
26198
+ await mkdir17(artifactRoot, { recursive: true });
25764
26199
  const diagnosticPath = join17(artifactRoot, "android-release-failure.json");
25765
- await writeFile17(diagnosticPath, `${JSON.stringify({
26200
+ await writeFile18(diagnosticPath, `${JSON.stringify({
25766
26201
  error: sanitizeNativeReportText(error instanceof Error ? error.message : String(error)),
25767
26202
  platform: "android",
25768
26203
  provider: mobile.engine,
@@ -25925,14 +26360,14 @@ var requireIosTestContext = (args, projectRoot) => {
25925
26360
  const port = Number(explicit);
25926
26361
  if (!Number.isInteger(port) || port < 1 || port > 65535)
25927
26362
  throw new TypeError("mobile test --port must be a valid TCP port.");
25928
- const instance2 = listLiveInstances().find((candidate) => resolve15(candidate.cwd) === resolve15(projectRoot) && candidate.source === "dev" && candidate.port === port);
26363
+ const instance2 = listLiveInstances().find((candidate) => resolve16(candidate.cwd) === resolve16(projectRoot) && candidate.source === "dev" && candidate.port === port);
25929
26364
  return {
25930
26365
  https: instance2?.https ?? args.includes("--https"),
25931
26366
  instance: instance2,
25932
26367
  port
25933
26368
  };
25934
26369
  }
25935
- const instances = listLiveInstances().filter((instance2) => resolve15(instance2.cwd) === resolve15(projectRoot) && instance2.source === "dev" && instance2.port !== null);
26370
+ const instances = listLiveInstances().filter((instance2) => resolve16(instance2.cwd) === resolve16(projectRoot) && instance2.source === "dev" && instance2.port !== null);
25936
26371
  if (instances.length !== 1)
25937
26372
  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>.");
25938
26373
  const [instance] = instances;
@@ -25950,7 +26385,7 @@ var waitForIosHmrClient = async (options) => {
25950
26385
  return;
25951
26386
  });
25952
26387
  const status = response?.ok ? await response.json().catch(() => null) : null;
25953
- const targets = isRecord8(status) && isRecord8(status.connectedTargets) ? status.connectedTargets : undefined;
26388
+ const targets = isRecord9(status) && isRecord9(status.connectedTargets) ? status.connectedTargets : undefined;
25954
26389
  if (targets && typeof targets["capacitor-ios"] === "number" && targets["capacitor-ios"] > 0)
25955
26390
  return;
25956
26391
  await Bun.sleep(100);
@@ -26076,7 +26511,7 @@ var requireCapturedCommand = (command, label) => {
26076
26511
  return result;
26077
26512
  };
26078
26513
  var writeIosFailureArtifacts = async (options) => {
26079
- await mkdir16(options.artifactRoot, { recursive: true });
26514
+ await mkdir17(options.artifactRoot, { recursive: true });
26080
26515
  const screenshot = join17(options.artifactRoot, "ios-failure.png");
26081
26516
  const screenshotResult = captureCommand4([
26082
26517
  options.xcrun,
@@ -26087,7 +26522,7 @@ var writeIosFailureArtifacts = async (options) => {
26087
26522
  screenshot
26088
26523
  ]);
26089
26524
  const diagnosticPath = join17(options.artifactRoot, "ios-failure.json");
26090
- await writeFile17(diagnosticPath, `${JSON.stringify({
26525
+ await writeFile18(diagnosticPath, `${JSON.stringify({
26091
26526
  appId: options.appId,
26092
26527
  error: options.error instanceof Error ? options.error.message : String(options.error),
26093
26528
  platform: "ios",
@@ -26114,8 +26549,8 @@ var nativeReportRoot = (args, projectRoot, platform2) => {
26114
26549
  var absolutejsVersionForReport = async () => {
26115
26550
  let absolutejsVersion = process.env.ABSOLUTE_VERSION ?? "unknown";
26116
26551
  const versions = await Promise.all([
26117
- resolve15(import.meta.dir, "..", "..", "package.json"),
26118
- resolve15(import.meta.dir, "..", "..", "..", "package.json")
26552
+ resolve16(import.meta.dir, "..", "..", "package.json"),
26553
+ resolve16(import.meta.dir, "..", "..", "..", "package.json")
26119
26554
  ].map((candidate) => readPackageVersionForIosReport(candidate).catch(() => "unknown")));
26120
26555
  for (const version of versions) {
26121
26556
  if (version === "unknown")
@@ -26492,9 +26927,9 @@ var testIosRelease = async (args, mobile, projectRoot) => {
26492
26927
  remote: remoteProfile !== undefined,
26493
26928
  success: false
26494
26929
  });
26495
- await mkdir16(artifactRoot, { recursive: true });
26930
+ await mkdir17(artifactRoot, { recursive: true });
26496
26931
  const diagnosticPath = join17(artifactRoot, "ios-release-failure.json");
26497
- await writeFile17(diagnosticPath, `${JSON.stringify({
26932
+ await writeFile18(diagnosticPath, `${JSON.stringify({
26498
26933
  error: sanitizeIosReportText(error instanceof Error ? error.message : String(error)),
26499
26934
  platform: "ios",
26500
26935
  provider: mobile.engine,
@@ -26587,7 +27022,7 @@ var testIos = async (args) => {
26587
27022
  mobile.appId
26588
27023
  ], "iOS app launch");
26589
27024
  await waitForIosHmrClient({ https, port, timeoutMs });
26590
- await mkdir16(artifactRoot, { recursive: true });
27025
+ await mkdir17(artifactRoot, { recursive: true });
26591
27026
  const screenshot = join17(artifactRoot, "ios-simulator.png");
26592
27027
  requireCapturedCommand([xcrun, "simctl", "io", simulator.udid, "screenshot", screenshot], "iOS simulator screenshot");
26593
27028
  const hmrApply = await waitForRequestedIosHmr(args, instance, timeoutMs);
@@ -26803,7 +27238,12 @@ var runMobile = async (args) => {
26803
27238
  return;
26804
27239
  }
26805
27240
  if (command === "ci") {
26806
- await generateGithubCi(args.slice(1));
27241
+ if (args[1] === "promote")
27242
+ await dispatchGithubMobilePromotion(args.slice(2));
27243
+ else if (args[1] === "verification")
27244
+ await createGithubCertificationVerification(args.slice(2));
27245
+ else
27246
+ await generateGithubCi(args.slice(1));
26807
27247
  return;
26808
27248
  }
26809
27249
  if (command === "doctor") {
@@ -26882,7 +27322,7 @@ var runMobile = async (args) => {
26882
27322
  await publishIos(args.slice(2));
26883
27323
  return;
26884
27324
  }
26885
- throw new TypeError("Usage: absolute mobile <pair mac <name> <user@host> [--port n] [--workspace path] | remotes [inspect [name] [--json] | clean [name] --yes | --json] | unpair mac <name> | init [--no-native] [--force] | sync [ios|android] | inspect [--json] [--require-bundle] | certify <release-dir> [--evidence report-dir]... [--require installed|simulator|device|store] [--outdir dir] [--json] | certify <release-dir> --verify certification-dir [--require installed|simulator|device|store] [--json] | associations [--outdir dir] [--verify] | ci github [server-entry] [--publish] [--registry module] [--secret-env NAME] [--output path] [--force] [--json] | doctor [ios|android|release [ios|android]] [--remote name] [--json|--fix [--yes]] | build <android|ios> [server-entry] [--remote name] [--registered-device-artifact] [--outdir dir] [--web-outdir dir] [--unsigned] | update provision [--storage local|s3] [--registry module] [--force] [--yes] | update signing generate --private-key path [--certificate path] [--public-key path] [--key-id id] [--common-name name] [--validity-years n] | update build [server-entry] --classification bug-fix|content|security --key-id id --signing-key path --within-submitted-purpose [--outdir dir] [--web-outdir dir] | update publish <release-directory> [--rollout fraction] [--registry module] | update promote --release id --rollout fraction [--registry module] | update rollback [--release id] [--registry module] | update status [--registry module] [--json] | update advance [--rollout fraction] [--registry module] [--json] | update pause|resume|cancel|reconcile [--registry module] [--json] | update storage [--retain count] [--min-age-days days] [--registry module] [--json] | update gc [--retain count] [--min-age-days days] [--grace-days days] [--apply] [--registry module] [--json] | publish android [server-entry] [--release release-dir] [--certification certification-dir] [--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] [--release release-dir] [--certification certification-dir] [--remote name] [--registry module] [--channel name] [--testflight-group name-or-id] [--testflight-notes locale=text] [--testflight-submit-review] [--outdir dir] [--web-outdir dir] [--unsigned] | test android [--release release-dir [--yes] | --route path [--wait-for-hmr] [--port n]] [--report [dir]] [--serial id] [--artifacts dir] [--json] [--config path] | test ios [--release release-dir [--remote name] [--device id [--testflight] --yes] | --wait-for-hmr] [--report [dir]] [--udid id] [--artifacts dir] [--json] [--config path]");
27325
+ throw new TypeError("Usage: absolute mobile <pair mac <name> <user@host> [--port n] [--workspace path] | remotes [inspect [name] [--json] | clean [name] --yes | --json] | unpair mac <name> | init [--no-native] [--force] | sync [ios|android] | inspect [--json] [--require-bundle] | certify <release-dir> [--evidence report-dir]... [--require installed|simulator|device|store] [--outdir dir] [--json] | certify <release-dir> --verify certification-dir [--require installed|simulator|device|store] [--json] | associations [--outdir dir] [--verify] | ci github [server-entry] [--publish] [--registry module] [--secret-env NAME] [--output path] [--force] [--json] | ci promote <android|ios> --run-id id --certification path [--channel name] [--play-track track|--testflight-group group] [--ref branch] | doctor [ios|android|release [ios|android]] [--remote name] [--json|--fix [--yes]] | build <android|ios> [server-entry] [--remote name] [--registered-device-artifact] [--outdir dir] [--web-outdir dir] [--unsigned] | update provision [--storage local|s3] [--registry module] [--force] [--yes] | update signing generate --private-key path [--certificate path] [--public-key path] [--key-id id] [--common-name name] [--validity-years n] | update build [server-entry] --classification bug-fix|content|security --key-id id --signing-key path --within-submitted-purpose [--outdir dir] [--web-outdir dir] | update publish <release-directory> [--rollout fraction] [--registry module] | update promote --release id --rollout fraction [--registry module] | update rollback [--release id] [--registry module] | update status [--registry module] [--json] | update advance [--rollout fraction] [--registry module] [--json] | update pause|resume|cancel|reconcile [--registry module] [--json] | update storage [--retain count] [--min-age-days days] [--registry module] [--json] | update gc [--retain count] [--min-age-days days] [--grace-days days] [--apply] [--registry module] [--json] | publish android [server-entry] [--release release-dir] [--certification certification-dir] [--certification-attestation verification-json] [--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] [--release release-dir] [--certification certification-dir] [--certification-attestation verification-json] [--remote name] [--registry module] [--channel name] [--testflight-group name-or-id] [--testflight-notes locale=text] [--testflight-submit-review] [--outdir dir] [--web-outdir dir] [--unsigned] | test android [--release release-dir [--yes] | --route path [--wait-for-hmr] [--port n]] [--report [dir]] [--serial id] [--artifacts dir] [--json] [--config path] | test ios [--release release-dir [--remote name] [--device id [--testflight] --yes] | --wait-for-hmr] [--report [dir]] [--udid id] [--artifacts dir] [--json] [--config path]");
26886
27326
  };
26887
27327
  export {
26888
27328
  runMobile