spaceship 0.0.1 → 0.0.2

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.
@@ -1,6 +1,72 @@
1
- require "spaceship/version"
2
- require "spaceship/ship"
1
+ require 'spaceship/version'
2
+ require 'spaceship/base'
3
+ require 'spaceship/client'
4
+ require 'spaceship/profile_types'
5
+ require 'spaceship/app'
6
+ require 'spaceship/certificate'
7
+ require 'spaceship/device'
8
+ require 'spaceship/provisioning_profile'
9
+ require 'spaceship/launcher'
3
10
 
4
11
  module Spaceship
5
- # Your code goes here...
12
+ # Use this to just setup the configuration attribute and set it later somewhere else
13
+ class << self
14
+ # This client stores the default client when using the lazy syntax
15
+ # Spaceship.app instead of using the spaceship launcher
16
+ attr_accessor :client
17
+
18
+ # Authenticates with Apple's web services. This method has to be called once
19
+ # to generate a valid session. The session will automatically be used from then
20
+ # on.
21
+ #
22
+ # This method will automatically use the username from the Appfile (if available)
23
+ # and fetch the password from the Keychain (if available)
24
+ #
25
+ # @param user (String) (optional): The username (usually the email address)
26
+ # @param password (String) (optional): The password
27
+ #
28
+ # @raise InvalidUserCredentialsError: raised if authentication failed
29
+ #
30
+ # @return (Spaceship::Client) The client the login method was called for
31
+ def login(user = nil, password = nil)
32
+ @client = Client.login(user, password)
33
+ end
34
+
35
+ # Open up the team selection for the user (if necessary).
36
+ #
37
+ # If the user is in multiple teams, a team selection is shown.
38
+ # The user can then select a team by entering the number
39
+ #
40
+ # Additionally, the team ID is shown next to each team name
41
+ # so that the user can use the environment variable `FASTLANE_TEAM_ID`
42
+ # for future user.
43
+ #
44
+ # @return (String) The ID of the select team. You also get the value if
45
+ # the user is only in one team.
46
+ def select_team
47
+ @client.select_team
48
+ end
49
+
50
+ # Helper methods for managing multiple instances of spaceship
51
+
52
+ # @return (Class) Access the apps for the spaceship
53
+ def app
54
+ Spaceship::App.set_client(@client)
55
+ end
56
+
57
+ # @return (Class) Access the devices for the spaceship
58
+ def device
59
+ Spaceship::Device.set_client(@client)
60
+ end
61
+
62
+ # @return (Class) Access the certificates for the spaceship
63
+ def certificate
64
+ Spaceship::Certificate.set_client(@client)
65
+ end
66
+
67
+ # @return (Class) Access the provisioning profiles for the spaceship
68
+ def provisioning_profile
69
+ Spaceship::ProvisioningProfile.set_client(@client)
70
+ end
71
+ end
6
72
  end
@@ -0,0 +1,96 @@
1
+ module Spaceship
2
+ # Represents an App ID from the Developer Portal
3
+ class App < Base
4
+
5
+ # @return (String) The identifier of this app, provided by the Dev Portal
6
+ # @example
7
+ # "RGAWZGXSAA"
8
+ attr_accessor :app_id
9
+
10
+ # @return (String) The name you provided for this app
11
+ # @example
12
+ # "Spaceship"
13
+ attr_accessor :name
14
+
15
+ # @return (String) the supported platform of this app
16
+ # @example
17
+ # "ios"
18
+ attr_accessor :platform
19
+
20
+ # Prefix provided by the Dev Portal
21
+ # @example
22
+ # "5A997XSHK2"
23
+ attr_accessor :prefix
24
+
25
+ # @return (String) The bundle_id (app identifier) of your app
26
+ # @example
27
+ # "com.krausefx.app"
28
+ attr_accessor :bundle_id
29
+
30
+ # @return (Bool) Is this app a wildcard app (e.g. com.krausefx.*)
31
+ attr_accessor :is_wildcard
32
+
33
+ # @return (Bool) Development Push Enabled?
34
+ attr_accessor :dev_push_enabled
35
+
36
+ # @return (Bool) Production Push Enabled?
37
+ attr_accessor :prod_push_enabled
38
+
39
+ attr_mapping(
40
+ 'appIdId' => :app_id,
41
+ 'name' => :name,
42
+ 'appIdPlatform' => :platform,
43
+ 'prefix' => :prefix,
44
+ 'identifier' => :bundle_id,
45
+ 'isWildCard' => :is_wildcard,
46
+ 'isDevPushEnabled' => :dev_push_enabled,
47
+ 'isProdPushEnabled' => :prod_push_enabled
48
+ )
49
+
50
+ class << self
51
+ # Create a new object based on a hash.
52
+ # This is used to create a new object based on the server response.
53
+ def factory(attrs)
54
+ self.new(attrs)
55
+ end
56
+
57
+ # @return (Array) Returns all apps available for this account
58
+ def all
59
+ client.apps.map { |app| self.factory(app) }
60
+ end
61
+
62
+ # Creates a new App ID on the Apple Dev Portal
63
+ #
64
+ # if bundle_id ends with '*' then it is a wildcard id otherwise, it is an explicit id
65
+ # @param bundle_id [String] the bundle id (app_identifier) of the app associated with this provisioning profile
66
+ # @param name [String] the name of the App
67
+ # @return (App) The app you just created
68
+ def create!(bundle_id: bundle_id, name: name)
69
+ if bundle_id.end_with?('*')
70
+ type = :wildcard
71
+ else
72
+ type = :explicit
73
+ end
74
+
75
+ new_app = client.create_app!(type, name, bundle_id)
76
+ self.new(new_app)
77
+ end
78
+
79
+ # Find a specific App ID based on the bundle_id
80
+ # @return (App) The app you're looking for. This is nil if the app can't be found.
81
+ def find(bundle_id)
82
+ all.find do |app|
83
+ app.bundle_id == bundle_id
84
+ end
85
+ end
86
+ end
87
+
88
+ # Delete this App ID. This action will most likely fail if the App ID is already in the store
89
+ # or there are active profiles
90
+ # @return (App) The app you just deletd
91
+ def delete!
92
+ client.delete_app!(app_id)
93
+ self
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,86 @@
1
+ module Spaceship
2
+ class Base
3
+ class << self
4
+ attr_accessor :client
5
+
6
+ def client
7
+ @client || Spaceship.client
8
+ end
9
+
10
+ #set client and return self for chaining
11
+ def set_client(client)
12
+ self.client = client
13
+ self
14
+ end
15
+
16
+ ##
17
+ # bang method since it changes the parameter in-place
18
+ def remap_keys!(attrs)
19
+ return if attr_mapping.nil?
20
+
21
+ attr_mapping.each do |from, to|
22
+ attrs[to] = attrs.delete(from)
23
+ end
24
+ end
25
+
26
+ def attr_mapping(attr_map = nil)
27
+ if attr_map
28
+ @attr_mapping = attr_map
29
+ else
30
+ @attr_mapping ||= ancestors[1].attr_mapping rescue nil
31
+ end
32
+ end
33
+
34
+ ##
35
+ # Call a method to return a subclass constant.
36
+ #
37
+ # If `method_sym` is an underscored name of a class,
38
+ # return the class with the current client passed into it.
39
+ # If the method does not match, NoMethodError is raised.
40
+ #
41
+ # Example:
42
+ #
43
+ # Certificate.production_push
44
+ # #=> Certificate::ProductionPush
45
+ #
46
+ # ProvisioningProfile.ad_hoc
47
+ # #=> ProvisioningProfile::AdHoc
48
+ #
49
+ # ProvisioningProfile.some_other_method
50
+ # #=> NoMethodError: undefined method `some_other_method' for ProvisioningProfile
51
+ def method_missing(method_sym, *args, &block)
52
+ module_name = method_sym.to_s
53
+ module_name.sub!(/^[a-z\d]/) { $&.upcase }
54
+ module_name.gsub!(/(?:_|(\/))([a-z\d])/) { $2.upcase }
55
+ const_name = "#{self.name}::#{module_name}"
56
+ # if const_defined?(const_name)
57
+ klass = const_get(const_name)
58
+ klass.set_client(@client)
59
+ # else
60
+ # super
61
+ # end
62
+ end
63
+ end
64
+
65
+ def initialize(attrs = {})
66
+ self.class.remap_keys!(attrs)
67
+ attrs.each do |key, val|
68
+ self.send("#{key}=", val) if respond_to?("#{key}=")
69
+ end
70
+ @client = self.class.client
71
+ end
72
+
73
+ def client
74
+ @client
75
+ end
76
+
77
+ def inspect
78
+ inspectables = instance_variables - [:@client]
79
+ inspect_vars = inspectables.map do |ivar|
80
+ val = instance_variable_get(ivar)
81
+ "#{ivar}=#{val.inspect}"
82
+ end
83
+ "\n#<#{self.class.name}\n\t#{inspect_vars.join("\n\t")}>"
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,270 @@
1
+ require 'openssl'
2
+
3
+ module Spaceship
4
+ # Represents a certificate from the Apple Developer Portal.
5
+ #
6
+ # This can either be a code signing identity or a push profile
7
+ class Certificate < Base
8
+ # @return (String) The ID given from the developer portal. You'll probably not need it.
9
+ # @example
10
+ # "P577TH3PAA"
11
+ attr_accessor :id
12
+
13
+ # @return (String) The name of the certificate
14
+ # @example Company
15
+ # "SunApps GmbH"
16
+ # @example Push Profile
17
+ # "com.krausefx.app"
18
+ attr_accessor :name
19
+
20
+ # @return (String) Status of the certificate
21
+ # @example
22
+ # "Issued"
23
+ attr_accessor :status
24
+
25
+ # @return (Date) The date and time when the certificate was created
26
+ # @example
27
+ # 2015-04-01 21:24:00 UTC
28
+ attr_accessor :created
29
+
30
+ # @return (Date) The date and time when the certificate will expire
31
+ # @example
32
+ # 2016-04-01 21:24:00 UTC
33
+ attr_accessor :expires
34
+
35
+ # @return (String) The owner type that defines if it's a push profile
36
+ # or a code signing identity
37
+ #
38
+ # @example Code Signing Identity
39
+ # "team"
40
+ # @example Push Certificate
41
+ # "bundle"
42
+ attr_accessor :owner_type
43
+
44
+ # @return (String) The name of the owner
45
+ #
46
+ # @example Code Signing Identity (usually the company name)
47
+ # "SunApps Gmbh"
48
+ # @example Push Certificate (the name of your App ID)
49
+ # "Awesome App"
50
+ attr_accessor :owner_name
51
+
52
+ # @return (String) The ID of the owner, that can be used to
53
+ # fetch more information
54
+ # @example
55
+ # "75B83SPLAA"
56
+ attr_accessor :owner_id
57
+
58
+ # Indicates the type of this certificate
59
+ # which is automatically used to determine the class of
60
+ # the certificate. Available values listed in CERTIFICATE_TYPE_IDS
61
+ # @return (String) The type of the certificate
62
+ # @example Production Certificate
63
+ # "R58UK2EWSO"
64
+ # @example Development Certificate
65
+ # "5QPB9NHCEI"
66
+ attr_accessor :type_display_id
67
+
68
+ attr_mapping({
69
+ 'certificateId' => :id,
70
+ 'name' => :name,
71
+ 'statusString' => :status,
72
+ 'dateCreated' => :created,
73
+ 'expirationDate' => :expires,
74
+ 'ownerType' => :owner_type,
75
+ 'ownerName' => :owner_name,
76
+ 'ownerId' => :owner_id,
77
+ 'certificateTypeDisplayId' => :type_display_id
78
+ })
79
+
80
+ #####################################################
81
+ # Certs are not associated with apps
82
+ #####################################################
83
+
84
+ # A development code signing certificate used for development environment
85
+ class Development < Certificate; end
86
+
87
+ # A production code signing certificate used for distribution environment
88
+ class Production < Certificate; end
89
+
90
+ # An In House code signing certificate used for enterprise distributions
91
+ class InHouse < Certificate; end
92
+
93
+ #####################################################
94
+ # Certs that are specific for one app
95
+ #####################################################
96
+
97
+ # Abstract class for push certificates. Check out the subclasses
98
+ # DevelopmentPush, ProductionPush, WebsitePush and VoipPush
99
+ class PushCertificate < Certificate; end
100
+
101
+ # A push notification certificate for development environment
102
+ class DevelopmentPush < PushCertificate; end
103
+
104
+ # A push notification certificate for production environment
105
+ class ProductionPush < PushCertificate; end
106
+
107
+ # A push notification certificate for websites
108
+ class WebsitePush < PushCertificate; end
109
+
110
+ # A push notification certificate for the VOIP environment
111
+ class VoipPush < PushCertificate; end
112
+
113
+ # Passbook certificate
114
+ class Passbook < Certificate; end
115
+
116
+ # ApplePay certificate
117
+ class ApplePay < Certificate; end
118
+
119
+ CERTIFICATE_TYPE_IDS = {
120
+ "5QPB9NHCEI" => Development,
121
+ "R58UK2EWSO" => Production,
122
+ "9RQEK7MSXA" => InHouse,
123
+ "LA30L5BJEU" => Certificate,
124
+ "BKLRAVXMGM" => DevelopmentPush,
125
+ "3BQKVH9I2X" => ProductionPush,
126
+ "Y3B2F3TYSI" => Passbook,
127
+ "3T2ZP62QW8" => WebsitePush,
128
+ "E5D663CMZW" => WebsitePush,
129
+ "4APLUP237T" => ApplePay
130
+ }
131
+
132
+ #class methods
133
+ class << self
134
+ # Create a new code signing request that can be used to
135
+ # generate a new certificate
136
+ # @example
137
+ # Create a new certificate signing request
138
+ # csr, pkey = Spaceship.certificate.create_certificate_signing_request
139
+ #
140
+ # # Use the signing request to create a new distribution certificate
141
+ # Spaceship.certificate.production.create!(csr: csr)
142
+ def create_certificate_signing_request
143
+ key = OpenSSL::PKey::RSA.new 2048
144
+ csr = OpenSSL::X509::Request.new
145
+ csr.version = 0
146
+ csr.subject = OpenSSL::X509::Name.new([
147
+ ['CN', 'PEM', OpenSSL::ASN1::UTF8STRING]
148
+ ])
149
+ csr.public_key = key.public_key
150
+ csr.sign(key, OpenSSL::Digest::SHA1.new)
151
+ return [csr, key]
152
+ end
153
+
154
+ # Create a new object based on a hash.
155
+ # This is used to create a new object based on the server response.
156
+ def factory(attrs)
157
+ # Example:
158
+ # => {"name"=>"iOS Distribution: SunApps GmbH",
159
+ # "certificateId"=>"XC5PH8DAAA",
160
+ # "serialNumber"=>"797E732CCE8B7AAA",
161
+ # "status"=>"Issued",
162
+ # "statusCode"=>0,
163
+ # "expirationDate"=>#<DateTime: 2015-11-25T22:45:50+00:00 ((2457352j,81950s,0n),+0s,2299161j)>,
164
+ # "certificatePlatform"=>"ios",
165
+ # "certificateType"=>
166
+ # {"certificateTypeDisplayId"=>"R58UK2EAAA",
167
+ # "name"=>"iOS Distribution",
168
+ # "platform"=>"ios",
169
+ # "permissionType"=>"distribution",
170
+ # "distributionType"=>"store",
171
+ # "distributionMethod"=>"app",
172
+ # "ownerType"=>"team",
173
+ # "daysOverlap"=>364,
174
+ # "maxActive"=>2}}
175
+
176
+ if attrs['certificateType']
177
+ # On some accounts this is nested, so we need to flatten it
178
+ attrs.merge!(attrs['certificateType'])
179
+ attrs.delete('certificateType')
180
+ end
181
+
182
+ # Parse the dates
183
+ attrs['expirationDate'] = (Time.parse(attrs['expirationDate']) rescue attrs['expirationDate'])
184
+ attrs['dateCreated'] = (Time.parse(attrs['dateCreated']) rescue attrs['dateCreated'])
185
+
186
+ # Here we go
187
+ klass = CERTIFICATE_TYPE_IDS[attrs['certificateTypeDisplayId']]
188
+ klass ||= Certificate
189
+ klass.client = @client
190
+ klass.new(attrs)
191
+ end
192
+
193
+ # @return (Array) Returns all certificates of this account.
194
+ # If this is called from a subclass of Certificate, this will
195
+ # only include certificates matching the current type.
196
+ def all
197
+ if (self == Certificate) # are we the base-class?
198
+ types = CERTIFICATE_TYPE_IDS.keys
199
+ else
200
+ types = [CERTIFICATE_TYPE_IDS.key(self)]
201
+ end
202
+
203
+ client.certificates(types).map do |cert|
204
+ factory(cert)
205
+ end
206
+ end
207
+
208
+ # @return (Certificate) Find a certificate based on the ID of the certificate.
209
+ def find(certificate_id)
210
+ all.find do |c|
211
+ c.id == certificate_id
212
+ end
213
+ end
214
+
215
+ # Generate a new certificate based on a code certificate signing request
216
+ # @param csr (required): The certificate signing request to use. Get one using
217
+ # `create_certificate_signing_request`
218
+ # @param bundle_id (String) (optional): The app identifier this certificate is for.
219
+ # This value is only needed if you create a push profile. For normal code signing
220
+ # certificates, you must only pass a certificate signing request.
221
+ # @example
222
+ # # Create a new certificate signing request
223
+ # csr, pkey = Spaceship::Certificate.create_certificate_signing_request
224
+ #
225
+ # # Use the signing request to create a new distribution certificate
226
+ # Spaceship::Certificate::Production.create!(csr: csr)
227
+ # @return (Device): The newly created device
228
+ def create!(csr: csr, bundle_id: nil)
229
+ type = CERTIFICATE_TYPE_IDS.key(self)
230
+
231
+ # look up the app_id by the bundle_id
232
+ if bundle_id
233
+ app = Spaceship::App.find(bundle_id)
234
+ raise "Could not find app with bundle id '#{bundle_id}'" unless app
235
+ app_id = app.app_id
236
+ end
237
+
238
+ # if this succeeds, we need to save the .cer and the private key in keychain access or wherever they go in linux
239
+ response = client.create_certificate!(type, csr.to_pem, app_id)
240
+ # munge the response to make it work for the factory
241
+ response['certificateTypeDisplayId'] = response['certificateType']['certificateTypeDisplayId']
242
+ self.new(response)
243
+ end
244
+ end
245
+
246
+ # instance methods
247
+
248
+ # @return (String) Download the raw data of the certificate without parsing
249
+ def download_raw
250
+ client.download_certificate(id, type_display_id)
251
+ end
252
+
253
+ # @return (OpenSSL::X509::Certificate) Downloads and parses the certificate
254
+ def download
255
+ OpenSSL::X509::Certificate.new(download_raw)
256
+ end
257
+
258
+ # Revoke the certificate. You shouldn't use this method probably.
259
+ def revoke!
260
+ client.revoke_certificate!(id, type_display_id)
261
+ end
262
+
263
+ # @return (Bool): Is this certificate a push profile for apps?
264
+ def is_push?
265
+ # does display_type_id match push?
266
+ [Client::ProfileTypes::Push.development, Client::ProfileTypes::Push.production].include?(type_display_id)
267
+ end
268
+
269
+ end
270
+ end