licensechain_ruby_sdk 1.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.
- checksums.yaml +7 -0
- data/.github/dependabot.yml +11 -0
- data/.github/workflows/ci.yml +60 -0
- data/.github/workflows/publish-rubygems.yml +24 -0
- data/.gitignore +96 -0
- data/CHANGELOG.md +26 -0
- data/Gemfile +16 -0
- data/LICENSE +95 -0
- data/README.md +492 -0
- data/ROADMAP.md +13 -0
- data/examples/basic_usage.rb +190 -0
- data/examples/jwks_only.rb +25 -0
- data/lib/licensechain/api_client.rb +139 -0
- data/lib/licensechain/client.rb +37 -0
- data/lib/licensechain/configuration.rb +41 -0
- data/lib/licensechain/enhanced_client.rb +320 -0
- data/lib/licensechain/errors.rb +15 -0
- data/lib/licensechain/license_assertion.rb +67 -0
- data/lib/licensechain/license_validator.rb +174 -0
- data/lib/licensechain/models.rb +354 -0
- data/lib/licensechain/services/license_service.rb +108 -0
- data/lib/licensechain/services/product_service.rb +49 -0
- data/lib/licensechain/services/user_service.rb +59 -0
- data/lib/licensechain/services/webhook_service.rb +74 -0
- data/lib/licensechain/utils.rb +211 -0
- data/lib/licensechain/webhook_handler.rb +157 -0
- data/lib/licensechain/webhook_verifier.rb +184 -0
- data/lib/licensechain_ruby_sdk/version.rb +3 -0
- data/lib/licensechain_ruby_sdk.rb +36 -0
- data/licensechain_ruby_sdk.gemspec +44 -0
- metadata +275 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
require 'net/http'
|
|
2
|
+
require 'uri'
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'timeout'
|
|
5
|
+
|
|
6
|
+
module LicenseChain
|
|
7
|
+
class ApiClient
|
|
8
|
+
include Utils
|
|
9
|
+
|
|
10
|
+
def initialize(config = nil)
|
|
11
|
+
@config = config || LicenseChain.configuration
|
|
12
|
+
raise ConfigurationError, "API key is required" unless @config.valid?
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def get(endpoint, params = {})
|
|
16
|
+
make_request(:get, endpoint, nil, params)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def post(endpoint, data = nil)
|
|
20
|
+
make_request(:post, endpoint, data)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def put(endpoint, data = nil)
|
|
24
|
+
make_request(:put, endpoint, data)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def patch(endpoint, data = nil)
|
|
28
|
+
make_request(:patch, endpoint, data)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def delete(endpoint, data = nil)
|
|
32
|
+
make_request(:delete, endpoint, data)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def make_request(method, endpoint, data = nil, params = {})
|
|
38
|
+
uri = build_uri(endpoint, params)
|
|
39
|
+
request = build_request(method, uri, data)
|
|
40
|
+
|
|
41
|
+
retry_with_backoff(@config.retries) do
|
|
42
|
+
response = send_request(uri, request)
|
|
43
|
+
handle_response(response)
|
|
44
|
+
end
|
|
45
|
+
rescue Timeout::Error
|
|
46
|
+
raise TimeoutError, "Request timed out"
|
|
47
|
+
rescue => e
|
|
48
|
+
raise NetworkError, "Network error: #{e.message}"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def build_uri(endpoint, params = {})
|
|
52
|
+
base_url = @config.base_url.sub(%r{/\z}, '')
|
|
53
|
+
base_has_v1 = base_url.end_with?('/v1')
|
|
54
|
+
normalized_endpoint = if endpoint.start_with?('/v1/')
|
|
55
|
+
base_has_v1 ? endpoint.sub(%r{\A/v1}, '') : endpoint
|
|
56
|
+
elsif endpoint.start_with?('/')
|
|
57
|
+
base_has_v1 ? endpoint : "/v1#{endpoint}"
|
|
58
|
+
else
|
|
59
|
+
base_has_v1 ? "/#{endpoint}" : "/v1/#{endpoint}"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
uri = URI.parse("#{base_url}#{normalized_endpoint}")
|
|
63
|
+
uri.query = URI.encode_www_form(params) unless params.empty?
|
|
64
|
+
uri
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def build_request(method, uri, data = nil)
|
|
68
|
+
case method
|
|
69
|
+
when :get
|
|
70
|
+
Net::HTTP::Get.new(uri)
|
|
71
|
+
when :post
|
|
72
|
+
request = Net::HTTP::Post.new(uri)
|
|
73
|
+
request.body = data.to_json if data
|
|
74
|
+
request
|
|
75
|
+
when :put
|
|
76
|
+
request = Net::HTTP::Put.new(uri)
|
|
77
|
+
request.body = data.to_json if data
|
|
78
|
+
request
|
|
79
|
+
when :patch
|
|
80
|
+
request = Net::HTTP::Patch.new(uri)
|
|
81
|
+
request.body = data.to_json if data
|
|
82
|
+
request
|
|
83
|
+
when :delete
|
|
84
|
+
request = Net::HTTP::Delete.new(uri)
|
|
85
|
+
request.body = data.to_json if data
|
|
86
|
+
request
|
|
87
|
+
else
|
|
88
|
+
raise ArgumentError, "Unsupported HTTP method: #{method}"
|
|
89
|
+
end.tap do |request|
|
|
90
|
+
add_headers(request)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def add_headers(request)
|
|
95
|
+
request['Authorization'] = "Bearer #{@config.api_key}"
|
|
96
|
+
request['Content-Type'] = 'application/json'
|
|
97
|
+
request['X-API-Version'] = '1.0'
|
|
98
|
+
request['X-Platform'] = 'ruby-sdk'
|
|
99
|
+
request['User-Agent'] = "LicenseChain-Ruby-SDK/1.0.0"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def send_request(uri, request)
|
|
103
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
|
|
104
|
+
http.read_timeout = @config.timeout
|
|
105
|
+
http.open_timeout = @config.timeout
|
|
106
|
+
http.request(request)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def handle_response(response)
|
|
111
|
+
case response.code.to_i
|
|
112
|
+
when 200..299
|
|
113
|
+
parse_response(response)
|
|
114
|
+
when 400
|
|
115
|
+
raise ValidationError, "Bad Request: #{response.body}"
|
|
116
|
+
when 401
|
|
117
|
+
raise AuthenticationError, "Unauthorized: #{response.body}"
|
|
118
|
+
when 403
|
|
119
|
+
raise AuthenticationError, "Forbidden: #{response.body}"
|
|
120
|
+
when 404
|
|
121
|
+
raise NotFoundError, "Not Found: #{response.body}"
|
|
122
|
+
when 429
|
|
123
|
+
raise RateLimitError, "Rate Limited: #{response.body}"
|
|
124
|
+
when 500..599
|
|
125
|
+
raise ApiError, "Server Error: #{response.body}"
|
|
126
|
+
else
|
|
127
|
+
raise UnknownError, "Unexpected response: #{response.code} #{response.body}"
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def parse_response(response)
|
|
132
|
+
return {} if response.body.nil? || response.body.empty?
|
|
133
|
+
|
|
134
|
+
JSON.parse(response.body, symbolize_names: true)
|
|
135
|
+
rescue JSON::ParserError => e
|
|
136
|
+
raise DeserializationError, "Failed to parse JSON response: #{e.message}"
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
require_relative 'configuration'
|
|
2
|
+
require_relative 'api_client'
|
|
3
|
+
require_relative 'services/license_service'
|
|
4
|
+
require_relative 'services/user_service'
|
|
5
|
+
require_relative 'services/product_service'
|
|
6
|
+
require_relative 'services/webhook_service'
|
|
7
|
+
require_relative 'models'
|
|
8
|
+
require_relative 'errors'
|
|
9
|
+
require_relative 'utils'
|
|
10
|
+
|
|
11
|
+
module LicenseChain
|
|
12
|
+
class Client
|
|
13
|
+
attr_reader :licenses, :users, :products, :webhooks
|
|
14
|
+
|
|
15
|
+
def initialize(config = nil)
|
|
16
|
+
@config = config || LicenseChain.configuration
|
|
17
|
+
@api_client = ApiClient.new(@config)
|
|
18
|
+
|
|
19
|
+
@licenses = Services::LicenseService.new(@api_client)
|
|
20
|
+
@users = Services::UserService.new(@api_client)
|
|
21
|
+
@products = Services::ProductService.new(@api_client)
|
|
22
|
+
@webhooks = Services::WebhookService.new(@api_client)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def configuration
|
|
26
|
+
@config
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def ping
|
|
30
|
+
@api_client.get('/health')
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def health
|
|
34
|
+
@api_client.get('/health')
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module LicenseChain
|
|
2
|
+
class Configuration
|
|
3
|
+
attr_accessor :api_key, :base_url, :timeout, :retries, :logger
|
|
4
|
+
|
|
5
|
+
def initialize
|
|
6
|
+
@api_key = ENV['LICENSECHAIN_API_KEY']
|
|
7
|
+
@base_url = ENV['LICENSECHAIN_BASE_URL'] || 'https://api.licensechain.app/v1'
|
|
8
|
+
@timeout = 30
|
|
9
|
+
@retries = 3
|
|
10
|
+
@logger = Logger.new(STDOUT)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def valid?
|
|
14
|
+
!@api_key.nil? && !@api_key.empty?
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def reset!
|
|
18
|
+
@api_key = nil
|
|
19
|
+
@base_url = 'https://api.licensechain.app/v1'
|
|
20
|
+
@timeout = 30
|
|
21
|
+
@retries = 3
|
|
22
|
+
@logger = Logger.new(STDOUT)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
class << self
|
|
27
|
+
attr_writer :configuration
|
|
28
|
+
|
|
29
|
+
def configuration
|
|
30
|
+
@configuration ||= Configuration.new
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def configure
|
|
34
|
+
yield(configuration)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def reset!
|
|
38
|
+
@configuration = Configuration.new
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'uri'
|
|
6
|
+
require 'openssl'
|
|
7
|
+
require 'base64'
|
|
8
|
+
require 'time'
|
|
9
|
+
require 'securerandom'
|
|
10
|
+
|
|
11
|
+
module LicenseChain
|
|
12
|
+
# Enhanced client with reverse-engineered advanced patterns
|
|
13
|
+
class EnhancedClient < Client
|
|
14
|
+
attr_reader :session_id, :user_data, :initialized
|
|
15
|
+
|
|
16
|
+
def initialize(app_name:, owner_id:, app_secret:, base_url: 'https://api.licensechain.app/v1', timeout: 30, retries: 3)
|
|
17
|
+
super
|
|
18
|
+
@session_id = nil
|
|
19
|
+
@user_data = nil
|
|
20
|
+
@initialized = false
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Initialize the client and establish connection
|
|
24
|
+
def init
|
|
25
|
+
return true if @initialized
|
|
26
|
+
|
|
27
|
+
response = make_request('init', {
|
|
28
|
+
type: 'init',
|
|
29
|
+
ver: '1.0',
|
|
30
|
+
hash: generate_hash,
|
|
31
|
+
enckey: generate_encryption_key,
|
|
32
|
+
name: @app_name,
|
|
33
|
+
ownerid: @owner_id
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
if response['success']
|
|
37
|
+
@session_id = response['sessionid']
|
|
38
|
+
@initialized = true
|
|
39
|
+
true
|
|
40
|
+
else
|
|
41
|
+
false
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# License-only login (advanced pattern)
|
|
46
|
+
def license_login(license:)
|
|
47
|
+
ensure_initialized
|
|
48
|
+
|
|
49
|
+
response = make_request('license', {
|
|
50
|
+
type: 'license',
|
|
51
|
+
key: license,
|
|
52
|
+
hwid: get_hwid
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
if response['success']
|
|
56
|
+
@user_data = response['info']
|
|
57
|
+
response
|
|
58
|
+
else
|
|
59
|
+
raise LicenseChainError, response['message'] || 'License login failed'
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Check if user is logged in
|
|
64
|
+
def logged_in?
|
|
65
|
+
!@user_data.nil?
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Get current user data
|
|
69
|
+
def user_data
|
|
70
|
+
@user_data
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Get user's subscription information
|
|
74
|
+
def subscription
|
|
75
|
+
return nil unless logged_in?
|
|
76
|
+
@user_data['subscriptions']
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Set user variable
|
|
80
|
+
def setvar(var:, data:)
|
|
81
|
+
ensure_logged_in
|
|
82
|
+
|
|
83
|
+
response = make_request('setvar', {
|
|
84
|
+
type: 'setvar',
|
|
85
|
+
var: var,
|
|
86
|
+
data: data,
|
|
87
|
+
sessionid: @session_id
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
response['success']
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Get user variable
|
|
94
|
+
def getvar(var:)
|
|
95
|
+
ensure_logged_in
|
|
96
|
+
|
|
97
|
+
response = make_request('getvar', {
|
|
98
|
+
type: 'getvar',
|
|
99
|
+
var: var,
|
|
100
|
+
sessionid: @session_id
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
response['success'] ? response['data'] : nil
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Log message to LicenseChain
|
|
107
|
+
def log(message:)
|
|
108
|
+
ensure_logged_in
|
|
109
|
+
|
|
110
|
+
response = make_request('log', {
|
|
111
|
+
type: 'log',
|
|
112
|
+
pcuser: get_pc_user,
|
|
113
|
+
message: message,
|
|
114
|
+
sessionid: @session_id
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
response['success']
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Download file from LicenseChain
|
|
121
|
+
def download(fileid:)
|
|
122
|
+
ensure_logged_in
|
|
123
|
+
|
|
124
|
+
response = make_request('file', {
|
|
125
|
+
type: 'file',
|
|
126
|
+
fileid: fileid,
|
|
127
|
+
sessionid: @session_id
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
response['success'] ? response['contents'] : nil
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Get application statistics
|
|
134
|
+
def app_stats
|
|
135
|
+
ensure_initialized
|
|
136
|
+
|
|
137
|
+
response = make_request('app', {
|
|
138
|
+
type: 'app',
|
|
139
|
+
sessionid: @session_id
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
response['success'] ? response : nil
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Get online users
|
|
146
|
+
def online_users
|
|
147
|
+
ensure_logged_in
|
|
148
|
+
|
|
149
|
+
response = make_request('online', {
|
|
150
|
+
type: 'online',
|
|
151
|
+
sessionid: @session_id
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
response['success'] ? response['users'] : nil
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Chat functionality
|
|
158
|
+
def chat_get(channel:)
|
|
159
|
+
ensure_logged_in
|
|
160
|
+
|
|
161
|
+
response = make_request('chatget', {
|
|
162
|
+
type: 'chatget',
|
|
163
|
+
channel: channel,
|
|
164
|
+
sessionid: @session_id
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
response['success'] ? response['messages'] : nil
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def chat_send(message:, channel:)
|
|
171
|
+
ensure_logged_in
|
|
172
|
+
|
|
173
|
+
response = make_request('chatsend', {
|
|
174
|
+
type: 'chatsend',
|
|
175
|
+
message: message,
|
|
176
|
+
channel: channel,
|
|
177
|
+
sessionid: @session_id
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
response['success']
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Admin functions
|
|
184
|
+
def ban_user(user:)
|
|
185
|
+
ensure_logged_in
|
|
186
|
+
|
|
187
|
+
response = make_request('ban', {
|
|
188
|
+
type: 'ban',
|
|
189
|
+
user: user,
|
|
190
|
+
sessionid: @session_id
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
response['success']
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def unban_user(user:)
|
|
197
|
+
ensure_logged_in
|
|
198
|
+
|
|
199
|
+
response = make_request('unban', {
|
|
200
|
+
type: 'unban',
|
|
201
|
+
user: user,
|
|
202
|
+
sessionid: @session_id
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
response['success']
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def all_users
|
|
209
|
+
ensure_logged_in
|
|
210
|
+
|
|
211
|
+
response = make_request('allusers', {
|
|
212
|
+
type: 'allusers',
|
|
213
|
+
sessionid: @session_id
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
response['success'] ? response['users'] : nil
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def get_user(username:)
|
|
220
|
+
ensure_logged_in
|
|
221
|
+
|
|
222
|
+
response = make_request('getuser', {
|
|
223
|
+
type: 'getuser',
|
|
224
|
+
user: username,
|
|
225
|
+
sessionid: @session_id
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
response['success'] ? response['info'] : nil
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def update_user(user:, data:)
|
|
232
|
+
ensure_logged_in
|
|
233
|
+
|
|
234
|
+
response = make_request('edituser', {
|
|
235
|
+
type: 'edituser',
|
|
236
|
+
user: user,
|
|
237
|
+
data: data,
|
|
238
|
+
sessionid: @session_id
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
response['success']
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def delete_user(user:)
|
|
245
|
+
ensure_logged_in
|
|
246
|
+
|
|
247
|
+
response = make_request('deleteuser', {
|
|
248
|
+
type: 'deleteuser',
|
|
249
|
+
user: user,
|
|
250
|
+
sessionid: @session_id
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
response['success']
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Webhook functionality
|
|
257
|
+
def webhook
|
|
258
|
+
ensure_logged_in
|
|
259
|
+
|
|
260
|
+
response = make_request('webhook', {
|
|
261
|
+
type: 'webhook',
|
|
262
|
+
sessionid: @session_id
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
response['success'] ? response : nil
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def verify_webhook(payload:, signature:)
|
|
269
|
+
expected_signature = generate_webhook_signature(payload)
|
|
270
|
+
expected_signature == signature
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def parse_webhook(payload:, signature:)
|
|
274
|
+
return nil unless verify_webhook(payload: payload, signature: signature)
|
|
275
|
+
|
|
276
|
+
JSON.parse(payload)
|
|
277
|
+
rescue JSON::ParserError
|
|
278
|
+
nil
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
private
|
|
282
|
+
|
|
283
|
+
def ensure_initialized
|
|
284
|
+
raise LicenseChainError, 'Client not initialized. Call init() first.' unless @initialized
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def ensure_logged_in
|
|
288
|
+
ensure_initialized
|
|
289
|
+
raise LicenseChainError, 'User not logged in' unless logged_in?
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def generate_hash
|
|
293
|
+
data = "#{@app_name}#{@owner_id}#{@app_secret}"
|
|
294
|
+
Digest::SHA256.hexdigest(data)
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def generate_encryption_key
|
|
298
|
+
SecureRandom.hex(16)
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def get_hwid
|
|
302
|
+
require 'socket'
|
|
303
|
+
require 'sys/uname'
|
|
304
|
+
|
|
305
|
+
hostname = Socket.gethostname
|
|
306
|
+
uname = Sys::Uname.uname
|
|
307
|
+
"#{hostname}-#{uname.machine}-#{uname.sysname}"
|
|
308
|
+
rescue
|
|
309
|
+
"unknown-hwid-#{SecureRandom.hex(8)}"
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def get_pc_user
|
|
313
|
+
ENV['USER'] || ENV['USERNAME'] || 'unknown'
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def generate_webhook_signature(payload)
|
|
317
|
+
"sha256=#{OpenSSL::HMAC.hexdigest('SHA256', @app_secret, payload)}"
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module LicenseChain
|
|
2
|
+
class Error < StandardError; end
|
|
3
|
+
|
|
4
|
+
class NetworkError < Error; end
|
|
5
|
+
class ApiError < Error; end
|
|
6
|
+
class ValidationError < Error; end
|
|
7
|
+
class AuthenticationError < Error; end
|
|
8
|
+
class NotFoundError < Error; end
|
|
9
|
+
class RateLimitError < Error; end
|
|
10
|
+
class TimeoutError < Error; end
|
|
11
|
+
class SerializationError < Error; end
|
|
12
|
+
class DeserializationError < Error; end
|
|
13
|
+
class ConfigurationError < Error; end
|
|
14
|
+
class UnknownError < Error; end
|
|
15
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'jwt'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'uri'
|
|
6
|
+
require 'net/http'
|
|
7
|
+
|
|
8
|
+
module LicenseChain
|
|
9
|
+
# RS256 license_token verification via JWKS (parity with Node verifyLicenseAssertionJwt).
|
|
10
|
+
module LicenseAssertion
|
|
11
|
+
LICENSE_TOKEN_USE_CLAIM = 'licensechain_license_v1'
|
|
12
|
+
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
# @param expected_app_id [String, nil]
|
|
16
|
+
# @param issuer [String, nil]
|
|
17
|
+
# @return [Hash] decoded JWT payload
|
|
18
|
+
def verify_license_assertion_jwt(token, jwks_url, expected_app_id: nil, issuer: nil)
|
|
19
|
+
token = token.to_s.strip
|
|
20
|
+
raise ArgumentError, 'empty token' if token.empty?
|
|
21
|
+
|
|
22
|
+
jwks_url = jwks_url.to_s.strip
|
|
23
|
+
raise ArgumentError, 'empty jwks_url' if jwks_url.empty?
|
|
24
|
+
|
|
25
|
+
jwks = fetch_jwks(jwks_url)
|
|
26
|
+
decode_opts = {
|
|
27
|
+
algorithms: ['RS256'],
|
|
28
|
+
jwks: jwks,
|
|
29
|
+
verify_aud: false
|
|
30
|
+
}
|
|
31
|
+
if issuer && !issuer.to_s.strip.empty?
|
|
32
|
+
decode_opts[:verify_iss] = true
|
|
33
|
+
decode_opts[:iss] = issuer.to_s.strip
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
payload, = JWT.decode(token, nil, true, decode_opts)
|
|
37
|
+
|
|
38
|
+
tu = payload['token_use']
|
|
39
|
+
if tu != LICENSE_TOKEN_USE_CLAIM
|
|
40
|
+
raise JWT::DecodeError, %(Invalid license token: expected token_use "#{LICENSE_TOKEN_USE_CLAIM}")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
if expected_app_id && !expected_app_id.to_s.strip.empty?
|
|
44
|
+
want = expected_app_id.to_s.strip
|
|
45
|
+
aud = payload['aud']
|
|
46
|
+
ok = aud == want || (aud.is_a?(Array) && aud.include?(want))
|
|
47
|
+
raise JWT::DecodeError, 'Invalid license token: aud does not match expected app id' unless ok
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
payload
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def fetch_jwks(jwks_url)
|
|
54
|
+
uri = URI.parse(jwks_url)
|
|
55
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
56
|
+
http.use_ssl = uri.scheme == 'https'
|
|
57
|
+
http.open_timeout = 20
|
|
58
|
+
http.read_timeout = 20
|
|
59
|
+
req = Net::HTTP::Get.new(uri.request_uri)
|
|
60
|
+
res = http.request(req)
|
|
61
|
+
raise JWT::DecodeError, "JWKS HTTP #{res.code}" unless res.is_a?(Net::HTTPSuccess)
|
|
62
|
+
|
|
63
|
+
JSON.parse(res.body)
|
|
64
|
+
end
|
|
65
|
+
private_class_method :fetch_jwks
|
|
66
|
+
end
|
|
67
|
+
end
|