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.
@@ -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