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

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 access16,
16783
- mkdir as mkdir17,
16784
- mkdtemp as mkdtemp6,
16785
- readFile as readFile21,
16786
- rm as rm9,
16787
- writeFile as writeFile18
16782
+ access as access17,
16783
+ mkdir as mkdir18,
16784
+ mkdtemp as mkdtemp7,
16785
+ readFile as readFile22,
16786
+ rm as rm10,
16787
+ writeFile as writeFile19
16788
16788
  } from "fs/promises";
16789
- import { createPublicKey as createPublicKey2 } from "crypto";
16790
- import { join as join17, relative as relative15, resolve as resolve16 } from "path";
16789
+ import { createPublicKey as createPublicKey2, randomUUID } from "crypto";
16790
+ import { join as join18, relative as relative16, resolve as resolve17 } from "path";
16791
16791
  import { createInterface } from "readline/promises";
16792
16792
 
16793
16793
  // src/mobile/nativeDeepLinks.ts
@@ -22982,7 +22982,7 @@ var renderAbsoluteMobileProjectInspection = (report) => {
22982
22982
  import { existsSync } from "fs";
22983
22983
  import { access as access12, mkdir as mkdir13, readFile as readFile18, writeFile as writeFile14 } from "fs/promises";
22984
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;
22985
+ var ABSOLUTE_MOBILE_CI_WORKFLOW_FORMAT = 4;
22986
22986
  var SECRET_NAME_PATTERN = /^[A-Z][A-Z0-9_]*$/u;
22987
22987
  var CI_ENV_INDENTATION = 6;
22988
22988
  var RESERVED_SECRET_NAMES = new Set([
@@ -23141,6 +23141,10 @@ var publishingInputs = (platforms, includePublishing) => {
23141
23141
  description: Source workflow run ID for promotion without rebuilding
23142
23142
  required: false
23143
23143
  type: string`,
23144
+ ` dispatch_id:
23145
+ description: Unique promotion correlation ID supplied by the AbsoluteJS CLI
23146
+ required: false
23147
+ type: string`,
23144
23148
  ` certification_base64:
23145
23149
  description: AbsoluteJS certification supplied by the promotion CLI
23146
23150
  required: false
@@ -23513,6 +23517,10 @@ var promotionValidationSteps = ` - name: Validate promotion handoff
23513
23517
  echo "Promotion requires a numeric source workflow run ID." >&2
23514
23518
  exit 1
23515
23519
  fi
23520
+ if [[ ! "$ABSOLUTE_PROMOTION_DISPATCH_ID" =~ ^amp_[a-f0-9]{32}$ ]]; then
23521
+ echo "Promotion requires a valid AbsoluteJS dispatch correlation ID." >&2
23522
+ exit 1
23523
+ fi
23516
23524
  if [[ -z "$ABSOLUTE_CERTIFICATION_BASE64" ]]; then
23517
23525
  echo "Promotion requires certification supplied by absolute mobile ci promote." >&2
23518
23526
  exit 1
@@ -23524,6 +23532,10 @@ var promotionSharedSteps = (platform) => ` - name: Restore exact ${platform
23524
23532
  path: .absolutejs/mobile/releases/${platform}
23525
23533
  run-id: \${{ inputs.source_run_id }}
23526
23534
  github-token: \${{ github.token }}
23535
+ - name: Record bounded promotion context
23536
+ shell: bash
23537
+ run: |
23538
+ bun -e 'const dispatchId = process.env.ABSOLUTE_PROMOTION_DISPATCH_ID; const sourceRunId = process.env.ABSOLUTE_SOURCE_RUN_ID; const promotionRunId = process.env.GITHUB_RUN_ID; if (!dispatchId?.match(/^amp_[a-f0-9]{32}$/) || !sourceRunId?.match(/^[1-9][0-9]*$/) || !promotionRunId?.match(/^[1-9][0-9]*$/)) throw new Error("Invalid promotion run identity"); await Bun.write(".absolutejs/mobile-ci/promotion-context.json", JSON.stringify({ dispatchId, format: 2, platform: ${JSON.stringify(platform)}, promotionRunId, sourceArtifact: ${JSON.stringify(`absolute-mobile-${platform}`)}, sourceRunId }, null, 2) + "\\n");'
23527
23539
  - name: Import and verify exact certification
23528
23540
  shell: bash
23529
23541
  run: |
@@ -23561,7 +23573,9 @@ var androidPromotionJob = (options) => {
23561
23573
  id-token: write
23562
23574
  env:
23563
23575
  ABSOLUTE_CERTIFICATION_BASE64: \${{ inputs.certification_base64 }}
23576
+ ABSOLUTE_PROMOTION_DISPATCH_ID: \${{ inputs.dispatch_id }}
23564
23577
  ABSOLUTE_RELEASE_CHANNEL: \${{ inputs.channel }}
23578
+ ABSOLUTE_SOURCE_RUN_ID: \${{ inputs.source_run_id }}
23565
23579
  ABSOLUTE_PLAY_TRACK: \${{ inputs.play_track }}
23566
23580
  ABSOLUTE_GOOGLE_CREDENTIALS_BASE64: \${{ secrets.ABSOLUTE_GOOGLE_CREDENTIALS_BASE64 }}
23567
23581
  GOOGLE_APPLICATION_CREDENTIALS: \${{ runner.temp }}/absolute-google-credentials.json${custom ? `
@@ -23601,6 +23615,7 @@ ${promotionSharedSteps("android")}
23601
23615
  name: absolute-mobile-android-promotion-\${{ github.run_id }}
23602
23616
  path: |
23603
23617
  .absolutejs/mobile-ci/imported-certification/
23618
+ .absolutejs/mobile-ci/promotion-context.json
23604
23619
  .absolutejs/mobile-ci/promotion-receipt.json
23605
23620
  if-no-files-found: error
23606
23621
  retention-days: 90
@@ -23624,7 +23639,9 @@ var iosPromotionJob = (options) => {
23624
23639
  id-token: write
23625
23640
  env:
23626
23641
  ABSOLUTE_CERTIFICATION_BASE64: \${{ inputs.certification_base64 }}
23642
+ ABSOLUTE_PROMOTION_DISPATCH_ID: \${{ inputs.dispatch_id }}
23627
23643
  ABSOLUTE_RELEASE_CHANNEL: \${{ inputs.channel }}
23644
+ ABSOLUTE_SOURCE_RUN_ID: \${{ inputs.source_run_id }}
23628
23645
  ABSOLUTE_TESTFLIGHT_GROUP: \${{ inputs.testflight_group }}
23629
23646
  ABSOLUTE_TESTFLIGHT_SUBMIT_REVIEW: \${{ inputs.submit_testflight_review }}
23630
23647
  APP_STORE_CONNECT_ISSUER_ID: \${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
@@ -23666,6 +23683,7 @@ ${promotionSharedSteps("ios")}
23666
23683
  name: absolute-mobile-ios-promotion-\${{ github.run_id }}
23667
23684
  path: |
23668
23685
  .absolutejs/mobile-ci/imported-certification/
23686
+ .absolutejs/mobile-ci/promotion-context.json
23669
23687
  .absolutejs/mobile-ci/promotion-receipt.json
23670
23688
  if-no-files-found: error
23671
23689
  retention-days: 90
@@ -23712,6 +23730,7 @@ var createAbsoluteMobileGithubWorkflow = (options) => {
23712
23730
  });
23713
23731
  let workflow = `# Generated by AbsoluteJS. Regenerate with: absolute mobile ci github${includePublishing ? " --publish" : ""}
23714
23732
  name: AbsoluteJS Mobile
23733
+ run-name: \${{ inputs.operation == 'promote' && format('AbsoluteJS mobile promotion [{0}]', inputs.dispatch_id) || format('AbsoluteJS mobile {0}', github.event_name) }}
23715
23734
 
23716
23735
  on:
23717
23736
  pull_request:
@@ -23737,6 +23756,7 @@ jobs:
23737
23756
  env:
23738
23757
  ${environment}${includePublishing ? `
23739
23758
  ABSOLUTE_PROMOTION_PLATFORM: \${{ inputs.platform }}
23759
+ ABSOLUTE_PROMOTION_DISPATCH_ID: \${{ inputs.dispatch_id }}
23740
23760
  ABSOLUTE_SOURCE_RUN_ID: \${{ inputs.source_run_id }}
23741
23761
  ABSOLUTE_CERTIFICATION_BASE64: \${{ inputs.certification_base64 }}` : ""}
23742
23762
  steps:
@@ -23785,47 +23805,521 @@ var writeAbsoluteMobileGithubWorkflow = async (options) => {
23785
23805
  };
23786
23806
  };
23787
23807
 
23788
- // src/mobile/updateSigning.ts
23789
- import { createHash as createHash6, sign, verify } from "crypto";
23808
+ // src/mobile/ciPromotionAudit.ts
23790
23809
  import {
23791
- cp,
23810
+ access as access13,
23792
23811
  mkdir as mkdir14,
23793
23812
  mkdtemp as mkdtemp5,
23794
- readdir as readdir6,
23795
23813
  readFile as readFile19,
23814
+ readdir as readdir6,
23796
23815
  rename as rename11,
23797
23816
  rm as rm8,
23798
23817
  stat as stat7,
23799
23818
  writeFile as writeFile15
23800
23819
  } from "fs/promises";
23801
- import { dirname as dirname12, join as join15, relative as relative12, resolve as resolve13 } from "path";
23820
+ import { dirname as dirname12, isAbsolute as isAbsolute8, join as join15, relative as relative12, resolve as resolve13, sep as sep9 } from "path";
23821
+ var ABSOLUTE_MOBILE_CI_PROMOTION_AUDIT_FORMAT = 1;
23822
+ var ABSOLUTE_MOBILE_CI_PROMOTION_CONTEXT_FORMAT = 2;
23823
+ var MAX_GITHUB_OUTPUT_BYTES = 1048576;
23824
+ var MAX_AUDIT_JSON_BYTES = 1048576;
23825
+ var MAX_SEARCH_FILES = 20000;
23826
+ var RUN_ID_PATTERN = /^[1-9][0-9]*$/u;
23827
+ var REPOSITORY_PATTERN2 = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u;
23828
+ var DISPATCH_ID_PATTERN = /^amp_[a-f0-9]{32}$/u;
23829
+ var PROMOTION_DISCOVERY_ATTEMPTS = 60;
23830
+ var PROMOTION_DISCOVERY_INTERVAL_MS = 1000;
23831
+ var isRecord9 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
23832
+ var requiredString = (value, name) => {
23833
+ if (typeof value !== "string" || value.trim() === "")
23834
+ throw new TypeError(`${name} is invalid.`);
23835
+ return value.trim();
23836
+ };
23837
+ var optionalString = (value, name) => {
23838
+ if (value === null || value === undefined)
23839
+ return null;
23840
+ return requiredString(value, name);
23841
+ };
23842
+ var runId = (value, name) => {
23843
+ if (!RUN_ID_PATTERN.test(value))
23844
+ throw new TypeError(`${name} is invalid.`);
23845
+ return value;
23846
+ };
23847
+ var dispatchId = (value) => {
23848
+ if (!DISPATCH_ID_PATTERN.test(value))
23849
+ throw new TypeError("Mobile promotion dispatch ID is invalid.");
23850
+ return value;
23851
+ };
23852
+ var promotionRunTitle = (value) => `AbsoluteJS mobile promotion [${dispatchId(value)}]`;
23853
+ var promotionDiscoveryCommand = (workflow, repository) => [
23854
+ "gh",
23855
+ "run",
23856
+ "list",
23857
+ "--workflow",
23858
+ workflow,
23859
+ "--event",
23860
+ "workflow_dispatch",
23861
+ "--limit",
23862
+ "50",
23863
+ ...repositoryArgs(repository),
23864
+ "--json",
23865
+ "databaseId,displayTitle,event,url"
23866
+ ];
23867
+ var repositoryArgs = (repository) => {
23868
+ if (repository === undefined)
23869
+ return [];
23870
+ if (!REPOSITORY_PATTERN2.test(repository))
23871
+ throw new TypeError("GitHub repository must be owner/name.");
23872
+ return ["--repo", repository];
23873
+ };
23874
+ var projectPath3 = (projectRoot, requested) => {
23875
+ const root = resolve13(projectRoot);
23876
+ const path = resolve13(root, requested);
23877
+ const portable = relative12(root, path);
23878
+ if (portable === ".." || portable.startsWith(`..${sep9}`) || isAbsolute8(portable))
23879
+ throw new TypeError("Mobile CI audit output must remain inside the project.");
23880
+ return path;
23881
+ };
23882
+ var defaultRunner = async (command, options) => {
23883
+ const child = Bun.spawn(command, {
23884
+ cwd: options.cwd,
23885
+ env: options.env,
23886
+ stderr: "pipe",
23887
+ stdin: "ignore",
23888
+ stdout: "pipe"
23889
+ });
23890
+ const [exitCode, stderr, stdout] = await Promise.all([
23891
+ child.exited,
23892
+ new Response(child.stderr).text(),
23893
+ new Response(child.stdout).text()
23894
+ ]);
23895
+ return { exitCode, stderr, stdout };
23896
+ };
23897
+ var requireCommand2 = async (run, command, options, label) => {
23898
+ const result = await run(command, options);
23899
+ if (result.exitCode !== 0)
23900
+ throw new TypeError(`${label} failed: ${result.stderr.trim() || result.stdout.trim() || `exit ${result.exitCode}`}`);
23901
+ if (result.stdout.length > MAX_GITHUB_OUTPUT_BYTES || result.stderr.length > MAX_GITHUB_OUTPUT_BYTES)
23902
+ throw new TypeError(`${label} output exceeded the 1 MiB limit.`);
23903
+ return result;
23904
+ };
23905
+ var parseJob = (value) => {
23906
+ if (!isRecord9(value))
23907
+ throw new TypeError("GitHub run job is invalid.");
23908
+ return {
23909
+ conclusion: optionalString(value.conclusion, "GitHub job conclusion"),
23910
+ name: requiredString(value.name, "GitHub job name"),
23911
+ status: requiredString(value.status, "GitHub job status")
23912
+ };
23913
+ };
23914
+ var discoverAbsoluteMobilePromotionRun = async (options) => {
23915
+ const requestedDispatchId = dispatchId(options.dispatchId);
23916
+ const attempts = options.attempts ?? PROMOTION_DISCOVERY_ATTEMPTS;
23917
+ if (!Number.isSafeInteger(attempts) || attempts < 1 || attempts > 300)
23918
+ throw new TypeError("Mobile promotion discovery attempts are invalid.");
23919
+ const pollIntervalMs = options.pollIntervalMs ?? PROMOTION_DISCOVERY_INTERVAL_MS;
23920
+ if (!Number.isSafeInteger(pollIntervalMs) || pollIntervalMs < 0 || pollIntervalMs > 1e4)
23921
+ throw new TypeError("Mobile promotion discovery interval is invalid.");
23922
+ const sleep = options.sleep ?? Bun.sleep;
23923
+ const discover = async (attempt) => {
23924
+ const result = await requireCommand2(options.run ?? defaultRunner, promotionDiscoveryCommand(options.workflow, options.repository), { cwd: options.projectRoot }, "GitHub promotion run discovery");
23925
+ const discovered = parseAbsoluteMobilePromotionRunList(JSON.parse(result.stdout), requestedDispatchId);
23926
+ if (discovered)
23927
+ return discovered;
23928
+ if (attempt >= attempts)
23929
+ throw new TypeError(`GitHub did not expose promotion dispatch ${requestedDispatchId} within ${attempts} attempts.`);
23930
+ await sleep(pollIntervalMs);
23931
+ return discover(attempt + 1);
23932
+ };
23933
+ return discover(1);
23934
+ };
23935
+ var inspectAbsoluteMobileCiRun = async (options) => {
23936
+ const requestedRunId = runId(options.runId, "GitHub run ID");
23937
+ const result = await requireCommand2(options.run ?? defaultRunner, [
23938
+ "gh",
23939
+ "run",
23940
+ "view",
23941
+ requestedRunId,
23942
+ ...repositoryArgs(options.repository),
23943
+ "--json",
23944
+ "conclusion,createdAt,databaseId,displayTitle,headBranch,headSha,jobs,startedAt,status,updatedAt,url,workflowName"
23945
+ ], { cwd: options.projectRoot }, "GitHub workflow inspection");
23946
+ return parseAbsoluteMobileCiRunStatus(JSON.parse(result.stdout), requestedRunId);
23947
+ };
23948
+ var parseAbsoluteMobileCiRunStatus = (value, requestedRunId) => {
23949
+ if (!isRecord9(value) || !Array.isArray(value.jobs))
23950
+ throw new TypeError("GitHub workflow run response is invalid.");
23951
+ const { databaseId } = value;
23952
+ if (typeof databaseId !== "number" && typeof databaseId !== "string" || String(databaseId) !== requestedRunId)
23953
+ throw new TypeError("GitHub workflow run identity does not match.");
23954
+ return {
23955
+ conclusion: optionalString(value.conclusion, "GitHub run conclusion"),
23956
+ createdAt: requiredString(value.createdAt, "GitHub run createdAt"),
23957
+ displayTitle: requiredString(value.displayTitle, "GitHub run displayTitle"),
23958
+ headBranch: requiredString(value.headBranch, "GitHub run headBranch"),
23959
+ headSha: requiredString(value.headSha, "GitHub run headSha"),
23960
+ jobs: value.jobs.map(parseJob),
23961
+ runId: requestedRunId,
23962
+ startedAt: optionalString(value.startedAt, "GitHub run startedAt"),
23963
+ status: requiredString(value.status, "GitHub run status"),
23964
+ updatedAt: requiredString(value.updatedAt, "GitHub run updatedAt"),
23965
+ url: requiredString(value.url, "GitHub run URL"),
23966
+ workflowName: requiredString(value.workflowName, "GitHub workflow name")
23967
+ };
23968
+ };
23969
+ var parseAbsoluteMobilePromotionContext = (value) => {
23970
+ if (!isRecord9(value) || value.format !== 1 && value.format !== ABSOLUTE_MOBILE_CI_PROMOTION_CONTEXT_FORMAT || value.platform !== "android" && value.platform !== "ios")
23971
+ throw new TypeError("Mobile promotion context is invalid.");
23972
+ const { platform } = value;
23973
+ const sourceArtifact = requiredString(value.sourceArtifact, "promotion source artifact");
23974
+ if (sourceArtifact !== `absolute-mobile-${platform}`)
23975
+ throw new TypeError("Mobile promotion source artifact is invalid.");
23976
+ return {
23977
+ dispatchId: value.format === 1 ? null : dispatchId(requiredString(value.dispatchId, "promotion dispatch ID")),
23978
+ format: value.format,
23979
+ platform,
23980
+ promotionRunId: runId(requiredString(value.promotionRunId, "promotion run ID"), "promotion run ID"),
23981
+ sourceArtifact,
23982
+ sourceRunId: runId(requiredString(value.sourceRunId, "source run ID"), "source run ID")
23983
+ };
23984
+ };
23985
+ var parseAbsoluteMobilePromotionRunList = (value, requestedDispatchId) => {
23986
+ if (!Array.isArray(value))
23987
+ throw new TypeError("GitHub workflow run list response is invalid.");
23988
+ const expectedTitle = promotionRunTitle(requestedDispatchId);
23989
+ const matches = value.filter((candidate) => isRecord9(candidate) && candidate.displayTitle === expectedTitle);
23990
+ if (matches.length === 0)
23991
+ return null;
23992
+ if (matches.length !== 1)
23993
+ throw new TypeError("GitHub returned multiple runs for one promotion dispatch identity.");
23994
+ const [match] = matches;
23995
+ if (!isRecord9(match) || match.event !== "workflow_dispatch")
23996
+ throw new TypeError("GitHub promotion run identity is invalid.");
23997
+ const databaseId = typeof match.databaseId === "number" || typeof match.databaseId === "string" ? String(match.databaseId) : "";
23998
+ const requestedRunId = runId(databaseId, "GitHub promotion run ID");
23999
+ const url = requiredString(match.url, "GitHub promotion run URL");
24000
+ if (!url.endsWith(`/actions/runs/${requestedRunId}`))
24001
+ throw new TypeError("GitHub promotion run URL does not match its ID.");
24002
+ return {
24003
+ dispatchId: requestedDispatchId,
24004
+ runId: requestedRunId,
24005
+ url
24006
+ };
24007
+ };
24008
+ var watchAbsoluteMobileCiRun = async (options) => {
24009
+ const requestedRunId = runId(options.runId, "GitHub run ID");
24010
+ await requireCommand2(options.run ?? defaultRunner, [
24011
+ "gh",
24012
+ "run",
24013
+ "watch",
24014
+ requestedRunId,
24015
+ ...repositoryArgs(options.repository),
24016
+ "--exit-status"
24017
+ ], { cwd: options.projectRoot }, "GitHub workflow watch");
24018
+ return inspectAbsoluteMobileCiRun({ ...options, runId: requestedRunId });
24019
+ };
24020
+ var readBoundedJson = async (path, label) => {
24021
+ const metadata = await stat7(path).catch(() => null);
24022
+ if (!metadata?.isFile() || metadata.size > MAX_AUDIT_JSON_BYTES)
24023
+ throw new TypeError(`${label} is missing or exceeds the 1 MiB limit.`);
24024
+ const value = JSON.parse(await readFile19(path, "utf8"));
24025
+ return value;
24026
+ };
24027
+ var findFiles = async (root, name) => {
24028
+ const found = [];
24029
+ let visited = 0;
24030
+ const walk = async (directory) => {
24031
+ const entries = await readdir6(directory, { withFileTypes: true });
24032
+ await Promise.all(entries.map(async (entry) => {
24033
+ visited += 1;
24034
+ if (visited > MAX_SEARCH_FILES)
24035
+ throw new TypeError("Downloaded GitHub artifact contains too many files.");
24036
+ const path = join15(directory, entry.name);
24037
+ if (entry.isDirectory()) {
24038
+ await walk(path);
24039
+ return;
24040
+ }
24041
+ if (entry.isFile() && entry.name === name)
24042
+ found.push(path);
24043
+ }));
24044
+ };
24045
+ await walk(root);
24046
+ return found;
24047
+ };
24048
+ var inferPromotionPlatform = (status) => {
24049
+ const matched = status.jobs.flatMap((job) => {
24050
+ if (job.name === "Promote exact certified Android release")
24051
+ return ["android"];
24052
+ if (job.name === "Promote exact certified iOS release")
24053
+ return ["ios"];
24054
+ return [];
24055
+ });
24056
+ if (matched.length !== 1)
24057
+ throw new TypeError("GitHub run does not contain exactly one mobile promotion job.");
24058
+ const [platform] = matched;
24059
+ if (!platform)
24060
+ throw new TypeError("Mobile promotion platform is missing.");
24061
+ return platform;
24062
+ };
24063
+ var onlyPath = (paths, label) => {
24064
+ const [path] = paths;
24065
+ if (paths.length !== 1 || !path)
24066
+ throw new TypeError(`${label} must contain exactly one matching file.`);
24067
+ return path;
24068
+ };
24069
+ var canonicalJson = (value) => {
24070
+ if (Array.isArray(value))
24071
+ return `[${value.map((entry) => canonicalJson(entry)).join(",")}]`;
24072
+ if (isRecord9(value))
24073
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(",")}}`;
24074
+ return JSON.stringify(value);
24075
+ };
24076
+ var publicationSummary = (value, platform, release, certification) => {
24077
+ if (!isRecord9(value) || !isRecord9(value.record) || !isRecord9(value.record.metadata))
24078
+ throw new TypeError("Mobile promotion receipt is invalid.");
24079
+ const { metadata } = value.record;
24080
+ if (metadata.appId !== release.metadata.appId || metadata.releaseId !== release.metadata.releaseId || metadata.sha256 !== release.metadata.sha256)
24081
+ throw new TypeError("Mobile promotion receipt release identity does not match.");
24082
+ if (!isRecord9(value.certification) || !isRecord9(value.certification.provenance))
24083
+ throw new TypeError("Mobile promotion receipt lacks trusted certification provenance.");
24084
+ if (value.certification.certificationId !== certification.certificationId || value.certification.releaseId !== release.metadata.releaseId || value.certification.requirement !== certification.requirement || value.certification.strength !== certification.strength || value.certification.provenance.subject !== release.metadata.releaseId)
24085
+ throw new TypeError("Mobile promotion certification receipt does not match.");
24086
+ requiredString(value.certification.provenance.issuer, "promotion provenance issuer");
24087
+ requiredString(value.certification.provenance.verificationId, "promotion provenance verification ID");
24088
+ const provenanceTime = requiredString(value.certification.provenance.verifiedAt, "promotion provenance verification time");
24089
+ if (Number.isNaN(Date.parse(provenanceTime)))
24090
+ throw new TypeError("Mobile promotion provenance time is invalid.");
24091
+ if (typeof value.reused !== "boolean")
24092
+ throw new TypeError("Mobile promotion reuse status is invalid.");
24093
+ const channel = isRecord9(value.channel) ? optionalString(value.channel.channel, "promotion channel") : null;
24094
+ if (isRecord9(value.channel) && value.channel.releaseId !== release.metadata.releaseId)
24095
+ throw new TypeError("Mobile promotion channel receipt does not match.");
24096
+ const providerReceipt = platform === "android" ? value.googlePlay : value.appStoreConnect;
24097
+ if (providerReceipt === undefined)
24098
+ return {
24099
+ channel,
24100
+ provider: "registry",
24101
+ reused: value.reused,
24102
+ stage: null
24103
+ };
24104
+ if (!isRecord9(providerReceipt) || !isRecord9(providerReceipt.receipt))
24105
+ throw new TypeError("Mobile store promotion receipt is invalid.");
24106
+ if (typeof providerReceipt.reused !== "boolean")
24107
+ throw new TypeError("Mobile store promotion reuse status is invalid.");
24108
+ const expectedProvider = platform === "android" ? "google-play" : "app-store-connect";
24109
+ if (providerReceipt.receipt.provider !== expectedProvider || providerReceipt.receipt.releaseId !== release.metadata.releaseId || providerReceipt.receipt.sha256 !== release.metadata.sha256)
24110
+ throw new TypeError("Mobile store promotion receipt does not match.");
24111
+ return {
24112
+ channel,
24113
+ provider: expectedProvider,
24114
+ reused: providerReceipt.reused,
24115
+ stage: requiredString(providerReceipt.receipt.stage, "Mobile store promotion stage")
24116
+ };
24117
+ };
24118
+ var renderAuditMarkdown = (audit) => `# AbsoluteJS mobile promotion audit
24119
+
24120
+ - Status: **VERIFIED**
24121
+ - Platform: ${audit.platform}
24122
+ - Release: \`${audit.release.releaseId}\`
24123
+ - Artifact SHA-256: \`${audit.release.sha256}\`
24124
+ - Source workflow run: ${audit.github.sourceRunId}
24125
+ - Promotion workflow run: ${audit.github.promotionRunId}
24126
+ - Dispatch correlation: ${audit.github.dispatchId ?? "legacy workflow (not recorded)"}
24127
+ - Workflow: ${audit.github.workflowName}
24128
+ - GitHub identity: \`${audit.provenance.repository}/${audit.provenance.workflowPath}@${audit.provenance.ref}\`
24129
+ - Certification: \`${audit.certification.certificationId}\` (${audit.certification.strength})
24130
+ - Publication: ${audit.publication.provider}${audit.publication.stage ? ` / ${audit.publication.stage}` : ""}
24131
+ - Verified at: ${audit.verifiedAt}
24132
+
24133
+ This report contains no credentials, device identifiers, local absolute paths, application data, or GitHub token.
24134
+ `;
24135
+ var auditAbsoluteMobileCiPromotion = async (options) => {
24136
+ const runner = options.run ?? defaultRunner;
24137
+ const promotionRunId = runId(options.runId, "GitHub promotion run ID");
24138
+ const status = await inspectAbsoluteMobileCiRun({
24139
+ projectRoot: options.projectRoot,
24140
+ ...options.repository ? { repository: options.repository } : {},
24141
+ run: runner,
24142
+ runId: promotionRunId
24143
+ });
24144
+ if (status.status !== "completed" || status.conclusion !== "success")
24145
+ throw new TypeError("Mobile promotion run has not completed successfully.");
24146
+ const platform = inferPromotionPlatform(status);
24147
+ const output = projectPath3(options.projectRoot, options.outputDirectory ?? `.absolutejs/mobile-ci/audits/${promotionRunId}`);
24148
+ if (await access13(output).then(() => true).catch(() => false))
24149
+ throw new TypeError(`Mobile CI audit output already exists: ${output}`);
24150
+ await mkdir14(dirname12(output), { recursive: true });
24151
+ const staging = await mkdtemp5(join15(dirname12(output), ".promotion-audit-"));
24152
+ try {
24153
+ const evidenceRoot = join15(staging, "promotion-evidence");
24154
+ await mkdir14(evidenceRoot);
24155
+ await requireCommand2(runner, [
24156
+ "gh",
24157
+ "run",
24158
+ "download",
24159
+ promotionRunId,
24160
+ ...repositoryArgs(options.repository),
24161
+ "--name",
24162
+ `absolute-mobile-${platform}-promotion-${promotionRunId}`,
24163
+ "--dir",
24164
+ evidenceRoot
24165
+ ], { cwd: options.projectRoot }, "GitHub promotion artifact download");
24166
+ const contextPaths = await findFiles(evidenceRoot, "promotion-context.json");
24167
+ let context;
24168
+ if (contextPaths.length === 1)
24169
+ context = parseAbsoluteMobilePromotionContext(await readBoundedJson(onlyPath(contextPaths, "Promotion context"), "Promotion context"));
24170
+ else if (contextPaths.length > 1)
24171
+ throw new TypeError("Promotion artifact contains duplicate contexts.");
24172
+ if (context && context.promotionRunId !== promotionRunId)
24173
+ throw new TypeError("Promotion context run identity does not match.");
24174
+ if (context && context.platform !== platform)
24175
+ throw new TypeError("Promotion context platform does not match.");
24176
+ if (context?.dispatchId && status.displayTitle !== promotionRunTitle(context.dispatchId))
24177
+ throw new TypeError("Promotion dispatch identity does not match the GitHub run.");
24178
+ const sourceRunId = options.sourceRunId ? runId(options.sourceRunId, "GitHub source run ID") : context?.sourceRunId;
24179
+ if (!sourceRunId)
24180
+ throw new TypeError("Promotion artifact predates source-run context; pass --source-run-id.");
24181
+ if (context && options.sourceRunId && context.sourceRunId !== sourceRunId)
24182
+ throw new TypeError("Requested source run does not match promotion context.");
24183
+ const sourceArtifact = context?.sourceArtifact ?? `absolute-mobile-${platform}`;
24184
+ const sourceRoot = join15(staging, "source-release");
24185
+ await mkdir14(sourceRoot);
24186
+ await requireCommand2(runner, [
24187
+ "gh",
24188
+ "run",
24189
+ "download",
24190
+ sourceRunId,
24191
+ ...repositoryArgs(options.repository),
24192
+ "--name",
24193
+ sourceArtifact,
24194
+ "--dir",
24195
+ sourceRoot
24196
+ ], { cwd: options.projectRoot }, "GitHub source release download");
24197
+ const releaseMetadata = await findFiles(sourceRoot, "release.json");
24198
+ if (releaseMetadata.length !== 1)
24199
+ throw new TypeError("Source artifact must contain exactly one immutable mobile release.");
24200
+ const releaseMetadataPath = onlyPath(releaseMetadata, "Source artifact");
24201
+ const release = platform === "android" ? await readAbsoluteAndroidRelease(options.projectRoot, releaseMetadataPath) : await readAbsoluteIosRelease(options.projectRoot, releaseMetadataPath);
24202
+ const certificationPaths = await findFiles(evidenceRoot, "certification.json");
24203
+ const verificationPaths = await findFiles(evidenceRoot, "verification.json");
24204
+ const receiptPaths = await findFiles(evidenceRoot, "promotion-receipt.json");
24205
+ if (certificationPaths.length !== 1 || verificationPaths.length !== 1 || receiptPaths.length !== 1)
24206
+ throw new TypeError("Promotion audit artifact is incomplete or ambiguous.");
24207
+ const certificationPath = onlyPath(certificationPaths, "Promotion certification");
24208
+ const verificationPath = onlyPath(verificationPaths, "Promotion verification");
24209
+ const receiptPath = onlyPath(receiptPaths, "Promotion receipt");
24210
+ const loadedCertification = await readAbsoluteMobileReleaseCertification(options.projectRoot, certificationPath);
24211
+ const certification = verifyAbsoluteMobileReleaseCertification(loadedCertification.certification, release);
24212
+ const verification = await readAbsoluteMobileCertificationVerification(options.projectRoot, verificationPath);
24213
+ if (verification.identity.sha !== status.headSha)
24214
+ throw new TypeError("Sigstore workflow commit does not match the promotion run.");
24215
+ if (options.repository && verification.identity.repository !== options.repository)
24216
+ throw new TypeError("Sigstore repository does not match the requested repository.");
24217
+ const bundlePath = `${certificationPath}.sigstore.json`;
24218
+ const bundle = await readAbsoluteSigstoreBundle(options.projectRoot, bundlePath);
24219
+ if (canonicalJson(bundle) !== canonicalJson(verification.bundle))
24220
+ throw new TypeError("Sigstore bundle does not match verification envelope.");
24221
+ await requireCommand2(runner, [
24222
+ "bunx",
24223
+ "@absolutejs/attest@0.2.0",
24224
+ "verify-blobs",
24225
+ certificationPath
24226
+ ], {
24227
+ cwd: options.projectRoot,
24228
+ env: {
24229
+ ...process.env,
24230
+ GITHUB_REF: verification.identity.ref,
24231
+ GITHUB_REPOSITORY: verification.identity.repository,
24232
+ GITHUB_SHA: verification.identity.sha,
24233
+ GITHUB_WORKFLOW_REF: `${verification.identity.repository}/${verification.identity.workflowPath}@${verification.identity.ref}`
24234
+ }
24235
+ }, "Sigstore certification verification");
24236
+ const publication = publicationSummary(await readBoundedJson(receiptPath, "Mobile promotion receipt"), platform, release, certification);
24237
+ const audit = {
24238
+ certification: {
24239
+ certificationId: certification.certificationId,
24240
+ requirement: certification.requirement,
24241
+ strength: certification.strength
24242
+ },
24243
+ format: ABSOLUTE_MOBILE_CI_PROMOTION_AUDIT_FORMAT,
24244
+ github: {
24245
+ conclusion: status.conclusion,
24246
+ dispatchId: context?.dispatchId ?? null,
24247
+ promotionRunId,
24248
+ repository: verification.identity.repository,
24249
+ sourceRunId,
24250
+ url: status.url,
24251
+ workflowName: status.workflowName
24252
+ },
24253
+ platform,
24254
+ provenance: verification.identity,
24255
+ publication,
24256
+ release: {
24257
+ appId: release.metadata.appId,
24258
+ bytes: release.metadata.bytes,
24259
+ engine: release.metadata.engine,
24260
+ releaseId: release.metadata.releaseId,
24261
+ runtime: release.metadata.runtime,
24262
+ sha256: release.metadata.sha256
24263
+ },
24264
+ status: "verified",
24265
+ verifiedAt: new Date().toISOString()
24266
+ };
24267
+ await Promise.all([
24268
+ writeFile15(join15(staging, "audit.json"), `${JSON.stringify(audit, null, 2)}
24269
+ `, { mode: 384 }),
24270
+ writeFile15(join15(staging, "audit.md"), renderAuditMarkdown(audit), {
24271
+ mode: 384
24272
+ })
24273
+ ]);
24274
+ await rename11(staging, output);
24275
+ return { audit, directory: output };
24276
+ } catch (error) {
24277
+ await rm8(staging, { force: true, recursive: true });
24278
+ throw error;
24279
+ }
24280
+ };
24281
+
24282
+ // src/mobile/updateSigning.ts
24283
+ import { createHash as createHash6, sign, verify } from "crypto";
24284
+ import {
24285
+ cp,
24286
+ mkdir as mkdir15,
24287
+ mkdtemp as mkdtemp6,
24288
+ readdir as readdir7,
24289
+ readFile as readFile20,
24290
+ rename as rename12,
24291
+ rm as rm9,
24292
+ stat as stat8,
24293
+ writeFile as writeFile16
24294
+ } from "fs/promises";
24295
+ import { dirname as dirname13, join as join16, relative as relative13, resolve as resolve14 } from "path";
23802
24296
  var UPDATE_MANIFEST_FILE = "update.json";
23803
24297
  var UPDATE_FILES_DIRECTORY = "files";
23804
24298
  var sha2564 = (value) => createHash6("sha256").update(value).digest("hex");
23805
24299
  var listFiles = async (root, directory = root) => {
23806
- const entries = await readdir6(directory, { withFileTypes: true });
24300
+ const entries = await readdir7(directory, { withFileTypes: true });
23807
24301
  const paths = await Promise.all(entries.map(async (entry) => {
23808
- const path = join15(directory, entry.name);
24302
+ const path = join16(directory, entry.name);
23809
24303
  if (entry.isDirectory())
23810
24304
  return listFiles(root, path);
23811
24305
  if (!entry.isFile())
23812
24306
  throw new TypeError("Mobile updates cannot contain links or special files.");
23813
- return [relative12(root, path).replaceAll("\\", "/")];
24307
+ return [relative13(root, path).replaceAll("\\", "/")];
23814
24308
  }));
23815
24309
  return paths.flat().sort((left, right) => left.localeCompare(right));
23816
24310
  };
23817
24311
  var inspectFiles = async (root, paths) => Promise.all(paths.map(async (path) => {
23818
- const source = join15(root, path);
24312
+ const source = join16(root, path);
23819
24313
  const [metadata, contents] = await Promise.all([
23820
- stat7(source),
23821
- readFile19(source)
24314
+ stat8(source),
24315
+ readFile20(source)
23822
24316
  ]);
23823
24317
  return { bytes: metadata.size, path, sha256: sha2564(contents) };
23824
24318
  }));
23825
24319
  var releaseIdFor = (value) => `amu_${sha2564(canonicalizeAbsoluteMobileUpdate(value))}`;
23826
24320
  var buildAbsoluteMobileUpdate = async (options) => {
23827
- const bundleDirectory = resolve13(options.bundleDirectory);
23828
- const outputRoot = resolve13(options.outputDirectory);
24321
+ const bundleDirectory = resolve14(options.bundleDirectory);
24322
+ const outputRoot = resolve14(options.outputDirectory);
23829
24323
  if (outputRoot === bundleDirectory || outputRoot.startsWith(`${bundleDirectory}/`))
23830
24324
  throw new TypeError("Mobile update output must be outside the embedded bundle.");
23831
24325
  const paths = await listFiles(bundleDirectory);
@@ -23853,30 +24347,30 @@ var buildAbsoluteMobileUpdate = async (options) => {
23853
24347
  value: signature.toString("base64")
23854
24348
  }
23855
24349
  });
23856
- await mkdir14(outputRoot, { recursive: true });
23857
- const outputDirectory = join15(outputRoot, manifest.releaseId);
23858
- const staging = await mkdtemp5(join15(outputRoot, ".stage-"));
24350
+ await mkdir15(outputRoot, { recursive: true });
24351
+ const outputDirectory = join16(outputRoot, manifest.releaseId);
24352
+ const staging = await mkdtemp6(join16(outputRoot, ".stage-"));
23859
24353
  try {
23860
- await cp(bundleDirectory, join15(staging, UPDATE_FILES_DIRECTORY), {
24354
+ await cp(bundleDirectory, join16(staging, UPDATE_FILES_DIRECTORY), {
23861
24355
  force: true,
23862
24356
  recursive: true
23863
24357
  });
23864
- await writeFile15(join15(staging, UPDATE_MANIFEST_FILE), `${JSON.stringify(manifest, null, "\t")}
24358
+ await writeFile16(join16(staging, UPDATE_MANIFEST_FILE), `${JSON.stringify(manifest, null, "\t")}
23865
24359
  `);
23866
- await rename11(staging, outputDirectory);
24360
+ await rename12(staging, outputDirectory);
23867
24361
  } catch (error) {
23868
- await rm8(staging, { force: true, recursive: true });
24362
+ await rm9(staging, { force: true, recursive: true });
23869
24363
  if (typeof error === "object" && error !== null && Reflect.get(error, "code") === "EEXIST")
23870
24364
  throw new TypeError(`Mobile update ${manifest.releaseId} already exists.`, { cause: error });
23871
24365
  throw error;
23872
24366
  }
23873
24367
  return {
23874
24368
  manifest,
23875
- manifestPath: join15(outputDirectory, UPDATE_MANIFEST_FILE),
24369
+ manifestPath: join16(outputDirectory, UPDATE_MANIFEST_FILE),
23876
24370
  outputDirectory
23877
24371
  };
23878
24372
  };
23879
- var readAbsoluteMobileUpdate = async (directory) => parseAbsoluteMobileUpdateManifest(JSON.parse(await readFile19(join15(resolve13(directory), UPDATE_MANIFEST_FILE), "utf8")));
24373
+ var readAbsoluteMobileUpdate = async (directory) => parseAbsoluteMobileUpdateManifest(JSON.parse(await readFile20(join16(resolve14(directory), UPDATE_MANIFEST_FILE), "utf8")));
23880
24374
  var verifyAbsoluteMobileUpdateSignature = (manifestValue, publicKey) => {
23881
24375
  const manifest = parseAbsoluteMobileUpdateManifest(manifestValue);
23882
24376
  const valid = verify("sha256", absoluteMobileUpdateSigningPayload(unsignedAbsoluteMobileUpdate(manifest)), { dsaEncoding: "ieee-p1363", key: publicKey }, Buffer.from(manifest.signature.value, "base64"));
@@ -23886,8 +24380,8 @@ var verifyAbsoluteMobileUpdateSignature = (manifestValue, publicKey) => {
23886
24380
  };
23887
24381
 
23888
24382
  // src/mobile/expoUpdate.ts
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";
24383
+ import { access as access14, mkdir as mkdir16, readFile as readFile21, writeFile as writeFile17 } from "fs/promises";
24384
+ import { dirname as dirname14, extname as extname3, join as join17 } from "path";
23891
24385
  var ABSOLUTE_EXPO_UPDATE_DESCRIPTOR = "_absolute/expo-update.json";
23892
24386
  var ABSOLUTE_EXPO_UPDATE_FORMAT = 1;
23893
24387
  var object2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
@@ -23953,7 +24447,7 @@ var parseAbsoluteExpoUpdateDescriptor = (value) => {
23953
24447
  };
23954
24448
  };
23955
24449
  var finalizeAbsoluteExpoUpdateExport = async (options) => {
23956
- const metadata = JSON.parse(await readFile20(join16(options.exportDirectory, "metadata.json"), "utf8"));
24450
+ const metadata = JSON.parse(await readFile21(join17(options.exportDirectory, "metadata.json"), "utf8"));
23957
24451
  if (!object2(metadata) || !object2(metadata.fileMetadata))
23958
24452
  throw new TypeError("Expo export metadata.json is invalid.");
23959
24453
  const descriptor = parseAbsoluteExpoUpdateDescriptor({
@@ -23964,30 +24458,30 @@ var finalizeAbsoluteExpoUpdateExport = async (options) => {
23964
24458
  runtimeVersion: options.runtimeVersion
23965
24459
  });
23966
24460
  const referenced = Object.values(descriptor.platforms).flatMap((entry) => entry ? [entry.launchAsset, ...entry.assets] : []);
23967
- await Promise.all(referenced.map(({ path }) => access13(join16(options.exportDirectory, path)).catch(() => {
24461
+ await Promise.all(referenced.map(({ path }) => access14(join17(options.exportDirectory, path)).catch(() => {
23968
24462
  throw new TypeError(`Expo export metadata references missing asset ${path}.`);
23969
24463
  })));
23970
- const destination = join16(options.exportDirectory, ABSOLUTE_EXPO_UPDATE_DESCRIPTOR);
23971
- await mkdir15(dirname13(destination), { recursive: true });
23972
- await writeFile16(destination, `${JSON.stringify(descriptor, null, "\t")}
24464
+ const destination = join17(options.exportDirectory, ABSOLUTE_EXPO_UPDATE_DESCRIPTOR);
24465
+ await mkdir16(dirname14(destination), { recursive: true });
24466
+ await writeFile17(destination, `${JSON.stringify(descriptor, null, "\t")}
23973
24467
  `);
23974
24468
  return { descriptor, path: destination };
23975
24469
  };
23976
24470
 
23977
24471
  // src/mobile/expoCodeSigning.ts
23978
24472
  var import_code_signing_certificates = __toESM(require_main(), 1);
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);
24473
+ import { access as access15, mkdir as mkdir17, writeFile as writeFile18 } from "fs/promises";
24474
+ import { dirname as dirname15, isAbsolute as isAbsolute9, relative as relative14, resolve as resolve15, sep as sep10 } from "path";
24475
+ var exists6 = async (path) => access15(path).then(() => true).catch(() => false);
23982
24476
  var inside4 = (root, path) => {
23983
- const location = relative13(root, path);
23984
- return location === "" || location !== ".." && !location.startsWith(`..${sep9}`) && !isAbsolute8(location);
24477
+ const location = relative14(root, path);
24478
+ return location === "" || location !== ".." && !location.startsWith(`..${sep10}`) && !isAbsolute9(location);
23985
24479
  };
23986
24480
  var generateAbsoluteExpoCodeSigning = async (options) => {
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"));
24481
+ const root = resolve15(options.projectRoot);
24482
+ const certificatePath = resolve15(root, options.certificatePath);
24483
+ const privateKeyPath = resolve15(root, options.privateKeyPath);
24484
+ const publicKeyPath = resolve15(root, options.publicKeyPath ?? resolve15(dirname15(privateKeyPath), "public-key.pem"));
23991
24485
  if (!inside4(root, certificatePath))
23992
24486
  throw new TypeError("Expo code-signing certificate must be written inside the project so store builds can embed it.");
23993
24487
  if (inside4(root, privateKeyPath) || inside4(root, publicKeyPath))
@@ -24017,11 +24511,11 @@ var generateAbsoluteExpoCodeSigning = async (options) => {
24017
24511
  import_code_signing_certificates.validateSelfSignedCertificate(certificate, keyPair);
24018
24512
  const { privateKeyPEM, publicKeyPEM } = import_code_signing_certificates.convertKeyPairToPEM(keyPair);
24019
24513
  const certificatePem = import_code_signing_certificates.convertCertificateToCertificatePEM(certificate);
24020
- await Promise.all([certificatePath, privateKeyPath, publicKeyPath].map((path) => mkdir16(dirname14(path), { recursive: true })));
24514
+ await Promise.all([certificatePath, privateKeyPath, publicKeyPath].map((path) => mkdir17(dirname15(path), { recursive: true })));
24021
24515
  await Promise.all([
24022
- writeFile17(certificatePath, certificatePem, { flag: "wx", mode: 420 }),
24023
- writeFile17(privateKeyPath, privateKeyPEM, { flag: "wx", mode: 384 }),
24024
- writeFile17(publicKeyPath, publicKeyPEM, { flag: "wx", mode: 420 })
24516
+ writeFile18(certificatePath, certificatePem, { flag: "wx", mode: 420 }),
24517
+ writeFile18(privateKeyPath, privateKeyPEM, { flag: "wx", mode: 384 }),
24518
+ writeFile18(publicKeyPath, publicKeyPEM, { flag: "wx", mode: 420 })
24025
24519
  ]);
24026
24520
  return {
24027
24521
  certificatePath,
@@ -24032,8 +24526,8 @@ var generateAbsoluteExpoCodeSigning = async (options) => {
24032
24526
  };
24033
24527
 
24034
24528
  // src/mobile/updatePublisher.ts
24035
- import { access as access15 } from "fs/promises";
24036
- import { isAbsolute as isAbsolute9, relative as relative14, resolve as resolve15, sep as sep10 } from "path";
24529
+ import { access as access16 } from "fs/promises";
24530
+ import { isAbsolute as isAbsolute10, relative as relative15, resolve as resolve16, sep as sep11 } from "path";
24037
24531
  import { pathToFileURL as pathToFileURL3 } from "url";
24038
24532
  var lifecycleMethod = (publisher, name) => {
24039
24533
  const method = publisher[name];
@@ -24141,17 +24635,17 @@ var pruneAbsoluteMobileUpdates = async (options) => {
24141
24635
  };
24142
24636
  var object3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
24143
24637
  var isPublisher2 = (value) => object3(value) && typeof value.publishUpdate === "function" && typeof value.promoteUpdate === "function" && typeof value.rollbackUpdate === "function";
24144
- var projectPath3 = (projectRoot, requested, label) => {
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))
24638
+ var projectPath4 = (projectRoot, requested, label) => {
24639
+ const root = resolve16(projectRoot);
24640
+ const path = resolve16(root, requested);
24641
+ const projectRelative = relative15(root, path);
24642
+ if (projectRelative === ".." || projectRelative.startsWith(`..${sep11}`) || isAbsolute10(projectRelative))
24149
24643
  throw new TypeError(`${label} must remain inside the project.`);
24150
24644
  return path;
24151
24645
  };
24152
24646
  var loadAbsoluteMobileUpdatePublisher = async (projectRoot, requestedModulePath) => {
24153
- const modulePath = projectPath3(projectRoot, requestedModulePath, "mobile update registry");
24154
- await access15(modulePath).catch(() => {
24647
+ const modulePath = projectPath4(projectRoot, requestedModulePath, "mobile update registry");
24648
+ await access16(modulePath).catch(() => {
24155
24649
  throw new TypeError(`Mobile update registry does not exist: ${modulePath}`);
24156
24650
  });
24157
24651
  const loaded = await import(pathToFileURL3(modulePath).href);
@@ -24173,7 +24667,7 @@ var promoteAbsoluteMobileUpdate = async (options) => {
24173
24667
  return result;
24174
24668
  };
24175
24669
  var publishAbsoluteMobileUpdate = async (options) => {
24176
- const releaseDirectory = projectPath3(options.projectRoot, options.releaseDirectory, "mobile update release directory");
24670
+ const releaseDirectory = projectPath4(options.projectRoot, options.releaseDirectory, "mobile update release directory");
24177
24671
  const manifest = await readAbsoluteMobileUpdate(releaseDirectory);
24178
24672
  const result = await options.publisher.publishUpdate({
24179
24673
  manifest,
@@ -24206,10 +24700,10 @@ var rollbackAbsoluteMobileUpdate = async (options) => {
24206
24700
  var NOT_FOUND4 = -1;
24207
24701
  var ANDROID_EMULATOR_BOOT_POLL_MS = 500;
24208
24702
  var ANDROID_EMULATOR_BOOT_TIMEOUT_MS = 180000;
24209
- var isRecord9 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
24703
+ var isRecord10 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
24210
24704
  var expoPublicConfig = (config) => {
24211
24705
  const result = { ...config };
24212
- if (!isRecord9(result.updates))
24706
+ if (!isRecord10(result.updates))
24213
24707
  return result;
24214
24708
  const {
24215
24709
  codeSigningCertificate: _codeSigningCertificate,
@@ -24249,13 +24743,13 @@ var CAPACITOR_SYNC_PACKAGE_SPECS = [
24249
24743
  ];
24250
24744
  var packageNameFromSpec = (spec) => spec.slice(0, spec.lastIndexOf("@"));
24251
24745
  var directProjectPackages = async (projectRoot) => {
24252
- const manifest = JSON.parse(await readFile21(join17(projectRoot, "package.json"), "utf8"));
24253
- if (!isRecord9(manifest))
24746
+ const manifest = JSON.parse(await readFile22(join18(projectRoot, "package.json"), "utf8"));
24747
+ if (!isRecord10(manifest))
24254
24748
  throw new TypeError("Application package.json must contain an object.");
24255
24749
  const names = new Set;
24256
24750
  for (const field2 of ["dependencies", "devDependencies"]) {
24257
24751
  const dependencies = Reflect.get(manifest, field2);
24258
- if (isRecord9(dependencies))
24752
+ if (isRecord10(dependencies))
24259
24753
  for (const name of Object.keys(dependencies))
24260
24754
  names.add(name);
24261
24755
  }
@@ -24263,8 +24757,8 @@ var directProjectPackages = async (projectRoot) => {
24263
24757
  };
24264
24758
  var resolvedPackageVersion = async (projectRoot, packageName) => {
24265
24759
  try {
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;
24760
+ const manifest = JSON.parse(await readFile22(join18(projectRoot, "node_modules", packageName, "package.json"), "utf8"));
24761
+ return isRecord10(manifest) && typeof manifest.version === "string" ? manifest.version : undefined;
24268
24762
  } catch {
24269
24763
  return;
24270
24764
  }
@@ -24327,9 +24821,9 @@ var runCapacitor = async (projectRoot, args) => {
24327
24821
  };
24328
24822
  var runCapacitorForPlatforms = (projectRoot, command, platforms) => platforms.reduce((pending, platform2) => pending.then(() => runCapacitor(projectRoot, [command, platform2])), Promise.resolve());
24329
24823
  var expoExecutable = async (project) => {
24330
- const executable = join17(project, "node_modules", ".bin", "expo");
24824
+ const executable = join18(project, "node_modules", ".bin", "expo");
24331
24825
  try {
24332
- await access16(executable);
24826
+ await access17(executable);
24333
24827
  return executable;
24334
24828
  } catch {
24335
24829
  throw new TypeError("Expo dependencies are not installed in the generated shell. Run `absolute mobile init --yes`.");
@@ -24361,14 +24855,14 @@ var runExpo = async (project, args, options = {}) => {
24361
24855
  };
24362
24856
  var ensureExpoPackages = async (project, args) => {
24363
24857
  try {
24364
- const manifest = JSON.parse(await readFile21(join17(project, "package.json"), "utf8"));
24858
+ const manifest = JSON.parse(await readFile22(join18(project, "package.json"), "utf8"));
24365
24859
  const dependencies = typeof manifest === "object" && manifest !== null ? Reflect.get(manifest, "dependencies") : undefined;
24366
24860
  if (typeof dependencies !== "object" || dependencies === null || Array.isArray(dependencies))
24367
24861
  throw new TypeError("Generated Expo dependencies are invalid.");
24368
24862
  await Promise.all(Object.entries(dependencies).map(async ([name, expected]) => {
24369
24863
  if (typeof expected !== "string")
24370
24864
  throw new TypeError("Generated Expo dependency version is invalid.");
24371
- const installed = JSON.parse(await readFile21(join17(project, "node_modules", name, "package.json"), "utf8"));
24865
+ const installed = JSON.parse(await readFile22(join18(project, "node_modules", name, "package.json"), "utf8"));
24372
24866
  const actual = typeof installed === "object" && installed !== null ? Reflect.get(installed, "version") : undefined;
24373
24867
  if (typeof actual !== "string")
24374
24868
  throw new TypeError("Installed Expo dependency version is invalid.");
@@ -24566,7 +25060,7 @@ var sync = async (args) => {
24566
25060
  };
24567
25061
  var associations = async (args) => {
24568
25062
  const { mobile, projectRoot } = await loadMobile(valueAfter(args, "--config"));
24569
- const outputDirectory = resolve16(projectRoot, valueAfter(args, "--outdir") ?? ".absolutejs/mobile/associations");
25063
+ const outputDirectory = resolve17(projectRoot, valueAfter(args, "--outdir") ?? ".absolutejs/mobile/associations");
24570
25064
  if (args.includes("--verify")) {
24571
25065
  const result2 = await verifyAbsoluteMobileAssociationFiles(mobile);
24572
25066
  console.log(`Verified ${result2.results.length} hosted association files`);
@@ -24618,7 +25112,7 @@ var generateGithubCi = async (args) => {
24618
25112
  const publicResult = {
24619
25113
  changed: result.changed,
24620
25114
  format: result.format,
24621
- path: relative15(projectRoot, result.path).replaceAll("\\", "/"),
25115
+ path: relative16(projectRoot, result.path).replaceAll("\\", "/"),
24622
25116
  platforms: result.platforms,
24623
25117
  publishing: result.publishing,
24624
25118
  requiredSecrets: result.requiredSecrets
@@ -24649,15 +25143,41 @@ var createGithubCertificationVerification = async (args) => {
24649
25143
  GITHUB_WORKFLOW_REF: process.env.GITHUB_WORKFLOW_REF
24650
25144
  });
24651
25145
  const path = await writeAbsoluteMobileCertificationVerification(projectRoot, outputPath, verification);
24652
- console.log(relative15(projectRoot, path).replaceAll("\\", "/"));
25146
+ console.log(relative16(projectRoot, path).replaceAll("\\", "/"));
24653
25147
  return verification;
24654
25148
  };
25149
+ var auditDispatchedGithubMobilePromotion = async (options) => {
25150
+ const startedAt = performance.now();
25151
+ try {
25152
+ const audited = await auditAbsoluteMobileCiPromotion({
25153
+ outputDirectory: valueAfter(options.args, "--outdir"),
25154
+ projectRoot: options.projectRoot,
25155
+ ...options.repository ? { repository: options.repository } : {},
25156
+ runId: options.runId
25157
+ });
25158
+ sendTelemetryEvent("mobile:ci-promotion-audit", {
25159
+ durationMs: Math.round(performance.now() - startedAt),
25160
+ platform: options.platform,
25161
+ provider: "github-actions",
25162
+ success: true
25163
+ });
25164
+ return audited;
25165
+ } catch (error) {
25166
+ sendTelemetryEvent("mobile:ci-promotion-audit", {
25167
+ durationMs: Math.round(performance.now() - startedAt),
25168
+ platform: options.platform,
25169
+ provider: "github-actions",
25170
+ success: false
25171
+ });
25172
+ throw error;
25173
+ }
25174
+ };
24655
25175
  var dispatchGithubMobilePromotion = async (args) => {
24656
25176
  const [platform2] = args;
24657
25177
  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))
25178
+ throw new TypeError("Usage: absolute mobile ci promote <android|ios> --run-id id --certification directory-or-json [--channel name] [store options] [--watch] [--audit]");
25179
+ const sourceRunId = valueAfter(args, "--run-id");
25180
+ if (!sourceRunId || !/^[1-9][0-9]*$/u.test(sourceRunId))
24661
25181
  throw new TypeError("mobile ci promote requires a numeric --run-id.");
24662
25182
  const requestedCertification = valueAfter(args, "--certification");
24663
25183
  if (!requestedCertification || requestedCertification.startsWith("-"))
@@ -24676,6 +25196,8 @@ var dispatchGithubMobilePromotion = async (args) => {
24676
25196
  if (certificationBase64.length > 48000)
24677
25197
  throw new TypeError("Mobile certification is too large for protected GitHub dispatch.");
24678
25198
  const workflow = valueAfter(args, "--workflow") ?? ".github/workflows/absolute-mobile.yml";
25199
+ const dispatchId2 = `amp_${randomUUID().replaceAll("-", "")}`;
25200
+ const repository = valueAfter(args, "--repo");
24679
25201
  const command = [
24680
25202
  "workflow",
24681
25203
  "run",
@@ -24685,7 +25207,9 @@ var dispatchGithubMobilePromotion = async (args) => {
24685
25207
  "-f",
24686
25208
  `platform=${platform2}`,
24687
25209
  "-f",
24688
- `source_run_id=${runId}`,
25210
+ `source_run_id=${sourceRunId}`,
25211
+ "-f",
25212
+ `dispatch_id=${dispatchId2}`,
24689
25213
  "-f",
24690
25214
  `certification_base64=${certificationBase64}`,
24691
25215
  "-f",
@@ -24700,21 +25224,149 @@ var dispatchGithubMobilePromotion = async (args) => {
24700
25224
  const ref = valueAfter(args, "--ref");
24701
25225
  if (ref)
24702
25226
  command.push("--ref", ref);
25227
+ if (repository)
25228
+ command.push("--repo", repository);
24703
25229
  const child = Bun.spawn(["gh", ...command], {
24704
25230
  cwd: projectRoot,
24705
- stderr: "inherit",
25231
+ stderr: "pipe",
24706
25232
  stdin: "inherit",
24707
- stdout: "inherit"
25233
+ stdout: "pipe"
24708
25234
  });
24709
- const exitCode = await child.exited;
25235
+ const [exitCode, stderr] = await Promise.all([
25236
+ child.exited,
25237
+ new Response(child.stderr).text(),
25238
+ new Response(child.stdout).text()
25239
+ ]);
24710
25240
  if (exitCode !== 0)
24711
- throw new TypeError(`GitHub promotion dispatch exited with status ${exitCode}.`);
25241
+ throw new TypeError(`GitHub promotion dispatch exited with status ${exitCode}: ${stderr.trim() || "unknown GitHub CLI error"}`);
25242
+ const promotion = await discoverAbsoluteMobilePromotionRun({
25243
+ dispatchId: dispatchId2,
25244
+ projectRoot,
25245
+ ...repository ? { repository } : {},
25246
+ workflow
25247
+ });
24712
25248
  sendTelemetryEvent("mobile:ci-promotion-dispatched", {
25249
+ discovery: "correlated",
24713
25250
  platform: platform2,
24714
25251
  provider: "github-actions",
24715
25252
  storeTarget: platform2 === "android" ? playTrack !== "registry-only" : testflightGroup !== ""
24716
25253
  });
24717
- console.log(`Dispatched protected ${platform2} promotion for ${certification.release.releaseId} from workflow run ${runId}.`);
25254
+ if (!args.includes("--json")) {
25255
+ console.log(`Dispatched protected ${platform2} promotion ${promotion.runId} for ${certification.release.releaseId} from workflow run ${sourceRunId}.`);
25256
+ console.log(promotion.url);
25257
+ }
25258
+ const shouldWatch = args.includes("--watch") || args.includes("--audit");
25259
+ if (shouldWatch && !args.includes("--json"))
25260
+ console.log(`Watching GitHub run ${promotion.runId} until completion\u2026`);
25261
+ const status = shouldWatch ? await watchAbsoluteMobileCiRun({
25262
+ projectRoot,
25263
+ ...repository ? { repository } : {},
25264
+ runId: promotion.runId
25265
+ }) : null;
25266
+ const audited = args.includes("--audit") ? await auditDispatchedGithubMobilePromotion({
25267
+ args,
25268
+ platform: platform2,
25269
+ projectRoot,
25270
+ ...repository ? { repository } : {},
25271
+ runId: promotion.runId
25272
+ }) : null;
25273
+ const result = {
25274
+ auditDirectory: audited ? relative16(projectRoot, audited.directory).replaceAll("\\", "/") : null,
25275
+ dispatchId: dispatchId2,
25276
+ platform: platform2,
25277
+ releaseId: certification.release.releaseId,
25278
+ runId: promotion.runId,
25279
+ sourceRunId,
25280
+ status: status?.status ?? "dispatched",
25281
+ url: promotion.url
25282
+ };
25283
+ if (args.includes("--json"))
25284
+ console.log(JSON.stringify(result, null, 2));
25285
+ if (args.includes("--json"))
25286
+ return result;
25287
+ if (status)
25288
+ renderGithubMobileRunStatus(status);
25289
+ if (audited)
25290
+ renderGithubMobilePromotionAudit(audited, projectRoot);
25291
+ return result;
25292
+ };
25293
+ var githubMobileRunId = (args) => {
25294
+ const value = valueAfter(args, "--run-id");
25295
+ if (!value || !/^[1-9][0-9]*$/u.test(value))
25296
+ throw new TypeError("mobile ci command requires a numeric --run-id.");
25297
+ return value;
25298
+ };
25299
+ var renderGithubMobileRunStatus = (status) => {
25300
+ console.log(`GitHub run ${status.runId}: ${status.status}${status.conclusion ? ` / ${status.conclusion}` : ""}`);
25301
+ console.log(`Workflow: ${status.workflowName}`);
25302
+ console.log(`Commit: ${status.headSha} (${status.headBranch})`);
25303
+ status.jobs.forEach((job) => console.log(` ${job.name}: ${job.status}${job.conclusion ? ` / ${job.conclusion}` : ""}`));
25304
+ console.log(status.url);
25305
+ };
25306
+ var inspectGithubMobileRun = async (args) => {
25307
+ const projectRoot = process.cwd();
25308
+ const requestedRunId = githubMobileRunId(args);
25309
+ const repository = valueAfter(args, "--repo");
25310
+ if (args.includes("--watch") && !args.includes("--json"))
25311
+ console.log(`Watching GitHub run ${requestedRunId} until completion\u2026`);
25312
+ const status = args.includes("--watch") ? await watchAbsoluteMobileCiRun({
25313
+ projectRoot,
25314
+ ...repository ? { repository } : {},
25315
+ runId: requestedRunId
25316
+ }) : await inspectAbsoluteMobileCiRun({
25317
+ projectRoot,
25318
+ ...repository ? { repository } : {},
25319
+ runId: requestedRunId
25320
+ });
25321
+ sendTelemetryEvent("mobile:ci-status", {
25322
+ conclusion: status.conclusion ?? "pending",
25323
+ provider: "github-actions",
25324
+ status: status.status,
25325
+ watched: args.includes("--watch")
25326
+ });
25327
+ if (args.includes("--json"))
25328
+ console.log(JSON.stringify(status, null, 2));
25329
+ else
25330
+ renderGithubMobileRunStatus(status);
25331
+ return status;
25332
+ };
25333
+ var renderGithubMobilePromotionAudit = (result, projectRoot) => {
25334
+ console.log(`\u2713 Verified ${result.audit.platform} promotion ${result.audit.github.promotionRunId} for ${result.audit.release.releaseId}.`);
25335
+ console.log(`Audit: ${relative16(projectRoot, result.directory)}`);
25336
+ };
25337
+ var auditGithubMobilePromotion = async (args) => {
25338
+ const projectRoot = process.cwd();
25339
+ const requestedRunId = githubMobileRunId(args);
25340
+ const repository = valueAfter(args, "--repo");
25341
+ const sourceRunId = valueAfter(args, "--source-run-id");
25342
+ const startedAt = performance.now();
25343
+ try {
25344
+ const result = await auditAbsoluteMobileCiPromotion({
25345
+ outputDirectory: valueAfter(args, "--outdir"),
25346
+ projectRoot,
25347
+ ...repository ? { repository } : {},
25348
+ runId: requestedRunId,
25349
+ ...sourceRunId ? { sourceRunId } : {}
25350
+ });
25351
+ sendTelemetryEvent("mobile:ci-promotion-audit", {
25352
+ durationMs: Math.round(performance.now() - startedAt),
25353
+ platform: result.audit.platform,
25354
+ provider: "github-actions",
25355
+ success: true
25356
+ });
25357
+ if (args.includes("--json"))
25358
+ console.log(JSON.stringify(result.audit, null, 2));
25359
+ else
25360
+ renderGithubMobilePromotionAudit(result, projectRoot);
25361
+ return result;
25362
+ } catch (error) {
25363
+ sendTelemetryEvent("mobile:ci-promotion-audit", {
25364
+ durationMs: Math.round(performance.now() - startedAt),
25365
+ provider: "github-actions",
25366
+ success: false
25367
+ });
25368
+ throw error;
25369
+ }
24718
25370
  };
24719
25371
  var doctorMark = (status) => {
24720
25372
  if (status === "pass")
@@ -24812,9 +25464,9 @@ var prepareExpoMobileUpdateExport = async (options) => {
24812
25464
  temporaryDirectory: undefined
24813
25465
  };
24814
25466
  await ensureExpoPackages(mobile.nativeProjectDirectory, options.args);
24815
- const temporaryParent = join17(options.projectRoot, ".absolutejs", "mobile", "expo-update-exports");
24816
- await mkdir17(temporaryParent, { recursive: true });
24817
- const temporaryDirectory = await mkdtemp6(join17(temporaryParent, ".stage-"));
25467
+ const temporaryParent = join18(options.projectRoot, ".absolutejs", "mobile", "expo-update-exports");
25468
+ await mkdir18(temporaryParent, { recursive: true });
25469
+ const temporaryDirectory = await mkdtemp7(join18(temporaryParent, ".stage-"));
24818
25470
  try {
24819
25471
  await runExpo(mobile.nativeProjectDirectory, [
24820
25472
  "export",
@@ -24826,9 +25478,9 @@ var prepareExpoMobileUpdateExport = async (options) => {
24826
25478
  temporaryDirectory,
24827
25479
  "--clear"
24828
25480
  ], { production: true });
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))
25481
+ const generatedApp = JSON.parse(await readFile22(join18(mobile.nativeProjectDirectory, "app.json"), "utf8"));
25482
+ const expoConfig = isRecord10(generatedApp) ? generatedApp.expo : undefined;
25483
+ if (!isRecord10(expoConfig))
24832
25484
  throw new TypeError("Generated Expo app configuration is invalid.");
24833
25485
  const publicExpoConfig = expoPublicConfig(expoConfig);
24834
25486
  const computedRuntime = resolveAbsoluteMobileUpdateRuntime(mobile, options.projectRoot).fingerprint;
@@ -24844,7 +25496,7 @@ var prepareExpoMobileUpdateExport = async (options) => {
24844
25496
  temporaryDirectory
24845
25497
  };
24846
25498
  } catch (error) {
24847
- await rm9(temporaryDirectory, { force: true, recursive: true });
25499
+ await rm10(temporaryDirectory, { force: true, recursive: true });
24848
25500
  throw error;
24849
25501
  }
24850
25502
  };
@@ -24869,11 +25521,11 @@ var buildMobileUpdate = async (args) => {
24869
25521
  let expoExportDirectory;
24870
25522
  try {
24871
25523
  await start(mobileBuildServerEntry(args), valueAfter(args, "--web-outdir"), configPath, { prepareOnly: true });
24872
- const embedded = JSON.parse(await readFile21(join17(mobile.bundleDirectory, "absolute-mobile-manifest.json"), "utf8"));
24873
- const runtimeFingerprint = isRecord9(embedded) ? embedded.nativeRuntime : undefined;
25524
+ const embedded = JSON.parse(await readFile22(join18(mobile.bundleDirectory, "absolute-mobile-manifest.json"), "utf8"));
25525
+ const runtimeFingerprint = isRecord10(embedded) ? embedded.nativeRuntime : undefined;
24874
25526
  if (typeof runtimeFingerprint !== "string" || !/^[a-f0-9]{64}$/u.test(runtimeFingerprint))
24875
25527
  throw new TypeError("Prepared mobile bundle is missing its native runtime fingerprint.");
24876
- const privateKey = await readFile21(resolve16(projectRoot, signingKeyPath));
25528
+ const privateKey = await readFile22(resolve17(projectRoot, signingKeyPath));
24877
25529
  const configuredPublicKey = Buffer.from(mobile.updates.publicKeys[keyId], "base64");
24878
25530
  const derivedPublicKey = createPublicKey2(privateKey).export({
24879
25531
  format: "der",
@@ -24894,7 +25546,7 @@ var buildMobileUpdate = async (args) => {
24894
25546
  channel: mobile.updates.channel,
24895
25547
  classification,
24896
25548
  keyId,
24897
- outputDirectory: valueAfter(args, "--outdir") ?? join17(projectRoot, ".absolutejs", "mobile", "updates"),
25549
+ outputDirectory: valueAfter(args, "--outdir") ?? join18(projectRoot, ".absolutejs", "mobile", "updates"),
24898
25550
  privateKey,
24899
25551
  runtimeFingerprint
24900
25552
  });
@@ -24906,7 +25558,7 @@ var buildMobileUpdate = async (args) => {
24906
25558
  return result;
24907
25559
  } finally {
24908
25560
  if (expoExportDirectory)
24909
- await rm9(expoExportDirectory, { force: true, recursive: true });
25561
+ await rm10(expoExportDirectory, { force: true, recursive: true });
24910
25562
  sendTelemetryEvent("mobile:update-build", {
24911
25563
  classification: classification ?? "invalid",
24912
25564
  durationMs: Math.round(performance.now() - startedAt),
@@ -24936,7 +25588,7 @@ var generateExpoUpdateSigning = async (args) => {
24936
25588
  ...valueAfter(args, "--public-key") ? { publicKeyPath: valueAfter(args, "--public-key") } : {},
24937
25589
  ...validityYears === undefined ? {} : { validityYears }
24938
25590
  });
24939
- const certificatePath = relative15(projectRoot, result.certificatePath).replaceAll("\\", "/");
25591
+ const certificatePath = relative16(projectRoot, result.certificatePath).replaceAll("\\", "/");
24940
25592
  sendTelemetryEvent("mobile:update-code-signing-generated", {
24941
25593
  engine: "expo",
24942
25594
  validityYears: validityYears ?? 10
@@ -24998,7 +25650,7 @@ var provisionMobileUpdate = async (args) => {
24998
25650
  publicKeys: mobile.updates.publicKeys,
24999
25651
  storage: requestedStorage
25000
25652
  });
25001
- console.log(`Provisioned ${requestedStorage === "local" ? "local development" : "durable S3-compatible"} mobile update storage in ${relative15(projectRoot, path).replaceAll("\\", "/")}.`);
25653
+ console.log(`Provisioned ${requestedStorage === "local" ? "local development" : "durable S3-compatible"} mobile update storage in ${relative16(projectRoot, path).replaceAll("\\", "/")}.`);
25002
25654
  if (requestedStorage === "local")
25003
25655
  console.log("Production release checks will require durable storage. Re-run with --storage s3 --force before deploying.");
25004
25656
  else
@@ -25475,7 +26127,7 @@ var buildAndroid = async (args, prepareVersionCode) => {
25475
26127
  const durationMs = Math.round(performance.now() - startedAt);
25476
26128
  console.log(`Built ${release.metadata.signed ? "signed" : "unsigned"} Android App Bundle in ${getDurationString(durationMs)}.`);
25477
26129
  console.log(`Artifact: ${release.artifactPath}`);
25478
- console.log(`Metadata: ${join17(release.releaseRoot, "release.json")}`);
26130
+ console.log(`Metadata: ${join18(release.releaseRoot, "release.json")}`);
25479
26131
  return release;
25480
26132
  } finally {
25481
26133
  sendTelemetryEvent("mobile:android-release-build", {
@@ -25698,7 +26350,7 @@ var buildIos = async (args, prepareBuildNumber) => {
25698
26350
  console.log(`Artifact: ${release.artifactPath}`);
25699
26351
  if (release.registeredArtifactPath)
25700
26352
  console.log(`Registered-device artifact: ${release.registeredArtifactPath}`);
25701
- console.log(`Metadata: ${join17(release.releaseRoot, "release.json")}`);
26353
+ console.log(`Metadata: ${join18(release.releaseRoot, "release.json")}`);
25702
26354
  return release;
25703
26355
  } finally {
25704
26356
  stopListeningForCancellation();
@@ -25849,7 +26501,7 @@ var appendSyncSchemaDoctorCheck = (checks, projectRoot) => {
25849
26501
  checks.push({
25850
26502
  id: "sync.storage-schema",
25851
26503
  label: `Offline schema ${schema.components.map((component) => `${component.id}@${component.version}`).join(", ")}`,
25852
- path: join17(projectRoot, "package.json"),
26504
+ path: join18(projectRoot, "package.json"),
25853
26505
  platform: "host",
25854
26506
  status: "pass"
25855
26507
  });
@@ -25857,7 +26509,7 @@ var appendSyncSchemaDoctorCheck = (checks, projectRoot) => {
25857
26509
  checks.push({
25858
26510
  id: "sync.storage-schema",
25859
26511
  label: "Offline schema metadata is invalid",
25860
- path: join17(projectRoot, "package.json"),
26512
+ path: join18(projectRoot, "package.json"),
25861
26513
  platform: "host",
25862
26514
  remediation: error instanceof Error ? error.message : String(error),
25863
26515
  status: "fail"
@@ -25945,7 +26597,7 @@ var requireAndroidTestPort = (args, projectRoot) => {
25945
26597
  }
25946
26598
  return { https: args.includes("--https"), port };
25947
26599
  }
25948
- const instances = listLiveInstances().filter((instance2) => resolve16(instance2.cwd) === resolve16(projectRoot) && instance2.source === "dev" && instance2.port !== null);
26600
+ const instances = listLiveInstances().filter((instance2) => resolve17(instance2.cwd) === resolve17(projectRoot) && instance2.source === "dev" && instance2.port !== null);
25949
26601
  if (instances.length !== 1) {
25950
26602
  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>.");
25951
26603
  }
@@ -26039,8 +26691,8 @@ var selectOrStartAndroidReleaseEmulator = async (adb2, emulator, explicitSerial,
26039
26691
  return waitForBoot();
26040
26692
  };
26041
26693
  var safeArtifactRoot = (projectRoot, value) => {
26042
- const root = resolve16(projectRoot, value ?? ".absolutejs/mobile/test-artifacts");
26043
- if (root !== projectRoot && !root.startsWith(`${resolve16(projectRoot)}/`)) {
26694
+ const root = resolve17(projectRoot, value ?? ".absolutejs/mobile/test-artifacts");
26695
+ if (root !== projectRoot && !root.startsWith(`${resolve17(projectRoot)}/`)) {
26044
26696
  throw new TypeError("mobile test --artifacts must remain inside the project.");
26045
26697
  }
26046
26698
  return root;
@@ -26068,12 +26720,12 @@ var waitForRequestedAndroidHmr = async (args, session, checks, timeoutMs) => {
26068
26720
  });
26069
26721
  };
26070
26722
  var writeAndroidFailureArtifacts = async (options) => {
26071
- await mkdir17(options.artifactRoot, { recursive: true });
26072
- const screenshot = options.session ? await options.session.screenshot(join17(options.artifactRoot, "android-failure.png")).catch(() => {
26723
+ await mkdir18(options.artifactRoot, { recursive: true });
26724
+ const screenshot = options.session ? await options.session.screenshot(join18(options.artifactRoot, "android-failure.png")).catch(() => {
26073
26725
  return;
26074
26726
  }) : undefined;
26075
- const diagnosticPath = join17(options.artifactRoot, "android-failure.json");
26076
- await writeFile18(diagnosticPath, `${JSON.stringify({
26727
+ const diagnosticPath = join18(options.artifactRoot, "android-failure.json");
26728
+ await writeFile19(diagnosticPath, `${JSON.stringify({
26077
26729
  diagnostics: options.session?.diagnostics ?? [],
26078
26730
  error: options.error instanceof Error ? options.error.message : String(options.error),
26079
26731
  platform: "android",
@@ -26094,7 +26746,7 @@ var captureAndroidReleaseScreenshot = async (adb2, serial, destination) => {
26094
26746
  ]);
26095
26747
  if (exitCode !== 0)
26096
26748
  throw new Error(`Android release screenshot failed: ${stderr.trim() || `status ${exitCode}`}`);
26097
- await writeFile18(destination, bytes);
26749
+ await writeFile19(destination, bytes);
26098
26750
  return destination;
26099
26751
  };
26100
26752
  var printAndroidReleaseAcceptance = (result, json) => {
@@ -26148,7 +26800,7 @@ var testAndroidRelease = async (args, mobile, projectRoot) => {
26148
26800
  release,
26149
26801
  serial
26150
26802
  });
26151
- const screenshot = reportRoot ? await captureAndroidReleaseScreenshot(adb2, serial, join17(reportRoot, "android-release.png")) : undefined;
26803
+ const screenshot = reportRoot ? await captureAndroidReleaseScreenshot(adb2, serial, join18(reportRoot, "android-release.png")) : undefined;
26152
26804
  sendTelemetryEvent("mobile:android-release-conformance", {
26153
26805
  durationMs: result.durationMs,
26154
26806
  engine: result.engine,
@@ -26195,9 +26847,9 @@ var testAndroidRelease = async (args, mobile, projectRoot) => {
26195
26847
  platform: "android",
26196
26848
  success: false
26197
26849
  });
26198
- await mkdir17(artifactRoot, { recursive: true });
26199
- const diagnosticPath = join17(artifactRoot, "android-release-failure.json");
26200
- await writeFile18(diagnosticPath, `${JSON.stringify({
26850
+ await mkdir18(artifactRoot, { recursive: true });
26851
+ const diagnosticPath = join18(artifactRoot, "android-release-failure.json");
26852
+ await writeFile19(diagnosticPath, `${JSON.stringify({
26201
26853
  error: sanitizeNativeReportText(error instanceof Error ? error.message : String(error)),
26202
26854
  platform: "android",
26203
26855
  provider: mobile.engine,
@@ -26291,7 +26943,7 @@ var testAndroid = async (args) => {
26291
26943
  console.log(JSON.stringify(report, null, 2));
26292
26944
  else
26293
26945
  printAndroidTestReport(report);
26294
- const screenshot = reportRoot ? await session.screenshot(join17(artifactRoot, "android-emulator.png")) : undefined;
26946
+ const screenshot = reportRoot ? await session.screenshot(join18(artifactRoot, "android-emulator.png")) : undefined;
26295
26947
  await writeRequestedAndroidReport({
26296
26948
  adb: adb2,
26297
26949
  args,
@@ -26360,14 +27012,14 @@ var requireIosTestContext = (args, projectRoot) => {
26360
27012
  const port = Number(explicit);
26361
27013
  if (!Number.isInteger(port) || port < 1 || port > 65535)
26362
27014
  throw new TypeError("mobile test --port must be a valid TCP port.");
26363
- const instance2 = listLiveInstances().find((candidate) => resolve16(candidate.cwd) === resolve16(projectRoot) && candidate.source === "dev" && candidate.port === port);
27015
+ const instance2 = listLiveInstances().find((candidate) => resolve17(candidate.cwd) === resolve17(projectRoot) && candidate.source === "dev" && candidate.port === port);
26364
27016
  return {
26365
27017
  https: instance2?.https ?? args.includes("--https"),
26366
27018
  instance: instance2,
26367
27019
  port
26368
27020
  };
26369
27021
  }
26370
- const instances = listLiveInstances().filter((instance2) => resolve16(instance2.cwd) === resolve16(projectRoot) && instance2.source === "dev" && instance2.port !== null);
27022
+ const instances = listLiveInstances().filter((instance2) => resolve17(instance2.cwd) === resolve17(projectRoot) && instance2.source === "dev" && instance2.port !== null);
26371
27023
  if (instances.length !== 1)
26372
27024
  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>.");
26373
27025
  const [instance] = instances;
@@ -26385,7 +27037,7 @@ var waitForIosHmrClient = async (options) => {
26385
27037
  return;
26386
27038
  });
26387
27039
  const status = response?.ok ? await response.json().catch(() => null) : null;
26388
- const targets = isRecord9(status) && isRecord9(status.connectedTargets) ? status.connectedTargets : undefined;
27040
+ const targets = isRecord10(status) && isRecord10(status.connectedTargets) ? status.connectedTargets : undefined;
26389
27041
  if (targets && typeof targets["capacitor-ios"] === "number" && targets["capacitor-ios"] > 0)
26390
27042
  return;
26391
27043
  await Bun.sleep(100);
@@ -26511,8 +27163,8 @@ var requireCapturedCommand = (command, label) => {
26511
27163
  return result;
26512
27164
  };
26513
27165
  var writeIosFailureArtifacts = async (options) => {
26514
- await mkdir17(options.artifactRoot, { recursive: true });
26515
- const screenshot = join17(options.artifactRoot, "ios-failure.png");
27166
+ await mkdir18(options.artifactRoot, { recursive: true });
27167
+ const screenshot = join18(options.artifactRoot, "ios-failure.png");
26516
27168
  const screenshotResult = captureCommand4([
26517
27169
  options.xcrun,
26518
27170
  "simctl",
@@ -26521,8 +27173,8 @@ var writeIosFailureArtifacts = async (options) => {
26521
27173
  "screenshot",
26522
27174
  screenshot
26523
27175
  ]);
26524
- const diagnosticPath = join17(options.artifactRoot, "ios-failure.json");
26525
- await writeFile18(diagnosticPath, `${JSON.stringify({
27176
+ const diagnosticPath = join18(options.artifactRoot, "ios-failure.json");
27177
+ await writeFile19(diagnosticPath, `${JSON.stringify({
26526
27178
  appId: options.appId,
26527
27179
  error: options.error instanceof Error ? options.error.message : String(options.error),
26528
27180
  platform: "ios",
@@ -26549,8 +27201,8 @@ var nativeReportRoot = (args, projectRoot, platform2) => {
26549
27201
  var absolutejsVersionForReport = async () => {
26550
27202
  let absolutejsVersion = process.env.ABSOLUTE_VERSION ?? "unknown";
26551
27203
  const versions = await Promise.all([
26552
- resolve16(import.meta.dir, "..", "..", "package.json"),
26553
- resolve16(import.meta.dir, "..", "..", "..", "package.json")
27204
+ resolve17(import.meta.dir, "..", "..", "package.json"),
27205
+ resolve17(import.meta.dir, "..", "..", "..", "package.json")
26554
27206
  ].map((candidate) => readPackageVersionForIosReport(candidate).catch(() => "unknown")));
26555
27207
  for (const version of versions) {
26556
27208
  if (version === "unknown")
@@ -26927,9 +27579,9 @@ var testIosRelease = async (args, mobile, projectRoot) => {
26927
27579
  remote: remoteProfile !== undefined,
26928
27580
  success: false
26929
27581
  });
26930
- await mkdir17(artifactRoot, { recursive: true });
26931
- const diagnosticPath = join17(artifactRoot, "ios-release-failure.json");
26932
- await writeFile18(diagnosticPath, `${JSON.stringify({
27582
+ await mkdir18(artifactRoot, { recursive: true });
27583
+ const diagnosticPath = join18(artifactRoot, "ios-release-failure.json");
27584
+ await writeFile19(diagnosticPath, `${JSON.stringify({
26933
27585
  error: sanitizeIosReportText(error instanceof Error ? error.message : String(error)),
26934
27586
  platform: "ios",
26935
27587
  provider: mobile.engine,
@@ -27022,8 +27674,8 @@ var testIos = async (args) => {
27022
27674
  mobile.appId
27023
27675
  ], "iOS app launch");
27024
27676
  await waitForIosHmrClient({ https, port, timeoutMs });
27025
- await mkdir17(artifactRoot, { recursive: true });
27026
- const screenshot = join17(artifactRoot, "ios-simulator.png");
27677
+ await mkdir18(artifactRoot, { recursive: true });
27678
+ const screenshot = join18(artifactRoot, "ios-simulator.png");
27027
27679
  requireCapturedCommand([xcrun, "simctl", "io", simulator.udid, "screenshot", screenshot], "iOS simulator screenshot");
27028
27680
  const hmrApply = await waitForRequestedIosHmr(args, instance, timeoutMs);
27029
27681
  const report = {
@@ -27240,6 +27892,10 @@ var runMobile = async (args) => {
27240
27892
  if (command === "ci") {
27241
27893
  if (args[1] === "promote")
27242
27894
  await dispatchGithubMobilePromotion(args.slice(2));
27895
+ else if (args[1] === "status")
27896
+ await inspectGithubMobileRun(args.slice(2));
27897
+ else if (args[1] === "audit")
27898
+ await auditGithubMobilePromotion(args.slice(2));
27243
27899
  else if (args[1] === "verification")
27244
27900
  await createGithubCertificationVerification(args.slice(2));
27245
27901
  else
@@ -27322,7 +27978,7 @@ var runMobile = async (args) => {
27322
27978
  await publishIos(args.slice(2));
27323
27979
  return;
27324
27980
  }
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]");
27981
+ 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] [--repo owner/name] [--watch] [--audit] [--outdir path] [--json] | ci status --run-id id [--watch] [--repo owner/name] [--json] | ci audit --run-id id [--source-run-id id] [--repo owner/name] [--outdir path] [--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] [--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]");
27326
27982
  };
27327
27983
  export {
27328
27984
  runMobile