civic_sip_sdk 0.1.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/lib/civic_sip_sdk/app_config.rb +42 -0
- data/lib/civic_sip_sdk/client.rb +101 -0
- data/lib/civic_sip_sdk/crypto.rb +112 -0
- data/lib/civic_sip_sdk/user_data.rb +41 -0
- data/lib/civic_sip_sdk/user_data_item.rb +22 -0
- data/lib/civic_sip_sdk/version.rb +5 -0
- data/lib/civic_sip_sdk.rb +21 -0
- metadata +149 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 8f25482aa5c37b9879c21c1a76ef9967337b9385832b6a24d03584f2561552d8
|
4
|
+
data.tar.gz: 071fe7ee8420f5c1fdf9fff5f026a33d8d316e5c400bbf2a6fb80a81a60a2b65
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d06a3a0612b446b09e7a3cc725b7ec6c38d768824ff3f61caca8ccca03c0fe87424ebe3e64215a5fe7ea584372c235b82e608b2c78466163f4dd60e0f41a2337
|
7
|
+
data.tar.gz: 9dda8d41853a87394f17c178d8386a65e5cc8f813aafd83dbfb4eb268994bade0b6f30c027de2b55b7a60ce89aa90c39086c940824e6c75bc4f81116442a2a7e
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CivicSIPSdk
|
4
|
+
class AppConfig
|
5
|
+
attr_reader :id, :env, :private_key, :secret
|
6
|
+
|
7
|
+
VALID_ENVS = %i[dev prod].freeze
|
8
|
+
REQUIRED_KEYS = [
|
9
|
+
{ name: :id, error: 'Civic application id is missing!' },
|
10
|
+
{ name: :private_key, error: 'Civic application private signing key is missing!' },
|
11
|
+
{ name: :secret, error: 'Civic application secret is missing!' }
|
12
|
+
].freeze
|
13
|
+
|
14
|
+
# Creates a new instance of <tt>CivicSIPSdk::AppConfig</tt>.
|
15
|
+
# This is used to configure the SDK connection parameters to the Civic SIP service.
|
16
|
+
#
|
17
|
+
# It raises an ArgumentError if any argument is nil.
|
18
|
+
#
|
19
|
+
# Args:
|
20
|
+
# * <tt>id</tt> - The application id.
|
21
|
+
# * <tt>env</tt> - The application environment. Defaults to +:prod+ if the value is incorrect.
|
22
|
+
# * <tt>private_key</tt> - The application's private signing key.
|
23
|
+
# * <tt>secret</tt> - The application secret
|
24
|
+
def initialize(id:, env:, private_key:, secret:)
|
25
|
+
@id = id
|
26
|
+
@env = VALID_ENVS.include?(env.to_sym) ? env.to_sym : VALID_ENVS.last
|
27
|
+
@private_key = private_key
|
28
|
+
@secret = secret
|
29
|
+
|
30
|
+
validate
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def validate
|
36
|
+
validation_errors = REQUIRED_KEYS.map { |rk| instance_variable_get("@#{rk[:name]}").nil? ? rk[:error] : nil }
|
37
|
+
.compact
|
38
|
+
|
39
|
+
raise ArgumentError.new(validation_errors.join("\n")) unless validation_errors.empty?
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'httparty'
|
5
|
+
require 'civic_sip_sdk/app_config'
|
6
|
+
require 'civic_sip_sdk/user_data'
|
7
|
+
require 'civic_sip_sdk/crypto'
|
8
|
+
|
9
|
+
module CivicSIPSdk
|
10
|
+
class Client
|
11
|
+
BASE_URL = 'https://api.civic.com/sip'
|
12
|
+
AUTH_CODE_PATH = 'scopeRequest/authCode'
|
13
|
+
PUBLIC_HEX = '049a45998638cfb3c4b211d72030d9ae8329a242db63bfb0076a54e7647370a8ac5708b57af6065805d5a6be72332620932dbb35e8d318fce18e7c980a0eb26aa1'
|
14
|
+
MIMETYPE_JSON = 'application/json'
|
15
|
+
|
16
|
+
ENV_VAR = 'ENVIRONMENT'
|
17
|
+
RAILS_ENV = 'RAILS_ENV'
|
18
|
+
PRODUCTION_ENV = 'production'
|
19
|
+
|
20
|
+
HTTP_REQUEST_METHOD = :POST
|
21
|
+
|
22
|
+
# Creates a client
|
23
|
+
#
|
24
|
+
# Args:
|
25
|
+
# * <tt>config</tt> - an instance of CivicSIPSdk::AppConfig
|
26
|
+
def initialize(config:)
|
27
|
+
@config = config
|
28
|
+
end
|
29
|
+
|
30
|
+
# Exchange authorization code in the form of a JWT Token for the user data
|
31
|
+
# requested in the scope request.
|
32
|
+
#
|
33
|
+
# Args:
|
34
|
+
# * <tt>jwt_token</tt> - a JWT token that contains the authorization code
|
35
|
+
def exchange_code(jwt_token:)
|
36
|
+
json_body_str = JSON.generate('authToken' => jwt_token)
|
37
|
+
|
38
|
+
response = HTTParty.post(
|
39
|
+
"#{BASE_URL}/#{@config.env}/#{AUTH_CODE_PATH}",
|
40
|
+
headers: {
|
41
|
+
'Content-Type' => MIMETYPE_JSON,
|
42
|
+
'Accept' => MIMETYPE_JSON,
|
43
|
+
'Content-Length' => json_body_str.size.to_s,
|
44
|
+
'Authorization' => authorization_header(target_path: AUTH_CODE_PATH, body: json_body_str)
|
45
|
+
},
|
46
|
+
body: json_body_str,
|
47
|
+
debug_output: ENV[ENV_VAR] == PRODUCTION_ENV || ENV[RAILS_ENV] == PRODUCTION_ENV ? nil : STDOUT
|
48
|
+
)
|
49
|
+
|
50
|
+
unless response.code == 200
|
51
|
+
raise StandardError.new(
|
52
|
+
"Failed to exchange JWT token. HTTP status: #{response.code}, response body: #{response.body}"
|
53
|
+
)
|
54
|
+
end
|
55
|
+
|
56
|
+
res_payload = JSON.parse(response.body)
|
57
|
+
extract_user_data(response: res_payload)
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def authorization_header(target_path:, body:)
|
63
|
+
jwt_token = Crypto.jwt_token(
|
64
|
+
app_id: @config.id,
|
65
|
+
sip_base_url: BASE_URL,
|
66
|
+
data: {
|
67
|
+
method: HTTP_REQUEST_METHOD,
|
68
|
+
path: target_path
|
69
|
+
},
|
70
|
+
private_key: @config.private_key
|
71
|
+
)
|
72
|
+
|
73
|
+
civic_extension = Crypto.civic_extension(secret: @config.secret, body: body)
|
74
|
+
|
75
|
+
"Civic #{jwt_token}.#{civic_extension}"
|
76
|
+
end
|
77
|
+
|
78
|
+
def extract_user_data(response:)
|
79
|
+
# only verify the token if it's in production (test is using an expired token)
|
80
|
+
decoded_token = Crypto.decode_jwt_token(
|
81
|
+
token: response['data'],
|
82
|
+
public_hex_key: PUBLIC_HEX,
|
83
|
+
should_verify: ENV[ENV_VAR] == PRODUCTION_ENV || ENV[RAILS_ENV] == PRODUCTION_ENV
|
84
|
+
)
|
85
|
+
data_text = if response['encrypted']
|
86
|
+
# decrypt the data attr
|
87
|
+
Crypto.decrypt(
|
88
|
+
text: decoded_token['data'],
|
89
|
+
secret: @config.secret
|
90
|
+
)
|
91
|
+
else
|
92
|
+
decoded_data['data']
|
93
|
+
end
|
94
|
+
|
95
|
+
UserData.new(
|
96
|
+
user_id: response['userId'],
|
97
|
+
data_items: JSON.parse(data_text)
|
98
|
+
)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'base64'
|
4
|
+
require 'securerandom'
|
5
|
+
require 'openssl'
|
6
|
+
require 'jwt'
|
7
|
+
|
8
|
+
module CivicSIPSdk
|
9
|
+
module Crypto
|
10
|
+
CIPHER_ALGO = 'AES-128-CBC'
|
11
|
+
IV_STRING_LENGTH = 32
|
12
|
+
# "prime256v1" is another name for "secp256r1"
|
13
|
+
ECDSA_CURVE = 'prime256v1'
|
14
|
+
JWT_ALGO = 'ES256'
|
15
|
+
HMAC_DIGEST = 'SHA256'
|
16
|
+
|
17
|
+
# Create encrypted text using AES-128-CBC with a IV of 16 bytes
|
18
|
+
#
|
19
|
+
# Args:
|
20
|
+
# * <tt>text</tt> - the plain text to be encrypted
|
21
|
+
# * <tt>secret</tt> - the Civic application secret in HEX format
|
22
|
+
def self.encrypt(text:, secret:)
|
23
|
+
cipher = OpenSSL::Cipher.new(CIPHER_ALGO)
|
24
|
+
cipher.encrypt
|
25
|
+
cipher.key = hex_to_string(hex: secret)
|
26
|
+
iv = cipher.random_iv
|
27
|
+
cipher.iv = iv
|
28
|
+
|
29
|
+
encrypted_text = "#{cipher.update(text)}#{cipher.final}"
|
30
|
+
"#{string_to_hex(str: iv)}#{Base64.encode64(encrypted_text)}"
|
31
|
+
end
|
32
|
+
|
33
|
+
# Decrypt the encrypted text using AES-128-CBC with a IV of 32 bytes
|
34
|
+
#
|
35
|
+
# Args:
|
36
|
+
# * <tt>text</tt> - the encrypted text to be decrypted
|
37
|
+
# * <tt>secret</tt> - the Civic application secret in HEX format
|
38
|
+
def self.decrypt(text:, secret:)
|
39
|
+
cipher = OpenSSL::Cipher.new(CIPHER_ALGO)
|
40
|
+
cipher.decrypt
|
41
|
+
cipher.key = hex_to_string(hex: secret)
|
42
|
+
iv_hex = text[0..(IV_STRING_LENGTH - 1)]
|
43
|
+
cipher.iv = hex_to_string(hex: iv_hex)
|
44
|
+
encrypted_text = Base64.decode64(text[IV_STRING_LENGTH..-1])
|
45
|
+
|
46
|
+
"#{cipher.update(encrypted_text)}#{cipher.final}"
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.jwt_token(app_id:, sip_base_url:, data:, private_key:)
|
50
|
+
now = Time.now.to_i
|
51
|
+
|
52
|
+
payload = {
|
53
|
+
iat: now,
|
54
|
+
exp: now + 60 * 3,
|
55
|
+
iss: app_id,
|
56
|
+
aud: sip_base_url,
|
57
|
+
sub: app_id,
|
58
|
+
jti: SecureRandom.uuid,
|
59
|
+
data: data
|
60
|
+
}
|
61
|
+
|
62
|
+
JWT.encode(
|
63
|
+
payload,
|
64
|
+
private_signing_key(private_hex_key: private_key),
|
65
|
+
JWT_ALGO
|
66
|
+
)
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.decode_jwt_token(token:, public_hex_key:, should_verify:)
|
70
|
+
public_key = public_signing_key(public_hex_key: public_hex_key)
|
71
|
+
data, = JWT.decode(token, public_key, should_verify, algorithm: JWT_ALGO)
|
72
|
+
|
73
|
+
data
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.civic_extension(secret:, body:)
|
77
|
+
hmac = OpenSSL::HMAC.digest(HMAC_DIGEST, secret, JSON.generate(body))
|
78
|
+
Base64.encode64(hmac)
|
79
|
+
end
|
80
|
+
|
81
|
+
class << self
|
82
|
+
private
|
83
|
+
|
84
|
+
def private_signing_key(private_hex_key:)
|
85
|
+
key = OpenSSL::PKey::EC.new(ECDSA_CURVE)
|
86
|
+
key.private_key = OpenSSL::BN.new(private_hex_key.to_i(16))
|
87
|
+
|
88
|
+
key
|
89
|
+
end
|
90
|
+
|
91
|
+
def public_signing_key(public_hex_key:)
|
92
|
+
group = OpenSSL::PKey::EC::Group.new(ECDSA_CURVE)
|
93
|
+
point = OpenSSL::PKey::EC::Point.new(
|
94
|
+
group,
|
95
|
+
OpenSSL::BN.new(public_hex_key.to_i(16))
|
96
|
+
)
|
97
|
+
key = OpenSSL::PKey::EC.new(ECDSA_CURVE)
|
98
|
+
key.public_key = point
|
99
|
+
|
100
|
+
key
|
101
|
+
end
|
102
|
+
|
103
|
+
def string_to_hex(str:)
|
104
|
+
str.unpack1('H*')
|
105
|
+
end
|
106
|
+
|
107
|
+
def hex_to_string(hex:)
|
108
|
+
hex.scan(/../).map(&:hex).pack('c*')
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'civic_sip_sdk/user_data_item'
|
4
|
+
|
5
|
+
module CivicSIPSdk
|
6
|
+
class UserData
|
7
|
+
attr_reader :user_id, :data_items
|
8
|
+
|
9
|
+
# Creates a UserData insteance, which creates a list of UserDataItem instances from
|
10
|
+
# +data_items+.
|
11
|
+
#
|
12
|
+
# Args:
|
13
|
+
# * <tt>user_id</tt> - user id
|
14
|
+
# * <tt>data_items</tt> - a list of Hash that contains the key-value pairs for
|
15
|
+
# instantiating CivicSIPSdk::UserDataItem instances
|
16
|
+
def initialize(user_id:, data_items:)
|
17
|
+
@user_id = user_id
|
18
|
+
@data_items = data_items
|
19
|
+
@indexed_data_items = index_data_items
|
20
|
+
end
|
21
|
+
|
22
|
+
# Returns a +UserDataItem+ instance by matching the value of +label+, or +nil+
|
23
|
+
# if the label doesn't exist
|
24
|
+
def by_label(label:)
|
25
|
+
@indexed_data_items.fetch(label, nil)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def index_data_items
|
31
|
+
@data_items.each_with_object({}) do |data_item, memo|
|
32
|
+
memo[data_item['label']] = UserDataItem.new(
|
33
|
+
label: data_item['label'],
|
34
|
+
value: data_item['value'],
|
35
|
+
is_valid: data_item['isValid'],
|
36
|
+
is_owner: data_item['isOwner']
|
37
|
+
)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CivicSIPSdk
|
4
|
+
class UserDataItem
|
5
|
+
attr_reader :label, :value, :is_valid, :is_owner
|
6
|
+
|
7
|
+
# Creates an instance of UserDataItem.
|
8
|
+
#
|
9
|
+
# Args:
|
10
|
+
# * <tt>label</tt> - Descriptive value identifier.
|
11
|
+
# * <tt>value</tt> - Item value of requested user data.
|
12
|
+
# * <tt>is_valid</tt> - Indicates whether or not the data is still considered valid on the blockchain.
|
13
|
+
# * <tt>is_owner</tt> - Civic SIP service challenges the user during scope request approval to ensure
|
14
|
+
# the user is in control of the private key originally used in the issuance of the data attestation.
|
15
|
+
def initialize(label:, value:, is_valid:, is_owner:)
|
16
|
+
@label = label
|
17
|
+
@value = value
|
18
|
+
@is_valid = is_valid
|
19
|
+
@is_owner = is_owner
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift('lib')
|
4
|
+
|
5
|
+
require 'civic_sip_sdk/app_config'
|
6
|
+
require 'civic_sip_sdk/client'
|
7
|
+
|
8
|
+
module CivicSIPSdk
|
9
|
+
module_function
|
10
|
+
|
11
|
+
def new_client(id, env, private_key, secret)
|
12
|
+
app_config = AppConfig.new(
|
13
|
+
id: id,
|
14
|
+
env: env,
|
15
|
+
private_key: private_key,
|
16
|
+
secret: secret
|
17
|
+
)
|
18
|
+
|
19
|
+
Client.new(config: app_config)
|
20
|
+
end
|
21
|
+
end
|
metadata
ADDED
@@ -0,0 +1,149 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: civic_sip_sdk
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Han Wang
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-11-13 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.16'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.16'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: coveralls
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.8.22
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.8.22
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '10.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '10.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: webmock
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '3.4'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '3.4'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: httparty
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0.16'
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0.16'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: jwt
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - '='
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 2.1.0
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - '='
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 2.1.0
|
111
|
+
description: A ruby SDK for Civic SIP integration
|
112
|
+
email:
|
113
|
+
- han@binarystorms.com
|
114
|
+
executables: []
|
115
|
+
extensions: []
|
116
|
+
extra_rdoc_files: []
|
117
|
+
files:
|
118
|
+
- lib/civic_sip_sdk.rb
|
119
|
+
- lib/civic_sip_sdk/app_config.rb
|
120
|
+
- lib/civic_sip_sdk/client.rb
|
121
|
+
- lib/civic_sip_sdk/crypto.rb
|
122
|
+
- lib/civic_sip_sdk/user_data.rb
|
123
|
+
- lib/civic_sip_sdk/user_data_item.rb
|
124
|
+
- lib/civic_sip_sdk/version.rb
|
125
|
+
homepage: https://github.com/BinaryStorms/civic-sip-ruby-sdk
|
126
|
+
licenses:
|
127
|
+
- MIT
|
128
|
+
metadata: {}
|
129
|
+
post_install_message:
|
130
|
+
rdoc_options: []
|
131
|
+
require_paths:
|
132
|
+
- lib
|
133
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
134
|
+
requirements:
|
135
|
+
- - ">="
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
version: '2.1'
|
138
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
139
|
+
requirements:
|
140
|
+
- - ">="
|
141
|
+
- !ruby/object:Gem::Version
|
142
|
+
version: '0'
|
143
|
+
requirements: []
|
144
|
+
rubyforge_project:
|
145
|
+
rubygems_version: 2.7.6
|
146
|
+
signing_key:
|
147
|
+
specification_version: 4
|
148
|
+
summary: A ruby SDK for Civic SIP integration
|
149
|
+
test_files: []
|