mysigner 0.2.0 → 0.3.1
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/.gitignore +4 -0
- data/CHANGELOG.md +30 -0
- data/Gemfile.lock +1 -1
- data/README.md +42 -0
- data/lib/mysigner/cli/auth_commands.rb +112 -7
- data/lib/mysigner/cli/build_commands.rb +2 -0
- data/lib/mysigner/cli/concerns/helpers.rb +28 -4
- data/lib/mysigner/cli/diagnostic_commands.rb +47 -35
- data/lib/mysigner/cli/resource_commands.rb +39 -0
- data/lib/mysigner/cli/validate_commands.rb +30 -0
- data/lib/mysigner/cli.rb +1 -1
- data/lib/mysigner/config.rb +38 -9
- data/lib/mysigner/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d5e0113872e921c7c4b1397411d1d67912ebdd1107ac7bfabdd2d45dc74e48b9
|
|
4
|
+
data.tar.gz: 6ab4d50d0e33e17a48b17365fef0756b45c633ba3ad9b7deaa5e82b3ff52ea5f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9d3b200af6f6f5d811238674f1bc1cfef9eacda498adbb260e64766e0ad9af1ad70c608af97333e14915b5caa5185e5efe8ea26a721901abc527a7ad42247cbd
|
|
7
|
+
data.tar.gz: 297534dd124838601767fa3e5c2926b640f06564c97b359e70988b43605f5e0c41f5bd85fa99aa389d14a133e2c529b9d257af417c9a6f7230588942a9052c08
|
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -97,6 +97,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
97
97
|
|
|
98
98
|
---
|
|
99
99
|
|
|
100
|
+
## [0.3.1] - 2026-05-29
|
|
101
|
+
|
|
102
|
+
### Fixed
|
|
103
|
+
- `mysigner android add PACKAGE_NAME` crashed with `NoMethodError: undefined method 'post' for nil` in local-only mode. The `android` dispatcher now gates `init` / `add` / `list` (which manage MySigner-registered records) — only `android build` is purely local. `android list`'s banner now reads "android list" instead of "apps" (it had been showing the underlying apps-command name because list is implemented as an alias).
|
|
104
|
+
- `mysigner status` reported `Source: MYSIGNER_LOCAL_ONLY env var` when `MYSIGNER_LOCAL_ONLY=0` was set in the environment AND the config file had `local_only: true`. The env value "0" is falsy per the cascade's truthy parser, so the source was actually the file. Status now uses the new `Mysigner::Config.local_only_from_env?` predicate (mirroring `local_only_from_file?`) for accurate attribution.
|
|
105
|
+
- README's local-only audit table classified `android init/add/build/list` as ✅ LOCAL across the board. Corrected: only `android build` is local; the other three are MySigner-only.
|
|
106
|
+
|
|
107
|
+
### Added
|
|
108
|
+
- `Mysigner::Config.local_only_from_env?` — public, mirrors `local_only_from_file?`. Symmetric source predicates so `mysigner status` can attribute the active source using the same truthy parser the cascade uses.
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## [0.3.0] - 2026-05-28
|
|
113
|
+
|
|
114
|
+
### Added
|
|
115
|
+
- Persistent local-only mode: `mysigner config set local-only true` writes to `~/.mysigner/config.yml`, no flag repetition required ([mysigner-22](https://mysigner.youtrack.cloud/issue/mysigner-22) follow-up).
|
|
116
|
+
- `mysigner config set <key> <value>` — extensible CLI knob for tweaking `~/.mysigner/config.yml`. Settable keys: `local-only`.
|
|
117
|
+
- `Helpers#exit_unless_local_supported!` — every MySigner-only command now exits cleanly with an explanation when local-only is active, instead of the generic "Not logged in" error.
|
|
118
|
+
|
|
119
|
+
### Changed
|
|
120
|
+
- `doctor` now announces local-only mode and skips MySigner-side checks instead of reporting "Not logged in" as an issue.
|
|
121
|
+
- `status` prints a local-mode credential-discovery summary when local-only is active.
|
|
122
|
+
- `validate` runs the local `Signing::Validator` (same one used by `ship`) and skips the server POST when local-only is active.
|
|
123
|
+
- `--local-only` Thor class_option no longer defaults to `false` — `--no-local-only` now correctly overrides the env var and the new persistent file setting.
|
|
124
|
+
|
|
125
|
+
### Fixed
|
|
126
|
+
- Precedence bug: with `MYSIGNER_LOCAL_ONLY=1` in the environment, `--no-local-only` did not actually disable local-only mode for that invocation.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
100
130
|
## [0.2.0] - 2026-05-26
|
|
101
131
|
|
|
102
132
|
### Added — `--local-only` mode
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -338,6 +338,48 @@ MYSIGNER_LOCAL_ONLY=1 mysigner ship testflight
|
|
|
338
338
|
| CI/CD setup | API token in CI secrets | Pre-populate `~/.mysigner/credentials/` from a secret store |
|
|
339
339
|
| Revocation surface | Revoke at server (audit log) | Wipe the Keychain entry / local file |
|
|
340
340
|
|
|
341
|
+
### Enabling local-only mode persistently
|
|
342
|
+
|
|
343
|
+
Set it once per machine — no flag repetition required:
|
|
344
|
+
|
|
345
|
+
```bash
|
|
346
|
+
mysigner config set local-only true
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
This writes `local_only: true` to `~/.mysigner/config.yml`. Every subsequent command behaves as if `--local-only` was passed.
|
|
350
|
+
|
|
351
|
+
**Precedence (most specific wins):**
|
|
352
|
+
|
|
353
|
+
1. `--local-only` / `--no-local-only` on the command line
|
|
354
|
+
2. `MYSIGNER_LOCAL_ONLY=1` in the environment
|
|
355
|
+
3. `local_only: true` in `~/.mysigner/config.yml`
|
|
356
|
+
4. Default off
|
|
357
|
+
|
|
358
|
+
To temporarily override the persistent setting:
|
|
359
|
+
|
|
360
|
+
```bash
|
|
361
|
+
mysigner --no-local-only sync # one-off server hit
|
|
362
|
+
mysigner config set local-only false # permanent disable
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
### What works locally
|
|
366
|
+
|
|
367
|
+
| Command | Local-only? |
|
|
368
|
+
|---|---|
|
|
369
|
+
| `ship testflight/appstore/internal/alpha/beta/production` | ✅ |
|
|
370
|
+
| `build`, `export`, `upload testflight` | ✅ |
|
|
371
|
+
| `onboard` | ✅ |
|
|
372
|
+
| `signing configure` | ✅ |
|
|
373
|
+
| `doctor`, `status`, `validate` | ✅ (limited — no MySigner-side checks) |
|
|
374
|
+
| `certificate check`, `device detect` | ✅ |
|
|
375
|
+
| `android build` | ✅ |
|
|
376
|
+
| `config`, `config set`, `version`, `help`, `tree`, `logout` | ✅ |
|
|
377
|
+
| `login`, `switch`, `orgs`, `sync` | ❌ MySigner-only |
|
|
378
|
+
| `android init`, `android add`, `android list` | ❌ MySigner-only (register or list MySigner-side records) |
|
|
379
|
+
| `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 |
|
|
380
|
+
|
|
381
|
+
Server-only commands in local-only mode exit 2 with a one-line explanation and the override hint.
|
|
382
|
+
|
|
341
383
|
### What local-only does NOT do (v1)
|
|
342
384
|
|
|
343
385
|
Local-only mode in v1 guards the **API-call-time credentials** (the ASC JWT and the Google OAuth token). The `ship` pipelines still talk to My Signer for orchestration. Be honest with yourself about which of these matter for your threat model:
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
module Mysigner
|
|
4
4
|
class CLI < Thor
|
|
5
5
|
module AuthCommands
|
|
6
|
+
SETTABLE_CONFIG_KEYS = %w[local-only].freeze
|
|
7
|
+
|
|
6
8
|
def self.included(base)
|
|
7
9
|
base.class_eval do
|
|
8
10
|
desc 'version', 'Show version information'
|
|
@@ -32,6 +34,8 @@ module Mysigner
|
|
|
32
34
|
grant access to the organization it was created in.
|
|
33
35
|
DESC
|
|
34
36
|
def login
|
|
37
|
+
exit_unless_local_supported!('login')
|
|
38
|
+
|
|
35
39
|
# Check if already logged in
|
|
36
40
|
config = Config.new
|
|
37
41
|
if config.exists?
|
|
@@ -676,6 +680,8 @@ module Mysigner
|
|
|
676
680
|
|
|
677
681
|
desc 'status', 'Check connection, credentials, and App Store Connect setup'
|
|
678
682
|
def status
|
|
683
|
+
return status_local_only if local_only?
|
|
684
|
+
|
|
679
685
|
config = load_config
|
|
680
686
|
|
|
681
687
|
say '📊 My Signer Status', :cyan
|
|
@@ -750,6 +756,8 @@ module Mysigner
|
|
|
750
756
|
|
|
751
757
|
desc 'orgs', "List all organizations you're a member of"
|
|
752
758
|
def orgs
|
|
759
|
+
exit_unless_local_supported!('orgs')
|
|
760
|
+
|
|
753
761
|
config = load_config
|
|
754
762
|
client = create_client(config)
|
|
755
763
|
|
|
@@ -823,6 +831,8 @@ module Mysigner
|
|
|
823
831
|
from different user accounts will be rejected.
|
|
824
832
|
DESC
|
|
825
833
|
def switch(target_org_id = nil)
|
|
834
|
+
exit_unless_local_supported!('switch')
|
|
835
|
+
|
|
826
836
|
config = load_config
|
|
827
837
|
client = create_client(config)
|
|
828
838
|
|
|
@@ -1000,27 +1010,122 @@ module Mysigner
|
|
|
1000
1010
|
end
|
|
1001
1011
|
end
|
|
1002
1012
|
|
|
1003
|
-
desc 'config
|
|
1004
|
-
|
|
1005
|
-
|
|
1013
|
+
desc 'config [ACTION] [ARGS...]',
|
|
1014
|
+
'Show or set CLI configuration (e.g. `mysigner config set local-only true`)'
|
|
1015
|
+
long_desc <<~DESC
|
|
1016
|
+
Without arguments: prints the current configuration.
|
|
1006
1017
|
|
|
1007
|
-
|
|
1018
|
+
Set a value:
|
|
1019
|
+
mysigner config set local-only true
|
|
1020
|
+
mysigner config set local-only false
|
|
1021
|
+
|
|
1022
|
+
Settable keys: local-only
|
|
1023
|
+
|
|
1024
|
+
`set` does NOT require a MySigner login — it is the bootstrap path
|
|
1025
|
+
for users who want to use local-only mode from a fresh machine.
|
|
1026
|
+
DESC
|
|
1027
|
+
def config(action = nil, *args)
|
|
1028
|
+
return config_set(*args) if action == 'set'
|
|
1029
|
+
|
|
1030
|
+
if action && action != 'set'
|
|
1031
|
+
error "Unknown config action: #{action}"
|
|
1032
|
+
say 'Did you mean: `mysigner config set <key> <value>`?', :yellow
|
|
1033
|
+
exit 1
|
|
1034
|
+
end
|
|
1035
|
+
|
|
1036
|
+
cfg = Config.new
|
|
1037
|
+
|
|
1038
|
+
unless cfg.exists?
|
|
1008
1039
|
error "No configuration found. Run 'mysigner login' first."
|
|
1009
1040
|
exit 1
|
|
1010
1041
|
end
|
|
1011
1042
|
|
|
1012
|
-
|
|
1043
|
+
cfg.load
|
|
1013
1044
|
|
|
1014
1045
|
say '⚙️ Configuration', :cyan
|
|
1015
1046
|
say ''
|
|
1016
|
-
|
|
1017
|
-
say " #{key.to_s.ljust(20)}: #{
|
|
1047
|
+
cfg.display.each do |key, val|
|
|
1048
|
+
say " #{key.to_s.ljust(20)}: #{val}"
|
|
1018
1049
|
end
|
|
1050
|
+
say " #{'local-only'.ljust(20)}: #{cfg.local_only?}"
|
|
1019
1051
|
say ''
|
|
1020
1052
|
say "Config file: #{Config::CONFIG_FILE}"
|
|
1021
1053
|
end
|
|
1022
1054
|
|
|
1023
1055
|
no_commands do
|
|
1056
|
+
def status_local_only
|
|
1057
|
+
require 'mysigner/credential_resolver'
|
|
1058
|
+
|
|
1059
|
+
source = if options[:local_only] == true
|
|
1060
|
+
'--local-only flag'
|
|
1061
|
+
elsif Mysigner::Config.local_only_from_env?
|
|
1062
|
+
'MYSIGNER_LOCAL_ONLY env var'
|
|
1063
|
+
else
|
|
1064
|
+
'config file (~/.mysigner/config.yml)'
|
|
1065
|
+
end
|
|
1066
|
+
|
|
1067
|
+
say '📡 My Signer Status', :cyan
|
|
1068
|
+
say '=' * 50, :cyan
|
|
1069
|
+
say ''
|
|
1070
|
+
say 'Local-only mode: ENABLED', :green
|
|
1071
|
+
say " Source: #{source}"
|
|
1072
|
+
say ''
|
|
1073
|
+
say 'Credential discovery:'
|
|
1074
|
+
say " ASC keys: #{count_or_dash { Mysigner::CredentialResolver.resolve_asc(options: options.to_h) }}"
|
|
1075
|
+
say " Play SA-JSON: #{count_or_dash { Mysigner::CredentialResolver.resolve_play(options: options.to_h) }}"
|
|
1076
|
+
say " Android keystore: #{count_or_dash { Mysigner::CredentialResolver.resolve_android_keystore(options: options.to_h) }}"
|
|
1077
|
+
say ''
|
|
1078
|
+
say "Config file: #{Mysigner::Config::CONFIG_FILE}"
|
|
1079
|
+
end
|
|
1080
|
+
|
|
1081
|
+
def count_or_dash
|
|
1082
|
+
yield
|
|
1083
|
+
'1 (discovered)'
|
|
1084
|
+
rescue Mysigner::CredentialResolver::CredentialNotFoundError
|
|
1085
|
+
'0'
|
|
1086
|
+
rescue Mysigner::CredentialResolver::AmbiguousCredentialsError
|
|
1087
|
+
'2+ (ambiguous — pass via flag)'
|
|
1088
|
+
rescue StandardError => e
|
|
1089
|
+
warn "[mysigner] Unexpected error probing credentials: #{e.class}: #{e.message}"
|
|
1090
|
+
'?'
|
|
1091
|
+
end
|
|
1092
|
+
|
|
1093
|
+
def config_set(key = nil, value = nil)
|
|
1094
|
+
if key.nil? || value.nil?
|
|
1095
|
+
error 'Usage: mysigner config set <key> <value>'
|
|
1096
|
+
say "Settable keys: #{SETTABLE_CONFIG_KEYS.join(', ')}", :yellow
|
|
1097
|
+
exit 1
|
|
1098
|
+
end
|
|
1099
|
+
|
|
1100
|
+
unless SETTABLE_CONFIG_KEYS.include?(key)
|
|
1101
|
+
error "Unknown config key: #{key}"
|
|
1102
|
+
say "Settable keys: #{SETTABLE_CONFIG_KEYS.join(', ')}", :yellow
|
|
1103
|
+
exit 1
|
|
1104
|
+
end
|
|
1105
|
+
|
|
1106
|
+
case key
|
|
1107
|
+
when 'local-only'
|
|
1108
|
+
bool = parse_bool_or_exit(value, key)
|
|
1109
|
+
cfg = Config.new
|
|
1110
|
+
cfg.load if cfg.exists?
|
|
1111
|
+
cfg.local_only = bool
|
|
1112
|
+
cfg.save
|
|
1113
|
+
say "✓ Saved #{key}: #{bool}", :green
|
|
1114
|
+
say " #{key}: #{bool}"
|
|
1115
|
+
end
|
|
1116
|
+
end
|
|
1117
|
+
|
|
1118
|
+
def parse_bool_or_exit(value, key)
|
|
1119
|
+
case value.to_s.downcase
|
|
1120
|
+
when 'true', '1', 'yes' then true
|
|
1121
|
+
when 'false', '0', 'no' then false
|
|
1122
|
+
else
|
|
1123
|
+
error "Invalid boolean for #{key}: #{value}"
|
|
1124
|
+
say 'Use: true / false (also accepts 1/0, yes/no)', :yellow
|
|
1125
|
+
exit 1
|
|
1126
|
+
end
|
|
1127
|
+
end
|
|
1128
|
+
|
|
1024
1129
|
# Helper method for yes/no prompts with Enter defaulting to yes.
|
|
1025
1130
|
# Defaults to NO when stdin is not a TTY so automation (CI, pipes)
|
|
1026
1131
|
# never silently opts-in to mutating operations.
|
|
@@ -2222,6 +2222,8 @@ module Mysigner
|
|
|
2222
2222
|
method_option :auto_submit, type: :boolean,
|
|
2223
2223
|
desc: 'Submit for review. Defaults to dashboard CLI Defaults, else true. Use --no-auto-submit to skip.'
|
|
2224
2224
|
def submit(track = nil)
|
|
2225
|
+
exit_unless_local_supported!('submit')
|
|
2226
|
+
|
|
2225
2227
|
config = load_config
|
|
2226
2228
|
client = create_client(config)
|
|
2227
2229
|
|
|
@@ -139,6 +139,10 @@ module Mysigner
|
|
|
139
139
|
config.instance_variable_set(:@organizations, {})
|
|
140
140
|
config.instance_variable_set(:@encryption_enabled, false)
|
|
141
141
|
config.instance_variable_set(:@from_env, false)
|
|
142
|
+
# The whole point of this sentinel is to BE a local-only config —
|
|
143
|
+
# set @local_only = true so `config.local_only?` (and any caller
|
|
144
|
+
# reading `config.local_only`) agrees.
|
|
145
|
+
config.instance_variable_set(:@local_only, true)
|
|
142
146
|
config
|
|
143
147
|
end
|
|
144
148
|
|
|
@@ -146,11 +150,15 @@ module Mysigner
|
|
|
146
150
|
say "✗ Error: #{message}", :red
|
|
147
151
|
end
|
|
148
152
|
|
|
149
|
-
# Local-only mode is active
|
|
150
|
-
#
|
|
151
|
-
#
|
|
153
|
+
# Local-only mode is active when any of, in precedence order:
|
|
154
|
+
# 1. --local-only / --no-local-only flag on this invocation
|
|
155
|
+
# 2. MYSIGNER_LOCAL_ONLY env var
|
|
156
|
+
# 3. `local_only: true` in ~/.mysigner/config.yml
|
|
157
|
+
# `Config.local_only?` (class method) walks #2 then #3.
|
|
152
158
|
def local_only?
|
|
153
|
-
options[:local_only]
|
|
159
|
+
return options[:local_only] unless options[:local_only].nil?
|
|
160
|
+
|
|
161
|
+
Mysigner::Config.local_only?
|
|
154
162
|
end
|
|
155
163
|
|
|
156
164
|
# mysigner-22 Phase 5 — resolve ASC creds via the cascade (flag →
|
|
@@ -222,6 +230,22 @@ module Mysigner
|
|
|
222
230
|
'(other MySigner APIs may still be used; see docs).'
|
|
223
231
|
end
|
|
224
232
|
|
|
233
|
+
# Server-only command guard. SERVER commands (apps, orgs, sync,
|
|
234
|
+
# certificates, etc.) hit MySigner-side resources and have no
|
|
235
|
+
# local equivalent. Print a clean explanation and exit 2 when
|
|
236
|
+
# local-only mode is active, instead of letting load_config bail
|
|
237
|
+
# with the generic "Not logged in" path.
|
|
238
|
+
def exit_unless_local_supported!(command_name)
|
|
239
|
+
return unless local_only?
|
|
240
|
+
|
|
241
|
+
say "✗ `#{command_name}` manages MySigner-side resources and " \
|
|
242
|
+
"isn't available in local-only mode.", :red
|
|
243
|
+
say ''
|
|
244
|
+
say 'Disable persistently: mysigner config set local-only false', :yellow
|
|
245
|
+
say "Override for one call: mysigner --no-local-only #{command_name}", :yellow
|
|
246
|
+
exit 2
|
|
247
|
+
end
|
|
248
|
+
|
|
225
249
|
class << self
|
|
226
250
|
def banner_emitted?
|
|
227
251
|
@local_only_banner_emitted == true
|
|
@@ -77,48 +77,54 @@ module Mysigner
|
|
|
77
77
|
say ''
|
|
78
78
|
|
|
79
79
|
# Check 4: My Signer Configuration
|
|
80
|
-
say 'Checking My Signer configuration...', :yellow
|
|
81
80
|
client = nil
|
|
82
81
|
org_data = nil
|
|
83
82
|
|
|
84
|
-
|
|
85
|
-
Config.from_env
|
|
86
|
-
else
|
|
87
|
-
Config.new
|
|
88
|
-
end
|
|
83
|
+
say 'Checking My Signer configuration...', :yellow
|
|
89
84
|
|
|
90
|
-
if
|
|
91
|
-
|
|
92
|
-
|
|
85
|
+
if local_only?
|
|
86
|
+
say ' ✓ Local-only mode active — MySigner login not required', :green
|
|
87
|
+
else
|
|
88
|
+
config = if Config.env_configured?
|
|
89
|
+
Config.from_env
|
|
90
|
+
else
|
|
91
|
+
Config.new
|
|
92
|
+
end
|
|
93
93
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
94
|
+
if config.from_env? || config.exists?
|
|
95
|
+
config.load unless config.from_env?
|
|
96
|
+
say config.from_env? ? ' ✓ Configured via environment variables' : ' ✓ Logged in', :green
|
|
97
|
+
|
|
98
|
+
begin
|
|
99
|
+
client = Client.new(api_url: config.api_url, api_token: config.api_token,
|
|
100
|
+
user_email: config.user_email)
|
|
101
|
+
client.test_connection
|
|
102
|
+
say ' ✓ API connection working', :green
|
|
103
|
+
|
|
104
|
+
# Get organization details
|
|
105
|
+
if config.current_organization_id
|
|
106
|
+
org_response = client.get("/api/v1/organizations/#{config.current_organization_id}")
|
|
107
|
+
org_data = org_response[:data]
|
|
108
|
+
end
|
|
109
|
+
rescue Mysigner::UnauthorizedError
|
|
110
|
+
say ' ✗ Token is invalid or expired', :red
|
|
111
|
+
issues << "Token authentication failed - run 'mysigner onboard' to re-authenticate"
|
|
112
|
+
client = nil
|
|
113
|
+
rescue Mysigner::ConnectionError => e
|
|
114
|
+
say " ✗ Cannot connect to API: #{e.message}", :red
|
|
115
|
+
issues << 'API connection failed - check your network or API URL'
|
|
116
|
+
client = nil
|
|
117
|
+
rescue StandardError => e
|
|
118
|
+
say " ✗ API error: #{e.message}", :red
|
|
119
|
+
issues << 'API connection failed - check your configuration'
|
|
120
|
+
client = nil
|
|
104
121
|
end
|
|
105
|
-
|
|
106
|
-
say ' ✗
|
|
107
|
-
issues << "
|
|
108
|
-
client = nil
|
|
109
|
-
rescue Mysigner::ConnectionError => e
|
|
110
|
-
say " ✗ Cannot connect to API: #{e.message}", :red
|
|
111
|
-
issues << 'API connection failed - check your network or API URL'
|
|
112
|
-
client = nil
|
|
113
|
-
rescue StandardError => e
|
|
114
|
-
say " ✗ API error: #{e.message}", :red
|
|
115
|
-
issues << 'API connection failed - check your configuration'
|
|
116
|
-
client = nil
|
|
122
|
+
else
|
|
123
|
+
say ' ✗ Not logged in', :red
|
|
124
|
+
issues << "Run 'mysigner onboard' to authenticate"
|
|
117
125
|
end
|
|
118
|
-
else
|
|
119
|
-
say ' ✗ Not logged in', :red
|
|
120
|
-
issues << "Run 'mysigner onboard' to authenticate"
|
|
121
126
|
end
|
|
127
|
+
|
|
122
128
|
say ''
|
|
123
129
|
|
|
124
130
|
# Check 4a: Signing Identity in Keychain (CRITICAL)
|
|
@@ -446,7 +452,11 @@ module Mysigner
|
|
|
446
452
|
end
|
|
447
453
|
say ''
|
|
448
454
|
elsif project_info && (!client || !org_data)
|
|
449
|
-
|
|
455
|
+
if local_only?
|
|
456
|
+
say '⚠️ Project detected — local-only mode (signing checks limited).', :yellow
|
|
457
|
+
else
|
|
458
|
+
say '⚠️ Project detected but cannot check signing (not logged in)', :yellow
|
|
459
|
+
end
|
|
450
460
|
say ''
|
|
451
461
|
end
|
|
452
462
|
end
|
|
@@ -893,6 +903,8 @@ module Mysigner
|
|
|
893
903
|
DESC
|
|
894
904
|
option :force, type: :boolean, aliases: '-f', desc: 'Force sync even if recently synced'
|
|
895
905
|
def sync(platform = 'ios')
|
|
906
|
+
exit_unless_local_supported!('sync')
|
|
907
|
+
|
|
896
908
|
config = load_config
|
|
897
909
|
client = create_client(config)
|
|
898
910
|
|
|
@@ -12,6 +12,8 @@ module Mysigner
|
|
|
12
12
|
method_option :page, type: :numeric, default: 1, desc: 'Page number'
|
|
13
13
|
method_option :per_page, type: :numeric, default: 50, desc: 'Devices per page'
|
|
14
14
|
def devices
|
|
15
|
+
exit_unless_local_supported!('devices')
|
|
16
|
+
|
|
15
17
|
config = load_config
|
|
16
18
|
client = create_client(config)
|
|
17
19
|
|
|
@@ -121,6 +123,7 @@ module Mysigner
|
|
|
121
123
|
DESC
|
|
122
124
|
method_option :platform, type: :string, default: 'IOS', aliases: '-p', desc: 'Platform (IOS, MAC_OS, TV_OS)'
|
|
123
125
|
def device(action, *args)
|
|
126
|
+
exit_unless_local_supported!("device #{action}") unless action == 'detect'
|
|
124
127
|
config = load_config
|
|
125
128
|
client = create_client(config)
|
|
126
129
|
|
|
@@ -457,6 +460,8 @@ module Mysigner
|
|
|
457
460
|
method_option :page, type: :numeric, default: 1, desc: 'Page number'
|
|
458
461
|
method_option :per_page, type: :numeric, default: 50, desc: 'Profiles per page'
|
|
459
462
|
def profiles
|
|
463
|
+
exit_unless_local_supported!('profiles')
|
|
464
|
+
|
|
460
465
|
config = load_config
|
|
461
466
|
client = create_client(config)
|
|
462
467
|
|
|
@@ -589,6 +594,7 @@ module Mysigner
|
|
|
589
594
|
|
|
590
595
|
case action
|
|
591
596
|
when 'download'
|
|
597
|
+
exit_unless_local_supported!('profile download')
|
|
592
598
|
if args.empty?
|
|
593
599
|
error 'Usage: mysigner profile download ID [--output path.mobileprovision]'
|
|
594
600
|
say ''
|
|
@@ -703,6 +709,7 @@ module Mysigner
|
|
|
703
709
|
exit 1
|
|
704
710
|
end
|
|
705
711
|
when 'delete'
|
|
712
|
+
exit_unless_local_supported!('profile delete')
|
|
706
713
|
if args.empty?
|
|
707
714
|
error 'Usage: mysigner profile delete ID'
|
|
708
715
|
say ''
|
|
@@ -760,6 +767,8 @@ module Mysigner
|
|
|
760
767
|
method_option :page, type: :numeric, default: 1, desc: 'Page number'
|
|
761
768
|
method_option :per_page, type: :numeric, default: 50, desc: 'Certificates per page'
|
|
762
769
|
def certificates
|
|
770
|
+
exit_unless_local_supported!('certificates')
|
|
771
|
+
|
|
763
772
|
config = load_config
|
|
764
773
|
client = create_client(config)
|
|
765
774
|
|
|
@@ -829,6 +838,7 @@ module Mysigner
|
|
|
829
838
|
DESC
|
|
830
839
|
method_option :output, type: :string, aliases: '-o', desc: 'Output file path (default: certificate name)'
|
|
831
840
|
def certificate(action, *args)
|
|
841
|
+
exit_unless_local_supported!("certificate #{action}") unless action == 'check'
|
|
832
842
|
config = load_config
|
|
833
843
|
client = create_client(config)
|
|
834
844
|
|
|
@@ -1104,6 +1114,8 @@ module Mysigner
|
|
|
1104
1114
|
method_option :app_id, type: :numeric, desc: 'Associate with Android app ID'
|
|
1105
1115
|
method_option :output, type: :string, aliases: '-o', desc: 'Output path for download'
|
|
1106
1116
|
def keystore(action, *args)
|
|
1117
|
+
exit_unless_local_supported!("keystore #{action}")
|
|
1118
|
+
|
|
1107
1119
|
config = load_config
|
|
1108
1120
|
client = create_client(config)
|
|
1109
1121
|
|
|
@@ -1409,6 +1421,13 @@ module Mysigner
|
|
|
1409
1421
|
DESC
|
|
1410
1422
|
method_option :name, type: :string, desc: 'Display name for the app'
|
|
1411
1423
|
def android(action, *args)
|
|
1424
|
+
# mysigner-22 follow-up — `android build` is the only LOCAL action;
|
|
1425
|
+
# init/add/list all manage MySigner-registered records. Gate the
|
|
1426
|
+
# rest at the dispatcher top so users see "android add" in the
|
|
1427
|
+
# banner (not the underlying "apps" call that `list` invokes) and
|
|
1428
|
+
# so `android add` doesn't crash with NoMethodError on the nil
|
|
1429
|
+
# client further down.
|
|
1430
|
+
exit_unless_local_supported!("android #{action}") unless %w[build help].include?(action)
|
|
1412
1431
|
config = load_config
|
|
1413
1432
|
client = create_client(config)
|
|
1414
1433
|
|
|
@@ -2217,6 +2236,8 @@ module Mysigner
|
|
|
2217
2236
|
• After registering, run 'mysigner sync ios' to update local cache
|
|
2218
2237
|
DESC
|
|
2219
2238
|
def bundleid(action, *args)
|
|
2239
|
+
exit_unless_local_supported!("bundleid #{action}")
|
|
2240
|
+
|
|
2220
2241
|
config = load_config
|
|
2221
2242
|
client = create_client(config)
|
|
2222
2243
|
|
|
@@ -2419,6 +2440,8 @@ module Mysigner
|
|
|
2419
2440
|
method_option :page, type: :numeric, default: 1, desc: 'Page number'
|
|
2420
2441
|
method_option :per_page, type: :numeric, default: 50, desc: 'Apps per page'
|
|
2421
2442
|
def apps
|
|
2443
|
+
exit_unless_local_supported!('apps')
|
|
2444
|
+
|
|
2422
2445
|
config = load_config
|
|
2423
2446
|
client = create_client(config)
|
|
2424
2447
|
|
|
@@ -2521,6 +2544,8 @@ module Mysigner
|
|
|
2521
2544
|
method_option :page, type: :numeric, default: 1, desc: 'Page number'
|
|
2522
2545
|
method_option :per_page, type: :numeric, default: 50, desc: 'Items per page'
|
|
2523
2546
|
def merchant_ids
|
|
2547
|
+
exit_unless_local_supported!('merchant-ids')
|
|
2548
|
+
|
|
2524
2549
|
config = load_config
|
|
2525
2550
|
client = create_client(config)
|
|
2526
2551
|
|
|
@@ -2584,6 +2609,8 @@ module Mysigner
|
|
|
2584
2609
|
DESC
|
|
2585
2610
|
method_option :name, type: :string, aliases: '-n', desc: 'Friendly name for the Merchant ID'
|
|
2586
2611
|
def merchant_id(action, identifier = nil)
|
|
2612
|
+
exit_unless_local_supported!("merchant-id #{action}")
|
|
2613
|
+
|
|
2587
2614
|
config = load_config
|
|
2588
2615
|
client = create_client(config)
|
|
2589
2616
|
|
|
@@ -2681,6 +2708,8 @@ module Mysigner
|
|
|
2681
2708
|
desc 'tracks PACKAGE_NAME', 'List Google Play tracks for an Android app'
|
|
2682
2709
|
method_option :sort, type: :boolean, desc: 'Sort by track name'
|
|
2683
2710
|
def tracks(package_name = nil)
|
|
2711
|
+
exit_unless_local_supported!('tracks')
|
|
2712
|
+
|
|
2684
2713
|
config = load_config
|
|
2685
2714
|
client = create_client(config)
|
|
2686
2715
|
|
|
@@ -2754,6 +2783,8 @@ module Mysigner
|
|
|
2754
2783
|
|
|
2755
2784
|
desc 'track PACKAGE_NAME TRACK_NAME', 'Show details for a specific Google Play track'
|
|
2756
2785
|
def track(package_name = nil, track_name = nil)
|
|
2786
|
+
exit_unless_local_supported!('track')
|
|
2787
|
+
|
|
2757
2788
|
config = load_config
|
|
2758
2789
|
client = create_client(config)
|
|
2759
2790
|
|
|
@@ -2851,6 +2882,8 @@ module Mysigner
|
|
|
2851
2882
|
method_option :page, type: :numeric, default: 1, desc: 'Page number'
|
|
2852
2883
|
method_option :per_page, type: :numeric, default: 50, desc: 'Items per page'
|
|
2853
2884
|
def app_groups
|
|
2885
|
+
exit_unless_local_supported!('app-groups')
|
|
2886
|
+
|
|
2854
2887
|
config = load_config
|
|
2855
2888
|
client = create_client(config)
|
|
2856
2889
|
|
|
@@ -2920,6 +2953,8 @@ module Mysigner
|
|
|
2920
2953
|
DESC
|
|
2921
2954
|
method_option :name, type: :string, aliases: '-n', desc: 'Friendly name for the App Group'
|
|
2922
2955
|
def app_group(action, identifier = nil)
|
|
2956
|
+
exit_unless_local_supported!("app-group #{action}")
|
|
2957
|
+
|
|
2923
2958
|
config = load_config
|
|
2924
2959
|
client = create_client(config)
|
|
2925
2960
|
|
|
@@ -3055,6 +3090,8 @@ module Mysigner
|
|
|
3055
3090
|
mysigner gp-credential delete 3
|
|
3056
3091
|
DESC
|
|
3057
3092
|
def gp_credential(action, *args)
|
|
3093
|
+
exit_unless_local_supported!("gp-credential #{action}")
|
|
3094
|
+
|
|
3058
3095
|
config = load_config
|
|
3059
3096
|
client = create_client(config)
|
|
3060
3097
|
|
|
@@ -3274,6 +3311,8 @@ module Mysigner
|
|
|
3274
3311
|
method_option :release_type, type: :string, desc: 'Release type: manual, after_approval, scheduled'
|
|
3275
3312
|
method_option :scheduled_date, type: :string, desc: 'Scheduled release date (ISO 8601)'
|
|
3276
3313
|
def release(action, *args)
|
|
3314
|
+
exit_unless_local_supported!("release #{action}")
|
|
3315
|
+
|
|
3277
3316
|
config = load_config
|
|
3278
3317
|
client = create_client(config)
|
|
3279
3318
|
|
|
@@ -37,6 +37,8 @@ module Mysigner
|
|
|
37
37
|
method_option :bundle_id, type: :string, aliases: '-b', desc: 'Bundle identifier (e.g., com.example.app)'
|
|
38
38
|
method_option :type, type: :string, aliases: '-t', desc: 'Signing type: development, appstore, adhoc, inhouse'
|
|
39
39
|
def validate
|
|
40
|
+
return validate_local_only if local_only?
|
|
41
|
+
|
|
40
42
|
config = load_config
|
|
41
43
|
client = create_client(config)
|
|
42
44
|
|
|
@@ -137,6 +139,34 @@ module Mysigner
|
|
|
137
139
|
end
|
|
138
140
|
end
|
|
139
141
|
|
|
142
|
+
no_commands do
|
|
143
|
+
def validate_local_only
|
|
144
|
+
say '🔍 Local-only validation', :cyan
|
|
145
|
+
say '=' * 50, :cyan
|
|
146
|
+
say ''
|
|
147
|
+
say 'Running local Signing::Validator (server checks skipped in local-only mode).', :yellow
|
|
148
|
+
say ''
|
|
149
|
+
|
|
150
|
+
project_info = Mysigner::Build::Detector.detect
|
|
151
|
+
parser = Mysigner::Build::Parser.new(project_info)
|
|
152
|
+
target_name = parser.main_target.name
|
|
153
|
+
|
|
154
|
+
validator = Mysigner::Signing::Validator.new(
|
|
155
|
+
parser, target_name, options[:configuration] || 'Release',
|
|
156
|
+
team_id: options[:team], local_only: true
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
begin
|
|
160
|
+
validator.validate!
|
|
161
|
+
rescue Mysigner::Signing::Validator::ValidationError => e
|
|
162
|
+
error e.message
|
|
163
|
+
exit 1
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
say '✓ Local validation passed.', :green
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
140
170
|
private
|
|
141
171
|
|
|
142
172
|
def detect_bundle_id_from_project
|
data/lib/mysigner/cli.rb
CHANGED
|
@@ -25,7 +25,7 @@ Mysigner::Cleanup::PrivateKeysPurger.new.call
|
|
|
25
25
|
module Mysigner
|
|
26
26
|
class CLI < Thor
|
|
27
27
|
class_option :verbose, type: :boolean, aliases: '-v', desc: 'Verbose output'
|
|
28
|
-
class_option :local_only, type: :boolean,
|
|
28
|
+
class_option :local_only, type: :boolean,
|
|
29
29
|
desc: 'Do not send credentials to the server (local-only mode)'
|
|
30
30
|
|
|
31
31
|
def self.exit_on_failure?
|
data/lib/mysigner/config.rb
CHANGED
|
@@ -25,7 +25,7 @@ module Mysigner
|
|
|
25
25
|
ENV_ORG_ID = 'MYSIGNER_ORG_ID'
|
|
26
26
|
ENV_LOCAL_ONLY = 'MYSIGNER_LOCAL_ONLY'
|
|
27
27
|
|
|
28
|
-
attr_accessor :api_url, :user_email, :current_organization_id, :encryption_enabled
|
|
28
|
+
attr_accessor :api_url, :user_email, :current_organization_id, :encryption_enabled, :local_only
|
|
29
29
|
attr_reader :organizations
|
|
30
30
|
|
|
31
31
|
def initialize
|
|
@@ -34,6 +34,7 @@ module Mysigner
|
|
|
34
34
|
@current_organization_id = nil
|
|
35
35
|
@organizations = {}
|
|
36
36
|
@encryption_enabled = true # Enable by default for security
|
|
37
|
+
@local_only = false
|
|
37
38
|
@from_env = false
|
|
38
39
|
load if exists?
|
|
39
40
|
end
|
|
@@ -49,6 +50,7 @@ module Mysigner
|
|
|
49
50
|
config = allocate
|
|
50
51
|
config.instance_variable_set(:@encryption_enabled, false)
|
|
51
52
|
config.instance_variable_set(:@from_env, true)
|
|
53
|
+
config.instance_variable_set(:@local_only, false)
|
|
52
54
|
|
|
53
55
|
org_id = ENV.fetch(ENV_ORG_ID, nil)
|
|
54
56
|
token = ENV.fetch(ENV_API_TOKEN, nil)
|
|
@@ -67,15 +69,38 @@ module Mysigner
|
|
|
67
69
|
@from_env
|
|
68
70
|
end
|
|
69
71
|
|
|
70
|
-
# Local-only mode
|
|
71
|
-
#
|
|
72
|
-
# on top in the CLI Helpers concern (which can read `options`).
|
|
72
|
+
# Local-only mode at the Config level: cascade ENV → file. The CLI
|
|
73
|
+
# Helpers concern layers --local-only / --no-local-only on top.
|
|
73
74
|
def self.local_only?
|
|
75
|
+
local_only_from_env? || local_only_from_file?
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Public predicate for the env-var source. Mirrors local_only_from_file?
|
|
79
|
+
# so status's "Source: …" attribution can distinguish env vs file
|
|
80
|
+
# using the same truthy parser the cascade uses (a literal env value
|
|
81
|
+
# of "0" / "false" reads as off, not as "env var enabled it").
|
|
82
|
+
def self.local_only_from_env?
|
|
74
83
|
truthy_env?(ENV_LOCAL_ONLY)
|
|
75
84
|
end
|
|
76
85
|
|
|
86
|
+
# Lightweight check that reads only ~/.mysigner/config.yml's
|
|
87
|
+
# `local_only:` key without invoking #load (which decrypts tokens
|
|
88
|
+
# via the Keychain). A user with no MySigner account still needs
|
|
89
|
+
# to flip this setting, so we never raise on a missing/corrupt
|
|
90
|
+
# or absent file — we just return false.
|
|
91
|
+
def self.local_only_from_file?
|
|
92
|
+
data = YAML.safe_load_file(CONFIG_FILE)
|
|
93
|
+
data.is_a?(Hash) && data['local_only'] == true
|
|
94
|
+
rescue Errno::ENOENT, Psych::SyntaxError
|
|
95
|
+
false
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Instance-level predicate. Merges two surfaces:
|
|
99
|
+
# - @local_only: set to true by Helpers#blank_local_only_config so a
|
|
100
|
+
# sentinel config always answers true without touching ENV or disk.
|
|
101
|
+
# - self.class.local_only?: the normal ENV → file cascade.
|
|
77
102
|
def local_only?
|
|
78
|
-
self.class.local_only?
|
|
103
|
+
@local_only || self.class.local_only?
|
|
79
104
|
end
|
|
80
105
|
|
|
81
106
|
# Get API token for current organization (or specific org)
|
|
@@ -135,9 +160,10 @@ module Mysigner
|
|
|
135
160
|
|
|
136
161
|
# mysigner-51 — safe_load_file rejects arbitrary Ruby object
|
|
137
162
|
# instantiation in the YAML (`!ruby/object:Foo` etc.). The config
|
|
138
|
-
# shape is just String/Integer/Hash (api_url, user_email,
|
|
139
|
-
# current_organization_id, organizations: {id => {name,
|
|
140
|
-
# all in safe_load's default allowed set, so no
|
|
163
|
+
# shape is just String/Integer/Boolean/Hash (api_url, user_email,
|
|
164
|
+
# current_organization_id, local_only, organizations: {id => {name,
|
|
165
|
+
# token}}), all in safe_load's default allowed set, so no
|
|
166
|
+
# permitted_classes
|
|
141
167
|
# extension is needed. Low risk (the file is 0600 and user-owned)
|
|
142
168
|
# but cheap hardening against a future RCE if config write or read
|
|
143
169
|
# ever moves outside the owner-only assumption.
|
|
@@ -147,6 +173,7 @@ module Mysigner
|
|
|
147
173
|
@user_email = data['user_email']
|
|
148
174
|
@current_organization_id = data['current_organization_id']
|
|
149
175
|
@organizations = data['organizations'] || {}
|
|
176
|
+
@local_only = data['local_only'] == true
|
|
150
177
|
|
|
151
178
|
# Auto-detect encryption from config
|
|
152
179
|
@encryption_enabled = encrypted_config?
|
|
@@ -164,7 +191,8 @@ module Mysigner
|
|
|
164
191
|
'api_url' => @api_url,
|
|
165
192
|
'user_email' => @user_email,
|
|
166
193
|
'current_organization_id' => @current_organization_id,
|
|
167
|
-
'organizations' => @organizations
|
|
194
|
+
'organizations' => @organizations,
|
|
195
|
+
'local_only' => @local_only
|
|
168
196
|
}
|
|
169
197
|
|
|
170
198
|
File.write(CONFIG_FILE, data.to_yaml)
|
|
@@ -180,6 +208,7 @@ module Mysigner
|
|
|
180
208
|
@user_email = nil
|
|
181
209
|
@current_organization_id = nil
|
|
182
210
|
@organizations = {}
|
|
211
|
+
@local_only = false
|
|
183
212
|
|
|
184
213
|
File.delete(CONFIG_FILE) if exists?
|
|
185
214
|
|
data/lib/mysigner/version.rb
CHANGED