electron-cli 0.3.0-alpha.19 → 0.3.0-alpha.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "electron-cli"
3
- version = "0.3.0-alpha.19"
3
+ version = "0.3.0-alpha.20"
4
4
  edition = "2021"
5
5
  description = "Experimental Rust CLI for Electron project diagnostics and workflow automation"
6
6
  license = "MIT"
@@ -8,7 +8,8 @@ repository = "https://github.com/Ikana/electron-cli"
8
8
 
9
9
  [dependencies]
10
10
  anyhow = "1.0"
11
- apple-codesign = { version = "0.29", default-features = false }
11
+ app-store-connect = "0.7"
12
+ apple-codesign = { version = "0.29", default-features = false, features = ["notarize"] }
12
13
  apple-dmg = "0.5"
13
14
  cab = "0.6"
14
15
  camino = { version = "1.1", features = ["serde1"] }
package/README.md CHANGED
@@ -41,9 +41,9 @@ The Rust-native flow currently owns:
41
41
 
42
42
  The GitHub publisher creates or reuses a release, uploads selected make artifacts, and can replace an existing asset with `--force`. It reads `GITHUB_TOKEN` or `GH_TOKEN` and can infer `OWNER/REPO` from package metadata, Forge GitHub publisher config, or `package.json` `repository`. You can also pass `--github-repo`.
43
43
 
44
- The package command recognizes macOS `packagerConfig.osxSign` and `packagerConfig.osxNotarize` options and reports the signing/notarization plan without serializing credential values. When `osxSign` is enabled on macOS and no certificate identity is configured, or the identity is `"-"`, `package` writes an experimental Rust-native ad-hoc signature for the generated `.app` bundle. When `osxSign.p12File` points at a `.p12`/PFX certificate export, `package` can sign the bundle with that certificate and can request a CMS timestamp token for notarization-compatible signatures. macOS keychain identity lookup and notarization submission/stapling are not implemented yet.
44
+ The package command recognizes macOS `packagerConfig.osxSign` and `packagerConfig.osxNotarize` options and reports the signing/notarization plan without serializing credential values. When `osxSign` is enabled on macOS and no certificate identity is configured, or the identity is `"-"`, `package` writes an experimental Rust-native ad-hoc signature for the generated `.app` bundle. When `osxSign.p12File` points at a `.p12`/PFX certificate export, `package` can sign the bundle with that certificate and can request a CMS timestamp token for notarization-compatible signatures. With p12 signing and App Store Connect API key auth, `package` can submit to Apple notarization, wait for the result, and staple the ticket natively in Rust. macOS keychain identity lookup and keychain/Apple ID notarization auth are not implemented yet.
45
45
 
46
- The DMG maker is currently a pure-Rust FAT32 image with the app bundle and an Applications entry. The MSI maker writes a compressed embedded CAB, Windows Installer database tables, and a Start Menu shortcut when the packaged executable is present. HFS+/APFS DMG layout customization, installer UI customization, Windows/Linux icon embedding, macOS keychain signing, and notarization execution are still TODO.
46
+ The DMG maker is currently a pure-Rust FAT32 image with the app bundle and an Applications entry. The MSI maker writes a compressed embedded CAB, Windows Installer database tables, and a Start Menu shortcut when the packaged executable is present. HFS+/APFS DMG layout customization, installer UI customization, Windows/Linux icon embedding, macOS keychain signing, and additional notarization auth modes are still TODO.
47
47
 
48
48
  Package metadata can be configured in `package.json`:
49
49
 
@@ -64,7 +64,10 @@ Package metadata can be configured in `package.json`:
64
64
  "hardenedRuntime": true
65
65
  },
66
66
  "osxNotarize": {
67
- "keychainProfile": "notary-profile"
67
+ "appleApiKey": "certs/AuthKey_ABC123DEFG.p8",
68
+ "appleApiKeyId": "ABC123DEFG",
69
+ "appleApiIssuer": "00000000-0000-0000-0000-000000000000",
70
+ "maxWaitSeconds": 600
68
71
  }
69
72
  },
70
73
  "makers": [
@@ -99,7 +102,7 @@ Package metadata can be configured in `package.json`:
99
102
  }
100
103
  ```
101
104
 
102
- Use `p12PasswordEnv`, `p12PasswordFile`, or `p12Password` for the `.p12` password; password values are not serialized in package reports. Set `osxSign.timestamp` to a timestamp server URL, `true` for Apple's default `http://timestamp.apple.com/ts01`, or `"none"` / `false` to disable timestamping. When `osxNotarize` is enabled with p12 signing, `electron-cli` automatically enables notarization-compatible signing and uses Apple's timestamp server unless timestamping is disabled explicitly. Set `identity` to a Developer ID certificate name when you want the plan to reflect Forge-style keychain release signing, but this project will report it as not executable until Rust-native keychain lookup exists. Use `identity: "-"` or omit `identity` for the current ad-hoc signing path.
105
+ Use `p12PasswordEnv`, `p12PasswordFile`, or `p12Password` for the `.p12` password; password values are not serialized in package reports. Set `osxSign.timestamp` to a timestamp server URL, `true` for Apple's default `http://timestamp.apple.com/ts01`, or `"none"` / `false` to disable timestamping. When `osxNotarize` is enabled with p12 signing, `electron-cli` automatically enables notarization-compatible signing and uses Apple's timestamp server unless timestamping is disabled explicitly. Rust-native notarization execution currently requires `appleApiKey`, `appleApiKeyId`, and `appleApiIssuer`; `appleApiKey` may point at the `.p8` file from App Store Connect or a unified API key JSON file. It waits up to `maxWaitSeconds` or 600 seconds by default and staples by default. Set `staple: false` to skip stapling, or set both `staple: false` and `wait: false` to submit without waiting. Set `identity` to a Developer ID certificate name when you want the plan to reflect Forge-style keychain release signing, but this project will report it as not executable until Rust-native keychain lookup exists. Use `identity: "-"` or omit `identity` for the current ad-hoc signing path.
103
106
 
104
107
  The package command also reads JSON-shaped `config.forge.packagerConfig` and `electronPackagerConfig` entries for the same fields. The make command maps JSON-shaped Forge maker names to the Rust-native targets it supports: zip, dmg, deb, rpm, and wix/msi. The publish command maps JSON-shaped publisher names to local and GitHub.
105
108
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "electron-cli",
3
- "version": "0.3.0-alpha.19",
3
+ "version": "0.3.0-alpha.20",
4
4
  "description": "Experimental Rust CLI for Electron project diagnostics and workflow automation",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -5,9 +5,12 @@ use std::{
5
5
  };
6
6
 
7
7
  use anyhow::{bail, Context, Result};
8
+ use app_store_connect::UnifiedApiKey;
8
9
  use apple_codesign::{
9
10
  cryptography::{parse_pfx_data, PrivateKey},
10
- BundleSigner, CodeSignatureFlags, SettingsScope, SigningSettings,
11
+ stapling::Stapler,
12
+ BundleSigner, CodeSignatureFlags, NotarizationUpload, Notarizer, SettingsScope,
13
+ SigningSettings,
11
14
  };
12
15
  use camino::Utf8PathBuf;
13
16
  use plist::{Dictionary as PlistDictionary, Value as PlistValue};
@@ -17,6 +20,7 @@ use serde_json::Value as JsonValue;
17
20
  use crate::{cli::PackageArgs, output, project::ProjectSnapshot};
18
21
 
19
22
  const APPLE_TIMESTAMP_URL: &str = "http://timestamp.apple.com/ts01";
23
+ const MACOS_NOTARIZATION_WAIT_TIMEOUT_SECONDS: u64 = 600;
20
24
 
21
25
  #[derive(Clone, Debug, Serialize)]
22
26
  pub(crate) struct PackageReport {
@@ -98,9 +102,18 @@ struct MacosSignPlan {
98
102
  struct MacosNotarizePlan {
99
103
  configured: bool,
100
104
  enabled: bool,
105
+ will_execute: bool,
101
106
  auth_method: Option<String>,
107
+ apple_api_key: Option<Utf8PathBuf>,
108
+ #[serde(skip)]
109
+ apple_api_key_id: RedactedSecret,
110
+ #[serde(skip)]
111
+ apple_api_issuer: RedactedSecret,
102
112
  keychain_profile: Option<String>,
103
113
  keychain: Option<String>,
114
+ wait: bool,
115
+ wait_timeout_seconds: u64,
116
+ staple: bool,
104
117
  }
105
118
 
106
119
  #[derive(Clone, Default)]
@@ -190,10 +203,13 @@ struct MacosNotarizeConfig {
190
203
  apple_id_password_set: bool,
191
204
  team_id_set: bool,
192
205
  apple_api_key: Option<String>,
193
- apple_api_key_id_set: bool,
194
- apple_api_issuer_set: bool,
206
+ apple_api_key_id: Option<String>,
207
+ apple_api_issuer: Option<String>,
195
208
  keychain_profile: Option<String>,
196
209
  keychain: Option<String>,
210
+ wait: Option<bool>,
211
+ wait_timeout_seconds: Option<u64>,
212
+ staple: Option<bool>,
197
213
  }
198
214
 
199
215
  pub fn run(args: PackageArgs) -> Result<()> {
@@ -415,6 +431,7 @@ pub(crate) fn execute_package(report: &PackageReport, force: bool) -> Result<()>
415
431
  &report.project,
416
432
  )?;
417
433
  execute_macos_signing(report)?;
434
+ execute_macos_notarization(report)?;
418
435
 
419
436
  Ok(())
420
437
  }
@@ -513,6 +530,106 @@ fn execute_macos_signing(report: &PackageReport) -> Result<()> {
513
530
  Ok(())
514
531
  }
515
532
 
533
+ fn execute_macos_notarization(report: &PackageReport) -> Result<()> {
534
+ if report.platform != "darwin" || !report.signing.macos.notarize.will_execute {
535
+ return Ok(());
536
+ }
537
+
538
+ let notarize = &report.signing.macos.notarize;
539
+ let bundle_dir = Path::new(report.bundle_dir.as_str());
540
+ let wait_limit = notarize.wait.then_some(std::time::Duration::from_secs(
541
+ notarize.wait_timeout_seconds,
542
+ ));
543
+ let notarizer = macos_notarizer(notarize)?;
544
+ let upload = notarizer
545
+ .notarize_path(bundle_dir, wait_limit)
546
+ .with_context(|| format!("Could not notarize macOS bundle {}", bundle_dir.display()))?;
547
+
548
+ if notarize.staple {
549
+ match upload {
550
+ NotarizationUpload::NotaryResponse(_) => {
551
+ let stapler =
552
+ Stapler::new().context("Could not prepare macOS notarization stapler")?;
553
+ stapler.staple_path(bundle_dir).with_context(|| {
554
+ format!(
555
+ "Could not staple notarization ticket to {}",
556
+ bundle_dir.display()
557
+ )
558
+ })?;
559
+ }
560
+ NotarizationUpload::UploadId(upload_id) => {
561
+ bail!(
562
+ "macOS notarization upload {upload_id} was submitted without waiting; stapling requires a completed notarization result."
563
+ );
564
+ }
565
+ }
566
+ }
567
+
568
+ Ok(())
569
+ }
570
+
571
+ fn macos_notarizer(notarize: &MacosNotarizePlan) -> Result<Notarizer> {
572
+ let api_key_path = notarize
573
+ .apple_api_key
574
+ .as_ref()
575
+ .context("macOS notarization requires appleApiKey")?;
576
+ let api_key_path = Path::new(api_key_path.as_str());
577
+ let key_id = notarize
578
+ .apple_api_key_id
579
+ .as_deref()
580
+ .context("macOS notarization requires appleApiKeyId")?;
581
+ let issuer = notarize
582
+ .apple_api_issuer
583
+ .as_deref()
584
+ .context("macOS notarization requires appleApiIssuer")?;
585
+
586
+ if path_extension(api_key_path) == Some("json") {
587
+ return Notarizer::from_api_key(api_key_path)
588
+ .with_context(|| format!("Could not load Apple API key {}", api_key_path.display()));
589
+ }
590
+
591
+ let temp_api_key = temporary_unified_api_key(issuer, key_id, api_key_path)?;
592
+ Notarizer::from_api_key(&temp_api_key.path)
593
+ .with_context(|| format!("Could not load Apple API key {}", api_key_path.display()))
594
+ }
595
+
596
+ struct TemporaryFile {
597
+ path: PathBuf,
598
+ }
599
+
600
+ impl Drop for TemporaryFile {
601
+ fn drop(&mut self) {
602
+ let _ = fs::remove_file(&self.path);
603
+ }
604
+ }
605
+
606
+ fn temporary_unified_api_key(
607
+ issuer: &str,
608
+ key_id: &str,
609
+ private_key_path: &Path,
610
+ ) -> Result<TemporaryFile> {
611
+ let unique_suffix = std::time::SystemTime::now()
612
+ .duration_since(std::time::UNIX_EPOCH)
613
+ .context("system clock is before the Unix epoch")?
614
+ .as_nanos();
615
+ let path = std::env::temp_dir().join(format!(
616
+ "electron-cli-notary-key-{}-{unique_suffix}.json",
617
+ std::process::id()
618
+ ));
619
+ let unified = UnifiedApiKey::from_ecdsa_pem_path(issuer, key_id, private_key_path)
620
+ .with_context(|| {
621
+ format!(
622
+ "Could not read Apple API private key {}",
623
+ private_key_path.display()
624
+ )
625
+ })?;
626
+ unified
627
+ .write_json_file(&path)
628
+ .with_context(|| format!("Could not write temporary Apple API key {}", path.display()))?;
629
+
630
+ Ok(TemporaryFile { path })
631
+ }
632
+
516
633
  fn macos_signing_settings<'key>(report: &PackageReport) -> Result<SigningSettings<'key>> {
517
634
  let sign = &report.signing.macos.sign;
518
635
  let mut settings = SigningSettings::default();
@@ -651,6 +768,35 @@ fn print_report(report: &PackageReport, json: bool) -> Result<()> {
651
768
  if let Some(method) = &report.signing.macos.notarize.auth_method {
652
769
  println!(" notarization auth: {method}");
653
770
  }
771
+ if let Some(path) = &report.signing.macos.notarize.apple_api_key {
772
+ println!(" Apple API key: {path}");
773
+ }
774
+ println!(
775
+ " notarization execution: {}",
776
+ if report.signing.macos.notarize.will_execute {
777
+ "enabled"
778
+ } else {
779
+ "not available"
780
+ }
781
+ );
782
+ if report.signing.macos.notarize.will_execute {
783
+ println!(
784
+ " notarization wait: {}",
785
+ if report.signing.macos.notarize.wait {
786
+ format!("{}s", report.signing.macos.notarize.wait_timeout_seconds)
787
+ } else {
788
+ "disabled".to_string()
789
+ }
790
+ );
791
+ println!(
792
+ " notarization stapling: {}",
793
+ if report.signing.macos.notarize.staple {
794
+ "enabled"
795
+ } else {
796
+ "disabled"
797
+ }
798
+ );
799
+ }
654
800
  }
655
801
 
656
802
  println!();
@@ -856,14 +1002,14 @@ fn parse_macos_notarize_config(value: Option<&JsonValue>) -> MacosNotarizeConfig
856
1002
  .get("appleApiKey")
857
1003
  .and_then(JsonValue::as_str)
858
1004
  .map(ToOwned::to_owned),
859
- apple_api_key_id_set: object
1005
+ apple_api_key_id: object
860
1006
  .get("appleApiKeyId")
861
1007
  .and_then(JsonValue::as_str)
862
- .is_some(),
863
- apple_api_issuer_set: object
1008
+ .map(ToOwned::to_owned),
1009
+ apple_api_issuer: object
864
1010
  .get("appleApiIssuer")
865
1011
  .and_then(JsonValue::as_str)
866
- .is_some(),
1012
+ .map(ToOwned::to_owned),
867
1013
  keychain_profile: object
868
1014
  .get("keychainProfile")
869
1015
  .and_then(JsonValue::as_str)
@@ -872,6 +1018,12 @@ fn parse_macos_notarize_config(value: Option<&JsonValue>) -> MacosNotarizeConfig
872
1018
  .get("keychain")
873
1019
  .and_then(JsonValue::as_str)
874
1020
  .map(ToOwned::to_owned),
1021
+ wait: object.get("wait").and_then(JsonValue::as_bool),
1022
+ wait_timeout_seconds: object
1023
+ .get("maxWaitSeconds")
1024
+ .or_else(|| object.get("waitTimeoutSeconds"))
1025
+ .and_then(JsonValue::as_u64),
1026
+ staple: object.get("staple").and_then(JsonValue::as_bool),
875
1027
  },
876
1028
  Some(_) => MacosNotarizeConfig {
877
1029
  configured: true,
@@ -964,7 +1116,7 @@ fn package_signing(
964
1116
  platform,
965
1117
  &mut warnings,
966
1118
  )?;
967
- let notarize = macos_notarize_plan(root, config, platform, &mut warnings);
1119
+ let notarize = macos_notarize_plan(root, config, platform, &sign, &mut warnings)?;
968
1120
 
969
1121
  Ok((
970
1122
  PackageSigningPlan {
@@ -1163,22 +1315,34 @@ fn macos_notarize_plan(
1163
1315
  root: &Path,
1164
1316
  package_config: &PackageJsonConfig,
1165
1317
  platform: &str,
1318
+ sign: &MacosSignPlan,
1166
1319
  warnings: &mut Vec<String>,
1167
- ) -> MacosNotarizePlan {
1320
+ ) -> Result<MacosNotarizePlan> {
1168
1321
  let config = &package_config.packager.osx_notarize;
1169
1322
  if config.invalid_type {
1170
1323
  warnings.push("packagerConfig.osxNotarize must be false, true, or an object.".to_string());
1171
1324
  }
1172
1325
 
1173
1326
  let auth_method = macos_notarize_auth_method(config);
1327
+ let apple_api_key = config
1328
+ .apple_api_key
1329
+ .as_deref()
1330
+ .filter(|path| !path.trim().is_empty())
1331
+ .map(|path| utf8_path(resolve_project_path(root, path)))
1332
+ .transpose()?;
1333
+ let api_key_auth = auth_method.as_deref() == Some("app-store-connect-api-key");
1334
+ let staple = config.staple.unwrap_or(true);
1335
+ let wait = staple || config.wait.unwrap_or(true);
1336
+ let wait_timeout_seconds = config
1337
+ .wait_timeout_seconds
1338
+ .unwrap_or(MACOS_NOTARIZATION_WAIT_TIMEOUT_SECONDS);
1339
+ let will_execute =
1340
+ config.enabled && platform == "darwin" && sign.for_notarization && api_key_auth;
1341
+
1174
1342
  if config.configured && platform != "darwin" {
1175
1343
  warnings.push(format!(
1176
1344
  "macOS notarization is configured but ignored for target platform {platform}."
1177
1345
  ));
1178
- } else if config.enabled {
1179
- warnings.push(
1180
- "macOS notarization is configured, but Rust-native notarization is not implemented yet.".to_string(),
1181
- );
1182
1346
  }
1183
1347
 
1184
1348
  if config.enabled && !package_config.packager.osx_sign.enabled {
@@ -1204,28 +1368,53 @@ fn macos_notarize_plan(
1204
1368
  "macOS notarization requires a Developer ID signature; Rust-native ad-hoc signing is not notarizable.".to_string(),
1205
1369
  );
1206
1370
  }
1371
+ if config.enabled
1372
+ && platform == "darwin"
1373
+ && package_config.packager.osx_sign.enabled
1374
+ && !sign.for_notarization
1375
+ {
1376
+ warnings.push(
1377
+ "macOS notarization execution requires Rust-native p12File Developer ID signing with a secure timestamp.".to_string(),
1378
+ );
1379
+ }
1207
1380
  if config.enabled && auth_method.is_none() {
1208
1381
  warnings.push(
1209
1382
  "macOS notarization config is missing a complete notarytool authentication set: appleId/appleIdPassword/teamId, appleApiKey/appleApiKeyId/appleApiIssuer, or keychainProfile.".to_string(),
1210
1383
  );
1211
1384
  }
1212
- if let Some(api_key) = &config.apple_api_key {
1213
- let path = resolve_project_path(root, api_key);
1214
- if !path.exists() {
1385
+ if config.enabled
1386
+ && platform == "darwin"
1387
+ && matches!(
1388
+ auth_method.as_deref(),
1389
+ Some("keychain-profile") | Some("apple-id")
1390
+ )
1391
+ {
1392
+ warnings.push(
1393
+ "Rust-native macOS notarization execution currently requires appleApiKey, appleApiKeyId, and appleApiIssuer; keychain profile and Apple ID auth are recognized for planning only.".to_string(),
1394
+ );
1395
+ }
1396
+ if let Some(path) = &apple_api_key {
1397
+ if !Path::new(path.as_str()).exists() {
1215
1398
  warnings.push(format!(
1216
1399
  "Configured Apple API key file does not exist: {}.",
1217
- path.display()
1400
+ path
1218
1401
  ));
1219
1402
  }
1220
1403
  }
1221
-
1222
- MacosNotarizePlan {
1404
+ Ok(MacosNotarizePlan {
1223
1405
  configured: config.configured,
1224
1406
  enabled: config.enabled,
1407
+ will_execute,
1225
1408
  auth_method,
1409
+ apple_api_key,
1410
+ apple_api_key_id: RedactedSecret::new(config.apple_api_key_id.clone()),
1411
+ apple_api_issuer: RedactedSecret::new(config.apple_api_issuer.clone()),
1226
1412
  keychain_profile: config.keychain_profile.clone(),
1227
1413
  keychain: config.keychain.clone(),
1228
- }
1414
+ wait,
1415
+ wait_timeout_seconds,
1416
+ staple,
1417
+ })
1229
1418
  }
1230
1419
 
1231
1420
  fn macos_notarize_auth_method(config: &MacosNotarizeConfig) -> Option<String> {
@@ -1236,8 +1425,14 @@ fn macos_notarize_auth_method(config: &MacosNotarizeConfig) -> Option<String> {
1236
1425
  {
1237
1426
  Some("keychain-profile".to_string())
1238
1427
  } else if config.apple_api_key.is_some()
1239
- && config.apple_api_key_id_set
1240
- && config.apple_api_issuer_set
1428
+ && config
1429
+ .apple_api_key_id
1430
+ .as_deref()
1431
+ .is_some_and(|value| !value.trim().is_empty())
1432
+ && config
1433
+ .apple_api_issuer
1434
+ .as_deref()
1435
+ .is_some_and(|value| !value.trim().is_empty())
1241
1436
  {
1242
1437
  Some("app-store-connect-api-key".to_string())
1243
1438
  } else if config.apple_id_set && config.apple_id_password_set && config.team_id_set {
@@ -2265,10 +2460,12 @@ mod tests {
2265
2460
  assert_eq!(report.signing.macos.sign.gatekeeper_assess, Some(false));
2266
2461
  assert_eq!(report.signing.macos.sign.entitlements.len(), 2);
2267
2462
  assert!(report.signing.macos.notarize.configured);
2463
+ assert!(!report.signing.macos.notarize.will_execute);
2268
2464
  assert_eq!(
2269
2465
  report.signing.macos.notarize.auth_method.as_deref(),
2270
2466
  Some("app-store-connect-api-key")
2271
2467
  );
2468
+ assert!(report.signing.macos.notarize.apple_api_key.is_some());
2272
2469
  assert!(report
2273
2470
  .warnings
2274
2471
  .iter()
@@ -2276,7 +2473,7 @@ mod tests {
2276
2473
  assert!(report
2277
2474
  .warnings
2278
2475
  .iter()
2279
- .any(|warning| warning.contains("Rust-native notarization is not implemented")));
2476
+ .any(|warning| warning.contains("p12File Developer ID signing")));
2280
2477
 
2281
2478
  let json = serde_json::to_string(&report).expect("report should serialize");
2282
2479
  assert!(!json.contains("SECRET_KEY_ID"));
@@ -2451,10 +2648,15 @@ mod tests {
2451
2648
  report.signing.macos.notarize.auth_method.as_deref(),
2452
2649
  Some("keychain-profile")
2453
2650
  );
2651
+ assert!(!report.signing.macos.notarize.will_execute);
2454
2652
  assert!(!report
2455
2653
  .warnings
2456
2654
  .iter()
2457
2655
  .any(|warning| warning.contains("ad-hoc signing is not notarizable")));
2656
+ assert!(report
2657
+ .warnings
2658
+ .iter()
2659
+ .any(|warning| warning.contains("requires appleApiKey")));
2458
2660
 
2459
2661
  let settings = macos_signing_settings(&report).expect("signing settings should build");
2460
2662
  assert!(settings.for_notarization());
@@ -2469,6 +2671,75 @@ mod tests {
2469
2671
  let _ = fs::remove_dir_all(root);
2470
2672
  }
2471
2673
 
2674
+ #[test]
2675
+ fn plans_macos_native_notarization_execution_with_api_key_auth() {
2676
+ let root = unique_temp_dir("macos-native-notarization-plan");
2677
+ write_package_json(&root);
2678
+ fs::write(root.join("developer-id.p12"), "not a real p12")
2679
+ .expect("p12 placeholder should be written");
2680
+ fs::write(root.join("AuthKey_TEST.p8"), "not a real api key")
2681
+ .expect("api key placeholder should be written");
2682
+ fs::write(
2683
+ root.join("forge.config.js"),
2684
+ r#"
2685
+ module.exports = {
2686
+ packagerConfig: {
2687
+ appBundleId: 'com.example.native-notarized',
2688
+ osxSign: {
2689
+ p12File: 'developer-id.p12',
2690
+ p12Password: 'p12-secret',
2691
+ hardenedRuntime: true,
2692
+ },
2693
+ osxNotarize: {
2694
+ appleApiKey: 'AuthKey_TEST.p8',
2695
+ appleApiKeyId: 'SECRET_KEY_ID',
2696
+ appleApiIssuer: 'SECRET_ISSUER_ID',
2697
+ maxWaitSeconds: 120,
2698
+ },
2699
+ },
2700
+ };
2701
+ "#,
2702
+ )
2703
+ .expect("forge config should be written");
2704
+ write_app_file(&root);
2705
+ write_fake_electron_dist(&root);
2706
+
2707
+ let args = PackageArgs {
2708
+ cwd: root.clone(),
2709
+ out_dir: PathBuf::from("out"),
2710
+ name: None,
2711
+ platform: Some("darwin".to_string()),
2712
+ arch: Some("arm64".to_string()),
2713
+ force: false,
2714
+ dry_run: true,
2715
+ json: true,
2716
+ };
2717
+ let snapshot = crate::project::inspect(&root).expect("project should inspect");
2718
+ let report = build_report(snapshot, &args).expect("report should build");
2719
+
2720
+ assert!(report.signing.macos.sign.for_notarization);
2721
+ assert!(report.signing.macos.notarize.will_execute);
2722
+ assert_eq!(
2723
+ report.signing.macos.notarize.auth_method.as_deref(),
2724
+ Some("app-store-connect-api-key")
2725
+ );
2726
+ assert!(report.signing.macos.notarize.wait);
2727
+ assert_eq!(report.signing.macos.notarize.wait_timeout_seconds, 120);
2728
+ assert!(report.signing.macos.notarize.staple);
2729
+ assert!(!report
2730
+ .warnings
2731
+ .iter()
2732
+ .any(|warning| warning.contains("not implemented")));
2733
+
2734
+ let json = serde_json::to_string(&report).expect("report should serialize");
2735
+ assert!(!json.contains("SECRET_KEY_ID"));
2736
+ assert!(!json.contains("SECRET_ISSUER_ID"));
2737
+ assert!(!json.contains("p12-secret"));
2738
+ assert!(!json.contains("not a real api key"));
2739
+
2740
+ let _ = fs::remove_dir_all(root);
2741
+ }
2742
+
2472
2743
  #[test]
2473
2744
  fn warns_when_macos_notarization_timestamp_is_disabled() {
2474
2745
  let root = unique_temp_dir("macos-p12-notarization-no-timestamp");