mysigner 0.1.7 → 0.2.0
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 +25 -0
- data/.rubocop_todo.yml +6 -1
- data/CHANGELOG.md +92 -0
- data/Gemfile.lock +7 -7
- data/README.md +94 -1
- data/exe/mysigner +55 -1
- data/lib/mysigner/auth/asc_jwt_minter.rb +68 -0
- data/lib/mysigner/auth/google_oauth_minter.rb +89 -0
- data/lib/mysigner/cleanup/private_keys_purger.rb +0 -1
- data/lib/mysigner/cli/auth_commands.rb +355 -5
- data/lib/mysigner/cli/build_commands.rb +540 -267
- data/lib/mysigner/cli/concerns/helpers.rb +135 -0
- data/lib/mysigner/cli.rb +3 -2
- data/lib/mysigner/config.rb +40 -1
- data/lib/mysigner/credential_resolver.rb +1099 -0
- data/lib/mysigner/local_credentials.rb +281 -0
- data/lib/mysigner/signing/keystore_manager.rb +7 -10
- data/lib/mysigner/signing/validator.rb +20 -9
- data/lib/mysigner/upload/asc_rest_uploader.rb +252 -35
- data/lib/mysigner/upload/asc_submitter.rb +432 -0
- data/lib/mysigner/upload/play_store_uploader.rb +95 -3
- data/lib/mysigner/version.rb +1 -1
- data/lib/mysigner.rb +1 -0
- metadata +6 -5
- data/certificate_.cer +0 -0
- data/iOS_App_Store_Profile.mobileprovision +0 -1
- data/iOS_Distribution_Certificate.cer +0 -1
- data/profile_.mobileprovision +0 -0
|
@@ -69,6 +69,14 @@ module Mysigner
|
|
|
69
69
|
end
|
|
70
70
|
|
|
71
71
|
def load_config
|
|
72
|
+
# mysigner-22 — local-only mode is allowed to run with ZERO MySigner
|
|
73
|
+
# auth: no ~/.mysigner/config.yml, no API token, no login. Return a
|
|
74
|
+
# blank Config sentinel and skip both the load and the
|
|
75
|
+
# "Not logged in" exit. Callers must treat the returned config as
|
|
76
|
+
# opaque (no api_url, no api_token, no organization_id) and must
|
|
77
|
+
# also treat create_client(config) as returning nil.
|
|
78
|
+
return blank_local_only_config if local_only?
|
|
79
|
+
|
|
72
80
|
# CI/CD mode: prefer environment variables when set
|
|
73
81
|
return Config.from_env if Config.env_configured?
|
|
74
82
|
|
|
@@ -82,6 +90,19 @@ module Mysigner
|
|
|
82
90
|
say ' export MYSIGNER_ORG_ID=your_org_id', :yellow
|
|
83
91
|
say ' export MYSIGNER_API_URL=https://mysigner.dev # optional', :yellow
|
|
84
92
|
say ' export MYSIGNER_EMAIL=you@example.com # optional', :yellow
|
|
93
|
+
say ''
|
|
94
|
+
# Discoverability: a first-time user who doesn't want a MySigner
|
|
95
|
+
# account at all (BYO-credentials, no server orchestration) needs
|
|
96
|
+
# to know `--local-only` exists. Without this tip the only error
|
|
97
|
+
# they see is "Not logged in", which strongly implies signup is
|
|
98
|
+
# the only path forward.
|
|
99
|
+
say 'Tip: To skip MySigner entirely and ship locally with your own', :yellow
|
|
100
|
+
say 'Apple/Google credentials, use --local-only:', :yellow
|
|
101
|
+
say ' mysigner --local-only ship appstore', :yellow
|
|
102
|
+
say ' (auto-discovers ASC .p8 from ~/.appstoreconnect/private_keys/,', :yellow
|
|
103
|
+
say ' Google Play SA-JSON from GOOGLE_APPLICATION_CREDENTIALS / eas.json,', :yellow
|
|
104
|
+
say ' keystore from key.properties / eas.json — or set them via flags / env.)', :yellow
|
|
105
|
+
say ' See "Local-only mode" section in README.', :yellow
|
|
85
106
|
exit 1
|
|
86
107
|
end
|
|
87
108
|
|
|
@@ -90,6 +111,12 @@ module Mysigner
|
|
|
90
111
|
end
|
|
91
112
|
|
|
92
113
|
def create_client(config)
|
|
114
|
+
# mysigner-22 — in local-only mode every MySigner API touchpoint is
|
|
115
|
+
# supposed to be bypassed at the call site. Returning nil here makes
|
|
116
|
+
# an accidental `client.get(...)` fail loud (NoMethodError on nil)
|
|
117
|
+
# rather than silently re-introducing a server hit.
|
|
118
|
+
return nil if local_only?
|
|
119
|
+
|
|
93
120
|
Client.new(
|
|
94
121
|
api_url: config.api_url,
|
|
95
122
|
api_token: config.api_token,
|
|
@@ -97,9 +124,117 @@ module Mysigner
|
|
|
97
124
|
)
|
|
98
125
|
end
|
|
99
126
|
|
|
127
|
+
# mysigner-22 — a Config built without touching disk, ENV, or the
|
|
128
|
+
# encryption key. We deliberately bypass Config#initialize (which
|
|
129
|
+
# auto-loads ~/.mysigner/config.yml when it exists) because the whole
|
|
130
|
+
# point of local-only is to work on a machine where that file might
|
|
131
|
+
# not exist or might be unreadable (e.g. broken Keychain key).
|
|
132
|
+
# Callers only read `current_organization_id` / `api_url` / etc., all
|
|
133
|
+
# of which legitimately return nil here.
|
|
134
|
+
def blank_local_only_config
|
|
135
|
+
config = Config.allocate
|
|
136
|
+
config.instance_variable_set(:@api_url, nil)
|
|
137
|
+
config.instance_variable_set(:@user_email, nil)
|
|
138
|
+
config.instance_variable_set(:@current_organization_id, nil)
|
|
139
|
+
config.instance_variable_set(:@organizations, {})
|
|
140
|
+
config.instance_variable_set(:@encryption_enabled, false)
|
|
141
|
+
config.instance_variable_set(:@from_env, false)
|
|
142
|
+
config
|
|
143
|
+
end
|
|
144
|
+
|
|
100
145
|
def error(message)
|
|
101
146
|
say "✗ Error: #{message}", :red
|
|
102
147
|
end
|
|
148
|
+
|
|
149
|
+
# Local-only mode is active if either the --local-only flag is set
|
|
150
|
+
# on this invocation OR MYSIGNER_LOCAL_ONLY is truthy in ENV.
|
|
151
|
+
# Subsequent tickets gate credential-sending behavior on this.
|
|
152
|
+
def local_only?
|
|
153
|
+
options[:local_only] || Mysigner::Config.local_only?
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# mysigner-22 Phase 5 — resolve ASC creds via the cascade (flag →
|
|
157
|
+
# env → keychain → ~/.appstoreconnect → prompt), surface a clear
|
|
158
|
+
# error and exit 1 on miss. Logs the winning source on stderr so
|
|
159
|
+
# CI runs leave an audit trail of where the credential came from.
|
|
160
|
+
# Returns a Mysigner::CredentialResolver::AscCreds struct.
|
|
161
|
+
def resolve_local_asc_creds_or_exit
|
|
162
|
+
require 'mysigner/credential_resolver'
|
|
163
|
+
creds = Mysigner::CredentialResolver.resolve_asc(options: options.to_h)
|
|
164
|
+
warn "[mysigner] ASC credentials source: #{creds.source}" if $stderr.respond_to?(:tty?) && $stderr.tty?
|
|
165
|
+
creds
|
|
166
|
+
rescue Mysigner::CredentialResolver::CredentialNotFoundError,
|
|
167
|
+
Mysigner::CredentialResolver::AmbiguousCredentialsError => e
|
|
168
|
+
# Preserve the historical wording the CLI specs were written
|
|
169
|
+
# against ("No local ASC credentials found") so users and tests
|
|
170
|
+
# have a stable identifier, while including the resolver's
|
|
171
|
+
# multi-line cascade trace + override knob list right after it.
|
|
172
|
+
say "✗ No local ASC credentials found via `mysigner onboard --local-only` or other sources:\n#{e.message}",
|
|
173
|
+
:red
|
|
174
|
+
exit 1
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# mysigner-22 Phase 5 — Google Play counterpart of
|
|
178
|
+
# resolve_local_asc_creds_or_exit. Same semantics: pre-resolve so the
|
|
179
|
+
# CLI can fail fast before the build kicks off.
|
|
180
|
+
def resolve_local_play_creds_or_exit
|
|
181
|
+
require 'mysigner/credential_resolver'
|
|
182
|
+
creds = Mysigner::CredentialResolver.resolve_play(options: options.to_h)
|
|
183
|
+
warn "[mysigner] Google Play credentials source: #{creds.source}" if $stderr.respond_to?(:tty?) && $stderr.tty?
|
|
184
|
+
creds
|
|
185
|
+
rescue Mysigner::CredentialResolver::CredentialNotFoundError,
|
|
186
|
+
Mysigner::CredentialResolver::AmbiguousCredentialsError => e
|
|
187
|
+
say "✗ No local Google Play credentials found via `mysigner onboard --local-only` or other sources:\n#{e.message}",
|
|
188
|
+
:red
|
|
189
|
+
exit 1
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# mysigner-22 Phase 7 — Android keystore counterpart of the ASC/Play
|
|
193
|
+
# resolvers. Pre-resolves the keystore (path + passwords + alias) via
|
|
194
|
+
# the cascade so `ship android --local-only` can skip the MySigner
|
|
195
|
+
# server entirely.
|
|
196
|
+
def resolve_local_android_keystore_or_exit
|
|
197
|
+
require 'mysigner/credential_resolver'
|
|
198
|
+
creds = Mysigner::CredentialResolver.resolve_android_keystore(options: options.to_h)
|
|
199
|
+
warn "[mysigner] Android keystore source: #{creds.source}" if $stderr.respond_to?(:tty?) && $stderr.tty?
|
|
200
|
+
creds
|
|
201
|
+
rescue Mysigner::CredentialResolver::CredentialNotFoundError,
|
|
202
|
+
Mysigner::CredentialResolver::AmbiguousCredentialsError => e
|
|
203
|
+
say "✗ No local Android keystore found:\n#{e.message}", :red
|
|
204
|
+
exit 1
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# One-time banner on stderr (TTY only) so users know they've opted
|
|
208
|
+
# into local-only mode. Module-level guard ensures it fires at most
|
|
209
|
+
# once per CLI invocation, even if multiple commands call it.
|
|
210
|
+
# (Module instance var, not @@, to avoid the class-var smell — every
|
|
211
|
+
# instance method sees the same Helpers module object.)
|
|
212
|
+
def emit_local_only_banner
|
|
213
|
+
return if Helpers.banner_emitted?
|
|
214
|
+
return unless $stderr.respond_to?(:tty?) && $stderr.tty?
|
|
215
|
+
|
|
216
|
+
Helpers.mark_banner_emitted!
|
|
217
|
+
# Honest scope: credential transport is what local-only currently
|
|
218
|
+
# guards. Non-credential MySigner endpoints (app/build registry,
|
|
219
|
+
# keystore download, etc.) are still used. Documented in the
|
|
220
|
+
# local-only docs (mysigner-45).
|
|
221
|
+
warn '[mysigner] local-only mode active — signing credentials stay on this machine ' \
|
|
222
|
+
'(other MySigner APIs may still be used; see docs).'
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
class << self
|
|
226
|
+
def banner_emitted?
|
|
227
|
+
@local_only_banner_emitted == true
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def mark_banner_emitted!
|
|
231
|
+
@local_only_banner_emitted = true
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def reset_banner!
|
|
235
|
+
@local_only_banner_emitted = false
|
|
236
|
+
end
|
|
237
|
+
end
|
|
103
238
|
end
|
|
104
239
|
end
|
|
105
240
|
end
|
data/lib/mysigner/cli.rb
CHANGED
|
@@ -19,13 +19,14 @@ require_relative 'cleanup/private_keys_purger'
|
|
|
19
19
|
# Phase 0: one-time cleanup of legacy plaintext .p8 files that older CLI
|
|
20
20
|
# versions wrote to ~/.private_keys/ and ~/.appstoreconnect/private_keys/.
|
|
21
21
|
# Idempotent — a marker file at ~/.mysigner/.private_keys_purged prevents
|
|
22
|
-
# re-running.
|
|
23
|
-
# back into the legacy altool path keep their existing keys.
|
|
22
|
+
# re-running.
|
|
24
23
|
Mysigner::Cleanup::PrivateKeysPurger.new.call
|
|
25
24
|
|
|
26
25
|
module Mysigner
|
|
27
26
|
class CLI < Thor
|
|
28
27
|
class_option :verbose, type: :boolean, aliases: '-v', desc: 'Verbose output'
|
|
28
|
+
class_option :local_only, type: :boolean, default: false,
|
|
29
|
+
desc: 'Do not send credentials to the server (local-only mode)'
|
|
29
30
|
|
|
30
31
|
def self.exit_on_failure?
|
|
31
32
|
true
|
data/lib/mysigner/config.rb
CHANGED
|
@@ -23,6 +23,7 @@ module Mysigner
|
|
|
23
23
|
ENV_API_URL = 'MYSIGNER_API_URL'
|
|
24
24
|
ENV_EMAIL = 'MYSIGNER_EMAIL'
|
|
25
25
|
ENV_ORG_ID = 'MYSIGNER_ORG_ID'
|
|
26
|
+
ENV_LOCAL_ONLY = 'MYSIGNER_LOCAL_ONLY'
|
|
26
27
|
|
|
27
28
|
attr_accessor :api_url, :user_email, :current_organization_id, :encryption_enabled
|
|
28
29
|
attr_reader :organizations
|
|
@@ -66,6 +67,17 @@ module Mysigner
|
|
|
66
67
|
@from_env
|
|
67
68
|
end
|
|
68
69
|
|
|
70
|
+
# Local-only mode: when true, credentials never leave the machine.
|
|
71
|
+
# Config-level check sees only ENV — Thor's --local-only flag is layered
|
|
72
|
+
# on top in the CLI Helpers concern (which can read `options`).
|
|
73
|
+
def self.local_only?
|
|
74
|
+
truthy_env?(ENV_LOCAL_ONLY)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def local_only?
|
|
78
|
+
self.class.local_only?
|
|
79
|
+
end
|
|
80
|
+
|
|
69
81
|
# Get API token for current organization (or specific org)
|
|
70
82
|
def api_token(org_id = nil)
|
|
71
83
|
org_id ||= @current_organization_id
|
|
@@ -121,7 +133,15 @@ module Mysigner
|
|
|
121
133
|
def load
|
|
122
134
|
return false unless exists?
|
|
123
135
|
|
|
124
|
-
|
|
136
|
+
# mysigner-51 — safe_load_file rejects arbitrary Ruby object
|
|
137
|
+
# 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, token}}),
|
|
140
|
+
# all in safe_load's default allowed set, so no permitted_classes
|
|
141
|
+
# extension is needed. Low risk (the file is 0600 and user-owned)
|
|
142
|
+
# but cheap hardening against a future RCE if config write or read
|
|
143
|
+
# ever moves outside the owner-only assumption.
|
|
144
|
+
data = YAML.safe_load_file(CONFIG_FILE)
|
|
125
145
|
|
|
126
146
|
@api_url = data['api_url']
|
|
127
147
|
@user_email = data['user_email']
|
|
@@ -262,6 +282,25 @@ module Mysigner
|
|
|
262
282
|
@organizations.values.any? { |org_data| encrypted?(org_data['token']) }
|
|
263
283
|
end
|
|
264
284
|
|
|
285
|
+
# Public accessor for the per-machine 32-byte AES-256-GCM key.
|
|
286
|
+
# Exposed so sibling stores (e.g. LocalCredentials) can encrypt secrets
|
|
287
|
+
# under the same key without duplicating the keychain/file fallback.
|
|
288
|
+
# The key itself is created on first read and is stable across calls.
|
|
289
|
+
def fetch_encryption_key
|
|
290
|
+
get_or_create_encryption_key
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def self.truthy_env?(name)
|
|
294
|
+
raw = ENV.fetch(name, nil)
|
|
295
|
+
return false if raw.nil?
|
|
296
|
+
|
|
297
|
+
value = raw.strip
|
|
298
|
+
return false if value.empty?
|
|
299
|
+
|
|
300
|
+
value.match?(/\A(1|true|yes)\z/i)
|
|
301
|
+
end
|
|
302
|
+
private_class_method :truthy_env?
|
|
303
|
+
|
|
265
304
|
private
|
|
266
305
|
|
|
267
306
|
def ensure_config_dir_exists
|