git-fit 0.10.9 → 0.10.11

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 990b506f035333d622b5112652251ecfb100fe152493439b2573fe002dcf2420
4
- data.tar.gz: 8935f8b3f86d62926666ff6fbd37401b230f4182260421c40b7a2e5ecc9e0d03
3
+ metadata.gz: 49ad8e07b35c5dc5be5cd8e9067fdaa21dc1684a6d5d5bb14ed873243c926de5
4
+ data.tar.gz: 1b4057796536c438ce997cccf15268fa121f5ad28f19f7ba0af30922c374d2c5
5
5
  SHA512:
6
- metadata.gz: 8c5306f042f21e18a8f5761342fd70a8ae13a5660fc03e02496b8dd0288e87d8711cb4bf826fa33d3509ac5497da70730708f40bb24d8cf13157ba93e94f52e6
7
- data.tar.gz: d5cabbcad852447fc7b0c50489de64f83d8c732f849f0c483704f6565244759b841864d09c72bba840cb2cc265192c1b1f3b364b01c306539dd55d31fc3977e9
6
+ metadata.gz: c835f47ca63aebfb8e7493fc522dca3f77768469d8b1b73d5293d882c1ccada07e03db64fae6063c5d2628ee579563e4842e75c6c5d1d2a4163a20801c337ec4
7
+ data.tar.gz: d0858df7943c64dd1ca22089be6eb8bfb09194fe3a2b8124c4b95051315edc095518388e6d76fe9a8d3f5c75b357a0f02cf0a7a9e075b6c9d68bf83a1bf27650
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../sync/garmin_base'
4
+ require 'json'
5
+ require 'fileutils'
6
+
7
+ module GitFit
8
+ module Auth
9
+ module Garmin
10
+ # Raised when MFA is required and no code is available. The login session
11
+ # (pre-auth cookie jar / storage_state) has already been persisted so the
12
+ # user can resume with `--mfa-code` without re-triggering MFA.
13
+ class MfaRequired < GitFit::Sync::AuthError; end
14
+
15
+ # Persists the pre-auth login session created during an interrupted MFA
16
+ # flow. This is a *temporary* session (30-minute TTL matching the MFA code
17
+ # validity window), distinct from the long-lived persistent session cookie
18
+ # (garmin_playwright_session_*.json) that Strategy B keeps after success.
19
+ module LoginSession
20
+ DIR = 'data/cache'
21
+ TTL_SECONDS = 30 * 60
22
+
23
+ module_function
24
+
25
+ def save(domain, state)
26
+ FileUtils.mkdir_p(DIR)
27
+ state['saved_at'] = Time.now.to_f
28
+ File.write(path(domain), JSON.generate(state))
29
+ end
30
+
31
+ def load(domain)
32
+ p = path(domain)
33
+ return nil unless File.exist?(p)
34
+
35
+ state = JSON.parse(File.read(p))
36
+ return nil if (Time.now.to_f - state['saved_at'].to_f) > TTL_SECONDS
37
+
38
+ state
39
+ rescue JSON::ParserError
40
+ nil
41
+ end
42
+
43
+ def clear(domain)
44
+ File.delete(path(domain)) if File.exist?(path(domain))
45
+ end
46
+
47
+ def path(domain)
48
+ File.join(DIR, "garmin_mfa_login_#{domain.tr('.', '_')}.json")
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'di_exchange'
4
+ require_relative 'login_session'
4
5
  require_relative '../../sync/garmin_base'
5
6
  require 'open3'
6
7
  require 'tmpdir'
@@ -36,15 +37,23 @@ module GitFit
36
37
  MOBILE_FINGERPRINT = 'safari18_0'
37
38
  TLS_FINGERPRINTS = %w[safari18_0 safari17_0 chrome120 edge101 chrome124].freeze
38
39
  CURL_BINARY = 'curl-impersonate'
40
+ AUTH_FLAG = '-A'
39
41
 
40
42
  module_function
41
43
 
42
44
  def call(email:, password:, domain: 'garmin.com', mfa_code: nil)
43
45
  check_curl_available!
44
46
 
47
+ code = mfa_code.to_s.strip
48
+ if !code.empty? && (saved = LoginSession.load(domain))
49
+ return resume_login(domain, code, saved)
50
+ end
51
+
45
52
  errors = []
46
53
  begin
47
54
  return finish(mobile_login(domain, email, password, mfa_code), domain)
55
+ rescue MfaRequired
56
+ raise
48
57
  rescue GitFit::Sync::AuthError => e
49
58
  errors << "mobile+cffi: #{e.message}"
50
59
  warn "[garmin_auth] #{errors.last}"
@@ -55,6 +64,8 @@ module GitFit
55
64
 
56
65
  begin
57
66
  return finish(portal_login(domain, email, password, mfa_code), domain)
67
+ rescue MfaRequired
68
+ raise
58
69
  rescue GitFit::Sync::AuthError => e
59
70
  errors << "portal+cffi: #{e.message}"
60
71
  warn "[garmin_auth] #{errors.last}"
@@ -75,11 +86,11 @@ module GitFit
75
86
  def mobile_login(domain, email, password, mfa_code)
76
87
  sso = "https://sso.#{domain}"
77
88
  with_session_files do |jar, body|
78
- mobile_login_session(sso, email, password, mfa_code, jar, body)
89
+ mobile_login_session(sso, domain, email, password, mfa_code, jar, body)
79
90
  end
80
91
  end
81
92
 
82
- def mobile_login_session(sso, email, password, mfa_code, jar, body)
93
+ def mobile_login_session(sso, domain, email, password, mfa_code, jar, body)
83
94
  random_delay('mobile+cffi')
84
95
  signin_url = "#{sso}/mobile/sso/en_US/sign-in"
85
96
  get_params = { clientId: MOBILE_CLIENT_ID, service: MOBILE_SERVICE }
@@ -129,7 +140,8 @@ module GitFit
129
140
  if resp_type == 'MFA_REQUIRED'
130
141
  method = res.dig('customerMfaInfo', 'mfaLastMethodUsed') || 'email'
131
142
  res = handle_mfa(sso, method, login_params, post_headers, mfa_code,
132
- impersonate: MOBILE_FINGERPRINT, jar: jar, output: body)
143
+ impersonate: MOBILE_FINGERPRINT, jar: jar, output: body,
144
+ email: email, domain: domain, service_url: MOBILE_SERVICE)
133
145
  resp_type = res.dig('responseStatus', 'type')
134
146
  end
135
147
 
@@ -145,19 +157,19 @@ module GitFit
145
157
  def portal_login(domain, email, password, mfa_code)
146
158
  sso = "https://sso.#{domain}"
147
159
  TLS_FINGERPRINTS.each do |imp|
148
- result = attempt_portal_fingerprint(sso, imp, email, password, mfa_code)
160
+ result = attempt_portal_fingerprint(sso, domain, imp, email, password, mfa_code)
149
161
  return result if result
150
162
  end
151
163
  raise GitFit::Sync::AuthError, 'portal+cffi: all TLS fingerprints exhausted'
152
164
  end
153
165
 
154
- def attempt_portal_fingerprint(sso, imp, email, password, mfa_code)
166
+ def attempt_portal_fingerprint(sso, domain, imp, email, password, mfa_code)
155
167
  with_session_files do |jar, body|
156
- attempt_portal_in_session(sso, imp, email, password, mfa_code, jar, body)
168
+ attempt_portal_in_session(sso, domain, imp, email, password, mfa_code, jar, body)
157
169
  end
158
170
  end
159
171
 
160
- def attempt_portal_in_session(sso, imp, email, password, mfa_code, jar, body)
172
+ def attempt_portal_in_session(sso, domain, imp, email, password, mfa_code, jar, body)
161
173
  random_delay("portal+cffi/#{imp}")
162
174
  signin_url = "#{sso}/portal/sso/en-US/sign-in"
163
175
  get_params = { clientId: PORTAL_CLIENT_ID, service: PORTAL_SERVICE }
@@ -215,7 +227,8 @@ module GitFit
215
227
  if resp_type == 'MFA_REQUIRED'
216
228
  method = res.dig('customerMfaInfo', 'mfaLastMethodUsed') || 'email'
217
229
  res = handle_mfa(sso, method, login_params, post_headers, mfa_code,
218
- impersonate: imp, jar: jar, output: body)
230
+ impersonate: imp, jar: jar, output: body,
231
+ email: email, domain: domain, service_url: PORTAL_SERVICE)
219
232
  resp_type = res.dig('responseStatus', 'type')
220
233
  end
221
234
 
@@ -230,8 +243,17 @@ module GitFit
230
243
  nil
231
244
  end
232
245
 
233
- def handle_mfa(sso, method, login_params, post_headers, mfa_code, impersonate:, jar:, output:)
234
- code = resolve_mfa_code(mfa_code)
246
+ def handle_mfa(sso, method, login_params, post_headers, mfa_code,
247
+ impersonate:, jar:, output:, email:, domain:, service_url:)
248
+ code = resolve_mfa_code(mfa_code, email)
249
+ if code.nil?
250
+ save_login_session(domain, sso, method, login_params, post_headers, impersonate, jar, service_url)
251
+ raise MfaRequired, mfa_required_message(email)
252
+ end
253
+ verify_mfa(sso, method, login_params, post_headers, code, impersonate:, jar:, output:)
254
+ end
255
+
256
+ def verify_mfa(sso, method, login_params, post_headers, code, impersonate:, jar:, output:)
235
257
  mfa_data = {
236
258
  'mfaMethod' => method,
237
259
  'mfaVerificationCode' => code,
@@ -266,22 +288,47 @@ module GitFit
266
288
  raise GitFit::Sync::AuthError, 'MFA verification failed on all endpoints'
267
289
  end
268
290
 
269
- def resolve_mfa_code(mfa_code)
270
- code = mfa_code
271
- code = ENV['SYNC__GARMIN__MFA_CODE'] if code.nil? || code.to_s.empty?
272
- if (code.nil? || code.to_s.empty?) && $stdin.tty?
273
- $stderr.write('Enter MFA code: ')
274
- $stderr.flush
275
- code = $stdin.gets
291
+ def save_login_session(domain, sso, method, login_params, post_headers, impersonate, jar, service_url)
292
+ LoginSession.save(domain, {
293
+ 'strategy' => 'A',
294
+ 'sso' => sso,
295
+ 'method' => method,
296
+ 'login_params' => login_params,
297
+ 'post_headers' => post_headers,
298
+ 'impersonate' => impersonate,
299
+ 'jar' => File.read(jar),
300
+ 'service_url' => service_url,
301
+ })
302
+ end
303
+
304
+ def resume_login(domain, mfa_code, saved)
305
+ with_session_files do |jar, body|
306
+ File.write(jar, saved['jar'])
307
+ res = verify_mfa(saved['sso'], saved['method'], saved['login_params'],
308
+ saved['post_headers'], mfa_code,
309
+ impersonate: saved['impersonate'], jar: jar, output: body)
310
+ LoginSession.clear(domain)
311
+ finish([res['serviceTicketId'], saved['service_url']], domain)
276
312
  end
277
- if code.nil? || code.to_s.empty?
278
- raise GitFit::Sync::AuthError,
279
- 'MFA required but SYNC__GARMIN__MFA_CODE env var not set and stdin unavailable'
313
+ end
314
+
315
+ def resolve_mfa_code(mfa_code, email)
316
+ code = mfa_code.to_s.strip
317
+ if code.empty?
318
+ warn "[garmin_auth] Garmin sent a verification code to #{email}"
319
+ $stderr.write('Enter the 6-digit code: ') if $stdin.tty?
320
+ code = $stdin.gets.to_s.strip
280
321
  end
281
- code = code.strip
282
- raise GitFit::Sync::AuthError, 'SYNC__GARMIN__MFA_CODE is empty' if code.empty?
322
+ code.empty? ? nil : code
323
+ end
283
324
 
284
- code
325
+ def mfa_required_message(email)
326
+ "MFA required — check your email (#{email}) for the code.\n" \
327
+ " This code is valid for a few minutes.\n\n" \
328
+ " To continue, choose one:\n" \
329
+ " echo \"<CODE>\" | git fit auth garmin #{AUTH_FLAG}\n" \
330
+ " git fit auth garmin #{AUTH_FLAG} --mfa-code <CODE>\n\n" \
331
+ " If the code has expired, request a new one: git fit auth garmin #{AUTH_FLAG}"
285
332
  end
286
333
 
287
334
  def check_curl_available!
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'di_exchange'
4
+ require_relative 'login_session'
4
5
  require_relative '../../sync/garmin_base'
5
6
  require 'json'
6
7
  require 'fileutils'
@@ -19,16 +20,20 @@ module GitFit
19
20
  LOGIN_DELAY_MAX = 45.0
20
21
  SESSION_CACHE_DIR = 'data/cache'
21
22
  REDIRECT_STATUSES = [301, 302, 303, 307, 308].freeze
23
+ AUTH_FLAG = '-B'
22
24
 
23
25
  module_function
24
26
 
25
27
  def call(email:, password:, domain: 'garmin.com', mfa_code: nil)
26
28
  mfa_code = mfa_code.to_s.strip
27
- mfa_code = ENV['SYNC__GARMIN__MFA_CODE'].to_s.strip if mfa_code.empty?
28
29
  sso = "https://sso.#{domain}"
29
30
  session_path = _session_file_path(domain)
30
31
  chrome_path = _find_chrome
31
32
 
33
+ if !mfa_code.empty? && (saved = LoginSession.load(domain))
34
+ return resume_login(domain, mfa_code, saved, chrome_path)
35
+ end
36
+
32
37
  _launch_playwright(chrome_path) do |browser|
33
38
  session = _load_session(session_path)
34
39
  warn "loaded session cookies (#{session[:age_days].round} days old)" if session
@@ -38,7 +43,7 @@ module GitFit
38
43
  context = browser.new_context(**opts)
39
44
 
40
45
  begin
41
- ticket, service_url = _mobile_api_login(context.request, domain, email, password, mfa_code, sso)
46
+ ticket, service_url = _mobile_api_login(context, domain, email, password, mfa_code, sso)
42
47
  token = DIExchange.call(domain, ticket, service_url)
43
48
  _save_session(context, session_path)
44
49
  token
@@ -48,7 +53,23 @@ module GitFit
48
53
  end
49
54
  end
50
55
 
51
- def _mobile_api_login(context, _domain, email, password, mfa_code, sso)
56
+ def resume_login(domain, mfa_code, saved, chrome_path)
57
+ _launch_playwright(chrome_path) do |browser|
58
+ context = browser.new_context(storageState: saved['storage_state'])
59
+ begin
60
+ ticket, = verify_mfa(context, saved['sso'], saved['method'], mfa_code)
61
+ token = DIExchange.call(domain, ticket, MOBILE_SERVICE)
62
+ _save_session(context, _session_file_path(domain))
63
+ LoginSession.clear(domain)
64
+ token
65
+ ensure
66
+ context.close
67
+ end
68
+ end
69
+ end
70
+
71
+ def _mobile_api_login(context, domain, email, password, mfa_code, sso)
72
+ req = context.request
52
73
  headers = {
53
74
  'User-Agent' => MOBILE_UA,
54
75
  'Accept' => 'application/json, text/plain, */*',
@@ -59,7 +80,7 @@ module GitFit
59
80
  params = { 'clientId' => MOBILE_CLIENT_ID, 'locale' => 'en-US', 'service' => MOBILE_SERVICE }
60
81
  referer = "#{sso}/mobile/sso/en_US/sign-in?clientId=#{MOBILE_CLIENT_ID}&service=#{MOBILE_SERVICE}"
61
82
 
62
- signin_resp = context.get(
83
+ signin_resp = req.get(
63
84
  "#{sso}/mobile/sso/en_US/sign-in",
64
85
  params: { 'clientId' => MOBILE_CLIENT_ID, 'service' => MOBILE_SERVICE },
65
86
  headers: { 'User-Agent' => MOBILE_UA, 'Accept' => 'text/html,...', 'Accept-Language' => 'en-US,en;q=0.9' },
@@ -78,7 +99,7 @@ module GitFit
78
99
 
79
100
  _random_delay('mobile')
80
101
  warn 'posting login credentials...'
81
- resp = context.post(
102
+ resp = req.post(
82
103
  "#{sso}/mobile/api/login",
83
104
  params: params,
84
105
  headers: headers.merge('Referer' => referer),
@@ -102,18 +123,31 @@ module GitFit
102
123
  raise GitFit::Sync::AuthError, "Unexpected login response: #{resp_type}" unless resp_type == 'MFA_REQUIRED'
103
124
 
104
125
  mfa_method = data.dig('customerMfaInfo', 'mfaLastMethodUsed') || 'email'
105
- warn "MFA required (method: #{mfa_method})"
106
-
107
- code = mfa_code.to_s.strip
108
- if code.empty? && $stdin.tty?
109
- warn 'Enter MFA code: '
110
- code = $stdin.gets.to_s.strip
111
- end
112
- if code.empty?
113
- raise GitFit::Sync::AuthError, 'MFA required but no code provided (set SYNC__GARMIN__MFA_CODE or use stdin)'
126
+ code = resolve_mfa_code(email, mfa_code)
127
+ if code.nil?
128
+ LoginSession.save(domain, {
129
+ 'strategy' => 'B',
130
+ 'sso' => sso,
131
+ 'method' => mfa_method,
132
+ 'storage_state' => context.storage_state,
133
+ })
134
+ raise MfaRequired, mfa_required_message(email)
114
135
  end
115
136
 
137
+ verify_mfa(context, sso, mfa_method, code)
138
+ end
139
+
140
+ def verify_mfa(context, sso, mfa_method, code)
116
141
  warn 'verifying MFA code...'
142
+ headers = {
143
+ 'User-Agent' => MOBILE_UA,
144
+ 'Accept' => 'application/json, text/plain, */*',
145
+ 'Content-Type' => 'application/json',
146
+ 'Origin' => sso,
147
+ 'Accept-Language' => 'en-US,en;q=0.9',
148
+ }
149
+ params = { 'clientId' => MOBILE_CLIENT_ID, 'locale' => 'en-US', 'service' => MOBILE_SERVICE }
150
+ referer = "#{sso}/mobile/sso/en_US/sign-in?clientId=#{MOBILE_CLIENT_ID}&service=#{MOBILE_SERVICE}"
117
151
  mfa_data = {
118
152
  'mfaMethod' => mfa_method,
119
153
  'mfaVerificationCode' => code,
@@ -122,7 +156,7 @@ module GitFit
122
156
  'mfaSetup' => false,
123
157
  }
124
158
 
125
- resp = context.post(
159
+ resp = context.request.post(
126
160
  "#{sso}/mobile/api/mfa/verifyCode",
127
161
  params: params,
128
162
  headers: headers.merge('Referer' => referer),
@@ -145,6 +179,25 @@ module GitFit
145
179
  url[/[?&]ticket=(ST-[^&\s]+)/, 1]
146
180
  end
147
181
 
182
+ def resolve_mfa_code(email, mfa_code)
183
+ code = mfa_code.to_s.strip
184
+ if code.empty?
185
+ warn "[garmin_auth_pw] Garmin sent a verification code to #{email}"
186
+ $stderr.write('Enter the 6-digit code: ') if $stdin.tty?
187
+ code = $stdin.gets.to_s.strip
188
+ end
189
+ code.empty? ? nil : code
190
+ end
191
+
192
+ def mfa_required_message(email)
193
+ "MFA required — check your email (#{email}) for the code.\n" \
194
+ " This code is valid for a few minutes.\n\n" \
195
+ " To continue, choose one:\n" \
196
+ " echo \"<CODE>\" | git fit auth garmin #{AUTH_FLAG}\n" \
197
+ " git fit auth garmin #{AUTH_FLAG} --mfa-code <CODE>\n\n" \
198
+ " If the code has expired, request a new one: git fit auth garmin #{AUTH_FLAG}"
199
+ end
200
+
148
201
  def _random_delay(label)
149
202
  delay = rand(LOGIN_DELAY_MIN..LOGIN_DELAY_MAX)
150
203
  warn "[garmin_auth_pw] #{label}: waiting #{delay.round}s for Cloudflare..."
@@ -3,6 +3,7 @@
3
3
  require_relative 'garmin/strategy_a'
4
4
  require_relative 'garmin/strategy_b'
5
5
  require_relative 'garmin/di_exchange'
6
+ require_relative 'garmin/login_session'
6
7
  require_relative '../config'
7
8
  require_relative '../cli/gh_cli'
8
9
  require 'json'
@@ -48,10 +49,10 @@ module GitFit
48
49
  if chosen.size > 1
49
50
  raise GitFit::Sync::AuthError,
50
51
  'git fit auth garmin: -A/-B/--auto/--sync are mutually exclusive — ' \
51
- 'use exactly one (default: -B)'
52
+ 'use exactly one (default: --auto)'
52
53
  end
53
54
 
54
- chosen.first || :B
55
+ chosen.first || :auto
55
56
  end
56
57
 
57
58
  def flag_enabled?(flag)
@@ -61,6 +62,7 @@ module GitFit
61
62
  def sso_token(mode)
62
63
  email, password = sso_credentials
63
64
  creds = { email: email, password: password, domain: domain }
65
+ creds[:mfa_code] = @options[:mfa_code] if mfa_code_provided?
64
66
  case mode
65
67
  when :A then single_strategy(:A, creds)
66
68
  when :B then single_strategy(:B, creds)
@@ -68,6 +70,11 @@ module GitFit
68
70
  end
69
71
  end
70
72
 
73
+ def mfa_code_provided?
74
+ code = @options[:mfa_code]
75
+ !code.nil? && !code.to_s.strip.empty?
76
+ end
77
+
71
78
  def sso_credentials
72
79
  cfg = @config.sync_config('garmin')
73
80
  email = cfg['email'].to_s
@@ -82,7 +89,7 @@ module GitFit
82
89
 
83
90
  def single_strategy(label, creds)
84
91
  token, error = run_strategy(label, creds)
85
- exit_with_error("Garmin auth failed: #{error}") unless token
92
+ exit_with_error("Garmin auth failed: #{error.message}") unless token
86
93
 
87
94
  token
88
95
  end
@@ -91,22 +98,35 @@ module GitFit
91
98
  token, b_error = run_strategy(:B, creds)
92
99
  return token if token
93
100
 
101
+ if b_error.is_a?(MfaRequired)
102
+ # B triggered MFA and saved the login session. Falling back to A
103
+ # would re-trigger a second MFA — stop and ask the user for the code.
104
+ exit_with_error(b_error.message)
105
+ end
106
+
94
107
  warn '[auto] B failed, falling back to A'
95
108
  token, a_error = run_strategy(:A, creds)
96
109
  return token if token
97
110
 
98
- exit_with_error("Garmin auth failed: B: #{b_error}; A: #{a_error}")
111
+ exit_with_error(a_error.message) if a_error.is_a?(MfaRequired)
112
+
113
+ exit_with_error("Garmin auth failed: B: #{b_error.message}; A: #{a_error.message}")
99
114
  end
100
115
 
101
116
  def run_strategy(label, creds)
102
117
  token = STRATEGIES.fetch(label).call(**creds)
103
- return [token, nil] if json_round_trips?(token)
104
-
105
- [nil, "Strategy #{label} returned a token that fails JSON round-trip"]
118
+ return [token, nil] if token && json_round_trips?(token)
119
+
120
+ message = if token.nil?
121
+ "Strategy #{label} failed (no token returned)"
122
+ else
123
+ "Strategy #{label} returned a token that fails JSON round-trip"
124
+ end
125
+ [nil, RuntimeError.new(message)]
106
126
  rescue GitFit::Sync::AuthError => e
107
- [nil, e.message]
127
+ [nil, e]
108
128
  rescue StandardError => e
109
- [nil, "unexpected error: #{e.message}"]
129
+ [nil, e]
110
130
  end
111
131
 
112
132
  def json_round_trips?(token)
data/lib/git_fit/cli.rb CHANGED
@@ -74,9 +74,10 @@ module GitFit
74
74
 
75
75
  desc 'auth SOURCE', 'Authenticate with a sync source (strava, garmin, garmin_cn)'
76
76
  option :A, type: :boolean, desc: 'Garmin Strategy A: curl-impersonate shellout'
77
- option :B, type: :boolean, desc: 'Garmin Strategy B: Playwright (default)'
78
- option :auto, type: :boolean, desc: 'Garmin auto: B first, fallback to A'
77
+ option :B, type: :boolean, desc: 'Garmin Strategy B: Playwright'
78
+ option :auto, type: :boolean, desc: 'Garmin auto: B first, fallback to A (default)'
79
79
  option :sync, type: :boolean, desc: 'Garmin: promote cached token to auth_seed'
80
+ option :mfa_code, type: :string, desc: 'Garmin MFA code (from email), re-run to continue'
80
81
  def auth(source)
81
82
  if source == 'garmin'
82
83
  GitFit::Auth::Garmin.new(options, git_fit_config).call
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GitFit
4
- VERSION = '0.10.9'
4
+ VERSION = '0.10.11'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: git-fit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.9
4
+ version: 0.10.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lax
@@ -248,6 +248,7 @@ files:
248
248
  - lib/git-fit.rb
249
249
  - lib/git_fit/auth/garmin.rb
250
250
  - lib/git_fit/auth/garmin/di_exchange.rb
251
+ - lib/git_fit/auth/garmin/login_session.rb
251
252
  - lib/git_fit/auth/garmin/strategy_a.rb
252
253
  - lib/git_fit/auth/garmin/strategy_b.rb
253
254
  - lib/git_fit/auth/garmin_token.rb