onboardbase 1.0.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 +7 -0
- data/CHANGELOG.md +0 -0
- data/Gemfile +2 -0
- data/README.md +0 -0
- data/Rakefile +0 -0
- data/lib/AES.rb +30 -0
- data/lib/onboardbase/version.rb +3 -0
- data/lib/onboardbase.rb +268 -0
- data/onboardbase.gemspec +29 -0
- metadata +178 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: b06da9457180637a621c7bb90cea5717e2290d23f42d64241e7632fd9e98a32b
|
4
|
+
data.tar.gz: 964d051e732645c0176ade32486eeb3bf53ef81b472c0933b721822652ec7504
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ccf1384fdd1a642a950347f3545d2cb757b0ef9d0b7d8102788b421febe3d9c25eaf242af1a710e04223789e28de690d1d00f319b89e152a823e1898ccc0c5a1
|
7
|
+
data.tar.gz: 1338c5e937270ad6dd67af4d928fe8f35958397fb5d11cfd59570f73486b1b91ee405542440d7772bcad791533049549e45609962a37eba6f33d5acac5a2f9aa
|
data/CHANGELOG.md
ADDED
File without changes
|
data/Gemfile
ADDED
data/README.md
ADDED
File without changes
|
data/Rakefile
ADDED
File without changes
|
data/lib/AES.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'base64'
|
2
|
+
require 'digest'
|
3
|
+
require 'openssl'
|
4
|
+
|
5
|
+
module AESCrypt
|
6
|
+
def AESCrypt.encrypt(password, iv, cleardata)
|
7
|
+
cipher = OpenSSL::Cipher.new('AES-256-CBC')
|
8
|
+
cipher.encrypt # set cipher to be encryption mode
|
9
|
+
cipher.key = password
|
10
|
+
cipher.iv = iv
|
11
|
+
encrypted = ''
|
12
|
+
encrypted << cipher.update(cleardata)
|
13
|
+
encrypted << cipher.final
|
14
|
+
AESCrypt.b64enc(encrypted)
|
15
|
+
end
|
16
|
+
|
17
|
+
def AESCrypt.decrypt(password, iv, secretdata)
|
18
|
+
secretdata = Base64::decode64(secretdata)
|
19
|
+
decipher = OpenSSL::Cipher::Cipher.new('aes-256-cbc')
|
20
|
+
decipher.decrypt
|
21
|
+
decipher.key = password
|
22
|
+
decipher.iv = iv if iv != nil
|
23
|
+
decipher.update(secretdata) + decipher.final
|
24
|
+
end
|
25
|
+
|
26
|
+
def AESCrypt.b64enc(data)
|
27
|
+
Base64.encode64(data).gsub(/\n/, '')
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
data/lib/onboardbase.rb
ADDED
@@ -0,0 +1,268 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'httparty'
|
3
|
+
require 'json'
|
4
|
+
require 'gibberish'
|
5
|
+
require 'digest/md5'
|
6
|
+
require 'AES'
|
7
|
+
require 'machineid'
|
8
|
+
|
9
|
+
module Onboardbase
|
10
|
+
class << self
|
11
|
+
attr_accessor :env, :is_dev
|
12
|
+
|
13
|
+
def is_dev
|
14
|
+
@is_dev.nil? ? false : @is_dev
|
15
|
+
end
|
16
|
+
|
17
|
+
def env
|
18
|
+
@env.nil?
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
def encrypt(password, iv, cleardata)
|
23
|
+
cipher = OpenSSL::Cipher.new('AES-256-CBC')
|
24
|
+
cipher.encrypt # set cipher to be encryption mode
|
25
|
+
cipher.key = password
|
26
|
+
cipher.iv = iv
|
27
|
+
encrypted = ''
|
28
|
+
encrypted << cipher.update(cleardata)
|
29
|
+
encrypted << cipher.final
|
30
|
+
b64enc(encrypted)
|
31
|
+
end
|
32
|
+
|
33
|
+
def decrypt(password, iv, secretdata)
|
34
|
+
secretdata = Base64::decode64(secretdata)
|
35
|
+
decipher = OpenSSL::Cipher::Cipher.new('aes-256-cbc')
|
36
|
+
decipher.decrypt
|
37
|
+
decipher.key = password
|
38
|
+
decipher.iv = iv if iv != nil
|
39
|
+
decipher.update(secretdata) + decipher.final
|
40
|
+
end
|
41
|
+
|
42
|
+
def b64enc(data)
|
43
|
+
Base64.encode64(data).gsub(/\n/, '')
|
44
|
+
end
|
45
|
+
|
46
|
+
def apiURL
|
47
|
+
return "https://devapi.onboardbase.com/graphql" if self.is_dev
|
48
|
+
"https://api.onboardbase.com/graphql"
|
49
|
+
end
|
50
|
+
|
51
|
+
def configuration
|
52
|
+
@configuration ||= {}
|
53
|
+
end
|
54
|
+
def initialize
|
55
|
+
super
|
56
|
+
end
|
57
|
+
|
58
|
+
def config
|
59
|
+
yield self
|
60
|
+
end
|
61
|
+
|
62
|
+
def getWorkingDirectory
|
63
|
+
Dir.pwd
|
64
|
+
end
|
65
|
+
|
66
|
+
def getOnboardbaseDir
|
67
|
+
"#{Dir.home}/.onboardbase"
|
68
|
+
end
|
69
|
+
|
70
|
+
def getFallbackDir
|
71
|
+
"#{self.getOnboardbaseDir}/fallback"
|
72
|
+
end
|
73
|
+
|
74
|
+
|
75
|
+
def getProjectFallbackDir
|
76
|
+
project = self.configuration['setup']['project']
|
77
|
+
"#{self.getFallbackDir}/#{project}"
|
78
|
+
end
|
79
|
+
|
80
|
+
def getEnvironmentFallbackDir
|
81
|
+
environment = self.configuration['setup']['environment']
|
82
|
+
"#{self.getProjectFallbackDir}_#{environment}"
|
83
|
+
end
|
84
|
+
|
85
|
+
def config_exists?(directory)
|
86
|
+
return File.exist?(directory)
|
87
|
+
end
|
88
|
+
|
89
|
+
|
90
|
+
def loadConfig
|
91
|
+
configPath = self.getWorkingDirectory + '/onboardbase.yaml'
|
92
|
+
unless self.config_exists?(configPath)
|
93
|
+
puts "Please create onboardbase.yaml in the root of the project at: " + configPath
|
94
|
+
exit 1
|
95
|
+
end
|
96
|
+
config = YAML.load_file(configPath)
|
97
|
+
if (config['api_key'] == nil)
|
98
|
+
puts "Your onboardbase.yaml file does not have an api_key"
|
99
|
+
exit 1
|
100
|
+
end
|
101
|
+
|
102
|
+
if (config['passcode'] == nil)
|
103
|
+
puts "Your onboardbase.yaml file does not have a passcode"
|
104
|
+
exit 1
|
105
|
+
end
|
106
|
+
@configuration = config
|
107
|
+
end
|
108
|
+
|
109
|
+
def makeRequest
|
110
|
+
url = self.apiURL
|
111
|
+
headers = {
|
112
|
+
KEY: self.configuration['api_key'],
|
113
|
+
}
|
114
|
+
body = {
|
115
|
+
query: %{
|
116
|
+
query {
|
117
|
+
generalPublicProjects(filterOptions: { title: "#{self .configuration['setup']['project']}", disableCustomSelect: true }) {
|
118
|
+
list {
|
119
|
+
id
|
120
|
+
title
|
121
|
+
publicEnvironments(filterOptions: { title: "#{self .configuration['setup']['environment']}" }) {
|
122
|
+
list {
|
123
|
+
id
|
124
|
+
key
|
125
|
+
title
|
126
|
+
}
|
127
|
+
}
|
128
|
+
}
|
129
|
+
}
|
130
|
+
}
|
131
|
+
}
|
132
|
+
}
|
133
|
+
response = HTTParty.post(url, headers: headers, body: body)
|
134
|
+
JSON.parse(response.body)
|
135
|
+
end
|
136
|
+
|
137
|
+
def parseResponse?(response)
|
138
|
+
error = response["errors"]
|
139
|
+
data = response["data"]
|
140
|
+
return data["generalPublicProjects"] if error == nil
|
141
|
+
{:error=> true, :message => error[0]["message"] }
|
142
|
+
end
|
143
|
+
|
144
|
+
def getProject?(data)
|
145
|
+
project = data["list"][0]
|
146
|
+
return project if project != nil
|
147
|
+
false
|
148
|
+
end
|
149
|
+
|
150
|
+
|
151
|
+
def getSecrets?(project)
|
152
|
+
env = project["publicEnvironments"]["list"][0]
|
153
|
+
return JSON.parse(env["key"]) if env != nil
|
154
|
+
false
|
155
|
+
end
|
156
|
+
|
157
|
+
def bytes_to_key(data, salt, output=48)
|
158
|
+
merged = data + salt
|
159
|
+
key = Digest::MD5.digest(merged)
|
160
|
+
final_key = key
|
161
|
+
while final_key.length < output
|
162
|
+
key = Digest::MD5.digest(key + merged)
|
163
|
+
final_key = final_key + key
|
164
|
+
end
|
165
|
+
final_key[0..output-1]
|
166
|
+
end
|
167
|
+
|
168
|
+
def aes256_cbc_decrypt(key, data, iv)
|
169
|
+
key = Digest::SHA256.digest(key) if(key.kind_of?(String) && 32 != key.bytesize)
|
170
|
+
iv = Digest::MD5.digest(iv) if(iv.kind_of?(String) && 16 != iv.bytesize)
|
171
|
+
aes = OpenSSL::Cipher.new('AES-256-CBC')
|
172
|
+
aes.decrypt
|
173
|
+
aes.key = key
|
174
|
+
aes.iv = iv
|
175
|
+
aes.update(data) + aes.final
|
176
|
+
end
|
177
|
+
|
178
|
+
def parseSecrets(secrets)
|
179
|
+
secrets.each_with_index do |secret, i|
|
180
|
+
secret = Base64.decode64(secret)
|
181
|
+
unless secret[0..7] == 'Salted__'
|
182
|
+
puts "Invalid encrypted data"
|
183
|
+
exit(1)
|
184
|
+
end
|
185
|
+
salt = secret[8..15]
|
186
|
+
key_iv = bytes_to_key(self.configuration["passcode"], salt, 48)
|
187
|
+
key = key_iv[0..31]
|
188
|
+
iv = key_iv[32..]
|
189
|
+
parsedSecret = aes256_cbc_decrypt(key, secret[16..], iv)
|
190
|
+
secrets[i] = JSON.parse(parsedSecret)
|
191
|
+
end
|
192
|
+
secrets
|
193
|
+
end
|
194
|
+
|
195
|
+
def setEnv(secretsHash)
|
196
|
+
secretsHash.keys.sort.each do |key|
|
197
|
+
ENV["#{key}"] = "#{secretsHash[key]}"
|
198
|
+
end
|
199
|
+
# Overried local secrets
|
200
|
+
configSecrets = self.configuration["secrets"]
|
201
|
+
unless configSecrets
|
202
|
+
configSecrets = { "local" => {} }
|
203
|
+
end
|
204
|
+
configSecrets["local"].keys.sort.each do |key|
|
205
|
+
ENV["#{key}"] = "#{configSecrets["local"][key]}"
|
206
|
+
end
|
207
|
+
ENV.to_hash
|
208
|
+
end
|
209
|
+
|
210
|
+
def hashSecrets?(secretsArr)
|
211
|
+
secretsHash = Hash.new
|
212
|
+
secretsArr.each do |secret|
|
213
|
+
secretsHash["#{secret["key"]}"] = "#{secret["value"]}"
|
214
|
+
end
|
215
|
+
secretsHash
|
216
|
+
end
|
217
|
+
|
218
|
+
def storeToFallback?(secrets)
|
219
|
+
unless File.directory?(self.getOnboardbaseDir)
|
220
|
+
Dir.new(self.getOnboardbaseDir)
|
221
|
+
end
|
222
|
+
unless File.directory?(self.getFallbackDir)
|
223
|
+
Dir.new(self.getFallbackDir)
|
224
|
+
end
|
225
|
+
password = MachineID.ID?
|
226
|
+
cipher = Gibberish::AES::CBC.new(password)
|
227
|
+
cipher_text = cipher.encrypt(JSON.generate(secrets))
|
228
|
+
data = cipher_text
|
229
|
+
File.write(self.getEnvironmentFallbackDir, data, nil , mode: 'w')
|
230
|
+
end
|
231
|
+
|
232
|
+
|
233
|
+
def readFallback
|
234
|
+
unless File.directory?(self.getOnboardbaseDir)
|
235
|
+
Dir.new(self.getOnboardbaseDir)
|
236
|
+
end
|
237
|
+
unless File.directory?(self.getFallbackDir)
|
238
|
+
Dir.new(self.getFallbackDir)
|
239
|
+
end
|
240
|
+
|
241
|
+
if !File.exist?(self.getEnvironmentFallbackDir) || File.read(self.getEnvironmentFallbackDir).length <= 0
|
242
|
+
puts "No valid fallback for #{self .configuration['setup']['project']} project using #{self .configuration['setup']['environment']} environment"
|
243
|
+
return JSON.parse("{}") # Graceful failure, ensure the application continues to run without secrets.
|
244
|
+
end
|
245
|
+
data = File.read(self.getEnvironmentFallbackDir)
|
246
|
+
password = MachineID.ID?
|
247
|
+
cipher = Gibberish::AES::CBC.new(password)
|
248
|
+
decoded_data = cipher.decrypt(data)
|
249
|
+
JSON.parse(decoded_data)
|
250
|
+
end
|
251
|
+
|
252
|
+
def loadSecrets
|
253
|
+
self.loadConfig
|
254
|
+
response = self.parseResponse?(self.makeRequest)
|
255
|
+
if response[:error]
|
256
|
+
puts "Unable to fetch secrets with the specified api key, reading from fallback file"
|
257
|
+
secrets = self.readFallback
|
258
|
+
else
|
259
|
+
project = self.getProject?(response)
|
260
|
+
projectSecrets = self.getSecrets?(project)
|
261
|
+
parsedSecrets = self.parseSecrets(projectSecrets)
|
262
|
+
secrets = self.hashSecrets?(parsedSecrets)
|
263
|
+
end
|
264
|
+
finalEnvs = self.setEnv(secrets)
|
265
|
+
self.storeToFallback?(finalEnvs)
|
266
|
+
end
|
267
|
+
end
|
268
|
+
end
|
data/onboardbase.gemspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
require File.expand_path('lib/onboardbase/version', __dir__)
|
2
|
+
|
3
|
+
Gem::Specification.new do |spec|
|
4
|
+
spec.name = 'onboardbase'
|
5
|
+
spec.version = Onboardbase::VERSION
|
6
|
+
spec.authors = ['Onboardbase']
|
7
|
+
spec.email = ['admin@treadie.com']
|
8
|
+
spec.description = "Access onboardbase secrets in your Ruby projects"
|
9
|
+
spec.summary = "Use Onboardbase in your Ruby projects"
|
10
|
+
spec.homepage= 'https://github.com/Onboardbase/onboardbase-ruby'
|
11
|
+
spec.license = 'MIT'
|
12
|
+
spec.platform = Gem::Platform::RUBY
|
13
|
+
spec.required_ruby_version = '>= 2.5.0'
|
14
|
+
|
15
|
+
spec.files = Dir['README.md', 'LICENSE', 'CHANGELOG.md', 'lib/**/*.rb', 'lib/**/*.rake', 'onboardbase.gemspec', '.github/*.md', 'Gemfile', 'Rakefile']
|
16
|
+
|
17
|
+
spec.extra_rdoc_files = ['README.md']
|
18
|
+
|
19
|
+
spec.add_dependency 'rubyzip', '~> 2.3'
|
20
|
+
spec.add_dependency 'http', '~> 5.0.4'
|
21
|
+
spec.add_dependency 'httparty', '~> 0.20.0'
|
22
|
+
spec.add_dependency 'aes', '~> 0.5.1'
|
23
|
+
spec.add_dependency 'machineid', '~> 1.0.0'
|
24
|
+
spec.add_dependency 'gibberish', '~> 2.1.1'
|
25
|
+
|
26
|
+
spec.add_development_dependency 'rubocop', '~> 0.60'
|
27
|
+
spec.add_development_dependency 'rubocop-performance', '~> 1.5'
|
28
|
+
spec.add_development_dependency 'rubocop-rspec', '~> 1.37'
|
29
|
+
end
|
metadata
ADDED
@@ -0,0 +1,178 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: onboardbase
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Onboardbase
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2022-02-17 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rubyzip
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.3'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.3'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: http
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 5.0.4
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 5.0.4
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: httparty
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 0.20.0
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.20.0
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: aes
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 0.5.1
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 0.5.1
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: machineid
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 1.0.0
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 1.0.0
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: gibberish
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 2.1.1
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 2.1.1
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rubocop
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0.60'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0.60'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rubocop-performance
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '1.5'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '1.5'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: rubocop-rspec
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "~>"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '1.37'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - "~>"
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '1.37'
|
139
|
+
description: Access onboardbase secrets in your Ruby projects
|
140
|
+
email:
|
141
|
+
- admin@treadie.com
|
142
|
+
executables: []
|
143
|
+
extensions: []
|
144
|
+
extra_rdoc_files:
|
145
|
+
- README.md
|
146
|
+
files:
|
147
|
+
- CHANGELOG.md
|
148
|
+
- Gemfile
|
149
|
+
- README.md
|
150
|
+
- Rakefile
|
151
|
+
- lib/AES.rb
|
152
|
+
- lib/onboardbase.rb
|
153
|
+
- lib/onboardbase/version.rb
|
154
|
+
- onboardbase.gemspec
|
155
|
+
homepage: https://github.com/Onboardbase/onboardbase-ruby
|
156
|
+
licenses:
|
157
|
+
- MIT
|
158
|
+
metadata: {}
|
159
|
+
post_install_message:
|
160
|
+
rdoc_options: []
|
161
|
+
require_paths:
|
162
|
+
- lib
|
163
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
164
|
+
requirements:
|
165
|
+
- - ">="
|
166
|
+
- !ruby/object:Gem::Version
|
167
|
+
version: 2.5.0
|
168
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
169
|
+
requirements:
|
170
|
+
- - ">="
|
171
|
+
- !ruby/object:Gem::Version
|
172
|
+
version: '0'
|
173
|
+
requirements: []
|
174
|
+
rubygems_version: 3.0.3.1
|
175
|
+
signing_key:
|
176
|
+
specification_version: 4
|
177
|
+
summary: Use Onboardbase in your Ruby projects
|
178
|
+
test_files: []
|