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,174 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LicenseChain
|
|
4
|
+
class LicenseValidator
|
|
5
|
+
attr_reader :client
|
|
6
|
+
|
|
7
|
+
def initialize(api_key: nil, base_url: nil, timeout: nil, retry_attempts: nil)
|
|
8
|
+
@client = Client.new(
|
|
9
|
+
api_key: api_key,
|
|
10
|
+
base_url: base_url,
|
|
11
|
+
timeout: timeout,
|
|
12
|
+
retry_attempts: retry_attempts
|
|
13
|
+
)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Validate a license key
|
|
17
|
+
def validate_license(license_key, app_id: nil)
|
|
18
|
+
response = @client.validate_license(license_key, app_id: app_id)
|
|
19
|
+
|
|
20
|
+
ValidationResult.new(
|
|
21
|
+
valid: response["valid"],
|
|
22
|
+
license: response["license"],
|
|
23
|
+
user: response["user"],
|
|
24
|
+
app: response["app"],
|
|
25
|
+
expires_at: response["expires_at"],
|
|
26
|
+
metadata: response["metadata"],
|
|
27
|
+
error: response["error"]
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Check if license is valid without full validation
|
|
32
|
+
def valid?(license_key, app_id: nil)
|
|
33
|
+
result = validate_license(license_key, app_id: app_id)
|
|
34
|
+
result.valid?
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Get license information
|
|
38
|
+
def get_license_info(license_key, app_id: nil)
|
|
39
|
+
result = validate_license(license_key, app_id: app_id)
|
|
40
|
+
result.license
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Check if license is expired
|
|
44
|
+
def expired?(license_key, app_id: nil)
|
|
45
|
+
result = validate_license(license_key, app_id: app_id)
|
|
46
|
+
return true unless result.valid?
|
|
47
|
+
|
|
48
|
+
return false unless result.expires_at
|
|
49
|
+
|
|
50
|
+
Time.parse(result.expires_at) < Time.now
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Get days until expiration
|
|
54
|
+
def days_until_expiration(license_key, app_id: nil)
|
|
55
|
+
result = validate_license(license_key, app_id: app_id)
|
|
56
|
+
return nil unless result.valid? && result.expires_at
|
|
57
|
+
|
|
58
|
+
expires_at = Time.parse(result.expires_at)
|
|
59
|
+
((expires_at - Time.now) / 1.day).ceil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Batch validate multiple licenses
|
|
63
|
+
def validate_licenses(license_keys, app_id: nil)
|
|
64
|
+
license_keys.map do |license_key|
|
|
65
|
+
validate_license(license_key, app_id: app_id)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Validate with custom validation rules
|
|
70
|
+
def validate_with_rules(license_key, app_id: nil, rules: {})
|
|
71
|
+
result = validate_license(license_key, app_id: app_id)
|
|
72
|
+
return result unless result.valid?
|
|
73
|
+
|
|
74
|
+
# Apply custom validation rules
|
|
75
|
+
if rules[:max_usage] && result.license&.dig("usage_count").to_i > rules[:max_usage]
|
|
76
|
+
result.instance_variable_set(:@valid, false)
|
|
77
|
+
result.instance_variable_set(:@error, "Usage limit exceeded")
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
if rules[:allowed_features] && result.license&.dig("features")
|
|
81
|
+
allowed_features = rules[:allowed_features]
|
|
82
|
+
license_features = result.license["features"] || []
|
|
83
|
+
invalid_features = license_features - allowed_features
|
|
84
|
+
|
|
85
|
+
if invalid_features.any?
|
|
86
|
+
result.instance_variable_set(:@valid, false)
|
|
87
|
+
result.instance_variable_set(:@error, "Invalid features: #{invalid_features.join(', ')}")
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
result
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Result object for license validation
|
|
96
|
+
class ValidationResult
|
|
97
|
+
attr_reader :valid, :license, :user, :app, :expires_at, :metadata, :error
|
|
98
|
+
|
|
99
|
+
def initialize(valid:, license: nil, user: nil, app: nil, expires_at: nil, metadata: nil, error: nil)
|
|
100
|
+
@valid = valid
|
|
101
|
+
@license = license
|
|
102
|
+
@user = user
|
|
103
|
+
@app = app
|
|
104
|
+
@expires_at = expires_at
|
|
105
|
+
@metadata = metadata || {}
|
|
106
|
+
@error = error
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def valid?
|
|
110
|
+
@valid
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def invalid?
|
|
114
|
+
!@valid
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def expired?
|
|
118
|
+
return false unless @expires_at
|
|
119
|
+
Time.parse(@expires_at) < Time.now
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def user_email
|
|
123
|
+
@user&.dig("email")
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def user_name
|
|
127
|
+
@user&.dig("name")
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def app_name
|
|
131
|
+
@app&.dig("name")
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def license_key
|
|
135
|
+
@license&.dig("key")
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def license_id
|
|
139
|
+
@license&.dig("id")
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def features
|
|
143
|
+
@license&.dig("features") || []
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def usage_count
|
|
147
|
+
@license&.dig("usage_count") || 0
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def created_at
|
|
151
|
+
@license&.dig("created_at")
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def updated_at
|
|
155
|
+
@license&.dig("updated_at")
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def to_h
|
|
159
|
+
{
|
|
160
|
+
valid: @valid,
|
|
161
|
+
license: @license,
|
|
162
|
+
user: @user,
|
|
163
|
+
app: @app,
|
|
164
|
+
expires_at: @expires_at,
|
|
165
|
+
metadata: @metadata,
|
|
166
|
+
error: @error
|
|
167
|
+
}
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def to_json(*args)
|
|
171
|
+
to_h.to_json(*args)
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LicenseChain
|
|
4
|
+
# Base model class
|
|
5
|
+
class Model
|
|
6
|
+
include Enumerable
|
|
7
|
+
|
|
8
|
+
def initialize(attributes = {})
|
|
9
|
+
@attributes = attributes.dup
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def [](key)
|
|
13
|
+
@attributes[key.to_s]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def []=(key, value)
|
|
17
|
+
@attributes[key.to_s] = value
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def each(&block)
|
|
21
|
+
@attributes.each(&block)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def keys
|
|
25
|
+
@attributes.keys
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def values
|
|
29
|
+
@attributes.values
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def to_h
|
|
33
|
+
@attributes.dup
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def to_json(*args)
|
|
37
|
+
@attributes.to_json(*args)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def inspect
|
|
41
|
+
"#<#{self.class.name}:#{object_id} #{@attributes.inspect}>"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def ==(other)
|
|
45
|
+
other.is_a?(self.class) && @attributes == other.instance_variable_get(:@attributes)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def method_missing(method_name, *args, &block)
|
|
49
|
+
if @attributes.key?(method_name.to_s)
|
|
50
|
+
@attributes[method_name.to_s]
|
|
51
|
+
elsif @attributes.key?(method_name.to_s.chomp("="))
|
|
52
|
+
@attributes[method_name.to_s.chomp("=")] = args.first
|
|
53
|
+
else
|
|
54
|
+
super
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
59
|
+
@attributes.key?(method_name.to_s) || @attributes.key?(method_name.to_s.chomp("=")) || super
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# User model
|
|
64
|
+
class User < Model
|
|
65
|
+
def id
|
|
66
|
+
self["id"]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def email
|
|
70
|
+
self["email"]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def name
|
|
74
|
+
self["name"]
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def company
|
|
78
|
+
self["company"]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def created_at
|
|
82
|
+
Time.parse(self["created_at"]) if self["created_at"]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def updated_at
|
|
86
|
+
Time.parse(self["updated_at"]) if self["updated_at"]
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def email_verified?
|
|
90
|
+
self["email_verified"] == true
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def active?
|
|
94
|
+
self["status"] == "active"
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Application model
|
|
99
|
+
class Application < Model
|
|
100
|
+
def id
|
|
101
|
+
self["id"]
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def name
|
|
105
|
+
self["name"]
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def description
|
|
109
|
+
self["description"]
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def api_key
|
|
113
|
+
self["api_key"]
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def webhook_url
|
|
117
|
+
self["webhook_url"]
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def allowed_origins
|
|
121
|
+
self["allowed_origins"] || []
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def created_at
|
|
125
|
+
Time.parse(self["created_at"]) if self["created_at"]
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def updated_at
|
|
129
|
+
Time.parse(self["updated_at"]) if self["updated_at"]
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def active?
|
|
133
|
+
self["status"] == "active"
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def license_count
|
|
137
|
+
self["license_count"] || 0
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# License model
|
|
142
|
+
class License < Model
|
|
143
|
+
def id
|
|
144
|
+
self["id"]
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def key
|
|
148
|
+
self["key"]
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def app_id
|
|
152
|
+
self["app_id"]
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def user_id
|
|
156
|
+
self["user_id"]
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def user_email
|
|
160
|
+
self["user_email"]
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def user_name
|
|
164
|
+
self["user_name"]
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def status
|
|
168
|
+
self["status"]
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def expires_at
|
|
172
|
+
Time.parse(self["expires_at"]) if self["expires_at"]
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def created_at
|
|
176
|
+
Time.parse(self["created_at"]) if self["created_at"]
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def updated_at
|
|
180
|
+
Time.parse(self["updated_at"]) if self["updated_at"]
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def metadata
|
|
184
|
+
self["metadata"] || {}
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def features
|
|
188
|
+
self["features"] || []
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def usage_count
|
|
192
|
+
self["usage_count"] || 0
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def active?
|
|
196
|
+
self["status"] == "active"
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def expired?
|
|
200
|
+
return false unless expires_at
|
|
201
|
+
expires_at < Time.now
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def revoked?
|
|
205
|
+
self["status"] == "revoked"
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def days_until_expiration
|
|
209
|
+
return nil unless expires_at
|
|
210
|
+
((expires_at - Time.now) / 1.day).ceil
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Webhook model
|
|
215
|
+
class Webhook < Model
|
|
216
|
+
def id
|
|
217
|
+
self["id"]
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def app_id
|
|
221
|
+
self["app_id"]
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def url
|
|
225
|
+
self["url"]
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def events
|
|
229
|
+
self["events"] || []
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def secret
|
|
233
|
+
self["secret"]
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def active?
|
|
237
|
+
self["status"] == "active"
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def created_at
|
|
241
|
+
Time.parse(self["created_at"]) if self["created_at"]
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def updated_at
|
|
245
|
+
Time.parse(self["updated_at"]) if self["updated_at"]
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def last_triggered_at
|
|
249
|
+
Time.parse(self["last_triggered_at"]) if self["last_triggered_at"]
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def failure_count
|
|
253
|
+
self["failure_count"] || 0
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Analytics model
|
|
258
|
+
class Analytics < Model
|
|
259
|
+
def total_licenses
|
|
260
|
+
self["total_licenses"] || 0
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def active_licenses
|
|
264
|
+
self["active_licenses"] || 0
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def expired_licenses
|
|
268
|
+
self["expired_licenses"] || 0
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def revoked_licenses
|
|
272
|
+
self["revoked_licenses"] || 0
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def validations_today
|
|
276
|
+
self["validations_today"] || 0
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def validations_this_week
|
|
280
|
+
self["validations_this_week"] || 0
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def validations_this_month
|
|
284
|
+
self["validations_this_month"] || 0
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def top_features
|
|
288
|
+
self["top_features"] || []
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def usage_by_day
|
|
292
|
+
self["usage_by_day"] || []
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def usage_by_week
|
|
296
|
+
self["usage_by_week"] || []
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def usage_by_month
|
|
300
|
+
self["usage_by_month"] || []
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# Paginated response model
|
|
305
|
+
class PaginatedResponse < Model
|
|
306
|
+
def data
|
|
307
|
+
self["data"] || []
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def page
|
|
311
|
+
self["page"] || 1
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def limit
|
|
315
|
+
self["limit"] || 20
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def total
|
|
319
|
+
self["total"] || 0
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def total_pages
|
|
323
|
+
self["total_pages"] || 1
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def has_next_page?
|
|
327
|
+
page < total_pages
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def has_previous_page?
|
|
331
|
+
page > 1
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def next_page
|
|
335
|
+
has_next_page? ? page + 1 : nil
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def previous_page
|
|
339
|
+
has_previous_page? ? page - 1 : nil
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def each(&block)
|
|
343
|
+
data.each(&block)
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def size
|
|
347
|
+
data.size
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def empty?
|
|
351
|
+
data.empty?
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
module LicenseChain
|
|
2
|
+
module Services
|
|
3
|
+
class LicenseService
|
|
4
|
+
def initialize(client)
|
|
5
|
+
@client = client
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def create(app_id, user_email, metadata = {})
|
|
9
|
+
Utils.validate_not_empty(app_id, 'app_id')
|
|
10
|
+
Utils.validate_not_empty(user_email, 'user_email')
|
|
11
|
+
|
|
12
|
+
data = {
|
|
13
|
+
appId: app_id,
|
|
14
|
+
plan: 'FREE',
|
|
15
|
+
issuedEmail: user_email,
|
|
16
|
+
metadata: Utils.sanitize_metadata(metadata)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
response = @client.post("/apps/#{app_id}/licenses", data)
|
|
20
|
+
License.new(normalize_license_payload(response[:data] || response))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def get(license_id)
|
|
24
|
+
Utils.validate_not_empty(license_id, 'license_id')
|
|
25
|
+
|
|
26
|
+
response = @client.get("/licenses/#{license_id}")
|
|
27
|
+
License.new(normalize_license_payload(response[:data] || response))
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def update(license_id, updates = {})
|
|
31
|
+
Utils.validate_not_empty(license_id, 'license_id')
|
|
32
|
+
|
|
33
|
+
response = @client.patch("/licenses/#{license_id}", Utils.sanitize_metadata(updates))
|
|
34
|
+
License.new(normalize_license_payload(response[:data] || response))
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def revoke(license_id)
|
|
38
|
+
Utils.validate_not_empty(license_id, 'license_id')
|
|
39
|
+
|
|
40
|
+
@client.delete("/licenses/#{license_id}")
|
|
41
|
+
true
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def validate(license_key, hwuid = nil)
|
|
45
|
+
Utils.validate_not_empty(license_key, 'license_key')
|
|
46
|
+
body = { key: license_key }
|
|
47
|
+
body[:hwuid] = hwuid.to_s.strip != '' ? hwuid.to_s.strip : Utils.default_hwuid
|
|
48
|
+
response = @client.post('/licenses/verify', body)
|
|
49
|
+
response[:valid]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Full POST /licenses/verify body (valid, optional license_token, license_jwks_uri, etc.).
|
|
53
|
+
def verify_with_details(license_key, hwuid = nil)
|
|
54
|
+
Utils.validate_not_empty(license_key, 'license_key')
|
|
55
|
+
body = { key: license_key }
|
|
56
|
+
body[:hwuid] = hwuid.to_s.strip != '' ? hwuid.to_s.strip : Utils.default_hwuid
|
|
57
|
+
@client.post('/licenses/verify', body)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def list_user_licenses(user_id, page = 1, limit = 10)
|
|
61
|
+
Utils.validate_not_empty(user_id, 'user_id')
|
|
62
|
+
page, limit = Utils.validate_pagination(page, limit)
|
|
63
|
+
|
|
64
|
+
response = @client.get('/licenses', { page: page, limit: limit })
|
|
65
|
+
items = response[:data] || response[:licenses] || []
|
|
66
|
+
filtered = items.select do |license|
|
|
67
|
+
license[:issuedEmail] == user_id || license[:email] == user_id || license[:user_id] == user_id
|
|
68
|
+
end
|
|
69
|
+
{
|
|
70
|
+
data: filtered.map { |license| License.new(normalize_license_payload(license)) },
|
|
71
|
+
total: filtered.length,
|
|
72
|
+
page: page,
|
|
73
|
+
limit: limit
|
|
74
|
+
}
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def stats
|
|
78
|
+
response = @client.get('/licenses/stats')
|
|
79
|
+
LicenseStats.new(response[:data] || response)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def validate_uuid(id, field_name)
|
|
85
|
+
Utils.validate_not_empty(id, field_name)
|
|
86
|
+
raise ValidationError, "Invalid #{field_name} format" unless Utils.validate_uuid(id)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def normalize_license_payload(payload)
|
|
90
|
+
{
|
|
91
|
+
id: payload[:id],
|
|
92
|
+
key: payload[:key] || payload[:licenseKey],
|
|
93
|
+
app_id: payload[:app_id] || payload[:appId] || '',
|
|
94
|
+
user_id: payload[:user_id],
|
|
95
|
+
user_email: payload[:user_email] || payload[:issuedEmail] || payload[:email] || '',
|
|
96
|
+
user_name: payload[:user_name] || payload[:issuedTo],
|
|
97
|
+
status: (payload[:status] || 'active').to_s.downcase,
|
|
98
|
+
expires_at: payload[:expires_at] || payload[:expiresAt],
|
|
99
|
+
created_at: payload[:created_at] || payload[:createdAt],
|
|
100
|
+
updated_at: payload[:updated_at] || payload[:updatedAt],
|
|
101
|
+
metadata: payload[:metadata] || {},
|
|
102
|
+
features: payload[:features] || [],
|
|
103
|
+
usage_count: payload[:usage_count] || 0
|
|
104
|
+
}
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
module LicenseChain
|
|
2
|
+
module Services
|
|
3
|
+
class ProductService
|
|
4
|
+
def initialize(client)
|
|
5
|
+
@client = client
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def create(name, description = nil, price = nil, currency = 'USD', metadata = {})
|
|
9
|
+
validate_required_params(name, price, currency)
|
|
10
|
+
raise ValidationError, 'Product endpoints are not available in API v1'
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def get(product_id)
|
|
14
|
+
raise ValidationError, 'Product endpoints are not available in API v1'
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def update(product_id, updates = {})
|
|
18
|
+
raise ValidationError, 'Product endpoints are not available in API v1'
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def delete(product_id)
|
|
22
|
+
raise ValidationError, 'Product endpoints are not available in API v1'
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def list(page = 1, limit = 10)
|
|
26
|
+
page, limit = Utils.validate_pagination(page, limit)
|
|
27
|
+
{
|
|
28
|
+
data: [],
|
|
29
|
+
total: 0,
|
|
30
|
+
page: page,
|
|
31
|
+
limit: limit
|
|
32
|
+
}
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def stats
|
|
36
|
+
ProductStats.new({ total: 0, active: 0, revenue: 0 })
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def validate_required_params(name, price, currency)
|
|
42
|
+
Utils.validate_not_empty(name, 'name')
|
|
43
|
+
Utils.validate_positive(price, 'price')
|
|
44
|
+
raise ValidationError, 'Invalid currency' unless Utils.validate_currency(currency)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|