source_license_sdk 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.
data/README.md ADDED
@@ -0,0 +1,329 @@
1
+ # Source-License Ruby SDK
2
+
3
+ A Ruby gem for easy integration with the Source-License platform for license validation and activation.
4
+
5
+ ## Features
6
+
7
+ - **Simple License Validation**: Check if a license key is valid with one method call
8
+ - **License Activation**: Activate licenses on specific machines with automatic machine fingerprinting
9
+ - **License Enforcement**: Automatically exit your application if license validation fails
10
+ - **Rate Limiting Handling**: Built-in handling of API rate limits with retry information
11
+ - **Secure Communication**: Uses HTTPS and handles all Source-License API security requirements
12
+ - **Cross-Platform Machine Identification**: Works on Windows, macOS, and Linux
13
+
14
+ ## Installation
15
+
16
+ Add this line to your application's Gemfile:
17
+
18
+ ```ruby
19
+ gem 'source_license_sdk'
20
+ ```
21
+
22
+ And then execute:
23
+
24
+ ```bash
25
+ bundle install
26
+ ```
27
+
28
+ Or install it yourself as:
29
+
30
+ ```bash
31
+ gem install source_license_sdk
32
+ ```
33
+
34
+ ## Quick Start
35
+
36
+ ### Basic Setup
37
+
38
+ ```ruby
39
+ require 'source_license_sdk'
40
+
41
+ # Configure the SDK
42
+ SourceLicenseSDK.setup(
43
+ server_url: 'https://your-license-server.com',
44
+ license_key: 'YOUR-LICENSE-KEY',
45
+ machine_id: 'unique-machine-identifier' # Optional, will auto-generate if not provided
46
+ )
47
+ ```
48
+
49
+ ### Method 1: License Validation
50
+
51
+ Check if a license is valid without activating it:
52
+
53
+ ```ruby
54
+ result = SourceLicenseSDK.validate_license
55
+
56
+ if result.valid?
57
+ puts "License is valid!"
58
+ puts "Expires at: #{result.expires_at}" if result.expires_at
59
+ else
60
+ puts "License validation failed: #{result.error_message}"
61
+ end
62
+ ```
63
+
64
+ ### Method 2: License Activation
65
+
66
+ Activate a license on the current machine:
67
+
68
+ ```ruby
69
+ result = SourceLicenseSDK.activate_license
70
+
71
+ if result.success?
72
+ puts "License activated successfully!"
73
+ puts "Activations remaining: #{result.activations_remaining}"
74
+ else
75
+ puts "Activation failed: #{result.error_message}"
76
+ end
77
+ ```
78
+
79
+ ### Method 3: License Enforcement
80
+
81
+ Automatically exit the application if license validation fails:
82
+
83
+ ```ruby
84
+ # This will exit the program with code 1 if the license is invalid
85
+ SourceLicenseSDK.enforce_license!
86
+
87
+ # Your application code continues here only if license is valid
88
+ puts "Application starting with valid license..."
89
+ ```
90
+
91
+ ## Advanced Usage
92
+
93
+ ### Custom Configuration
94
+
95
+ ```ruby
96
+ SourceLicenseSDK.configure do |config|
97
+ config.server_url = 'https://your-license-server.com'
98
+ config.license_key = 'YOUR-LICENSE-KEY'
99
+ config.machine_id = 'custom-machine-id'
100
+ config.timeout = 30
101
+ config.verify_ssl = true
102
+ config.user_agent = 'MyApp/1.0.0'
103
+ end
104
+ ```
105
+
106
+ ### Manual Machine ID Generation
107
+
108
+ ```ruby
109
+ # Generate a unique machine identifier
110
+ machine_id = SourceLicenseSDK::MachineIdentifier.generate
111
+ puts "Machine ID: #{machine_id}"
112
+
113
+ # Generate a machine fingerprint (more detailed)
114
+ fingerprint = SourceLicenseSDK::MachineIdentifier.generate_fingerprint
115
+ puts "Machine Fingerprint: #{fingerprint}"
116
+ ```
117
+
118
+ ### Error Handling
119
+
120
+ ```ruby
121
+ begin
122
+ result = SourceLicenseSDK.validate_license
123
+
124
+ if result.valid?
125
+ puts "License is valid"
126
+ else
127
+ puts "License invalid: #{result.error_message}"
128
+ end
129
+ rescue SourceLicenseSDK::NetworkError => e
130
+ puts "Network error: #{e.message} (Code: #{e.response_code})"
131
+ rescue SourceLicenseSDK::RateLimitError => e
132
+ puts "Rate limited. Retry after #{e.retry_after} seconds"
133
+ rescue SourceLicenseSDK::ConfigurationError => e
134
+ puts "Configuration error: #{e.message}"
135
+ end
136
+ ```
137
+
138
+ ### Working with Results
139
+
140
+ ```ruby
141
+ result = SourceLicenseSDK.validate_license
142
+
143
+ # Check various result properties
144
+ puts "Valid: #{result.valid?}"
145
+ puts "Expires at: #{result.expires_at}"
146
+ puts "Rate limited: #{result.rate_limited?}"
147
+ puts "Rate limit remaining: #{result.rate_limit_remaining}"
148
+ puts "Error code: #{result.error_code}" if result.error_code
149
+
150
+ # Convert to hash
151
+ puts result.to_h
152
+ ```
153
+
154
+ ### Custom License Enforcement
155
+
156
+ ```ruby
157
+ # Custom exit code and message
158
+ SourceLicenseSDK.enforce_license!(
159
+ exit_code: 2,
160
+ custom_message: "This software requires a valid license to run."
161
+ )
162
+
163
+ # Use specific license key and machine ID
164
+ SourceLicenseSDK.enforce_license!(
165
+ 'SPECIFIC-LICENSE-KEY',
166
+ machine_id: 'specific-machine-id'
167
+ )
168
+ ```
169
+
170
+ ## Integration Examples
171
+
172
+ ### Ruby on Rails Application
173
+
174
+ ```ruby
175
+ # config/initializers/source_license.rb
176
+ SourceLicenseSDK.setup(
177
+ server_url: Rails.application.credentials.license_server_url,
178
+ license_key: Rails.application.credentials.license_key
179
+ )
180
+
181
+ # In your application controller or concern
182
+ class ApplicationController < ActionController::Base
183
+ before_action :validate_license
184
+
185
+ private
186
+
187
+ def validate_license
188
+ result = SourceLicenseSDK.validate_license
189
+
190
+ unless result.valid?
191
+ render json: { error: 'Invalid license' }, status: :forbidden
192
+ end
193
+ end
194
+ end
195
+ ```
196
+
197
+ ### Command Line Tool
198
+
199
+ ```ruby
200
+ #!/usr/bin/env ruby
201
+ require 'source_license_sdk'
202
+
203
+ # Setup license checking
204
+ SourceLicenseSDK.setup(
205
+ server_url: 'https://license.mycompany.com',
206
+ license_key: ARGV[0] || ENV['LICENSE_KEY']
207
+ )
208
+
209
+ # Enforce license before running
210
+ SourceLicenseSDK.enforce_license!(
211
+ custom_message: "Please provide a valid license key to use this tool."
212
+ )
213
+
214
+ # Your application logic here
215
+ puts "Tool is running with valid license!"
216
+ ```
217
+
218
+ ### Desktop Application
219
+
220
+ ```ruby
221
+ require 'source_license_sdk'
222
+
223
+ class MyApplication
224
+ def initialize
225
+ setup_licensing
226
+ end
227
+
228
+ private
229
+
230
+ def setup_licensing
231
+ SourceLicenseSDK.setup(
232
+ server_url: 'https://licensing.myapp.com',
233
+ license_key: load_license_key,
234
+ auto_generate_machine_id: true
235
+ )
236
+
237
+ # Try to activate license if not already done
238
+ activate_license_if_needed
239
+
240
+ # Validate license on startup
241
+ validate_license!
242
+ end
243
+
244
+ def load_license_key
245
+ # Load from config file, registry, etc.
246
+ File.read('license.key').strip
247
+ rescue
248
+ nil
249
+ end
250
+
251
+ def activate_license_if_needed
252
+ result = SourceLicenseSDK.validate_license
253
+
254
+ unless result.valid?
255
+ puts "Activating license..."
256
+ activation_result = SourceLicenseSDK.activate_license
257
+
258
+ unless activation_result.success?
259
+ puts "Failed to activate license: #{activation_result.error_message}"
260
+ exit 1
261
+ end
262
+ end
263
+ end
264
+
265
+ def validate_license!
266
+ SourceLicenseSDK.enforce_license!(
267
+ custom_message: "This application requires a valid license."
268
+ )
269
+ end
270
+ end
271
+ ```
272
+
273
+ ## Error Types
274
+
275
+ The SDK defines several exception types for different error scenarios:
276
+
277
+ - `SourceLicenseSDK::ConfigurationError` - Invalid SDK configuration
278
+ - `SourceLicenseSDK::NetworkError` - HTTP/network related errors
279
+ - `SourceLicenseSDK::LicenseError` - General license validation errors
280
+ - `SourceLicenseSDK::RateLimitError` - API rate limiting errors
281
+ - `SourceLicenseSDK::LicenseNotFoundError` - License not found
282
+ - `SourceLicenseSDK::LicenseExpiredError` - License has expired
283
+ - `SourceLicenseSDK::ActivationError` - License activation errors
284
+ - `SourceLicenseSDK::MachineError` - Machine identification errors
285
+
286
+ ## Configuration Options
287
+
288
+ | Option | Type | Default | Description |
289
+ |--------|------|---------|-------------|
290
+ | `server_url` | String | nil | Source-License server URL (required) |
291
+ | `license_key` | String | nil | License key to validate/activate |
292
+ | `machine_id` | String | nil | Unique machine identifier |
293
+ | `auto_generate_machine_id` | Boolean | true | Auto-generate machine ID if not provided |
294
+ | `timeout` | Integer | 30 | HTTP request timeout in seconds |
295
+ | `user_agent` | String | "SourceLicenseSDK/VERSION" | HTTP User-Agent header |
296
+ | `verify_ssl` | Boolean | true | Verify SSL certificates |
297
+
298
+ ## Development
299
+
300
+ After checking out the repo, run:
301
+
302
+ ```bash
303
+ bundle install
304
+ ```
305
+
306
+ To build and install the gem locally:
307
+
308
+ ```bash
309
+ gem build source_license_sdk.gemspec
310
+ gem install source_license_sdk-*.gem
311
+ ```
312
+
313
+ ## Contributing
314
+
315
+ 1. Fork the repository
316
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
317
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
318
+ 4. Push to the branch (`git push origin my-new-feature`)
319
+ 5. Create a new Pull Request
320
+
321
+ ## License
322
+
323
+ This gem is available as open source under the terms of the [GPL-3.0 License](LICENSE).
324
+
325
+ ## Support
326
+
327
+ For support with this SDK, please open an issue on the [Source-License repository](https://github.com/PixelRidgeSoftworks/Source-License).
328
+
329
+ For general Source-License platform support, please contact your license provider.
data/Rakefile ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+
5
+ task default: :build
@@ -0,0 +1,180 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'uri'
5
+ require 'json'
6
+
7
+ # HTTP client for communicating with Source-License API
8
+ class SourceLicenseSDK::Client
9
+ attr_reader :config
10
+
11
+ def initialize(config)
12
+ @config = config
13
+ validate_config!
14
+ end
15
+
16
+ # Validate a license key
17
+ def validate_license(license_key, machine_id: nil, machine_fingerprint: nil)
18
+ machine_fingerprint ||= MachineIdentifier.generate_fingerprint if machine_id
19
+
20
+ path = "/api/license/#{license_key}/validate"
21
+ params = {}
22
+ params[:machine_id] = machine_id if machine_id
23
+ params[:machine_fingerprint] = machine_fingerprint if machine_fingerprint
24
+
25
+ response = make_request(:get, path, params: params)
26
+ LicenseValidationResult.new(response)
27
+ rescue NetworkError => e
28
+ handle_network_error(e)
29
+ end
30
+
31
+ # Activate a license on this machine
32
+ def activate_license(license_key, machine_id:, machine_fingerprint: nil)
33
+ machine_fingerprint ||= MachineIdentifier.generate_fingerprint
34
+
35
+ path = "/api/license/#{license_key}/activate"
36
+ body = {
37
+ machine_id: machine_id,
38
+ machine_fingerprint: machine_fingerprint,
39
+ }
40
+
41
+ response = make_request(:post, path, body: body)
42
+ LicenseValidationResult.new(response)
43
+ rescue NetworkError => e
44
+ handle_network_error(e)
45
+ end
46
+
47
+ private
48
+
49
+ def validate_config!
50
+ raise ConfigurationError, 'Server URL is required' unless config.server_url
51
+ raise ConfigurationError, 'Invalid server URL format' unless valid_url?(config.server_url)
52
+ end
53
+
54
+ def valid_url?(url)
55
+ uri = URI.parse(url)
56
+ uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
57
+ rescue URI::InvalidURIError
58
+ false
59
+ end
60
+
61
+ def make_request(method, path, params: {}, body: nil)
62
+ uri = build_uri(path, params)
63
+ http = create_http_client(uri)
64
+ request = create_request(method, uri, body)
65
+
66
+ response = http.request(request)
67
+ handle_response(response)
68
+ end
69
+
70
+ def build_uri(path, params = {})
71
+ base_uri = URI.parse(config.server_url)
72
+ base_uri.path = path
73
+
74
+ base_uri.query = URI.encode_www_form(params) if params.any?
75
+
76
+ base_uri
77
+ end
78
+
79
+ def create_http_client(uri)
80
+ http = Net::HTTP.new(uri.host, uri.port)
81
+ http.use_ssl = uri.scheme == 'https'
82
+ http.verify_mode = config.verify_ssl ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
83
+ http.read_timeout = config.timeout
84
+ http.open_timeout = config.timeout
85
+ http
86
+ end
87
+
88
+ def create_request(method, uri, body)
89
+ request_class = case method
90
+ when :get then Net::HTTP::Get
91
+ when :post then Net::HTTP::Post
92
+ when :put then Net::HTTP::Put
93
+ when :delete then Net::HTTP::Delete
94
+ else raise ArgumentError, "Unsupported HTTP method: #{method}"
95
+ end
96
+
97
+ request = request_class.new(uri)
98
+ request['User-Agent'] = config.user_agent
99
+ request['Accept'] = 'application/json'
100
+ request['Content-Type'] = 'application/json' if body
101
+
102
+ if body
103
+ request.body = body.is_a?(String) ? body : JSON.generate(body)
104
+ end
105
+
106
+ request
107
+ end
108
+
109
+ def handle_response(response)
110
+ case response.code.to_i
111
+ when 200..299
112
+ parse_json_response(response.body)
113
+ when 400
114
+ data = parse_json_response(response.body)
115
+ raise_license_error(data, response.code.to_i)
116
+ when 404
117
+ data = parse_json_response(response.body)
118
+ raise LicenseNotFoundError, data['error'] || 'License not found'
119
+ when 429
120
+ data = parse_json_response(response.body)
121
+ retry_after = response['Retry-After']&.to_i || data['retry_after']
122
+ raise RateLimitError.new(data['error'] || 'Rate limit exceeded', retry_after: retry_after)
123
+ when 500..599
124
+ raise NetworkError.new('Server error occurred', response_code: response.code.to_i, response_body: response.body)
125
+ else
126
+ raise NetworkError.new("Unexpected response: #{response.code}", response_code: response.code.to_i,
127
+ response_body: response.body)
128
+ end
129
+ end
130
+
131
+ def parse_json_response(body)
132
+ return {} if body.nil? || body.empty?
133
+
134
+ JSON.parse(body)
135
+ rescue JSON::ParserError
136
+ raise NetworkError.new('Invalid JSON response from server', response_body: body)
137
+ end
138
+
139
+ def raise_license_error(data, _status_code)
140
+ error_message = data['error'] || data['message'] || 'License validation failed'
141
+
142
+ case error_message.downcase
143
+ when /expired/
144
+ raise LicenseExpiredError, error_message
145
+ when /rate limit/
146
+ retry_after = data['retry_after']
147
+ raise RateLimitError.new(error_message, retry_after: retry_after)
148
+ when /not found/
149
+ raise LicenseNotFoundError, error_message
150
+ when /activation/
151
+ raise ActivationError, error_message
152
+ else
153
+ raise LicenseError.new(error_message, error_code: data['error_code'])
154
+ end
155
+ end
156
+
157
+ def handle_network_error(error)
158
+ # Convert network errors to license validation results for consistency
159
+ case error
160
+ when RateLimitError
161
+ LicenseValidationResult.new(
162
+ valid: false,
163
+ success: false,
164
+ error: error.message,
165
+ error_code: error.error_code,
166
+ retry_after: error.retry_after
167
+ )
168
+ when LicenseNotFoundError, LicenseExpiredError, ActivationError
169
+ LicenseValidationResult.new(
170
+ valid: false,
171
+ success: false,
172
+ error: error.message,
173
+ error_code: error.error_code
174
+ )
175
+ else
176
+ # Re-raise other network errors
177
+ raise error
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SourceLicenseSDK
4
+ # Base exception class for all SDK errors
5
+ class Error < StandardError; end
6
+
7
+ # Configuration related errors
8
+ class ConfigurationError < Error; end
9
+
10
+ # Network and HTTP related errors
11
+ class NetworkError < Error
12
+ attr_reader :response_code, :response_body
13
+
14
+ def initialize(message, response_code: nil, response_body: nil)
15
+ super(message)
16
+ @response_code = response_code
17
+ @response_body = response_body
18
+ end
19
+ end
20
+
21
+ # License validation errors
22
+ class LicenseError < Error
23
+ attr_reader :error_code, :retry_after
24
+
25
+ def initialize(message, error_code: nil, retry_after: nil)
26
+ super(message)
27
+ @error_code = error_code
28
+ @retry_after = retry_after
29
+ end
30
+ end
31
+
32
+ # Rate limiting errors
33
+ class RateLimitError < LicenseError
34
+ def initialize(message = 'Rate limit exceeded', retry_after: nil)
35
+ super(message, error_code: 'RATE_LIMIT_EXCEEDED', retry_after: retry_after)
36
+ end
37
+ end
38
+
39
+ # License not found errors
40
+ class LicenseNotFoundError < LicenseError
41
+ def initialize(message = 'License not found')
42
+ super(message, error_code: 'LICENSE_NOT_FOUND')
43
+ end
44
+ end
45
+
46
+ # License expired errors
47
+ class LicenseExpiredError < LicenseError
48
+ def initialize(message = 'License has expired')
49
+ super(message, error_code: 'LICENSE_EXPIRED')
50
+ end
51
+ end
52
+
53
+ # License activation errors
54
+ class ActivationError < LicenseError
55
+ def initialize(message, error_code: 'ACTIVATION_FAILED')
56
+ super
57
+ end
58
+ end
59
+
60
+ # Machine ID related errors
61
+ class MachineError < Error; end
62
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Represents the result of a license validation or activation request
4
+ class SourceLicenseSDK::LicenseValidationResult
5
+ attr_reader :valid, :success, :error_message, :error_code, :expires_at,
6
+ :activations_remaining, :retry_after, :token, :timestamp,
7
+ :rate_limit_remaining, :rate_limit_reset_at
8
+
9
+ def initialize(data)
10
+ @data = data
11
+
12
+ initialize_validation_fields
13
+ initialize_activation_fields
14
+ initialize_common_fields
15
+ initialize_rate_limit_fields
16
+ end
17
+
18
+ def valid?
19
+ @valid == true
20
+ end
21
+
22
+ def success?
23
+ @success == true
24
+ end
25
+
26
+ def expired?
27
+ return false unless @expires_at
28
+
29
+ @expires_at < Time.now
30
+ end
31
+
32
+ def rate_limited?
33
+ @error_message&.downcase&.include?('rate limit') ||
34
+ @error_code == 'RATE_LIMIT_EXCEEDED'
35
+ end
36
+
37
+ def to_h
38
+ {
39
+ valid: @valid,
40
+ success: @success,
41
+ error_message: @error_message,
42
+ error_code: @error_code,
43
+ expires_at: @expires_at,
44
+ activations_remaining: @activations_remaining,
45
+ retry_after: @retry_after,
46
+ token: @token,
47
+ timestamp: @timestamp,
48
+ rate_limit_remaining: @rate_limit_remaining,
49
+ rate_limit_reset_at: @rate_limit_reset_at,
50
+ }
51
+ end
52
+
53
+ def inspect
54
+ "#<#{self.class.name} valid=#{@valid} success=#{@success} error='#{@error_message}'>"
55
+ end
56
+
57
+ private
58
+
59
+ def initialize_validation_fields
60
+ @valid = extract_value(:valid, false)
61
+ @token = extract_value(:token)
62
+ end
63
+
64
+ def initialize_activation_fields
65
+ @success = extract_value(:success, false)
66
+ @activations_remaining = extract_value(:activations_remaining)
67
+ end
68
+
69
+ def initialize_common_fields
70
+ @error_message = extract_error_message
71
+ @error_code = extract_value(:error_code)
72
+ @expires_at = parse_datetime(extract_value(:expires_at))
73
+ @retry_after = extract_value(:retry_after)
74
+ @timestamp = parse_datetime(extract_value(:timestamp))
75
+ end
76
+
77
+ def initialize_rate_limit_fields
78
+ @rate_limit_remaining = extract_rate_limit_value(:remaining)
79
+ @rate_limit_reset_at = parse_datetime(extract_rate_limit_value(:reset_at))
80
+ end
81
+
82
+ def extract_value(key, default = nil)
83
+ @data[key] || @data[key.to_s] || default
84
+ end
85
+
86
+ def extract_error_message
87
+ extract_value(:error) || extract_value(:message)
88
+ end
89
+
90
+ def extract_rate_limit_value(key)
91
+ @data[:rate_limit]&.dig(key) || @data['rate_limit']&.dig(key.to_s)
92
+ end
93
+
94
+ def parse_datetime(value)
95
+ return nil if value.nil?
96
+ return value if value.is_a?(Time)
97
+ return Time.at(value) if value.is_a?(Numeric)
98
+
99
+ Time.parse(value.to_s)
100
+ rescue ArgumentError
101
+ nil
102
+ end
103
+ end