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,59 @@
1
+ module LicenseChain
2
+ module Services
3
+ class UserService
4
+ def initialize(client)
5
+ @client = client
6
+ end
7
+
8
+ def create(email, name = nil, metadata = {})
9
+ validate_email(email)
10
+
11
+ data = {
12
+ email: email,
13
+ name: name,
14
+ password: 'ChangeMe123!',
15
+ metadata: Utils.sanitize_metadata(metadata)
16
+ }
17
+
18
+ response = @client.post('/auth/register', data)
19
+ User.new(response[:user] || response[:data] || response)
20
+ end
21
+
22
+ def get(user_id)
23
+ Utils.validate_not_empty(user_id, 'user_id')
24
+ response = @client.get('/auth/me')
25
+ User.new(response[:data] || response)
26
+ end
27
+
28
+ def update(user_id, updates = {})
29
+ raise ValidationError, 'User update endpoint is not available in API v1'
30
+ end
31
+
32
+ def delete(user_id)
33
+ raise ValidationError, 'User delete endpoint is not available in API v1'
34
+ end
35
+
36
+ def list(page = 1, limit = 10)
37
+ page, limit = Utils.validate_pagination(page, limit)
38
+ {
39
+ data: [],
40
+ total: 0,
41
+ page: page,
42
+ limit: limit
43
+ }
44
+ end
45
+
46
+ def stats
47
+ UserStats.new({ total: 0, active: 0, inactive: 0 })
48
+ end
49
+
50
+ private
51
+
52
+ def validate_email(email)
53
+ Utils.validate_not_empty(email, 'email')
54
+ raise ValidationError, 'Invalid email format' unless Utils.validate_email(email)
55
+ end
56
+
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,74 @@
1
+ module LicenseChain
2
+ module Services
3
+ class WebhookService
4
+ def initialize(client)
5
+ @client = client
6
+ end
7
+
8
+ def create(url, events, secret = nil)
9
+ validate_webhook_params(url, events)
10
+
11
+ data = {
12
+ url: url,
13
+ events: events,
14
+ secret: secret
15
+ }
16
+
17
+ response = @client.post('/webhooks', data)
18
+ Webhook.new(normalize_webhook_payload(response[:data] || response))
19
+ end
20
+
21
+ def get(webhook_id)
22
+ validate_uuid(webhook_id, 'webhook_id')
23
+
24
+ response = @client.get("/webhooks/#{webhook_id}")
25
+ Webhook.new(normalize_webhook_payload(response[:data] || response))
26
+ end
27
+
28
+ def update(webhook_id, updates = {})
29
+ validate_uuid(webhook_id, 'webhook_id')
30
+
31
+ response = @client.put("/webhooks/#{webhook_id}", Utils.sanitize_metadata(updates))
32
+ Webhook.new(normalize_webhook_payload(response[:data] || response))
33
+ end
34
+
35
+ def delete(webhook_id)
36
+ validate_uuid(webhook_id, 'webhook_id')
37
+
38
+ @client.delete("/webhooks/#{webhook_id}")
39
+ true
40
+ end
41
+
42
+ def list
43
+ response = @client.get('/webhooks')
44
+ (response[:data] || []).map { |webhook| Webhook.new(normalize_webhook_payload(webhook)) }
45
+ end
46
+
47
+ private
48
+
49
+ def validate_webhook_params(url, events)
50
+ Utils.validate_not_empty(url, 'url')
51
+ raise ValidationError, 'Events must be an array' unless events.is_a?(Array)
52
+ raise ValidationError, 'Events cannot be empty' if events.empty?
53
+ end
54
+
55
+ def validate_uuid(id, field_name)
56
+ Utils.validate_not_empty(id, field_name)
57
+ raise ValidationError, "Invalid #{field_name} format" unless Utils.validate_uuid(id)
58
+ end
59
+
60
+ def normalize_webhook_payload(payload)
61
+ {
62
+ id: payload[:id],
63
+ app_id: payload[:app_id] || '',
64
+ url: payload[:url] || '',
65
+ events: payload[:events] || [],
66
+ secret: payload[:secret],
67
+ status: ((payload[:active].nil? || payload[:active]) ? 'active' : 'inactive'),
68
+ created_at: payload[:created_at] || payload[:createdAt],
69
+ updated_at: payload[:updated_at] || payload[:updatedAt]
70
+ }
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,211 @@
1
+ module LicenseChain
2
+ module Utils
3
+ def self.validate_email(email)
4
+ email_regex = /\A[^\s@]+@[^\s@]+\.[^\s@]+\z/
5
+ email_regex.match?(email)
6
+ end
7
+
8
+ def self.validate_license_key(license_key)
9
+ return false if license_key.nil? || license_key.length != 32
10
+ license_key.match?(/\A[A-Z0-9]+\z/)
11
+ end
12
+
13
+ def self.validate_uuid(uuid)
14
+ uuid_regex = /\A[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\z/i
15
+ uuid_regex.match?(uuid)
16
+ end
17
+
18
+ def self.validate_amount(amount)
19
+ amount.is_a?(Numeric) && amount > 0 && amount.finite?
20
+ end
21
+
22
+ def self.validate_currency(currency)
23
+ valid_currencies = %w[USD EUR GBP CAD AUD JPY CHF CNY]
24
+ valid_currencies.include?(currency.upcase)
25
+ end
26
+
27
+ def self.validate_status(status, allowed_statuses)
28
+ allowed_statuses.include?(status)
29
+ end
30
+
31
+ def self.sanitize_input(input)
32
+ return input unless input.is_a?(String)
33
+ input.gsub(/[<>&"']/, {
34
+ '<' => '&lt;',
35
+ '>' => '&gt;',
36
+ '&' => '&amp;',
37
+ '"' => '&quot;',
38
+ "'" => '&#x27;'
39
+ })
40
+ end
41
+
42
+ def self.sanitize_metadata(metadata)
43
+ return metadata unless metadata.is_a?(Hash)
44
+ metadata.transform_values do |value|
45
+ if value.is_a?(String)
46
+ sanitize_input(value)
47
+ else
48
+ value
49
+ end
50
+ end
51
+ end
52
+
53
+ def self.generate_license_key
54
+ charset = ('A'..'Z').to_a + ('0'..'9').to_a
55
+ (0...32).map { charset.sample }.join
56
+ end
57
+
58
+ def self.generate_uuid
59
+ SecureRandom.uuid
60
+ end
61
+
62
+ def self.format_timestamp(timestamp)
63
+ Time.at(timestamp).utc.iso8601
64
+ end
65
+
66
+ def self.parse_timestamp(timestamp)
67
+ Time.parse(timestamp).to_i
68
+ rescue ArgumentError
69
+ raise ValidationError, "Invalid timestamp format: #{timestamp}"
70
+ end
71
+
72
+ def self.validate_pagination(page, limit)
73
+ page = [page || 1, 1].max
74
+ limit = [[limit || 10, 1].max, 100].min
75
+ [page, limit]
76
+ end
77
+
78
+ def self.validate_date_range(start_date, end_date)
79
+ start_time = parse_timestamp(start_date)
80
+ end_time = parse_timestamp(end_date)
81
+
82
+ if start_time > end_time
83
+ raise ValidationError, "Start date must be before or equal to end date"
84
+ end
85
+ rescue ValidationError
86
+ raise
87
+ rescue => e
88
+ raise ValidationError, "Invalid date format: #{e.message}"
89
+ end
90
+
91
+ def self.create_webhook_signature(payload, secret)
92
+ require 'openssl'
93
+ digest = OpenSSL::Digest.new('sha256')
94
+ OpenSSL::HMAC.hexdigest(digest, secret, payload)
95
+ end
96
+
97
+ def self.verify_webhook_signature(payload, signature, secret)
98
+ expected_signature = create_webhook_signature(payload, secret)
99
+ require 'securerandom'
100
+ SecureRandom.secure_compare(signature, expected_signature)
101
+ end
102
+
103
+ def self.default_hwuid
104
+ require 'socket'
105
+ require 'rbconfig'
106
+ require 'digest'
107
+ raw = [
108
+ 'licensechain',
109
+ 'ruby',
110
+ Socket.gethostname,
111
+ RbConfig::CONFIG['host_os'],
112
+ RbConfig::CONFIG['host_cpu']
113
+ ].join('|')
114
+ Digest::SHA256.hexdigest(raw)
115
+ end
116
+
117
+ def self.retry_with_backoff(max_retries = 3, initial_delay = 1.0)
118
+ delay = initial_delay
119
+
120
+ (0..max_retries).each do |attempt|
121
+ begin
122
+ return yield
123
+ rescue => e
124
+ if attempt == max_retries
125
+ raise e
126
+ end
127
+
128
+ sleep(delay)
129
+ delay *= 2
130
+ end
131
+ end
132
+ end
133
+
134
+ def self.format_bytes(bytes)
135
+ units = %w[B KB MB GB TB PB]
136
+ threshold = 1024
137
+
138
+ return "#{bytes} B" if bytes < threshold
139
+
140
+ size = bytes.to_f
141
+ unit_index = 0
142
+
143
+ while size >= threshold && unit_index < units.length - 1
144
+ size /= threshold
145
+ unit_index += 1
146
+ end
147
+
148
+ "#{size.round(1)} #{units[unit_index]}"
149
+ end
150
+
151
+ def self.format_duration(seconds)
152
+ if seconds < 60
153
+ "#{seconds}s"
154
+ elsif seconds < 3600
155
+ minutes = seconds / 60
156
+ remaining_seconds = seconds % 60
157
+ "#{minutes}m #{remaining_seconds}s"
158
+ elsif seconds < 86400
159
+ hours = seconds / 3600
160
+ minutes = (seconds % 3600) / 60
161
+ "#{hours}h #{minutes}m"
162
+ else
163
+ days = seconds / 86400
164
+ hours = (seconds % 86400) / 3600
165
+ "#{days}d #{hours}h"
166
+ end
167
+ end
168
+
169
+ def self.capitalize_first(str)
170
+ return str if str.empty?
171
+ str[0].upcase + str[1..-1].downcase
172
+ end
173
+
174
+ def self.to_snake_case(str)
175
+ str.gsub(/([A-Z])/, '_\1').downcase.gsub(/^_/, '')
176
+ end
177
+
178
+ def self.to_pascal_case(str)
179
+ str.split('_').map(&:capitalize).join
180
+ end
181
+
182
+ def self.truncate_string(str, max_length)
183
+ return str if str.length <= max_length
184
+ "#{str[0...max_length - 3]}..."
185
+ end
186
+
187
+ def self.remove_special_chars(str)
188
+ str.gsub(/[^a-zA-Z0-9\s]/, '')
189
+ end
190
+
191
+ def self.slugify(str)
192
+ str.downcase
193
+ .gsub(/[^a-z0-9\s-]/, '')
194
+ .gsub(/\s+/, '-')
195
+ .gsub(/-+/, '-')
196
+ .gsub(/^-|-$/, '')
197
+ end
198
+
199
+ def self.validate_not_empty(str, field_name)
200
+ raise ValidationError, "#{field_name} cannot be empty" if str.nil? || str.strip.empty?
201
+ end
202
+
203
+ def self.validate_positive(number, field_name)
204
+ raise ValidationError, "#{field_name} must be positive" if number.nil? || number <= 0
205
+ end
206
+
207
+ def self.validate_range(number, min, max, field_name)
208
+ raise ValidationError, "#{field_name} must be between #{min} and #{max}" if number < min || number > max
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,157 @@
1
+ require 'openssl'
2
+ require 'json'
3
+ require 'time'
4
+
5
+ module LicenseChain
6
+ class WebhookHandler
7
+ attr_reader :secret, :tolerance
8
+
9
+ def initialize(secret, tolerance = 300)
10
+ @secret = secret
11
+ @tolerance = tolerance
12
+ end
13
+
14
+ def verify_signature(payload, signature)
15
+ Utils.verify_webhook_signature(payload, signature, @secret)
16
+ end
17
+
18
+ def verify_timestamp(timestamp)
19
+ webhook_time = Time.parse(timestamp)
20
+ current_time = Time.now
21
+ time_diff = (current_time - webhook_time).abs
22
+
23
+ if time_diff > @tolerance
24
+ raise ValidationError, "Webhook timestamp too old: #{time_diff} seconds"
25
+ end
26
+ rescue ArgumentError => e
27
+ raise ValidationError, "Invalid timestamp format: #{e.message}"
28
+ end
29
+
30
+ def verify_webhook(payload, signature, timestamp)
31
+ verify_timestamp(timestamp)
32
+
33
+ unless verify_signature(payload, signature)
34
+ raise AuthenticationError, "Invalid webhook signature"
35
+ end
36
+ end
37
+
38
+ def process_event(event_data)
39
+ payload = event_data[:data].to_json
40
+ verify_webhook(payload, event_data[:signature], event_data[:timestamp])
41
+
42
+ case event_data[:type]
43
+ when 'license.created'
44
+ handle_license_created(event_data)
45
+ when 'license.updated'
46
+ handle_license_updated(event_data)
47
+ when 'license.revoked'
48
+ handle_license_revoked(event_data)
49
+ when 'license.expired'
50
+ handle_license_expired(event_data)
51
+ when 'user.created'
52
+ handle_user_created(event_data)
53
+ when 'user.updated'
54
+ handle_user_updated(event_data)
55
+ when 'user.deleted'
56
+ handle_user_deleted(event_data)
57
+ when 'product.created'
58
+ handle_product_created(event_data)
59
+ when 'product.updated'
60
+ handle_product_updated(event_data)
61
+ when 'product.deleted'
62
+ handle_product_deleted(event_data)
63
+ when 'payment.completed'
64
+ handle_payment_completed(event_data)
65
+ when 'payment.failed'
66
+ handle_payment_failed(event_data)
67
+ when 'payment.refunded'
68
+ handle_payment_refunded(event_data)
69
+ else
70
+ puts "Unknown webhook event type: #{event_data[:type]}"
71
+ end
72
+ end
73
+
74
+ private
75
+
76
+ def handle_license_created(event_data)
77
+ puts "License created: #{event_data[:id]}"
78
+ # Add custom logic for license created event
79
+ end
80
+
81
+ def handle_license_updated(event_data)
82
+ puts "License updated: #{event_data[:id]}"
83
+ # Add custom logic for license updated event
84
+ end
85
+
86
+ def handle_license_revoked(event_data)
87
+ puts "License revoked: #{event_data[:id]}"
88
+ # Add custom logic for license revoked event
89
+ end
90
+
91
+ def handle_license_expired(event_data)
92
+ puts "License expired: #{event_data[:id]}"
93
+ # Add custom logic for license expired event
94
+ end
95
+
96
+ def handle_user_created(event_data)
97
+ puts "User created: #{event_data[:id]}"
98
+ # Add custom logic for user created event
99
+ end
100
+
101
+ def handle_user_updated(event_data)
102
+ puts "User updated: #{event_data[:id]}"
103
+ # Add custom logic for user updated event
104
+ end
105
+
106
+ def handle_user_deleted(event_data)
107
+ puts "User deleted: #{event_data[:id]}"
108
+ # Add custom logic for user deleted event
109
+ end
110
+
111
+ def handle_product_created(event_data)
112
+ puts "Product created: #{event_data[:id]}"
113
+ # Add custom logic for product created event
114
+ end
115
+
116
+ def handle_product_updated(event_data)
117
+ puts "Product updated: #{event_data[:id]}"
118
+ # Add custom logic for product updated event
119
+ end
120
+
121
+ def handle_product_deleted(event_data)
122
+ puts "Product deleted: #{event_data[:id]}"
123
+ # Add custom logic for product deleted event
124
+ end
125
+
126
+ def handle_payment_completed(event_data)
127
+ puts "Payment completed: #{event_data[:id]}"
128
+ # Add custom logic for payment completed event
129
+ end
130
+
131
+ def handle_payment_failed(event_data)
132
+ puts "Payment failed: #{event_data[:id]}"
133
+ # Add custom logic for payment failed event
134
+ end
135
+
136
+ def handle_payment_refunded(event_data)
137
+ puts "Payment refunded: #{event_data[:id]}"
138
+ # Add custom logic for payment refunded event
139
+ end
140
+ end
141
+
142
+ module WebhookEvents
143
+ LICENSE_CREATED = 'license.created'
144
+ LICENSE_UPDATED = 'license.updated'
145
+ LICENSE_REVOKED = 'license.revoked'
146
+ LICENSE_EXPIRED = 'license.expired'
147
+ USER_CREATED = 'user.created'
148
+ USER_UPDATED = 'user.updated'
149
+ USER_DELETED = 'user.deleted'
150
+ PRODUCT_CREATED = 'product.created'
151
+ PRODUCT_UPDATED = 'product.updated'
152
+ PRODUCT_DELETED = 'product.deleted'
153
+ PAYMENT_COMPLETED = 'payment.completed'
154
+ PAYMENT_FAILED = 'payment.failed'
155
+ PAYMENT_REFUNDED = 'payment.refunded'
156
+ end
157
+ end
@@ -0,0 +1,184 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+ require "json"
5
+
6
+ module LicenseChain
7
+ class WebhookVerifier
8
+ attr_reader :secret
9
+
10
+ def initialize(secret)
11
+ @secret = secret
12
+ end
13
+
14
+ # Verify webhook signature
15
+ def verify_signature(payload, signature, algorithm: "sha256")
16
+ expected_signature = generate_signature(payload, algorithm)
17
+ secure_compare(signature, expected_signature)
18
+ end
19
+
20
+ # Parse and verify webhook payload
21
+ def parse_payload(payload, signature, algorithm: "sha256")
22
+ raise ValidationError, "Invalid signature" unless verify_signature(payload, signature, algorithm: algorithm)
23
+
24
+ JSON.parse(payload)
25
+ rescue JSON::ParserError => e
26
+ raise ValidationError, "Invalid JSON payload: #{e.message}"
27
+ end
28
+
29
+ # Generate signature for testing
30
+ def generate_signature(payload, algorithm = "sha256")
31
+ digest = case algorithm.downcase
32
+ when "sha1"
33
+ OpenSSL::Digest::SHA1
34
+ when "sha256"
35
+ OpenSSL::Digest::SHA256
36
+ when "sha512"
37
+ OpenSSL::Digest::SHA512
38
+ else
39
+ raise ArgumentError, "Unsupported algorithm: #{algorithm}"
40
+ end
41
+
42
+ signature = OpenSSL::HMAC.hexdigest(digest.new, @secret, payload)
43
+ "#{algorithm}=#{signature}"
44
+ end
45
+
46
+ # Verify webhook event type
47
+ def verify_event_type(payload, expected_type)
48
+ event_type = payload.dig("type") || payload.dig("event")
49
+ event_type == expected_type
50
+ end
51
+
52
+ # Extract event data
53
+ def extract_event_data(payload)
54
+ {
55
+ id: payload["id"],
56
+ type: payload["type"] || payload["event"],
57
+ created_at: payload["created_at"],
58
+ data: payload["data"] || payload["object"]
59
+ }
60
+ end
61
+
62
+ # Verify webhook timestamp (prevent replay attacks)
63
+ def verify_timestamp(payload, tolerance: 300) # 5 minutes
64
+ timestamp = payload["timestamp"] || payload["created_at"]
65
+ return true unless timestamp
66
+
67
+ event_time = Time.parse(timestamp)
68
+ current_time = Time.now
69
+
70
+ (current_time - event_time).abs <= tolerance
71
+ rescue ArgumentError
72
+ false
73
+ end
74
+
75
+ private
76
+
77
+ def secure_compare(a, b)
78
+ return false if a.nil? || b.nil?
79
+ return false if a.length != b.length
80
+
81
+ result = 0
82
+ a.bytes.zip(b.bytes) { |x, y| result |= x ^ y }
83
+ result == 0
84
+ end
85
+ end
86
+
87
+ # Webhook event handler
88
+ class WebhookHandler
89
+ attr_reader :verifier
90
+
91
+ def initialize(secret)
92
+ @verifier = WebhookVerifier.new(secret)
93
+ end
94
+
95
+ def handle(payload, signature, algorithm: "sha256")
96
+ # Parse and verify the payload
97
+ data = @verifier.parse_payload(payload, signature, algorithm: algorithm)
98
+
99
+ # Verify timestamp to prevent replay attacks
100
+ unless @verifier.verify_timestamp(data)
101
+ raise ValidationError, "Webhook timestamp is too old"
102
+ end
103
+
104
+ # Extract event information
105
+ event_data = @verifier.extract_event_data(data)
106
+
107
+ # Route to appropriate handler
108
+ handle_event(event_data)
109
+ end
110
+
111
+ private
112
+
113
+ def handle_event(event_data)
114
+ case event_data[:type]
115
+ when "license.created"
116
+ handle_license_created(event_data)
117
+ when "license.updated"
118
+ handle_license_updated(event_data)
119
+ when "license.revoked"
120
+ handle_license_revoked(event_data)
121
+ when "license.expired"
122
+ handle_license_expired(event_data)
123
+ when "license.validated"
124
+ handle_license_validated(event_data)
125
+ when "app.created"
126
+ handle_app_created(event_data)
127
+ when "app.updated"
128
+ handle_app_updated(event_data)
129
+ when "app.deleted"
130
+ handle_app_deleted(event_data)
131
+ when "user.created"
132
+ handle_user_created(event_data)
133
+ when "user.updated"
134
+ handle_user_updated(event_data)
135
+ else
136
+ handle_unknown_event(event_data)
137
+ end
138
+ end
139
+
140
+ def handle_license_created(event_data)
141
+ # Override in subclass
142
+ end
143
+
144
+ def handle_license_updated(event_data)
145
+ # Override in subclass
146
+ end
147
+
148
+ def handle_license_revoked(event_data)
149
+ # Override in subclass
150
+ end
151
+
152
+ def handle_license_expired(event_data)
153
+ # Override in subclass
154
+ end
155
+
156
+ def handle_license_validated(event_data)
157
+ # Override in subclass
158
+ end
159
+
160
+ def handle_app_created(event_data)
161
+ # Override in subclass
162
+ end
163
+
164
+ def handle_app_updated(event_data)
165
+ # Override in subclass
166
+ end
167
+
168
+ def handle_app_deleted(event_data)
169
+ # Override in subclass
170
+ end
171
+
172
+ def handle_user_created(event_data)
173
+ # Override in subclass
174
+ end
175
+
176
+ def handle_user_updated(event_data)
177
+ # Override in subclass
178
+ end
179
+
180
+ def handle_unknown_event(event_data)
181
+ # Override in subclass
182
+ end
183
+ end
184
+ end
@@ -0,0 +1,3 @@
1
+ module LicenseChainRubySdk
2
+ VERSION = '1.0.0'
3
+ end