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

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.lock CHANGED
@@ -953,7 +953,7 @@ checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
953
953
 
954
954
  [[package]]
955
955
  name = "electron-cli"
956
- version = "0.3.0-alpha.18"
956
+ version = "0.3.0-alpha.19"
957
957
  dependencies = [
958
958
  "anyhow",
959
959
  "apple-codesign",
package/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "electron-cli"
3
- version = "0.3.0-alpha.18"
3
+ version = "0.3.0-alpha.19"
4
4
  edition = "2021"
5
5
  description = "Experimental Rust CLI for Electron project diagnostics and workflow automation"
6
6
  license = "MIT"
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. macOS keychain identity lookup and notarization execution 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. macOS keychain identity lookup and notarization submission/stapling 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, timestamping, 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 notarization execution are still TODO.
47
47
 
48
48
  Package metadata can be configured in `package.json`:
49
49
 
@@ -59,6 +59,7 @@ Package metadata can be configured in `package.json`:
59
59
  "osxSign": {
60
60
  "p12File": "certs/developer-id.p12",
61
61
  "p12PasswordEnv": "ELECTRON_CLI_P12_PASSWORD",
62
+ "timestamp": true,
62
63
  "entitlements": "assets/entitlements.plist",
63
64
  "hardenedRuntime": true
64
65
  },
@@ -98,7 +99,7 @@ Package metadata can be configured in `package.json`:
98
99
  }
99
100
  ```
100
101
 
101
- Use `p12PasswordEnv`, `p12PasswordFile`, or `p12Password` for the `.p12` password; password values are not serialized in package reports. 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.
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.
102
103
 
103
104
  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.
104
105
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "electron-cli",
3
- "version": "0.3.0-alpha.18",
3
+ "version": "0.3.0-alpha.19",
4
4
  "description": "Experimental Rust CLI for Electron project diagnostics and workflow automation",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -16,6 +16,8 @@ use serde_json::Value as JsonValue;
16
16
 
17
17
  use crate::{cli::PackageArgs, output, project::ProjectSnapshot};
18
18
 
19
+ const APPLE_TIMESTAMP_URL: &str = "http://timestamp.apple.com/ts01";
20
+
19
21
  #[derive(Clone, Debug, Serialize)]
20
22
  pub(crate) struct PackageReport {
21
23
  project: ProjectSnapshot,
@@ -84,6 +86,8 @@ struct MacosSignPlan {
84
86
  p12_password_file: Option<Utf8PathBuf>,
85
87
  #[serde(skip)]
86
88
  p12_password: RedactedSecret,
89
+ timestamp_url: Option<String>,
90
+ for_notarization: bool,
87
91
  entitlements: Vec<Utf8PathBuf>,
88
92
  entitlements_inherit: Option<Utf8PathBuf>,
89
93
  hardened_runtime: Option<bool>,
@@ -163,12 +167,20 @@ struct MacosSignConfig {
163
167
  p12_password: Option<String>,
164
168
  p12_password_env: Option<String>,
165
169
  p12_password_file: Option<String>,
170
+ timestamp: Option<MacosTimestampConfig>,
166
171
  entitlements: Vec<String>,
167
172
  entitlements_inherit: Option<String>,
168
173
  hardened_runtime: Option<bool>,
169
174
  gatekeeper_assess: Option<bool>,
170
175
  }
171
176
 
177
+ #[derive(Clone, Debug)]
178
+ enum MacosTimestampConfig {
179
+ Default,
180
+ Disabled,
181
+ Url(String),
182
+ }
183
+
172
184
  #[derive(Clone, Debug, Default)]
173
185
  struct MacosNotarizeConfig {
174
186
  configured: bool,
@@ -457,6 +469,9 @@ fn execute_macos_signing(report: &PackageReport) -> Result<()> {
457
469
  settings.set_signing_key(signing_key.as_key_info_signer(), certificate);
458
470
  settings.chain_apple_certificates();
459
471
  settings.set_team_id_from_signing_certificate();
472
+ settings
473
+ .ensure_for_notarization_settings()
474
+ .context("macOS signing settings are not compatible with notarization")?;
460
475
  signer
461
476
  .write_signed_bundle(&signed_bundle_dir, &settings)
462
477
  .with_context(|| {
@@ -502,6 +517,13 @@ fn macos_signing_settings<'key>(report: &PackageReport) -> Result<SigningSetting
502
517
  let sign = &report.signing.macos.sign;
503
518
  let mut settings = SigningSettings::default();
504
519
  settings.set_binary_identifier(SettingsScope::Main, &report.metadata.bundle_identifier);
520
+ settings.set_for_notarization(sign.for_notarization);
521
+
522
+ if let Some(timestamp_url) = &sign.timestamp_url {
523
+ settings
524
+ .set_time_stamp_url(timestamp_url)
525
+ .with_context(|| format!("Invalid macOS signing timestamp URL: {timestamp_url}"))?;
526
+ }
505
527
 
506
528
  if sign.hardened_runtime.unwrap_or(false) {
507
529
  settings.add_code_signature_flags(SettingsScope::Main, CodeSignatureFlags::RUNTIME);
@@ -601,6 +623,12 @@ fn print_report(report: &PackageReport, json: bool) -> Result<()> {
601
623
  if let Some(source) = &report.signing.macos.sign.p12_password_source {
602
624
  println!(" p12 password: {source}");
603
625
  }
626
+ if let Some(timestamp_url) = &report.signing.macos.sign.timestamp_url {
627
+ println!(" timestamp server: {timestamp_url}");
628
+ }
629
+ if report.signing.macos.sign.for_notarization {
630
+ println!(" signing mode: notarization-compatible");
631
+ }
604
632
  if let Some(method) = &report.signing.macos.sign.method {
605
633
  println!(" signing method: {method}");
606
634
  }
@@ -762,6 +790,12 @@ fn parse_macos_sign_config(value: Option<&JsonValue>) -> MacosSignConfig {
762
790
  .or_else(|| object.get("pfxPasswordFile"))
763
791
  .and_then(JsonValue::as_str)
764
792
  .map(ToOwned::to_owned),
793
+ timestamp: parse_macos_timestamp_config(
794
+ object
795
+ .get("timestamp")
796
+ .or_else(|| object.get("timestampUrl"))
797
+ .or_else(|| object.get("timestampURL")),
798
+ ),
765
799
  entitlements,
766
800
  entitlements_inherit: object
767
801
  .get("entitlementsInherit")
@@ -779,6 +813,22 @@ fn parse_macos_sign_config(value: Option<&JsonValue>) -> MacosSignConfig {
779
813
  }
780
814
  }
781
815
 
816
+ fn parse_macos_timestamp_config(value: Option<&JsonValue>) -> Option<MacosTimestampConfig> {
817
+ match value {
818
+ Some(JsonValue::String(value)) => {
819
+ let value = value.trim();
820
+ if value.is_empty() || value.eq_ignore_ascii_case("none") {
821
+ Some(MacosTimestampConfig::Disabled)
822
+ } else {
823
+ Some(MacosTimestampConfig::Url(value.to_string()))
824
+ }
825
+ }
826
+ Some(JsonValue::Bool(true)) => Some(MacosTimestampConfig::Default),
827
+ Some(JsonValue::Bool(false)) => Some(MacosTimestampConfig::Disabled),
828
+ _ => None,
829
+ }
830
+ }
831
+
782
832
  fn parse_macos_notarize_config(value: Option<&JsonValue>) -> MacosNotarizeConfig {
783
833
  match value {
784
834
  None => MacosNotarizeConfig::default(),
@@ -907,7 +957,13 @@ fn package_signing(
907
957
  platform: &str,
908
958
  ) -> Result<(PackageSigningPlan, Vec<String>)> {
909
959
  let mut warnings = Vec::new();
910
- let sign = macos_sign_plan(root, &config.packager.osx_sign, platform, &mut warnings)?;
960
+ let sign = macos_sign_plan(
961
+ root,
962
+ &config.packager.osx_sign,
963
+ &config.packager.osx_notarize,
964
+ platform,
965
+ &mut warnings,
966
+ )?;
911
967
  let notarize = macos_notarize_plan(root, config, platform, &mut warnings);
912
968
 
913
969
  Ok((
@@ -921,6 +977,7 @@ fn package_signing(
921
977
  fn macos_sign_plan(
922
978
  root: &Path,
923
979
  config: &MacosSignConfig,
980
+ notarize_config: &MacosNotarizeConfig,
924
981
  platform: &str,
925
982
  warnings: &mut Vec<String>,
926
983
  ) -> Result<MacosSignPlan> {
@@ -1008,6 +1065,9 @@ fn macos_sign_plan(
1008
1065
  let ad_hoc_identity = matches!(identity, None | Some("-"));
1009
1066
  let p12_identity = p12_file.is_some();
1010
1067
  let will_execute = config.enabled && platform == "darwin" && (ad_hoc_identity || p12_identity);
1068
+ let timestamp_url = macos_timestamp_url(config, p12_identity, notarize_config.enabled);
1069
+ let for_notarization =
1070
+ will_execute && p12_identity && notarize_config.enabled && timestamp_url.is_some();
1011
1071
  let method = if config.enabled && platform == "darwin" {
1012
1072
  if p12_identity {
1013
1073
  Some("certificate-p12".to_string())
@@ -1049,6 +1109,16 @@ fn macos_sign_plan(
1049
1109
  "packagerConfig.osxSign.gatekeeperAssess is recognized but Gatekeeper assessment is not implemented yet.".to_string(),
1050
1110
  );
1051
1111
  }
1112
+ if config.timestamp.is_some() && !p12_identity {
1113
+ warnings.push(
1114
+ "packagerConfig.osxSign.timestamp is recognized but ignored without p12File certificate signing.".to_string(),
1115
+ );
1116
+ }
1117
+ if notarize_config.enabled && p12_identity && timestamp_url.is_none() {
1118
+ warnings.push(
1119
+ "macOS notarization requires a secure timestamp; packagerConfig.osxSign.timestamp disabled timestamping.".to_string(),
1120
+ );
1121
+ }
1052
1122
  }
1053
1123
 
1054
1124
  Ok(MacosSignPlan {
@@ -1062,6 +1132,8 @@ fn macos_sign_plan(
1062
1132
  p12_password_env: config.p12_password_env.clone(),
1063
1133
  p12_password_file,
1064
1134
  p12_password: RedactedSecret::new(config.p12_password.clone()),
1135
+ timestamp_url,
1136
+ for_notarization,
1065
1137
  entitlements,
1066
1138
  entitlements_inherit,
1067
1139
  hardened_runtime: config.hardened_runtime,
@@ -1069,6 +1141,24 @@ fn macos_sign_plan(
1069
1141
  })
1070
1142
  }
1071
1143
 
1144
+ fn macos_timestamp_url(
1145
+ config: &MacosSignConfig,
1146
+ p12_identity: bool,
1147
+ notarize_enabled: bool,
1148
+ ) -> Option<String> {
1149
+ if !p12_identity {
1150
+ return None;
1151
+ }
1152
+
1153
+ match &config.timestamp {
1154
+ Some(MacosTimestampConfig::Default) => Some(APPLE_TIMESTAMP_URL.to_string()),
1155
+ Some(MacosTimestampConfig::Disabled) => None,
1156
+ Some(MacosTimestampConfig::Url(url)) => Some(url.clone()),
1157
+ None if notarize_enabled => Some(APPLE_TIMESTAMP_URL.to_string()),
1158
+ None => None,
1159
+ }
1160
+ }
1161
+
1072
1162
  fn macos_notarize_plan(
1073
1163
  root: &Path,
1074
1164
  package_config: &PackageJsonConfig,
@@ -2259,6 +2349,7 @@ mod tests {
2259
2349
  identity: 'Developer ID Application: Example, Inc. (TEAMID1234)',
2260
2350
  p12File: 'developer-id.p12',
2261
2351
  p12Password: 'p12-secret',
2352
+ timestamp: 'http://timestamp.example.test/tsa',
2262
2353
  hardenedRuntime: true,
2263
2354
  },
2264
2355
  },
@@ -2293,6 +2384,11 @@ mod tests {
2293
2384
  report.signing.macos.sign.p12_password_source.as_deref(),
2294
2385
  Some("config")
2295
2386
  );
2387
+ assert_eq!(
2388
+ report.signing.macos.sign.timestamp_url.as_deref(),
2389
+ Some("http://timestamp.example.test/tsa")
2390
+ );
2391
+ assert!(!report.signing.macos.sign.for_notarization);
2296
2392
  assert!(report.signing.macos.sign.p12_file.is_some());
2297
2393
  assert!(report
2298
2394
  .warnings
@@ -2305,6 +2401,124 @@ mod tests {
2305
2401
  let _ = fs::remove_dir_all(root);
2306
2402
  }
2307
2403
 
2404
+ #[test]
2405
+ fn plans_macos_p12_signing_for_notarization_with_default_timestamp() {
2406
+ let root = unique_temp_dir("macos-p12-notarization-signing-plan");
2407
+ write_package_json(&root);
2408
+ fs::write(root.join("developer-id.p12"), "not a real p12")
2409
+ .expect("p12 placeholder should be written");
2410
+ fs::write(
2411
+ root.join("forge.config.js"),
2412
+ r#"
2413
+ module.exports = {
2414
+ packagerConfig: {
2415
+ appBundleId: 'com.example.notarized',
2416
+ osxSign: {
2417
+ p12File: 'developer-id.p12',
2418
+ p12PasswordEnv: 'P12_PASSWORD',
2419
+ },
2420
+ osxNotarize: {
2421
+ keychainProfile: 'notary-profile',
2422
+ },
2423
+ },
2424
+ };
2425
+ "#,
2426
+ )
2427
+ .expect("forge config should be written");
2428
+ write_app_file(&root);
2429
+ write_fake_electron_dist(&root);
2430
+
2431
+ let args = PackageArgs {
2432
+ cwd: root.clone(),
2433
+ out_dir: PathBuf::from("out"),
2434
+ name: None,
2435
+ platform: Some("darwin".to_string()),
2436
+ arch: Some("arm64".to_string()),
2437
+ force: false,
2438
+ dry_run: true,
2439
+ json: true,
2440
+ };
2441
+ let snapshot = crate::project::inspect(&root).expect("project should inspect");
2442
+ let report = build_report(snapshot, &args).expect("report should build");
2443
+
2444
+ assert!(report.signing.macos.sign.will_execute);
2445
+ assert_eq!(
2446
+ report.signing.macos.sign.timestamp_url.as_deref(),
2447
+ Some(APPLE_TIMESTAMP_URL)
2448
+ );
2449
+ assert!(report.signing.macos.sign.for_notarization);
2450
+ assert_eq!(
2451
+ report.signing.macos.notarize.auth_method.as_deref(),
2452
+ Some("keychain-profile")
2453
+ );
2454
+ assert!(!report
2455
+ .warnings
2456
+ .iter()
2457
+ .any(|warning| warning.contains("ad-hoc signing is not notarizable")));
2458
+
2459
+ let settings = macos_signing_settings(&report).expect("signing settings should build");
2460
+ assert!(settings.for_notarization());
2461
+ assert_eq!(
2462
+ settings.time_stamp_url().map(|url| url.as_str()),
2463
+ Some(APPLE_TIMESTAMP_URL)
2464
+ );
2465
+
2466
+ let json = serde_json::to_string(&report).expect("report should serialize");
2467
+ assert!(!json.contains("P12_PASSWORD="));
2468
+
2469
+ let _ = fs::remove_dir_all(root);
2470
+ }
2471
+
2472
+ #[test]
2473
+ fn warns_when_macos_notarization_timestamp_is_disabled() {
2474
+ let root = unique_temp_dir("macos-p12-notarization-no-timestamp");
2475
+ write_package_json(&root);
2476
+ fs::write(root.join("developer-id.p12"), "not a real p12")
2477
+ .expect("p12 placeholder should be written");
2478
+ fs::write(
2479
+ root.join("forge.config.js"),
2480
+ r#"
2481
+ module.exports = {
2482
+ packagerConfig: {
2483
+ osxSign: {
2484
+ p12File: 'developer-id.p12',
2485
+ timestamp: 'none',
2486
+ },
2487
+ osxNotarize: {
2488
+ keychainProfile: 'notary-profile',
2489
+ },
2490
+ },
2491
+ };
2492
+ "#,
2493
+ )
2494
+ .expect("forge config should be written");
2495
+ write_app_file(&root);
2496
+ write_fake_electron_dist(&root);
2497
+
2498
+ let args = PackageArgs {
2499
+ cwd: root.clone(),
2500
+ out_dir: PathBuf::from("out"),
2501
+ name: None,
2502
+ platform: Some("darwin".to_string()),
2503
+ arch: Some("arm64".to_string()),
2504
+ force: false,
2505
+ dry_run: true,
2506
+ json: true,
2507
+ };
2508
+ let snapshot = crate::project::inspect(&root).expect("project should inspect");
2509
+ let report = build_report(snapshot, &args).expect("report should build");
2510
+
2511
+ assert!(report.signing.macos.sign.will_execute);
2512
+ assert!(report.signing.macos.sign.timestamp_url.is_none());
2513
+ assert!(!report.signing.macos.sign.for_notarization);
2514
+ assert!(report
2515
+ .warnings
2516
+ .iter()
2517
+ .any(|warning| warning.contains("requires a secure timestamp")));
2518
+
2519
+ let _ = fs::remove_dir_all(root);
2520
+ }
2521
+
2308
2522
  #[test]
2309
2523
  fn warns_when_macos_notarization_is_configured_without_signing() {
2310
2524
  let root = unique_temp_dir("notarize-without-sign");