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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +2 -2
- data/CHANGELOG.md +47 -0
- data/Gemfile +0 -1
- data/Gemfile.lock +2 -6
- data/README.md +16 -16
- data/lib/mysigner/build/android_executor.rb +16 -21
- data/lib/mysigner/build/detector.rb +3 -1
- data/lib/mysigner/build/executor.rb +6 -1
- data/lib/mysigner/cli/auth_commands.rb +14 -3
- data/lib/mysigner/cli/build_commands.rb +14 -11
- data/lib/mysigner/cli/concerns/actionable_suggestions.rb +1 -2
- data/lib/mysigner/cli/concerns/api_helpers.rb +16 -3
- data/lib/mysigner/cli/concerns/error_handlers.rb +0 -1
- data/lib/mysigner/cli/concerns/helpers.rb +14 -14
- data/lib/mysigner/cli/diagnostic_commands.rb +16 -5
- data/lib/mysigner/cli/resource_commands.rb +8 -1
- data/lib/mysigner/client.rb +52 -0
- data/lib/mysigner/config.rb +9 -4
- data/lib/mysigner/export/exporter.rb +6 -1
- data/lib/mysigner/formatting.rb +23 -0
- data/lib/mysigner/signing/certificate_checker.rb +6 -6
- data/lib/mysigner/signing/keystore_manager.rb +2 -0
- data/lib/mysigner/signing/wizard.rb +2 -0
- data/lib/mysigner/upload/app_store_automation.rb +13 -1
- data/lib/mysigner/upload/app_store_submission.rb +2 -14
- data/lib/mysigner/upload/asc_rest_uploader.rb +44 -3
- data/lib/mysigner/upload/asc_submitter.rb +5 -0
- data/lib/mysigner/upload/play_store_uploader.rb +2 -7
- data/lib/mysigner/upload/uploader.rb +9 -366
- data/lib/mysigner/version.rb +1 -1
- data/lib/mysigner.rb +1 -0
- data/mysigner.gemspec +6 -2
- metadata +2 -20
- data/.travis.yml +0 -7
- data/MANUAL_TEST.md +0 -341
- data/bin/console +0 -15
- data/bin/setup +0 -11
- data/test_manual.rb +0 -103
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a7b51b5ca8891aa0e71405e1c5a2f8d0aef349ef8aaeca1e870925eb5403bcea
|
|
4
|
+
data.tar.gz: 45ceb49f0a2015ecbe689e17510af4c6a8b8c1c3c4c25c036f637ff4a541a842
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e8ac1f132fbe6e912a918c923789c28784b46e5d978a7fb7fdfeca2b56a8ed33f23f613287459c65825868896df86029097ea0410482533667ab9800b799a8df
|
|
7
|
+
data.tar.gz: 3e7fbb4faf3dd82f831920ca7b7c306568cd70a911da7361da1080c7540449f75e0ba2bf54b8743f0560162490aa8308ac92e7e496b09fa5fc2e24607459c5e2
|
data/.github/workflows/ci.yml
CHANGED
|
@@ -9,7 +9,7 @@ jobs:
|
|
|
9
9
|
lint:
|
|
10
10
|
runs-on: ubuntu-latest
|
|
11
11
|
steps:
|
|
12
|
-
- uses: actions/checkout@
|
|
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@
|
|
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
data/Gemfile.lock
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
mysigner (0.3.
|
|
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/
|
|
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/
|
|
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/
|
|
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 --
|
|
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`, `
|
|
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
|
|
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.
|
|
491
|
+
**Current Version**: 0.3.4
|
|
492
492
|
|
|
493
493
|
✅ **Complete**:
|
|
494
|
-
- ✅ Gem structure and dependencies (Thor, Faraday,
|
|
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
|
-
- ✅
|
|
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/
|
|
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/
|
|
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/
|
|
683
|
-
- **[My Signer Docs](https://github.com/
|
|
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/
|
|
691
|
-
- See [ROADMAP.md](https://github.com/
|
|
692
|
-
- Review [CHANGELOG.md](https://github.com/
|
|
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
|
-
#
|
|
297
|
-
#
|
|
298
|
-
#
|
|
299
|
-
#
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
21
|
-
say 'Issues: https://github.com/
|
|
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
|
|
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 =
|
|
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 =
|
|
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:
|
|
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:
|
|
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 =
|
|
1888
|
-
|
|
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
|
-
|
|
66
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
#
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
742
|
-
|
|
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
|
-
|
|
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
|