mysigner 0.3.4 → 0.3.7

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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +2 -2
  3. data/CHANGELOG.md +47 -0
  4. data/Gemfile +0 -1
  5. data/Gemfile.lock +2 -6
  6. data/README.md +16 -16
  7. data/lib/mysigner/build/android_executor.rb +16 -21
  8. data/lib/mysigner/build/detector.rb +3 -1
  9. data/lib/mysigner/build/executor.rb +6 -1
  10. data/lib/mysigner/cli/auth_commands.rb +14 -3
  11. data/lib/mysigner/cli/build_commands.rb +14 -11
  12. data/lib/mysigner/cli/concerns/actionable_suggestions.rb +1 -2
  13. data/lib/mysigner/cli/concerns/api_helpers.rb +16 -3
  14. data/lib/mysigner/cli/concerns/error_handlers.rb +0 -1
  15. data/lib/mysigner/cli/concerns/helpers.rb +14 -14
  16. data/lib/mysigner/cli/diagnostic_commands.rb +16 -5
  17. data/lib/mysigner/cli/resource_commands.rb +8 -1
  18. data/lib/mysigner/client.rb +52 -0
  19. data/lib/mysigner/config.rb +9 -4
  20. data/lib/mysigner/export/exporter.rb +6 -1
  21. data/lib/mysigner/formatting.rb +23 -0
  22. data/lib/mysigner/signing/certificate_checker.rb +6 -6
  23. data/lib/mysigner/signing/keystore_manager.rb +2 -0
  24. data/lib/mysigner/signing/wizard.rb +2 -0
  25. data/lib/mysigner/upload/app_store_automation.rb +13 -1
  26. data/lib/mysigner/upload/app_store_submission.rb +2 -14
  27. data/lib/mysigner/upload/asc_rest_uploader.rb +44 -3
  28. data/lib/mysigner/upload/asc_submitter.rb +5 -0
  29. data/lib/mysigner/upload/play_store_uploader.rb +2 -7
  30. data/lib/mysigner/upload/uploader.rb +9 -366
  31. data/lib/mysigner/version.rb +1 -1
  32. data/lib/mysigner.rb +1 -0
  33. data/mysigner.gemspec +6 -2
  34. metadata +2 -20
  35. data/.travis.yml +0 -7
  36. data/MANUAL_TEST.md +0 -341
  37. data/bin/console +0 -15
  38. data/bin/setup +0 -11
  39. data/test_manual.rb +0 -103
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c8d828d29516e7df219e7a43cc21f66fe5dcfdce881f7ffb2168217626b6e204
4
- data.tar.gz: 75444b456a228e57784a3b5376b57da4abe1963550b53b735d32836de3d0e981
3
+ metadata.gz: a7b51b5ca8891aa0e71405e1c5a2f8d0aef349ef8aaeca1e870925eb5403bcea
4
+ data.tar.gz: 45ceb49f0a2015ecbe689e17510af4c6a8b8c1c3c4c25c036f637ff4a541a842
5
5
  SHA512:
6
- metadata.gz: 6aa433a444788a4205115f0165d4d264137921b6c06d00af81b4096272297141f1c275f848a798e8327e64d0151b50611ba7b313d7ceaa7a10ead7854a70863f
7
- data.tar.gz: 18c7b935f4d59817d9286fbb95c9b9d21803c17930b74bb770fda57494c862a064764abfd3cd2000e5a2ec1c5738823d5df83021febc00adb90d2a39727db909
6
+ metadata.gz: e8ac1f132fbe6e912a918c923789c28784b46e5d978a7fb7fdfeca2b56a8ed33f23f613287459c65825868896df86029097ea0410482533667ab9800b799a8df
7
+ data.tar.gz: 3e7fbb4faf3dd82f831920ca7b7c306568cd70a911da7361da1080c7540449f75e0ba2bf54b8743f0560162490aa8308ac92e7e496b09fa5fc2e24607459c5e2
@@ -9,7 +9,7 @@ jobs:
9
9
  lint:
10
10
  runs-on: ubuntu-latest
11
11
  steps:
12
- - uses: actions/checkout@v4
12
+ - uses: actions/checkout@v5
13
13
  - uses: ruby/setup-ruby@v1
14
14
  with:
15
15
  ruby-version: "3.2"
@@ -20,7 +20,7 @@ jobs:
20
20
  test:
21
21
  runs-on: ubuntu-latest
22
22
  steps:
23
- - uses: actions/checkout@v4
23
+ - uses: actions/checkout@v5
24
24
  - uses: ruby/setup-ruby@v1
25
25
  with:
26
26
  ruby-version: "3.2"
data/CHANGELOG.md CHANGED
@@ -5,6 +5,53 @@ All notable changes to My Signer CLI will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.3.7] - 2026-06-26
9
+
10
+ Small robustness + dependency-hygiene follow-ups.
11
+
12
+ ### Fixed
13
+ - Pressing Ctrl-C while `ship appstore --wait` is polling now prints a clean "still processing on Apple's side — re-run to resume" hint and exits with the standard SIGINT code, instead of leaving a half-drawn "Waiting…" line.
14
+
15
+ ### Changed
16
+ - Dependency hygiene: removed the redundant `xcodeproj` declaration from the Gemfile (it's already a gemspec dependency), and added `x86_64-linux` to `Gemfile.lock` so Linux/CI installs use the committed, audited dependency graph rather than re-resolving.
17
+
18
+ ## [0.3.6] - 2026-06-26
19
+
20
+ Follow-up hardening from the audit's remaining medium/low findings.
21
+
22
+ ### Security
23
+ - `status` no longer overstates at-rest protection: on Linux/Windows it now reports the encryption key as a local file (obfuscation, not vault-grade) rather than a bare "Encryption: ✓ Enabled", since the key sits next to the encrypted token; macOS reports the Keychain.
24
+ - TLS certificate verification is now asserted explicitly on the API client instead of relying on the adapter default.
25
+ - Release-metadata YAML parsing disables alias expansion (`aliases: false`), closing a YAML alias-bomb / billion-laughs DoS on a hostile project metadata file.
26
+ - `devices` USB detection (`ideviceinfo`) and the `doctor` keychain key-import (`security import`) now run via argv (no shell), so a device-reported UDID or a `$HOME` containing a space can't break or inject.
27
+
28
+ ### Changed
29
+ - The published gem no longer ships the dev-only `bin/` helpers or the internal `MANUAL_TEST.md`.
30
+
31
+ ## [0.3.5] - 2026-06-26
32
+
33
+ Security & robustness hardening from a full multi-agent audit of the CLI. No breaking changes.
34
+
35
+ ### Security
36
+ - iOS build/export no longer run `xcodebuild` through a shell: arguments are passed as a literal argv array, so a scheme, bundle ID, or project path containing a space or shell metacharacter (`;`, `$(...)`, backticks) can no longer be split or executed by `/bin/sh`.
37
+ - The API token is never sent over an insecure connection: the CLI refuses any non-`https` `api_url` unless the host is loopback (localhost), and a scheme-less host now defaults to `https://`. This prevents the org token from being sent in cleartext or redirected to an attacker-set host via a poisoned config/env.
38
+ - Local-only App Store keys are cleaned up: the materialized `~/.appstoreconnect/private_keys/AuthKey_*.p8` is deleted after upload (only the one the CLI created) instead of lingering as a plaintext signing key.
39
+ - `logout` no longer orphans local credentials: a plain (non-`--purge`) logout keeps your local-only credentials AND keeps them decryptable — it no longer deletes the per-machine encryption key out from under them.
40
+ - Android signing passwords stay off the process table: they are passed to Gradle via the child process environment instead of an `export VAR=… &&` shell string visible in `ps`.
41
+ - Certificate and `doctor` checks no longer build shell strings from untrusted values — a certificate CN containing `$(...)` and the server-supplied `team_id` are handled as literal data.
42
+
43
+ ### Fixed
44
+ - The default API client now sets explicit request/connect timeouts, so a stalled server can no longer hang the CLI indefinitely.
45
+ - Non-interactive (CI/piped) runs fail loudly with an actionable hint when a keystore password, version code, or app target is required, instead of silently reading an empty value and failing later with a confusing "wrong password" error.
46
+ - `mysigner export` runs `xcodebuild` via argv, so archive/output paths with spaces work; Expo prebuild likewise runs via `Dir.chdir` + argv.
47
+ - App Store upload reads the IPA once (single-pass MD5 + SHA-256) instead of three times.
48
+
49
+ ### Changed
50
+ - The `version` command and README point at the correct repository, and the README version/test-count are current.
51
+
52
+ ### Removed
53
+ - Removed the unused `reline` dependency, ~360 lines of dead legacy uploader code, and stale references to the removed `--submit-for-review` / `--asc-timeout-seconds` flags.
54
+
8
55
  ## [0.3.4] - 2026-06-25
9
56
 
10
57
  ### Added
data/Gemfile CHANGED
@@ -6,4 +6,3 @@ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
6
6
 
7
7
  # Specify your gem's dependencies in mysigner.gemspec
8
8
  gemspec
9
- gem 'xcodeproj', '~> 1.27'
data/Gemfile.lock CHANGED
@@ -1,14 +1,13 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- mysigner (0.3.4)
4
+ mysigner (0.3.7)
5
5
  base64 (~> 0.2)
6
6
  faraday (~> 2.14)
7
7
  faraday-retry (~> 2.2)
8
8
  google-apis-androidpublisher_v3 (~> 0.54)
9
9
  googleauth (~> 1.11)
10
10
  plist (~> 3.7)
11
- reline (~> 0.5)
12
11
  thor (~> 1.4)
13
12
  xcodeproj (~> 1.27)
14
13
 
@@ -65,7 +64,6 @@ GEM
65
64
  os (>= 0.9, < 2.0)
66
65
  signet (>= 0.16, < 2.a)
67
66
  hashdiff (1.2.1)
68
- io-console (0.8.1)
69
67
  json (2.19.5)
70
68
  jwt (3.2.0)
71
69
  base64
@@ -90,8 +88,6 @@ GEM
90
88
  rainbow (3.1.1)
91
89
  rake (13.3.0)
92
90
  regexp_parser (2.11.3)
93
- reline (0.6.2)
94
- io-console (~> 0.5)
95
91
  representable (3.2.0)
96
92
  declarative (< 0.1.0)
97
93
  trailblazer-option (>= 0.1.1, < 0.2.0)
@@ -153,6 +149,7 @@ GEM
153
149
  PLATFORMS
154
150
  arm64-darwin-24
155
151
  ruby
152
+ x86_64-linux
156
153
 
157
154
  DEPENDENCIES
158
155
  bundler (~> 2.5)
@@ -161,7 +158,6 @@ DEPENDENCIES
161
158
  rspec (~> 3.0)
162
159
  rubocop (~> 1.79)
163
160
  webmock (~> 3.24)
164
- xcodeproj (~> 1.27)
165
161
 
166
162
  BUNDLED WITH
167
163
  2.7.2
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  **One command from code to TestFlight or Google Play. No provisioning hell, no manual certificate wrangling.**
4
4
 
5
- Command-line interface for [My Signer](https://github.com/jurgenleka/my-signer) - the modern mobile app signing and deployment automation tool for iOS and Android.
5
+ Command-line interface for [My Signer](https://github.com/jouleka/my-signer) - the modern mobile app signing and deployment automation tool for iOS and Android.
6
6
 
7
7
  ---
8
8
 
@@ -39,7 +39,7 @@ Mobile developers spend hours dealing with:
39
39
  - Ruby 3.2+ (recommended: 3.4.5)
40
40
 
41
41
  **Vault mode** (default — server-orchestrated):
42
- - [My Signer API](https://github.com/jurgenleka/my-signer) account and API token
42
+ - [My Signer API](https://github.com/jouleka/my-signer) account and API token
43
43
 
44
44
  **Local-only mode** (`--local-only` — no MySigner account or token required):
45
45
  - Your own signing credentials: Apple `.p8` key / Google Play service-account JSON / Android keystore (set up via `mysigner --local-only onboard`, flags, or env vars)
@@ -61,7 +61,7 @@ gem install mysigner
61
61
  ### Install from source
62
62
 
63
63
  ```bash
64
- git clone https://github.com/jurgenleka/my-signer-cli.git
64
+ git clone https://github.com/jouleka/my-signer-cli.git
65
65
  cd my-signer-cli
66
66
  bundle install
67
67
  bundle exec rake install
@@ -136,7 +136,7 @@ mysigner status
136
136
  # iOS
137
137
  mysigner ship testflight # Build + upload to TestFlight
138
138
  mysigner ship appstore # Build + submit to App Store
139
- mysigner ship appstore --submit-for-review # Auto-submit for review
139
+ mysigner ship appstore --no-auto-submit # Build + upload only (submit for review later)
140
140
 
141
141
  # Android
142
142
  mysigner ship internal --platform android # Build + upload to internal testing
@@ -389,7 +389,7 @@ mysigner config set local-only false # permanent disable
389
389
  | `doctor`, `status`, `validate` | ✅ (limited — no MySigner-side checks) |
390
390
  | `certificate check`, `device detect` | ✅ |
391
391
  | `android build` | ✅ |
392
- | `config`, `config set`, `version`, `help`, `tree`, `logout` | ✅ |
392
+ | `config`, `config set`, `version`, `help`, `logout` | ✅ |
393
393
  | `login`, `switch`, `orgs`, `sync` | ❌ MySigner-only |
394
394
  | `android init`, `android add`, `android list` | ❌ MySigner-only (register or list MySigner-side records) |
395
395
  | `apps`, `devices`, `certificates`, `profiles`, `bundleid`, `app-group(s)`, `merchant-id(s)`, `keystore`, `gp-credential`, `release`, `tracks`, `track`, `submit`, `device add/update`, `certificate download`, `profile download/delete` | ❌ MySigner-only |
@@ -446,7 +446,7 @@ Storage:
446
446
  ```bash
447
447
  # iOS — local-mint ASC JWT, upload direct to Apple
448
448
  mysigner --local-only ship testflight
449
- mysigner --local-only ship appstore --submit-for-review
449
+ mysigner --local-only ship appstore
450
450
 
451
451
  # Android — local-mint Google OAuth token, call Play Publishing API direct
452
452
  mysigner --local-only ship internal --platform android
@@ -488,10 +488,10 @@ mysigner config set KEY VAL # Update configuration value
488
488
 
489
489
  ## Development Status
490
490
 
491
- **Current Version**: 0.1.0
491
+ **Current Version**: 0.3.4
492
492
 
493
493
  ✅ **Complete**:
494
- - ✅ Gem structure and dependencies (Thor, Faraday, Reline, Google APIs)
494
+ - ✅ Gem structure and dependencies (Thor, Faraday, Google APIs)
495
495
  - ✅ Config management (`~/.mysigner/config.yml`)
496
496
  - ✅ API client (Faraday with retry & error handling)
497
497
  - ✅ Core commands (login, logout, config, status, orgs, switch, onboard)
@@ -506,7 +506,7 @@ mysigner config set KEY VAL # Update configuration value
506
506
  - ✅ Server-side signing validation (`mysigner validate`)
507
507
  - ✅ Project detection (Native iOS/Android, React Native, Flutter, Capacitor/Ionic)
508
508
  - ✅ `mysigner doctor` health check with auto-fix capabilities
509
- - ✅ 260+ RSpec tests
509
+ - ✅ Comprehensive RSpec suite (1,900+ examples)
510
510
  - ✅ Interactive prompts and wizards
511
511
 
512
512
  📅 **Future**:
@@ -515,7 +515,7 @@ mysigner config set KEY VAL # Update configuration value
515
515
  - `--json` flag for scripting
516
516
  - CI/CD templates (GitHub Actions, GitLab CI)
517
517
 
518
- See the [main project roadmap](https://github.com/jurgenleka/my-signer/blob/main/ROADMAP.md) for detailed plans.
518
+ See the [main project roadmap](https://github.com/jouleka/my-signer/blob/main/ROADMAP.md) for detailed plans.
519
519
 
520
520
  ---
521
521
 
@@ -524,7 +524,7 @@ See the [main project roadmap](https://github.com/jurgenleka/my-signer/blob/main
524
524
  ### Setup
525
525
 
526
526
  ```bash
527
- git clone https://github.com/jurgenleka/my-signer-cli.git
527
+ git clone https://github.com/jouleka/my-signer-cli.git
528
528
  cd my-signer-cli
529
529
  bundle install
530
530
  ```
@@ -679,17 +679,17 @@ This is currently a private project. Contributions are not being accepted at thi
679
679
 
680
680
  ## Related Projects
681
681
 
682
- - **[My Signer API](https://github.com/jurgenleka/my-signer)** - The backend API and web dashboard
683
- - **[My Signer Docs](https://github.com/jurgenleka/my-signer/tree/main/app/views/docs)** - In-app documentation source
682
+ - **[My Signer API](https://github.com/jouleka/my-signer)** - The backend API and web dashboard
683
+ - **[My Signer Docs](https://github.com/jouleka/my-signer/tree/main/app/views/docs)** - In-app documentation source
684
684
 
685
685
  ---
686
686
 
687
687
  ## Support
688
688
 
689
689
  For questions or issues:
690
- - Check the [main project README](https://github.com/jurgenleka/my-signer/blob/main/README.md)
691
- - See [ROADMAP.md](https://github.com/jurgenleka/my-signer/blob/main/ROADMAP.md) for development plans
692
- - Review [CHANGELOG.md](https://github.com/jurgenleka/my-signer/blob/main/CHANGELOG.md) for recent updates
690
+ - Check the [main project README](https://github.com/jouleka/my-signer/blob/main/README.md)
691
+ - See [ROADMAP.md](https://github.com/jouleka/my-signer/blob/main/ROADMAP.md) for development plans
692
+ - Review [CHANGELOG.md](https://github.com/jouleka/my-signer/blob/main/CHANGELOG.md) for recent updates
693
693
 
694
694
  ---
695
695
 
@@ -293,27 +293,21 @@ module Mysigner
293
293
  cmd_parts << '&&'
294
294
  end
295
295
 
296
- # Phase 0: export signing env vars inline so they're only visible to
297
- # the child process, not in argv. The Gradle init script below reads
298
- # MYSIGNER_STORE_FILE / MYSIGNER_STORE_PASSWORD / MYSIGNER_KEY_ALIAS /
299
- # MYSIGNER_KEY_PASSWORD and configures signingConfigs.release.
296
+ # M3: pass the signing secrets to the build via an env hash on the
297
+ # spawn (execute_with_output IO.popen(env, …)) rather than an
298
+ # `export VAR=… &&` shell string, which would expose the keystore/key
299
+ # passwords on the process table (ps, /proc/<pid>/cmdline) for the
300
+ # build's lifetime. The Gradle init script reads them from ENV either
301
+ # way; argv-form already kept them off the -P flags.
302
+ @signing_env = {}
300
303
  if @keystore_path && File.exist?(@keystore_path) && @signing_init_script_path
301
- cmd_parts << "export MYSIGNER_STORE_FILE=#{shell_escape(File.absolute_path(@keystore_path))}"
302
- cmd_parts << '&&'
303
- cmd_parts << "export MYSIGNER_STORE_PASSWORD=#{shell_escape(@keystore_password)}" if @keystore_password
304
- cmd_parts << '&&' if @keystore_password
305
- cmd_parts << "export MYSIGNER_KEY_ALIAS=#{shell_escape(@key_alias)}" if @key_alias
306
- cmd_parts << '&&' if @key_alias
307
- cmd_parts << "export MYSIGNER_KEY_PASSWORD=#{shell_escape(@key_password)}" if @key_password
308
- cmd_parts << '&&' if @key_password
309
- end
310
-
311
- # Export the versionCode override so the init script applies it to
312
- # android.defaultConfig (a bare -PversionCode property is inert).
313
- if @version_code && @signing_init_script_path
314
- cmd_parts << "export MYSIGNER_VERSION_CODE=#{shell_escape(@version_code.to_s)}"
315
- cmd_parts << '&&'
304
+ @signing_env['MYSIGNER_STORE_FILE'] = File.absolute_path(@keystore_path)
305
+ @signing_env['MYSIGNER_STORE_PASSWORD'] = @keystore_password if @keystore_password
306
+ @signing_env['MYSIGNER_KEY_ALIAS'] = @key_alias if @key_alias
307
+ @signing_env['MYSIGNER_KEY_PASSWORD'] = @key_password if @key_password
316
308
  end
309
+ # The versionCode override also flows to the init script via ENV.
310
+ @signing_env['MYSIGNER_VERSION_CODE'] = @version_code.to_s if @version_code && @signing_init_script_path
317
311
 
318
312
  # Change to android directory and run gradle
319
313
  cmd_parts << "cd #{shell_escape(android_dir)}"
@@ -364,8 +358,9 @@ module Mysigner
364
358
  puts "🏗️ Running: gradle #{@variant}..."
365
359
  puts ''
366
360
 
367
- # Run command and capture output in real-time
368
- IO.popen("#{cmd} 2>&1", 'r') do |io|
361
+ # Run command and capture output in real-time. The signing secrets ride
362
+ # in the env hash (M3), never the command string / process table.
363
+ IO.popen(@signing_env || {}, "#{cmd} 2>&1", 'r') do |io|
369
364
  io.each_line do |line|
370
365
  next if line.strip.empty?
371
366
 
@@ -72,7 +72,9 @@ module Mysigner
72
72
 
73
73
  puts "\n📦 Expo managed workflow detected (no #{platform}/ folder)"
74
74
  puts "🔧 Running: npx expo prebuild --platform #{platform}\n\n"
75
- result = system("cd #{directory} && npx expo prebuild --platform #{platform}")
75
+ # Run via Dir.chdir + argv (no shell) so a project directory containing
76
+ # a space or shell metacharacter isn't split/interpreted by /bin/sh.
77
+ result = Dir.chdir(directory) { system('npx', 'expo', 'prebuild', '--platform', platform.to_s) }
76
78
 
77
79
  native_dir = platform == :android ? 'android' : 'ios'
78
80
  return if result && Dir.exist?("#{directory}/#{native_dir}")
@@ -125,7 +125,12 @@ module Mysigner
125
125
  '-quiet'
126
126
  ]
127
127
 
128
- cmd.join(' ')
128
+ # Return the argv ARRAY (not cmd.join(' ')). execute_with_output runs
129
+ # it via IO.popen(array) which execs xcodebuild directly with no
130
+ # shell, so a scheme / bundle_id / archive path containing a space or
131
+ # shell metacharacter ($(), ;, backticks) is passed as one literal
132
+ # argument instead of being split or executed by /bin/sh.
133
+ cmd
129
134
  end
130
135
 
131
136
  def execute_with_output(cmd)
@@ -17,8 +17,8 @@ module Mysigner
17
17
  say "Install: #{File.expand_path('../../..', __dir__)}", :white
18
18
  say "Config: #{Config::CONFIG_FILE}", :white
19
19
  say ''
20
- say 'Repository: https://github.com/mysigner-dev/mysigner-cli', :white
21
- say 'Issues: https://github.com/mysigner-dev/mysigner-cli/issues', :white
20
+ say 'Repository: https://github.com/jouleka/my-signer-cli', :white
21
+ say 'Issues: https://github.com/jouleka/my-signer-cli/issues', :white
22
22
  say ''
23
23
  say 'Docs: https://mysigner.dev/docs/commands', :white
24
24
  say 'Support: https://mysigner.dev/landing#contact', :white
@@ -764,7 +764,7 @@ module Mysigner
764
764
  say 'Configuration:', :bold
765
765
  say " API URL: #{config.api_url}"
766
766
  say " User: #{config.user_email || '(unknown)'}"
767
- say " Encryption: #{config.encrypted_config? ? '✓ Enabled' : '✗ Disabled'}"
767
+ say " Encryption: #{encryption_status_line(config)}"
768
768
  say ''
769
769
 
770
770
  # Show current organization
@@ -1176,6 +1176,17 @@ module Mysigner
1176
1176
  '?'
1177
1177
  end
1178
1178
 
1179
+ # Honest at-rest description for `status`. On macOS the AES key is
1180
+ # in the system Keychain (OS-protected); elsewhere it's a 0600 file
1181
+ # right next to the encrypted token, so it's obfuscation — anyone
1182
+ # who can read config.yml can read the key. Don't imply more.
1183
+ def encryption_status_line(config)
1184
+ return '✗ Disabled' unless config.encrypted_config?
1185
+ return '✓ Enabled (macOS Keychain)' if RbConfig::CONFIG['host_os'] =~ /darwin/i
1186
+
1187
+ '✓ Enabled (local key file — obfuscation, not vault-grade; prefer MYSIGNER_API_TOKEN in CI)'
1188
+ end
1189
+
1179
1190
  def config_set(key = nil, value = nil)
1180
1191
  if key.nil? || value.nil?
1181
1192
  error 'Usage: mysigner config set <key> <value>'
@@ -660,11 +660,6 @@ module Mysigner
660
660
  say " Target: #{target_name}"
661
661
  say " IPA Size: #{format_bytes(File.size(ipa_path))}"
662
662
  say ''
663
- if is_appstore && options[:submit_for_review]
664
- poll_msg = options[:wait] ? "every #{automation.poll_interval}s" : 'skipped (--no-wait)'
665
- say " ASC Polling: #{poll_msg}"
666
- say " ASC Timeout: #{format_duration(options[:asc_timeout_seconds])}" if options[:asc_timeout_seconds]
667
- end
668
663
 
669
664
  # Timing breakdown
670
665
  say '⏱️ Time Breakdown', :bold
@@ -1119,7 +1114,11 @@ module Mysigner
1119
1114
  unless keystore_password
1120
1115
  say '⚠️ Keystore password not found in My Signer', :yellow
1121
1116
  say ' Upload your keystore with password: mysigner keystore upload FILE', :yellow
1122
- keystore_password = ask('Keystore password:', echo: false)
1117
+ keystore_password = ask_required(
1118
+ 'Keystore password:',
1119
+ 'Pass --keystore-password or set MYSIGNER_KEYSTORE_PASSWORD to build non-interactively.',
1120
+ echo: false
1121
+ )
1123
1122
  say ''
1124
1123
  key_password ||= keystore_password
1125
1124
  end
@@ -1621,7 +1620,8 @@ module Mysigner
1621
1620
  unless version_code
1622
1621
  say ''
1623
1622
  say "Enter the version code to promote to #{track}:", :yellow
1624
- version_code = ask('Version code:')
1623
+ version_code = ask_required('Version code:',
1624
+ 'Pass --version-code to promote non-interactively.')
1625
1625
  end
1626
1626
 
1627
1627
  say ''
@@ -1740,12 +1740,12 @@ module Mysigner
1740
1740
  stripped = content.lstrip
1741
1741
 
1742
1742
  begin
1743
- return YAML.safe_load(content, aliases: true) || {} if stripped.start_with?('---') || stripped.start_with?('- ')
1743
+ return YAML.safe_load(content, aliases: false) || {} if stripped.start_with?('---') || stripped.start_with?('- ')
1744
1744
 
1745
1745
  JSON.parse(content)
1746
1746
  rescue JSON::ParserError
1747
1747
  begin
1748
- YAML.safe_load(content, aliases: true) || {}
1748
+ YAML.safe_load(content, aliases: false) || {}
1749
1749
  rescue Psych::Exception => e
1750
1750
  raise MetadataFileError, "Failed to parse metadata file #{path}: #{e.message}"
1751
1751
  end
@@ -1884,8 +1884,11 @@ module Mysigner
1884
1884
  end
1885
1885
  say ''
1886
1886
 
1887
- choice = ask("Select app to build (1-#{app_targets.count}):",
1888
- limited_to: (1..app_targets.count).map(&:to_s))
1887
+ choice = ask_required(
1888
+ "Select app to build (1-#{app_targets.count}):",
1889
+ 'Pass --target <app> to select the app non-interactively.',
1890
+ limited_to: (1..app_targets.count).map(&:to_s)
1891
+ )
1889
1892
  target_name = app_targets[choice.to_i - 1].name
1890
1893
  else
1891
1894
  target_name = options[:target] || parser.main_target.name
@@ -36,8 +36,7 @@ module Mysigner
36
36
  suggestions: [
37
37
  'Apple typically takes 5-15 minutes to process builds',
38
38
  'Use --wait flag to automatically wait: mysigner ship appstore --wait',
39
- 'Check App Store Connect for processing status',
40
- 'Increase timeout: mysigner ship appstore --wait --asc-timeout-seconds 1800'
39
+ 'Check App Store Connect for processing status'
41
40
  ]
42
41
  },
43
42
 
@@ -62,8 +62,16 @@ module Mysigner
62
62
 
63
63
  # Normalize API URL (add protocol, remove trailing slash)
64
64
  def normalize_api_url(url)
65
- # Add http:// if no protocol specified
66
- url = "http://#{url}" unless url.match?(%r{^https?://})
65
+ url = url.to_s.strip
66
+
67
+ # Add a scheme if none was given. Default to https; fall back to
68
+ # http ONLY for an obvious loopback host, so a scheme-less remote
69
+ # host never silently downgrades the API token to cleartext.
70
+ unless url.match?(%r{^https?://})
71
+ bare_host = url[%r{\A[^/:]+}].to_s.downcase
72
+ scheme = Mysigner::Client::LOOPBACK_HOSTS.include?(bare_host) ? 'http' : 'https'
73
+ url = "#{scheme}://#{url}"
74
+ end
67
75
 
68
76
  # Remove trailing slash
69
77
  url.chomp('/')
@@ -79,10 +87,15 @@ module Mysigner
79
87
  # Must have a host
80
88
  return false if uri.host.nil? || uri.host.empty?
81
89
 
90
+ # Plain http may only target a loopback host (local dev). The API
91
+ # token is sent as a Bearer header, so http to a remote host would
92
+ # leak it in cleartext. uri.hostname strips IPv6 brackets ([::1]).
93
+ return false if uri.scheme == 'http' &&
94
+ !Mysigner::Client::LOOPBACK_HOSTS.include?(uri.hostname.downcase)
95
+
82
96
  # Valid formats:
83
97
  # - http://localhost:3000
84
98
  # - https://api.example.com
85
- # - http://192.168.1.1:8080
86
99
  true
87
100
  rescue URI::InvalidURIError
88
101
  false
@@ -235,7 +235,6 @@ module Mysigner
235
235
  say ''
236
236
  say ' → Apple typically takes 5-15 minutes to process builds', :yellow
237
237
  say ' → Use --wait flag: mysigner ship appstore --wait', :yellow
238
- say ' → Increase timeout: --asc-timeout-seconds 1800', :yellow
239
238
  say ' → Check App Store Connect for processing status', :yellow
240
239
  say ''
241
240
  end
@@ -4,14 +4,6 @@ module Mysigner
4
4
  class CLI < Thor
5
5
  module Concerns
6
6
  module Helpers
7
- # Helper for timing operations
8
- def with_timing(_label)
9
- start = Time.now
10
- result = yield
11
- duration = Time.now - start
12
- [result, duration]
13
- end
14
-
15
7
  def format_duration(seconds)
16
8
  if seconds < 60
17
9
  "#{seconds.round}s"
@@ -27,13 +19,21 @@ module Mysigner
27
19
  end
28
20
 
29
21
  def format_bytes(bytes)
30
- if bytes < 1024
31
- "#{bytes} B"
32
- elsif bytes < 1024 * 1024
33
- "#{(bytes / 1024.0).round(1)} KB"
34
- else
35
- "#{(bytes / (1024.0 * 1024)).round(1)} MB"
22
+ Mysigner::Formatting.format_bytes(bytes)
23
+ end
24
+
25
+ # ask() that fails loud in non-interactive mode (no TTY) instead of
26
+ # silently consuming EOF — Thor's ask returns '' on EOF, which surfaces
27
+ # later as a confusing downstream error (e.g. "wrong keystore password"
28
+ # for an empty password). `hint` tells the user which flag/env var to
29
+ # pass instead. Behaves exactly like ask() when a terminal is attached.
30
+ def ask_required(prompt, hint, **)
31
+ unless $stdin.tty?
32
+ error "#{prompt.to_s.sub(/:\s*\z/, '')} is required, but no terminal is attached for input."
33
+ say hint, :yellow
34
+ exit 1
36
35
  end
36
+ ask(prompt, **)
37
37
  end
38
38
 
39
39
  # Client-side UDID validity check for iOS devices. Matches the two
@@ -148,9 +148,13 @@ module Mysigner
148
148
  say 'Checking signing identity for team...', :yellow
149
149
  team_id = org_data['app_store_connect_team_id']
150
150
 
151
- # Check if signing identities exist in keychain for this team
152
- identities = `security find-identity -v -p codesigning 2>/dev/null | grep -i "#{team_id}"`
153
- has_identity = $CHILD_STATUS.success? && !identities.strip.empty?
151
+ # Capture identities with a CONSTANT command (no interpolation)
152
+ # and filter in Ruby, so a server-supplied team_id can never be
153
+ # a shell / grep-regex injection sink. Matches `grep -i`
154
+ # (case-insensitive substring) semantics.
155
+ all_identities = `security find-identity -v -p codesigning 2>/dev/null`
156
+ has_identity = $CHILD_STATUS.success? &&
157
+ all_identities.downcase.include?(team_id.to_s.downcase)
154
158
 
155
159
  if has_identity
156
160
  say " ✓ Signing identity found for team #{team_id}", :green
@@ -738,8 +742,13 @@ module Mysigner
738
742
  # Import private key directly to keychain (so certificate can pair)
739
743
  File.write(key_path, key.to_pem)
740
744
 
741
- `security import #{key_path} -k ~/Library/Keychains/login.keychain-db -T /usr/bin/codesign -T /usr/bin/security 2>&1`
742
- import_success = $CHILD_STATUS.success?
745
+ # argv form (no shell); expand the keychain path in Ruby so a
746
+ # $HOME containing a space doesn't break the import.
747
+ login_keychain = File.expand_path('~/Library/Keychains/login.keychain-db')
748
+ import_success = system('security', 'import', key_path,
749
+ '-k', login_keychain,
750
+ '-T', '/usr/bin/codesign', '-T', '/usr/bin/security',
751
+ out: File::NULL, err: File::NULL)
743
752
 
744
753
  say ' ✓ CSR saved to Downloads', :green
745
754
  if import_success
@@ -830,6 +839,8 @@ module Mysigner
830
839
  say ' Downloading profile...', :cyan
831
840
  download_url = "/api/v1/organizations/#{config.current_organization_id}/profiles/#{profile['id']}/download"
832
841
 
842
+ # Never attach the API token to a non-https (non-loopback) endpoint.
843
+ Mysigner::Client.assert_secure_api_url!(config.api_url)
833
844
  conn = Faraday.new(url: config.api_url) do |f|
834
845
  f.request :authorization, 'Bearer', config.api_token
835
846
  f.adapter Faraday.default_adapter
@@ -305,7 +305,10 @@ module Mysigner
305
305
  # Get device name if ideviceinfo is available
306
306
  name = 'iOS Device'
307
307
  if system('which ideviceinfo > /dev/null 2>&1')
308
- device_name = `ideviceinfo -u #{udid} -k DeviceName 2>/dev/null`.strip
308
+ # argv form (no shell): a USB device reporting a UDID with
309
+ # shell metacharacters can't inject. err → NULL mirrors 2>/dev/null.
310
+ device_name = IO.popen(['ideviceinfo', '-u', udid, '-k', 'DeviceName'],
311
+ err: File::NULL, &:read).to_s.strip
309
312
  name = device_name unless device_name.empty?
310
313
  end
311
314
 
@@ -638,6 +641,8 @@ module Mysigner
638
641
  say 'Fetching profile content...', :yellow
639
642
 
640
643
  # Use Faraday directly with proper auth for binary download
644
+ # (never attach the API token to a non-https/non-loopback host).
645
+ Mysigner::Client.assert_secure_api_url!(config.api_url)
641
646
  conn = Faraday.new(url: config.api_url) do |f|
642
647
  f.request :authorization, 'Bearer', config.api_token
643
648
  f.headers['X-User-Email'] = config.user_email if config.user_email
@@ -978,6 +983,8 @@ module Mysigner
978
983
  say 'Fetching certificate content...', :yellow
979
984
 
980
985
  # Use Faraday directly with proper auth for binary download
986
+ # (never attach the API token to a non-https/non-loopback host).
987
+ Mysigner::Client.assert_secure_api_url!(config.api_url)
981
988
  conn = Faraday.new(url: config.api_url) do |f|
982
989
  f.request :authorization, 'Bearer', config.api_token
983
990
  f.headers['X-User-Email'] = config.user_email if config.user_email