adsedare 0.0.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.
data/lib/adsedare.rb ADDED
@@ -0,0 +1,518 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "xcodeproj"
4
+ require "base64"
5
+ require "fileutils"
6
+ require "tempfile"
7
+ require "plist"
8
+
9
+ require_relative "adsedare/version"
10
+ require_relative "adsedare/capabilities"
11
+ require_relative "logging"
12
+
13
+ require_relative "starship"
14
+ require_relative "appstoreconnect"
15
+
16
+ module Adsedare
17
+ class Error < StandardError; end
18
+
19
+ class << self
20
+ include Logging
21
+
22
+ def renew_profiles(project_path = nil, certificate_id = nil, team_id = nil)
23
+ raise "Project path is not set" unless project_path
24
+ raise "Certificate ID is not set" unless certificate_id
25
+
26
+ project = Xcodeproj::Project.open(project_path)
27
+ project_dir = File.dirname(project_path)
28
+
29
+ bundle_entitlements = {}
30
+
31
+ project.targets.each do |target|
32
+ target.build_configurations.each do |config|
33
+ bundle_identifier = config.build_settings["PRODUCT_BUNDLE_IDENTIFIER"]
34
+ entitlements_path = config.build_settings["CODE_SIGN_ENTITLEMENTS"]
35
+
36
+ # If team_id is not set, use the first one from the project
37
+ team_id ||= config.build_settings["DEVELOPMENT_TEAM"]
38
+
39
+ if entitlements_path
40
+ full_entitlements_path = File.join(project_dir, entitlements_path)
41
+ bundle_entitlements[bundle_identifier] = full_entitlements_path
42
+ end
43
+ end
44
+ end
45
+
46
+ bundle_by_identifier = get_bundle_map(team_id)
47
+ profiles_by_bundle = get_profiles_map(team_id)
48
+
49
+ bundle_entitlements.each do |bundle_identifier, entitlements_path|
50
+ bundle_id = bundle_by_identifier[bundle_identifier]
51
+ unless bundle_id
52
+ logger.warn "Bundle '#{bundle_identifier}' is missing in Apple Developer portal. Will create."
53
+ bundle_id = Starship::Client.create_bundle(
54
+ bundle_identifier,
55
+ team_id,
56
+ # You cannot create bundle without this capability
57
+ [ SimpleCapability.new("IN_APP_PURCHASE").to_bundle_capability(nil, nil) ]
58
+ )["data"]["id"]
59
+ bundle_by_identifier[bundle_identifier] = bundle_id
60
+ logger.info "Bundle '#{bundle_identifier}' created with ID '#{bundle_id}'"
61
+ else
62
+ logger.info "Bundle '#{bundle_identifier}' resolved to Bundle ID '#{bundle_id}'"
63
+ end
64
+
65
+ renew_bundle_id(bundle_id, team_id, entitlements_path)
66
+
67
+ profile = profiles_by_bundle[bundle_id]
68
+ unless profile
69
+ logger.warn "Profile for Bundle ID '#{bundle_id}' is missing in Apple Developer portal. Will create."
70
+ devices = get_devices(team_id)
71
+ profile_id = Starship::Client.create_provisioning_profile(
72
+ team_id,
73
+ bundle_id,
74
+ bundle_identifier,
75
+ certificate_id,
76
+ devices
77
+ )["data"]["id"]
78
+ profiles_by_bundle[bundle_id] = Starship::Client.get_provisioning_profile(profile_id, team_id)
79
+ profile = profiles_by_bundle[bundle_id]
80
+ logger.info "Profile for Bundle ID '#{bundle_id}' created with ID '#{profile_id}'"
81
+ else
82
+ logger.info "Bundle ID '#{bundle_id}' resolved to Profile '#{profile["provisioningProfile"]["name"]}'"
83
+ end
84
+
85
+ renew_provisioning_profile(profile, team_id)
86
+ end
87
+ end
88
+
89
+ def install_profiles(project_path = nil)
90
+ raise "Project path is not set" unless project_path
91
+
92
+ project = Xcodeproj::Project.open(project_path)
93
+
94
+ project_bundles = project.targets.map do |target|
95
+ target.build_configurations.map do |config|
96
+ config.build_settings["PRODUCT_BUNDLE_IDENTIFIER"]
97
+ end
98
+ end.flatten.uniq
99
+
100
+ bundles_with_profiles = AppStoreConnect::Client.get_bundles_with_profiles(project_bundles)
101
+ bundle_by_identifier = {}
102
+ profiles_by_id = {}
103
+
104
+ bundles_with_profiles["data"].each do |bundle_id|
105
+ bundle_by_identifier[bundle_id["attributes"]["identifier"]] = bundle_id
106
+ end
107
+
108
+ bundles_with_profiles["included"].each do |profile|
109
+ profiles_by_id[profile["id"]] = profile
110
+ end
111
+
112
+ project_bundles.each do |bundle_identifier|
113
+ bundle_id = bundle_by_identifier[bundle_identifier]
114
+ unless bundle_id
115
+ logger.warn "Bundle '#{bundle_identifier}' is missing in App Store Connect. Skipping."
116
+ next
117
+ end
118
+
119
+ logger.info "Bundle '#{bundle_identifier}' resolved to Bundle ID '#{bundle_id['id']}'"
120
+
121
+ profiles = bundle_id["relationships"]["profiles"]["data"]
122
+ unless profiles
123
+ logger.warn "Profile for Bundle ID '#{bundle_id['id']}' is missing in App Store Connect. Skipping."
124
+ next
125
+ end
126
+
127
+ ad_hoc_profile = nil
128
+ profiles.each do |profile|
129
+ profile_id = profile["id"]
130
+ profile = profiles_by_id[profile_id]
131
+
132
+ if profile["attributes"]["profileType"] == "IOS_APP_ADHOC" && profile["attributes"]["profileState"] == "ACTIVE"
133
+ ad_hoc_profile = profile
134
+ break
135
+ end
136
+ end
137
+
138
+ unless ad_hoc_profile
139
+ logger.warn "Profile for Bundle ID '#{bundle_id['id']}' is missing in App Store Connect. Skipping."
140
+ next
141
+ end
142
+
143
+ logger.info "Profile for Bundle ID '#{bundle_id['id']}' resolved to Profile '#{ad_hoc_profile['attributes']['name']}'"
144
+
145
+ uuid = ad_hoc_profile["attributes"]["uuid"]
146
+ profile_content = Base64.decode64(ad_hoc_profile["attributes"]["profileContent"])
147
+ profile_path = "#{Dir.home}/Library/MobileDevice/Provisioning Profiles/#{uuid}.mobileprovision"
148
+
149
+ FileUtils.mkdir_p(File.dirname(profile_path))
150
+ File.write(profile_path, profile_content)
151
+
152
+ logger.info "Profile '#{ad_hoc_profile['attributes']['name']}' installed to '#{profile_path}'"
153
+ end
154
+ end
155
+
156
+ def create_keychain(keychain_path = nil, keychain_password = nil, make_default = true)
157
+ raise "Keychain path is not set" unless keychain_path
158
+ raise "Keychain password is not set" unless keychain_password
159
+
160
+ keychain_path = File.expand_path(keychain_path)
161
+
162
+ logger.info "Creating keychain at '#{keychain_path}'"
163
+
164
+ FileUtils.mkdir_p(File.dirname(keychain_path))
165
+ status = system("security create-keychain -p #{keychain_password} #{keychain_path}")
166
+ unless status
167
+ logger.error "Failed to create keychain at '#{keychain_path}'"
168
+ return
169
+ end
170
+
171
+ apple_certs = [
172
+ "AppleWWDRCAG2.cer",
173
+ "AppleWWDRCAG3.cer",
174
+ "AppleWWDRCAG4.cer",
175
+ "AppleWWDRCAG5.cer",
176
+ "AppleWWDRCAG6.cer",
177
+ "AppleWWDRCAG7.cer",
178
+ "AppleWWDRCAG8.cer",
179
+ "DeveloperIDG2CA.cer"
180
+ ]
181
+
182
+ apple_certs.each do |cert|
183
+ logger.info "Downloading certificate '#{cert}'"
184
+
185
+ response = Faraday.get(
186
+ "https://www.apple.com/certificateauthority/#{cert}"
187
+ )
188
+ unless response.status == 200
189
+ logger.error "Failed to download certificate '#{cert}'"
190
+ next
191
+ end
192
+
193
+ file = Tempfile.new(cert)
194
+ file.write(response.body)
195
+ file.close
196
+
197
+ install_certificate(file.path, keychain_path)
198
+
199
+ file.unlink
200
+ end
201
+
202
+ logger.info "Downloading certificate 'AppleWWDRCA.cer'"
203
+ response = Faraday.get(
204
+ "https://developer.apple.com/certificationauthority/AppleWWDRCA.cer"
205
+ )
206
+ unless response.status == 200
207
+ logger.error "Failed to download certificate 'AppleWWDRCA.cer'"
208
+ else
209
+ file = Tempfile.new("AppleWWDRCA.cer")
210
+ file.write(response.body)
211
+ file.close
212
+
213
+ install_certificate(file.path, keychain_path)
214
+
215
+ file.unlink
216
+ end
217
+
218
+ ad_hoc_certificate = ENV["AD_HOC_CERTIFICATE"]
219
+ ad_hoc_private_key = ENV["AD_HOC_PRIVATE_KEY"]
220
+ ad_hoc_key_password = ENV["AD_HOC_KEY_PASSWORD"]
221
+
222
+ unless ad_hoc_certificate || ad_hoc_private_key || ad_hoc_key_password
223
+ logger.warn "AD_HOC_CERTIFICATE, AD_HOC_PRIVATE_KEY, or AD_HOC_KEY_PASSWORD is not set"
224
+ return
225
+ end
226
+
227
+ install_certificate(ad_hoc_private_key, keychain_path, ad_hoc_key_password, "priv")
228
+ install_certificate(ad_hoc_certificate, keychain_path, "", "cert")
229
+
230
+ if make_default
231
+ status = system("security default-keychain -d user -s #{keychain_path}")
232
+ unless status
233
+ logger.warn "Failed to set default keychain"
234
+ return
235
+ end
236
+ end
237
+
238
+ status = system("security set-keychain-settings #{keychain_path}")
239
+ unless status
240
+ logger.error "Failed to set keychain settings"
241
+ return
242
+ end
243
+
244
+ status = system("security set-key-partition-list -S apple-tool:,apple: -k #{keychain_password} #{keychain_path}")
245
+ unless status
246
+ logger.error "Failed to set keychain partition list"
247
+ return
248
+ end
249
+
250
+ status = system("security unlock-keychain -p #{keychain_password} #{keychain_path}")
251
+ unless status
252
+ logger.error "Failed to unlock keychain"
253
+ return
254
+ end
255
+
256
+ logger.info "Keychain created at '#{keychain_path}'"
257
+ end
258
+
259
+ def make_export_options(project_path = nil, export_path = nil, team_id = nil, options = {})
260
+ raise "Project path is not set" unless project_path
261
+ raise "Export path is not set" unless export_path
262
+
263
+ project = Xcodeproj::Project.open(project_path)
264
+ export_options = {
265
+ "method" => "ad-hoc",
266
+ "destination" => "export",
267
+ "signingStyle" => "manual",
268
+ "provisioningProfiles" => {}
269
+ }.merge(options)
270
+
271
+ project_bundles = []
272
+
273
+ project.targets.each do |target|
274
+ target.build_configurations.each do |config|
275
+ team_id ||= config.build_settings["DEVELOPMENT_TEAM"]
276
+ project_bundles << config.build_settings["PRODUCT_BUNDLE_IDENTIFIER"]
277
+ end
278
+ end
279
+
280
+ export_options["teamID"] = team_id
281
+
282
+ logger.info "Fetching bundles with profiles for team ID '#{team_id}'"
283
+
284
+ bundles_with_profiles = AppStoreConnect::Client.get_bundles_with_profiles(project_bundles)
285
+ bundle_by_identifier = {}
286
+ profiles_by_id = {}
287
+
288
+ bundles_with_profiles["data"].each do |bundle_id|
289
+ bundle_by_identifier[bundle_id["attributes"]["identifier"]] = bundle_id
290
+ end
291
+
292
+ bundles_with_profiles["included"].each do |profile|
293
+ profiles_by_id[profile["id"]] = profile
294
+ end
295
+
296
+ project_bundles.each do |bundle_identifier|
297
+ bundle_id = bundle_by_identifier[bundle_identifier]
298
+ unless bundle_id
299
+ logger.warn "Bundle '#{bundle_identifier}' is missing in App Store Connect. Skipping."
300
+ next
301
+ end
302
+
303
+ logger.info "Bundle '#{bundle_identifier}' resolved to Bundle ID '#{bundle_id['id']}'"
304
+
305
+ profiles = bundle_id["relationships"]["profiles"]["data"]
306
+ unless profiles
307
+ logger.warn "Profile for Bundle ID '#{bundle_id['id']}' is missing in App Store Connect. Skipping."
308
+ next
309
+ end
310
+
311
+ ad_hoc_profile = nil
312
+ profiles.each do |profile|
313
+ profile_id = profile["id"]
314
+ profile = profiles_by_id[profile_id]
315
+
316
+ if profile["attributes"]["profileType"] == "IOS_APP_ADHOC" && profile["attributes"]["profileState"] == "ACTIVE"
317
+ ad_hoc_profile = profile
318
+ break
319
+ end
320
+ end
321
+
322
+ unless ad_hoc_profile
323
+ logger.warn "Profile for Bundle ID '#{bundle_id['id']}' is missing in App Store Connect. Skipping."
324
+ next
325
+ end
326
+
327
+ logger.info "Profile for Bundle ID '#{bundle_id['id']}' resolved to Profile '#{ad_hoc_profile['attributes']['name']}'"
328
+
329
+ profile_name = ad_hoc_profile["attributes"]["name"]
330
+
331
+ export_options["provisioningProfiles"][bundle_identifier] = profile_name
332
+ end
333
+
334
+ options_plist = Plist::Emit.dump(export_options)
335
+ File.write(export_path, options_plist)
336
+
337
+ return export_options
338
+ end
339
+
340
+ def patch_project(project_path, team_id = nil)
341
+ raise "Project path is not set" unless project_path
342
+
343
+ project = Xcodeproj::Project.open(project_path)
344
+
345
+ project_bundles = project.targets.map do |target|
346
+ target.build_configurations.map do |config|
347
+ config.build_settings["PRODUCT_BUNDLE_IDENTIFIER"]
348
+ end
349
+ end.flatten.uniq
350
+
351
+ bundles_with_profiles = AppStoreConnect::Client.get_bundles_with_profiles(project_bundles)
352
+ bundle_by_identifier = {}
353
+ profiles_by_id = {}
354
+
355
+ bundles_with_profiles["data"].each do |bundle_id|
356
+ bundle_by_identifier[bundle_id["attributes"]["identifier"]] = bundle_id
357
+ end
358
+
359
+ bundles_with_profiles["included"].each do |profile|
360
+ profiles_by_id[profile["id"]] = profile
361
+ end
362
+
363
+ project.targets.each do |target|
364
+ target.build_configurations.each do |config|
365
+ bundle_identifier = config.build_settings["PRODUCT_BUNDLE_IDENTIFIER"]
366
+ bundle_id = bundle_by_identifier[bundle_identifier]
367
+ unless bundle_id
368
+ logger.warn "Bundle '#{bundle_identifier}' is missing in App Store Connect. Skipping."
369
+ next
370
+ end
371
+
372
+ logger.info "Bundle '#{bundle_identifier}' resolved to Bundle ID '#{bundle_id['id']}'"
373
+
374
+ profiles = bundle_id["relationships"]["profiles"]["data"]
375
+ unless profiles
376
+ logger.warn "Profile for Bundle ID '#{bundle_id['id']}' is missing in App Store Connect. Skipping."
377
+ next
378
+ end
379
+
380
+ ad_hoc_profile = nil
381
+ profiles.each do |profile|
382
+ profile_id = profile["id"]
383
+ profile = profiles_by_id[profile_id]
384
+
385
+ if profile["attributes"]["profileType"] == "IOS_APP_ADHOC" && profile["attributes"]["profileState"] == "ACTIVE"
386
+ ad_hoc_profile = profile
387
+ break
388
+ end
389
+ end
390
+
391
+ unless ad_hoc_profile
392
+ logger.warn "Profile for Bundle ID '#{bundle_id['id']}' is missing in App Store Connect. Skipping."
393
+ next
394
+ end
395
+
396
+ config.build_settings["CODE_SIGN_IDENTITY"] = "iPhone Distribution"
397
+ config.build_settings["CODE_SIGN_STYLE"] = "Manual"
398
+ if team_id
399
+ config.build_settings["DEVELOPMENT_TEAM"] = team_id
400
+ end
401
+ config.build_settings["PROVISIONING_PROFILE_SPECIFIER"] = ad_hoc_profile["attributes"]["name"]
402
+ end
403
+ end
404
+
405
+ project.save
406
+ end
407
+
408
+ private
409
+
410
+ def install_certificate(certificate_path, keychain_path, certificate_password = "", certificate_type = "cert")
411
+ certificate_name = File.basename(certificate_path)
412
+ logger.info "Installing certificate '#{certificate_name}' to keychain '#{keychain_path}'"
413
+
414
+ status = system("security import #{certificate_path} -k #{keychain_path} -t #{certificate_type} -A -P #{certificate_password} -T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild")
415
+ unless status
416
+ logger.error "Failed to install certificate '#{certificate_name}' to keychain '#{keychain_path}'"
417
+ return
418
+ end
419
+
420
+ logger.info "Certificate '#{certificate_name}' installed to keychain '#{keychain_path}'"
421
+ end
422
+
423
+ def get_devices(team_id)
424
+ Starship::Client.get_devices(team_id)
425
+ end
426
+
427
+ def get_profiles_map(team_id)
428
+ logger.info "Fetching profiles for team ID '#{team_id}'"
429
+
430
+ registered_profiles = Starship::Client.get_profiles(team_id)
431
+ profiles = {}
432
+
433
+ registered_profiles.each do |profile|
434
+ provisioning_profile = Starship::Client::get_provisioning_profile(profile["id"], team_id)
435
+ app_id = provisioning_profile["provisioningProfile"]["appIdId"]
436
+
437
+ profiles[app_id] = provisioning_profile
438
+ end
439
+
440
+ return profiles
441
+ end
442
+
443
+ def get_bundle_map(team_id)
444
+ logger.info "Fetching bundle IDs for team ID '#{team_id}'"
445
+
446
+ registered_bundles = Starship::Client.get_bundle_ids(team_id)
447
+ bundle_ids = {}
448
+
449
+ registered_bundles.each do |bundle|
450
+ bundle_ids[bundle["attributes"]["identifier"]] = bundle["id"]
451
+ end
452
+
453
+ return bundle_ids
454
+ end
455
+
456
+ def renew_provisioning_profile(profile, team_id)
457
+ devices = get_devices(team_id)
458
+ deviceIds = devices.map { |device| device["id"] }
459
+
460
+ need_update = false
461
+
462
+ profile["provisioningProfile"]["devices"].each do |device|
463
+ if !deviceIds.include?(device["deviceId"])
464
+ need_update = true
465
+ break
466
+ end
467
+ end
468
+
469
+ logger.info "Profile '#{profile["provisioningProfile"]["name"]}' status: '#{profile["provisioningProfile"]["status"]}'"
470
+
471
+ if profile["provisioningProfile"]["status"] != "Active"
472
+ need_update = true
473
+ end
474
+
475
+ if need_update
476
+ logger.warn "Profile '#{profile["provisioningProfile"]["name"]}' is missing one or more devices."
477
+
478
+ Starship::Client.regen_provisioning_profile(profile, team_id, deviceIds)
479
+
480
+ logger.info "Profile '#{profile["provisioningProfile"]["name"]}' updated."
481
+ else
482
+ logger.info "Profile '#{profile["provisioningProfile"]["name"]}' is up to date."
483
+ end
484
+ end
485
+
486
+ def renew_bundle_id(bundle_id, team_id, entitlements_path)
487
+ bundle_info = Starship::Client.get_bundle_info(bundle_id, team_id)
488
+ bundle_identifier = bundle_info["data"]["attributes"]["identifier"]
489
+
490
+ logger.info "Checking capabilities for bundle '#{bundle_identifier}'"
491
+
492
+ capabilities = parse_entitlements(entitlements_path)
493
+
494
+ need_update = false
495
+
496
+ capabilities.each do |capability|
497
+ if capability.check?(bundle_info)
498
+ else
499
+ need_update = true
500
+ end
501
+ end
502
+
503
+ if need_update
504
+ logger.warn "Bundle '#{bundle_identifier}' is missing one or more capabilities."
505
+ new_capabilities = (
506
+ # You can't remove IN_APP_PURCHASE capability for some reason
507
+ capabilities + [ SimpleCapability.new("IN_APP_PURCHASE") ]
508
+ ).map { |capability| capability.to_bundle_capability(bundle_info, team_id) }
509
+
510
+ Starship::Client.patch_bundle(bundle_info, team_id, new_capabilities)
511
+
512
+ logger.info "Bundle '#{bundle_identifier}' capabilities updated."
513
+ else
514
+ logger.info "Bundle '#{bundle_identifier}' capabilities are up to date."
515
+ end
516
+ end
517
+ end
518
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+ require "json"
5
+ require "jwt"
6
+
7
+ module AppStoreConnect
8
+ class Client
9
+ BASE_URL = "https://api.appstoreconnect.apple.com/v1"
10
+
11
+ class << self
12
+ def get_bundles_with_profiles(bundle_identifiers)
13
+ response = request(
14
+ BASE_URL + "/bundleIds",
15
+ method: :get,
16
+ params: {
17
+ "fields[bundleIds]" => "name,platform,identifier,profiles",
18
+ "filter[identifier]" => bundle_identifiers.join(","),
19
+ "fields[profiles]" => "name,profileType,profileState,profileContent,uuid",
20
+ "include" => "profiles",
21
+ }
22
+ )
23
+
24
+ if response.status == 200
25
+ JSON.parse(response.body)
26
+ else
27
+ puts response.body
28
+ raise "Failed to get bundle IDs: #{response.status}"
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def request(endpoint, method: :get, params: nil, body: nil, headers: nil)
35
+ default_headers = {
36
+ "Authorization" => "Bearer #{jwt_token}",
37
+ "Content-Type" => "application/json",
38
+ "Accept" => "application/json",
39
+ }
40
+
41
+ if headers
42
+ default_headers = default_headers.merge(headers)
43
+ end
44
+
45
+ response = case method
46
+ when :get
47
+ Faraday.get(endpoint, params, default_headers)
48
+ when :post
49
+ Faraday.post(endpoint, body, default_headers)
50
+ when :put
51
+ Faraday.put(endpoint, body, default_headers)
52
+ when :delete
53
+ Faraday.delete(endpoint, default_headers)
54
+ when :patch
55
+ Faraday.patch(endpoint, body, default_headers)
56
+ end
57
+
58
+ return response
59
+ end
60
+
61
+ def jwt_token
62
+ key_id = ENV["APPSTORE_CONNECT_KEY_ID"]
63
+ key = ENV["APPSTORE_CONNECT_KEY"]
64
+ issuer_id = ENV["APPSTORE_CONNECT_ISSUER_ID"]
65
+
66
+ private_key = OpenSSL::PKey.read(key)
67
+ token = JWT.encode(
68
+ {
69
+ iss: issuer_id,
70
+ exp: Time.now.to_i + 20 * 60,
71
+ aud: "appstoreconnect-v1",
72
+ },
73
+ private_key,
74
+ "ES256",
75
+ header_fields = {
76
+ alg: "ES256",
77
+ kid: key_id,
78
+ typ: "JWT"
79
+ }
80
+ )
81
+
82
+ return token
83
+ end
84
+ end
85
+ end
86
+ end
data/lib/logging.rb ADDED
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+
5
+ module Logging
6
+ def logger
7
+ @logger ||= Logging.logger_for(self.class.name)
8
+ end
9
+
10
+ @loggers = {}
11
+
12
+ class << self
13
+ def logger_for(classname)
14
+ @loggers[classname] ||= configure_logger_for(classname)
15
+ end
16
+
17
+ def configure_logger_for(classname)
18
+ logger = ::Logger.new(STDOUT)
19
+ logger.formatter = proc do |severity, datetime, progname, msg|
20
+ "[AdSedare.#{progname}] [#{severity.capitalize}]: #{msg}\n"
21
+ end
22
+ logger.progname = classname
23
+ logger
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../logging"
4
+
5
+ module Starship
6
+ class TwoFactorProvider
7
+ def initialize
8
+ end
9
+
10
+ # Get the 2FA code
11
+ # @param session_id [String] The session ID from Apple
12
+ # @param scnt [String] The scnt value from Apple
13
+ # @return [String] The 2FA code
14
+ def get_code(session_id, scnt)
15
+ raise NotImplementedError, "Subclasses must implement get_code"
16
+ end
17
+
18
+ # Verify if this provider can handle the given 2FA type
19
+ # @param type [String] The 2FA type (sms, voice, etc.)
20
+ # @return [Boolean] Whether this provider can handle the given type
21
+ def can_handle?(type)
22
+ raise NotImplementedError, "Subclasses must implement can_handle?"
23
+ end
24
+ end
25
+
26
+ # Manual 2FA provider that prompts the user for a code
27
+ class ManualTwoFactorProvider < TwoFactorProvider
28
+ include Logging
29
+
30
+ def get_code(session_id, scnt)
31
+ logger.info "Please enter the code you received: "
32
+ code = gets.chomp.strip
33
+ code
34
+ end
35
+
36
+ def can_handle?(type)
37
+ true
38
+ end
39
+ end
40
+ end