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
|
@@ -210,6 +210,16 @@ module Mysigner
|
|
|
210
210
|
4. Configure your CLI
|
|
211
211
|
DESC
|
|
212
212
|
def onboard
|
|
213
|
+
# mysigner-44 — local-only mode short-circuits the server-mediated
|
|
214
|
+
# onboarding entirely. We never POST credentials and never ask the
|
|
215
|
+
# user for an API token; everything is captured into the local
|
|
216
|
+
# Keychain-backed store. The server-mediated path below is left
|
|
217
|
+
# untouched for backward compatibility.
|
|
218
|
+
if local_only?
|
|
219
|
+
emit_local_only_banner
|
|
220
|
+
return onboard_local_only
|
|
221
|
+
end
|
|
222
|
+
|
|
213
223
|
say '🚀 My Signer Setup Guide', :cyan
|
|
214
224
|
say '=' * 80, :cyan
|
|
215
225
|
say ''
|
|
@@ -595,6 +605,27 @@ module Mysigner
|
|
|
595
605
|
end
|
|
596
606
|
|
|
597
607
|
desc 'logout', 'Log out and clear stored credentials'
|
|
608
|
+
long_desc <<~DESC
|
|
609
|
+
Log out of MySigner. Always clears local CLI config.
|
|
610
|
+
|
|
611
|
+
By default, ALSO asks whether to delete your stored credentials
|
|
612
|
+
(App Store Connect .p8 keys, Apple Search Ads keys, Google Play
|
|
613
|
+
service-account JSON, Android keystores) on the server and in
|
|
614
|
+
your local Keychain. The prompt defaults to No — the safer
|
|
615
|
+
choice — because logging out and back in restores access to
|
|
616
|
+
them otherwise.
|
|
617
|
+
|
|
618
|
+
--purge Skip the prompt and DELETE the credentials.
|
|
619
|
+
--no-purge Skip the prompt and KEEP them on the server.
|
|
620
|
+
|
|
621
|
+
In non-interactive contexts (CI, pipes), the prompt defaults
|
|
622
|
+
to No as well, matching `yes_with_default?` elsewhere.
|
|
623
|
+
|
|
624
|
+
See docs/policy/credential-retention.md (server repo) for the
|
|
625
|
+
authoritative retention policy.
|
|
626
|
+
DESC
|
|
627
|
+
method_option :purge, type: :boolean, default: nil,
|
|
628
|
+
desc: 'Also delete stored credentials on the server and in local Keychain (skips prompt)'
|
|
598
629
|
def logout
|
|
599
630
|
config = Config.new
|
|
600
631
|
|
|
@@ -603,13 +634,44 @@ module Mysigner
|
|
|
603
634
|
return
|
|
604
635
|
end
|
|
605
636
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
637
|
+
# Preserve the existing "Are you sure?" gate. Tests pin this exact
|
|
638
|
+
# prompt and a No answer must abort the entire logout. With
|
|
639
|
+
# --purge / --no-purge a user has already declared intent, but
|
|
640
|
+
# we still require the top-level confirmation when interactive —
|
|
641
|
+
# less surprising than two layered changes in one release.
|
|
642
|
+
unless yes?('Are you sure you want to logout? (y/n)')
|
|
611
643
|
say 'Logout cancelled', :yellow
|
|
644
|
+
return
|
|
645
|
+
end
|
|
646
|
+
|
|
647
|
+
# Now we know the local logout is happening. Decide whether to
|
|
648
|
+
# ALSO purge server + local-Keychain credentials. Resolution
|
|
649
|
+
# order: explicit flag > interactive prompt > non-TTY default No.
|
|
650
|
+
should_purge = resolve_purge_decision(options[:purge])
|
|
651
|
+
|
|
652
|
+
if should_purge
|
|
653
|
+
# Load the config so we have the api_url/token/email needed
|
|
654
|
+
# for the server call. Failure here is loud — we don't fall
|
|
655
|
+
# back to "local-only clear" silently, that would leave the
|
|
656
|
+
# user's server credentials on disk against their explicit
|
|
657
|
+
# request.
|
|
658
|
+
begin
|
|
659
|
+
config.load
|
|
660
|
+
purge_server_credentials(config)
|
|
661
|
+
purge_local_credentials
|
|
662
|
+
rescue Mysigner::ClientError, Mysigner::ConfigError => e
|
|
663
|
+
error "Failed to purge credentials on the server: #{e.message}"
|
|
664
|
+
say ''
|
|
665
|
+
say 'Local config was NOT cleared so you can retry. Options:', :yellow
|
|
666
|
+
say " • Re-run 'mysigner logout --purge' once the server is reachable", :yellow
|
|
667
|
+
say " • Run 'mysigner logout --no-purge' to log out locally only", :yellow
|
|
668
|
+
exit 1
|
|
669
|
+
end
|
|
612
670
|
end
|
|
671
|
+
|
|
672
|
+
config.clear
|
|
673
|
+
say '✓ Successfully logged out', :green
|
|
674
|
+
say "Config file removed: #{Config::CONFIG_FILE}", :green
|
|
613
675
|
end
|
|
614
676
|
|
|
615
677
|
desc 'status', 'Check connection, credentials, and App Store Connect setup'
|
|
@@ -971,6 +1033,93 @@ module Mysigner
|
|
|
971
1033
|
response.empty? || response == 'y' || response == 'yes'
|
|
972
1034
|
end
|
|
973
1035
|
|
|
1036
|
+
# Default-NO variant of yes_with_default? — used when the
|
|
1037
|
+
# destructive answer must be opt-in (mysigner-47 logout purge).
|
|
1038
|
+
# Non-TTY also defaults to No so CI never silently wipes server
|
|
1039
|
+
# credentials. Only an explicit "y" or "yes" returns true.
|
|
1040
|
+
def no_default_yes?(statement, color = nil)
|
|
1041
|
+
unless $stdin.tty?
|
|
1042
|
+
say "#{statement} [y/N] (non-interactive: assuming no)", color
|
|
1043
|
+
return false
|
|
1044
|
+
end
|
|
1045
|
+
response = ask("#{statement} [y/N]", color).to_s.strip.downcase
|
|
1046
|
+
%w[y yes].include?(response)
|
|
1047
|
+
end
|
|
1048
|
+
|
|
1049
|
+
# mysigner-47 — resolve the purge decision for `mysigner logout`.
|
|
1050
|
+
# `flag` is options[:purge] (true / false / nil).
|
|
1051
|
+
# true → --purge passed; skip prompt, purge
|
|
1052
|
+
# false → --no-purge passed; skip prompt, keep
|
|
1053
|
+
# nil → no flag; ask the user (default No), CI defaults to No
|
|
1054
|
+
def resolve_purge_decision(flag)
|
|
1055
|
+
return flag unless flag.nil?
|
|
1056
|
+
|
|
1057
|
+
no_default_yes?(
|
|
1058
|
+
'Also delete your stored credentials on the server? ' \
|
|
1059
|
+
'They will be gone forever.',
|
|
1060
|
+
:yellow
|
|
1061
|
+
)
|
|
1062
|
+
end
|
|
1063
|
+
|
|
1064
|
+
# mysigner-47 — call DELETE /api/v1/organizations/:org/credentials
|
|
1065
|
+
# using the loaded config. Surfaces per-kind counts on success.
|
|
1066
|
+
# Raises Mysigner::ClientError on transport/auth failure so the
|
|
1067
|
+
# caller can decide whether to abort the local clear.
|
|
1068
|
+
def purge_server_credentials(config)
|
|
1069
|
+
org_id = config.current_organization_id
|
|
1070
|
+
if org_id.nil?
|
|
1071
|
+
say '⚠️ No active organization in local config; skipping server purge.', :yellow
|
|
1072
|
+
return
|
|
1073
|
+
end
|
|
1074
|
+
|
|
1075
|
+
client = Client.new(
|
|
1076
|
+
api_url: config.api_url,
|
|
1077
|
+
api_token: config.api_token,
|
|
1078
|
+
user_email: config.user_email
|
|
1079
|
+
)
|
|
1080
|
+
|
|
1081
|
+
say 'Deleting stored credentials on the server...', :yellow
|
|
1082
|
+
response = client.delete("/api/v1/organizations/#{org_id}/credentials")
|
|
1083
|
+
|
|
1084
|
+
deleted = response.dig(:data, 'deleted') || {}
|
|
1085
|
+
asc = deleted['asc'].to_i
|
|
1086
|
+
ads = deleted['apple_ads'].to_i
|
|
1087
|
+
gp = deleted['google_play'].to_i
|
|
1088
|
+
ks = deleted['android_keystore'].to_i
|
|
1089
|
+
|
|
1090
|
+
say '✓ Server credentials deleted:', :green
|
|
1091
|
+
say " • App Store Connect: #{asc}"
|
|
1092
|
+
say " • Apple Search Ads: #{ads}"
|
|
1093
|
+
say " • Google Play: #{gp}"
|
|
1094
|
+
say " • Android keystore: #{ks}"
|
|
1095
|
+
end
|
|
1096
|
+
|
|
1097
|
+
# mysigner-47 — wipe every locally stored credential the
|
|
1098
|
+
# LocalCredentials store knows about, across all four kinds.
|
|
1099
|
+
# We swallow per-entry deletion errors with a loud log line
|
|
1100
|
+
# rather than aborting (Rule 12 — fail loud, but a corrupted
|
|
1101
|
+
# Keychain entry must not block the rest of the wipe).
|
|
1102
|
+
def purge_local_credentials
|
|
1103
|
+
return unless defined?(Mysigner::LocalCredentials)
|
|
1104
|
+
|
|
1105
|
+
total = 0
|
|
1106
|
+
Mysigner::LocalCredentials::KINDS.each do |kind|
|
|
1107
|
+
ids = Mysigner::LocalCredentials.list(kind: kind)
|
|
1108
|
+
ids.each do |id|
|
|
1109
|
+
Mysigner::LocalCredentials.delete(kind: kind, id: id)
|
|
1110
|
+
total += 1
|
|
1111
|
+
rescue Mysigner::LocalCredentials::LocalCredentialsError => e
|
|
1112
|
+
say "⚠️ Failed to delete local credential #{kind}/#{id}: #{e.message}", :yellow
|
|
1113
|
+
end
|
|
1114
|
+
end
|
|
1115
|
+
|
|
1116
|
+
if total.positive?
|
|
1117
|
+
say "✓ Local Keychain / file credentials deleted: #{total}", :green
|
|
1118
|
+
else
|
|
1119
|
+
say 'No local-only credentials to delete.', :white
|
|
1120
|
+
end
|
|
1121
|
+
end
|
|
1122
|
+
|
|
974
1123
|
# Helper method for App Store Connect credential setup
|
|
975
1124
|
# Returns true if successfully configured, false otherwise
|
|
976
1125
|
def setup_app_store_connect_credentials(client, _config, org_id)
|
|
@@ -1380,9 +1529,210 @@ module Mysigner
|
|
|
1380
1529
|
false
|
|
1381
1530
|
end
|
|
1382
1531
|
end
|
|
1532
|
+
|
|
1533
|
+
# mysigner-44 — local-only onboarding. Captures ASC and/or Google
|
|
1534
|
+
# Play credentials and persists them via LocalCredentials (Keychain
|
|
1535
|
+
# on macOS, encrypted file fallback elsewhere). Never calls the
|
|
1536
|
+
# server. Raises LocalOnlyOnboardError on invalid input so callers
|
|
1537
|
+
# see the failure (Rule 12 — fail loud).
|
|
1538
|
+
def onboard_local_only
|
|
1539
|
+
say '🚀 My Signer Setup (local-only)', :cyan
|
|
1540
|
+
say '=' * 80, :cyan
|
|
1541
|
+
say ''
|
|
1542
|
+
say 'Local-only mode: credentials stay on this machine.', :bold
|
|
1543
|
+
say ''
|
|
1544
|
+
|
|
1545
|
+
# mysigner-22 Phase 5 — discovery first. If the user already
|
|
1546
|
+
# has credentials elsewhere (env vars, ~/.appstoreconnect, a
|
|
1547
|
+
# service-account JSON at the project root, GOOGLE_APPLICATION_
|
|
1548
|
+
# CREDENTIALS) we tell them they don't need to onboard for that
|
|
1549
|
+
# platform. We avoid prompting on discovery itself by using a
|
|
1550
|
+
# non-TTY stdin proxy, so the resolver fails fast on miss
|
|
1551
|
+
# instead of blocking the user.
|
|
1552
|
+
asc_already, asc_hint = discover_local_asc_silently
|
|
1553
|
+
play_already, play_hint = discover_local_play_silently
|
|
1554
|
+
|
|
1555
|
+
if asc_already
|
|
1556
|
+
say "✓ Detected App Store Connect credentials (#{asc_hint}). No onboarding needed.", :green
|
|
1557
|
+
say ''
|
|
1558
|
+
end
|
|
1559
|
+
if play_already
|
|
1560
|
+
say "✓ Detected Google Play credentials (#{play_hint}). No onboarding needed.", :green
|
|
1561
|
+
say ''
|
|
1562
|
+
end
|
|
1563
|
+
|
|
1564
|
+
stored_asc = []
|
|
1565
|
+
stored_play = []
|
|
1566
|
+
|
|
1567
|
+
if !asc_already && yes_with_default?('Set up App Store Connect credentials now?', :cyan)
|
|
1568
|
+
say ''
|
|
1569
|
+
stored_asc = collect_local_asc_credential
|
|
1570
|
+
end
|
|
1571
|
+
say ''
|
|
1572
|
+
|
|
1573
|
+
if !play_already && yes_with_default?('Set up Google Play credentials now?', :cyan)
|
|
1574
|
+
say ''
|
|
1575
|
+
stored_play = collect_local_google_play_credential
|
|
1576
|
+
end
|
|
1577
|
+
|
|
1578
|
+
say ''
|
|
1579
|
+
say '=' * 80, :green
|
|
1580
|
+
say '✓ Local-only setup complete.', :green
|
|
1581
|
+
say '=' * 80, :green
|
|
1582
|
+
say ''
|
|
1583
|
+
if stored_asc.empty? && stored_play.empty?
|
|
1584
|
+
say 'No credentials were stored.', :yellow
|
|
1585
|
+
say "Re-run 'mysigner --local-only onboard' when you're ready.", :yellow
|
|
1586
|
+
else
|
|
1587
|
+
say 'Stored credentials:', :cyan
|
|
1588
|
+
stored_asc.each { |id| say " • ASC key: #{id}" }
|
|
1589
|
+
stored_play.each { |id| say " • Google Play SA: #{id}" }
|
|
1590
|
+
say ''
|
|
1591
|
+
say 'To ship:', :bold
|
|
1592
|
+
say ' mysigner --local-only ship appstore' unless stored_asc.empty?
|
|
1593
|
+
say ' mysigner --local-only ship play' unless stored_play.empty?
|
|
1594
|
+
end
|
|
1595
|
+
say ''
|
|
1596
|
+
end
|
|
1597
|
+
|
|
1598
|
+
# Returns Array<String> of ids actually stored (empty on skip).
|
|
1599
|
+
def collect_local_asc_credential
|
|
1600
|
+
say '📱 App Store Connect (local-only)', :cyan
|
|
1601
|
+
say ''
|
|
1602
|
+
|
|
1603
|
+
p8_path = ask('Path to your .p8 private key:').to_s.strip.gsub(/^['"]|['"]$/, '')
|
|
1604
|
+
p8_path = File.expand_path(p8_path)
|
|
1605
|
+
raise_local_onboard_error!(".p8 file not found: #{p8_path}") unless File.exist?(p8_path)
|
|
1606
|
+
|
|
1607
|
+
p8_pem = File.read(p8_path)
|
|
1608
|
+
validate_p8_pem!(p8_pem)
|
|
1609
|
+
|
|
1610
|
+
# Auto-detect Key ID from filename (AuthKey_ABC123.p8 → ABC123).
|
|
1611
|
+
key_id = nil
|
|
1612
|
+
if File.basename(p8_path) =~ /AuthKey_([A-Z0-9]+)\.p8/i
|
|
1613
|
+
key_id = ::Regexp.last_match(1)
|
|
1614
|
+
say "✓ Auto-detected Key ID: #{key_id}", :green
|
|
1615
|
+
end
|
|
1616
|
+
key_id = ask('Enter your Key ID (e.g., ABC12345):').to_s.strip if key_id.nil? || key_id.empty?
|
|
1617
|
+
raise_local_onboard_error!('Key ID cannot be empty') if key_id.empty?
|
|
1618
|
+
|
|
1619
|
+
issuer_id = ask('Enter your Issuer ID (UUID):').to_s.strip
|
|
1620
|
+
raise_local_onboard_error!('Issuer ID cannot be empty') if issuer_id.empty?
|
|
1621
|
+
|
|
1622
|
+
# Storage shape matches mysigner-42's Option A: id == key_id,
|
|
1623
|
+
# secret is a JSON envelope so AscJwtMinter can reconstruct
|
|
1624
|
+
# (key_id, issuer_id, p8_pem) from one lookup.
|
|
1625
|
+
secret = JSON.generate('issuer_id' => issuer_id, 'p8_pem' => p8_pem)
|
|
1626
|
+
Mysigner::LocalCredentials.store(kind: :asc, id: key_id, secret: secret)
|
|
1627
|
+
|
|
1628
|
+
say '✓ ASC credential stored locally.', :green
|
|
1629
|
+
[key_id]
|
|
1630
|
+
end
|
|
1631
|
+
|
|
1632
|
+
# Returns Array<String> of ids actually stored (empty on skip).
|
|
1633
|
+
def collect_local_google_play_credential
|
|
1634
|
+
say '🤖 Google Play (local-only)', :cyan
|
|
1635
|
+
say ''
|
|
1636
|
+
|
|
1637
|
+
json_path = ask('Path to your service-account JSON:').to_s.strip.gsub(/^['"]|['"]$/, '')
|
|
1638
|
+
json_path = File.expand_path(json_path)
|
|
1639
|
+
raise_local_onboard_error!("SA-JSON file not found: #{json_path}") unless File.exist?(json_path)
|
|
1640
|
+
|
|
1641
|
+
raw = File.read(json_path)
|
|
1642
|
+
parsed = validate_sa_json!(raw)
|
|
1643
|
+
client_email = parsed['client_email']
|
|
1644
|
+
|
|
1645
|
+
Mysigner::LocalCredentials.store(kind: :google_play, id: client_email, secret: raw)
|
|
1646
|
+
|
|
1647
|
+
say "✓ Google Play credential stored locally (#{client_email}).", :green
|
|
1648
|
+
[client_email]
|
|
1649
|
+
end
|
|
1650
|
+
|
|
1651
|
+
# Verifies the file looks like an EC private key in the form
|
|
1652
|
+
# AscJwtMinter requires. Fails loud — any malformed input raises
|
|
1653
|
+
# before we touch the Keychain.
|
|
1654
|
+
def validate_p8_pem!(pem)
|
|
1655
|
+
key = OpenSSL::PKey.read(pem.to_s)
|
|
1656
|
+
return if key.is_a?(OpenSSL::PKey::EC)
|
|
1657
|
+
|
|
1658
|
+
raise_local_onboard_error!("invalid .p8: expected EC private key, got #{key.class}")
|
|
1659
|
+
rescue OpenSSL::PKey::PKeyError => e
|
|
1660
|
+
raise_local_onboard_error!("invalid .p8: #{e.message}")
|
|
1661
|
+
end
|
|
1662
|
+
|
|
1663
|
+
# Returns the parsed hash. Validates the three fields the
|
|
1664
|
+
# GoogleOauthMinter (and the SA JWT spec) actually need.
|
|
1665
|
+
def validate_sa_json!(raw)
|
|
1666
|
+
parsed = JSON.parse(raw)
|
|
1667
|
+
missing = []
|
|
1668
|
+
missing << "type=='service_account'" unless parsed['type'] == 'service_account'
|
|
1669
|
+
missing << 'client_email' if parsed['client_email'].to_s.empty?
|
|
1670
|
+
missing << 'private_key' if parsed['private_key'].to_s.empty?
|
|
1671
|
+
raise_local_onboard_error!("invalid service-account JSON (missing/wrong: #{missing.join(', ')})") if missing.any?
|
|
1672
|
+
|
|
1673
|
+
parsed
|
|
1674
|
+
rescue JSON::ParserError => e
|
|
1675
|
+
raise_local_onboard_error!("invalid service-account JSON: #{e.message}")
|
|
1676
|
+
end
|
|
1677
|
+
|
|
1678
|
+
def raise_local_onboard_error!(message)
|
|
1679
|
+
raise Mysigner::CLI::LocalOnlyOnboardError, message
|
|
1680
|
+
end
|
|
1681
|
+
|
|
1682
|
+
# mysigner-22 Phase 5 — silent ASC discovery for onboard. Returns
|
|
1683
|
+
# [bool, hint_string]. We feed the resolver a non-TTY stdin proxy
|
|
1684
|
+
# so the prompt tier is OFF — discovery either resolves from a
|
|
1685
|
+
# higher tier or returns false-with-no-hint. Any structural
|
|
1686
|
+
# surprises (Keychain corruption, multiple disk files needing
|
|
1687
|
+
# disambiguation) bubble out as "no, prompt for it" rather than
|
|
1688
|
+
# crashing the onboard flow.
|
|
1689
|
+
def discover_local_asc_silently
|
|
1690
|
+
require 'mysigner/credential_resolver'
|
|
1691
|
+
no_tty = Object.new
|
|
1692
|
+
def no_tty.tty?
|
|
1693
|
+
false
|
|
1694
|
+
end
|
|
1695
|
+
creds = Mysigner::CredentialResolver.resolve_asc(stdin: no_tty, stderr: StringIO.new)
|
|
1696
|
+
hint = case creds.source
|
|
1697
|
+
when :flag then 'from --asc-* flags'
|
|
1698
|
+
when :env then 'from APP_STORE_CONNECT_API_KEY_* env vars'
|
|
1699
|
+
when :keychain then "from Keychain (#{creds.key_id})"
|
|
1700
|
+
when :disk then "from #{Mysigner::CredentialResolver::APPLE_PRIVATE_KEYS_DIR}/AuthKey_#{creds.key_id}.p8"
|
|
1701
|
+
else 'from cascade'
|
|
1702
|
+
end
|
|
1703
|
+
[true, hint]
|
|
1704
|
+
rescue Mysigner::CredentialResolver::CredentialNotFoundError,
|
|
1705
|
+
Mysigner::CredentialResolver::AmbiguousCredentialsError
|
|
1706
|
+
[false, nil]
|
|
1707
|
+
end
|
|
1708
|
+
|
|
1709
|
+
def discover_local_play_silently
|
|
1710
|
+
require 'mysigner/credential_resolver'
|
|
1711
|
+
no_tty = Object.new
|
|
1712
|
+
def no_tty.tty?
|
|
1713
|
+
false
|
|
1714
|
+
end
|
|
1715
|
+
creds = Mysigner::CredentialResolver.resolve_play(stdin: no_tty, stderr: StringIO.new)
|
|
1716
|
+
hint = case creds.source
|
|
1717
|
+
when :flag then 'from --play-credentials flag'
|
|
1718
|
+
when :env then 'from GOOGLE_APPLICATION_CREDENTIALS'
|
|
1719
|
+
when :keychain then "from Keychain (#{creds.client_email})"
|
|
1720
|
+
when :disk then "from project (#{creds.client_email})"
|
|
1721
|
+
else 'from cascade'
|
|
1722
|
+
end
|
|
1723
|
+
[true, hint]
|
|
1724
|
+
rescue Mysigner::CredentialResolver::CredentialNotFoundError,
|
|
1725
|
+
Mysigner::CredentialResolver::AmbiguousCredentialsError
|
|
1726
|
+
[false, nil]
|
|
1727
|
+
end
|
|
1383
1728
|
end
|
|
1384
1729
|
end
|
|
1385
1730
|
end
|
|
1386
1731
|
end
|
|
1732
|
+
|
|
1733
|
+
# Raised by `onboard` in local-only mode when user input is unusable
|
|
1734
|
+
# (missing file, malformed PEM, malformed SA-JSON). Surfaces the failure
|
|
1735
|
+
# to the caller rather than silently writing a broken credential.
|
|
1736
|
+
class LocalOnlyOnboardError < StandardError; end
|
|
1387
1737
|
end
|
|
1388
1738
|
end
|