civic_sip_sdk 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|