evoleap-licensing 1.0.0.12

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.
Files changed (31) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +480 -0
  3. data/lib/evoleap_licensing/configuration.rb +34 -0
  4. data/lib/evoleap_licensing/control_logic.rb +38 -0
  5. data/lib/evoleap_licensing/control_manager_helper.rb +230 -0
  6. data/lib/evoleap_licensing/control_strategy.rb +16 -0
  7. data/lib/evoleap_licensing/encryption_handler.rb +42 -0
  8. data/lib/evoleap_licensing/errors.rb +9 -0
  9. data/lib/evoleap_licensing/identity/instance_identity.rb +39 -0
  10. data/lib/evoleap_licensing/identity/user_identity.rb +32 -0
  11. data/lib/evoleap_licensing/platform_info.rb +75 -0
  12. data/lib/evoleap_licensing/results/component_checkin_result.rb +16 -0
  13. data/lib/evoleap_licensing/results/component_checkout_result.rb +18 -0
  14. data/lib/evoleap_licensing/results/components_status.rb +18 -0
  15. data/lib/evoleap_licensing/results/instance_validity.rb +46 -0
  16. data/lib/evoleap_licensing/results/license_info.rb +19 -0
  17. data/lib/evoleap_licensing/results/registration_result.rb +16 -0
  18. data/lib/evoleap_licensing/results/session_validity.rb +47 -0
  19. data/lib/evoleap_licensing/server_control_manager.rb +50 -0
  20. data/lib/evoleap_licensing/state/server_state.rb +78 -0
  21. data/lib/evoleap_licensing/state/session_state.rb +68 -0
  22. data/lib/evoleap_licensing/state/user_state.rb +78 -0
  23. data/lib/evoleap_licensing/types/component_license_model.rb +22 -0
  24. data/lib/evoleap_licensing/types/invalid_reason.rb +49 -0
  25. data/lib/evoleap_licensing/types/validation_status.rb +91 -0
  26. data/lib/evoleap_licensing/user_control_manager.rb +198 -0
  27. data/lib/evoleap_licensing/version.rb +6 -0
  28. data/lib/evoleap_licensing/web_client.rb +65 -0
  29. data/lib/evoleap_licensing/web_service.rb +168 -0
  30. data/lib/evoleap_licensing.rb +41 -0
  31. metadata +129 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 67b4c0932275275ba02caaba0df4638c1117ea8aaf8f7273c784984fdd59dc1c
4
+ data.tar.gz: 0ec91a2dc11db0b3175ce2f2bd9c6170cdab60fae9056dad335e7172e78f7c2f
5
+ SHA512:
6
+ metadata.gz: 5b4e155db880b75292d4b7c2a6b4ff891351c3b806dd5dc35cd621f9bbebb8850024f71a574824ac66ded3b7dc3fe9d7e2582330ded9e985f92088432badc395
7
+ data.tar.gz: 8a540fe81ec5abc4d271acafab92270bfc8c29c27e2fe1d1b6084b04a4b5dcf4643e04c972248da5e07028cec89effc41e5c2f300b81e1eec9c92184e23709fc
data/README.md ADDED
@@ -0,0 +1,480 @@
1
+ # evoleap-licensing
2
+
3
+ Ruby SDK for [evoleap License Manager (ELM)](https://docs.elm.io) — license your Ruby and Rails applications using evoleap's cloud licensing system.
4
+
5
+ Supports server instance registration, per-user session licensing, component checkout/checkin, feature flags, and graceful offline handling.
6
+
7
+ ## Installation
8
+
9
+ Add to your Gemfile:
10
+
11
+ ```ruby
12
+ gem 'evoleap-licensing'
13
+ ```
14
+
15
+ Then run:
16
+
17
+ ```bash
18
+ bundle install
19
+ ```
20
+
21
+ ## Quick Start
22
+
23
+ ### 1. Get Your Credentials
24
+
25
+ Sign up at the [ELM portal](https://elm.evoleap.com) and create a product. You'll need:
26
+
27
+ - **Product ID** (GUID) — identifies your product
28
+ - **Public Key** (PEM file) — used to encrypt API communication
29
+ - **License Key** — provided to your customers
30
+
31
+ ### 2. Configure the Gem
32
+
33
+ Create a Rails initializer at `config/initializers/evoleap_licensing.rb`:
34
+
35
+ ```ruby
36
+ EvoleapLicensing.configure do |config|
37
+ config.host = "https://elm.evoleap.com" # default; change for self-hosted/elm Lite
38
+ config.timeout = 60 # HTTP timeout in seconds
39
+ end
40
+ ```
41
+
42
+ Store your public key securely (e.g., `config/elm_public_key.pem`).
43
+
44
+ ### 3. Register the Server Instance
45
+
46
+ Do this once per deployment (e.g., in a Rake task or during setup):
47
+
48
+ ```ruby
49
+ public_key = File.read(Rails.root.join("config", "elm_public_key.pem"))
50
+
51
+ scm = EvoleapLicensing::ServerControlManager.new(
52
+ product_id: "your-product-guid",
53
+ version: "1.0.0",
54
+ public_key: public_key
55
+ )
56
+
57
+ identity = EvoleapLicensing::InstanceIdentity.from_hardware
58
+ result = scm.register(license_key: "XXXXX-XXXXX-XXXXX-XXXXX-XXXXX", instance_identity: identity)
59
+
60
+ if result.success?
61
+ # Persist scm.server_state.to_h as JSON (database, file, etc.)
62
+ File.write("config/server_state.json", scm.server_state.to_h.to_json)
63
+ puts "Instance registered: #{scm.server_state.instance_guid}"
64
+ else
65
+ puts "Registration failed: #{result.error_message}"
66
+ end
67
+ ```
68
+
69
+ ### 4. Register Users
70
+
71
+ When a user first accesses your application:
72
+
73
+ ```ruby
74
+ public_key = File.read(Rails.root.join("config", "elm_public_key.pem"))
75
+ server_state = EvoleapLicensing::ServerState.new(JSON.parse(File.read("config/server_state.json")))
76
+
77
+ ucm = EvoleapLicensing::UserControlManager.new(
78
+ product_id: "your-product-guid",
79
+ version: "1.0.0",
80
+ instance_id: server_state.instance_guid,
81
+ public_key: public_key
82
+ )
83
+
84
+ user_identity = EvoleapLicensing::UserIdentity.new("uid" => current_user.id.to_s)
85
+
86
+ result = ucm.register(
87
+ user_info: { name: current_user.name, email: current_user.email },
88
+ user_identity: user_identity
89
+ )
90
+
91
+ if result.success?
92
+ # Persist ucm.user_state.to_h per user (database JSON column)
93
+ current_user.update!(elm_user_state: ucm.user_state.to_h.to_json)
94
+ end
95
+ ```
96
+
97
+ ### 5. Validate Sessions
98
+
99
+ On each request (via `before_action` or middleware):
100
+
101
+ ```ruby
102
+ ucm = EvoleapLicensing::UserControlManager.new(
103
+ product_id: "your-product-guid",
104
+ version: "1.0.0",
105
+ instance_id: server_state.instance_guid,
106
+ public_key: public_key,
107
+ user_state: EvoleapLicensing::UserState.new(JSON.parse(current_user.elm_user_state)),
108
+ session_state: EvoleapLicensing::SessionState.new(session[:elm_session_state])
109
+ )
110
+
111
+ user_identity = EvoleapLicensing::UserIdentity.new("uid" => current_user.id.to_s)
112
+ validity = ucm.validate_session(user_identity: user_identity)
113
+
114
+ if validity.valid?
115
+ # Save updated state
116
+ current_user.update!(elm_user_state: ucm.user_state.to_h.to_json)
117
+ session[:elm_session_state] = ucm.session_state.to_h
118
+
119
+ # Check feature flags
120
+ if ucm.session_state.features.include?("premium")
121
+ # Enable premium features
122
+ end
123
+ else
124
+ # Handle invalid license — see Error Handling below
125
+ handle_license_failure(validity)
126
+ end
127
+ ```
128
+
129
+ ## Component Checkout/Checkin
130
+
131
+ Components are independently licensable features within your product (seat-based or token-based):
132
+
133
+ ```ruby
134
+ # Check out a component during an active session
135
+ result = ucm.check_out_components("ReportGenerator", "DataExport")
136
+ if result.success?
137
+ # Component is now checked out for this session
138
+ # result.components and result.component_entitlements have details
139
+ else
140
+ puts "Checkout failed: #{result.failure_reason}"
141
+ # :insufficient_tokens, :invalid_component, etc.
142
+ end
143
+
144
+ # Check in when done
145
+ ucm.check_in_components("ReportGenerator", "DataExport")
146
+
147
+ # Query all component statuses
148
+ status = ucm.components_status
149
+ status.components.each do |comp|
150
+ puts "#{comp['name']}: #{comp['license_model']}"
151
+ end
152
+ ```
153
+
154
+ ## License Info
155
+
156
+ Get information about the license owner during an active session:
157
+
158
+ ```ruby
159
+ info = ucm.license_info
160
+ if info.success?
161
+ puts "Licensed to: #{info.owner_name}"
162
+ puts "Expires: #{info.expiry}"
163
+ end
164
+ ```
165
+
166
+ ## State Persistence
167
+
168
+ The SDK uses three state objects that must be persisted between requests:
169
+
170
+ | State | Scope | Where to Store | Lifetime |
171
+ |-------|-------|---------------|----------|
172
+ | `ServerState` | Per deployment | JSON file or database | Until re-registration |
173
+ | `UserState` | Per user | Database (JSON column) | Until re-registration |
174
+ | `SessionState` | Per user session | Rails session or Redis | Until session ends |
175
+
176
+ All state objects support `to_h` for serialization and accept a Hash in their constructor:
177
+
178
+ ```ruby
179
+ # Save
180
+ json = state.to_h.to_json
181
+
182
+ # Restore
183
+ state = EvoleapLicensing::ServerState.new(JSON.parse(json))
184
+ ```
185
+
186
+ ### Rails Database Migration Example
187
+
188
+ ```ruby
189
+ class AddElmLicensingToUsers < ActiveRecord::Migration[7.0]
190
+ def change
191
+ add_column :users, :elm_user_state, :jsonb, default: {}
192
+ end
193
+ end
194
+ ```
195
+
196
+ ## Error Handling
197
+
198
+ ### Connection Failures (Server Unreachable)
199
+
200
+ When the ELM server is unreachable, the SDK uses **grace periods** to allow continued operation:
201
+
202
+ ```ruby
203
+ validity = ucm.validate_session(user_identity: user_identity)
204
+
205
+ if validity.valid?
206
+ if validity.in_validation_failure_grace_period?
207
+ # Working offline — warn the user
208
+ flash[:warning] = "License server unreachable. " \
209
+ "License valid until #{validity.grace_period_expiration.strftime('%Y-%m-%d %H:%M')}"
210
+ end
211
+ # Allow access
212
+ else
213
+ if validity.invalid_reason == :service_unreachable
214
+ # Grace period expired or never had a successful validation
215
+ render_service_unavailable
216
+ end
217
+ end
218
+ ```
219
+
220
+ ### Registration Failures
221
+
222
+ ```ruby
223
+ result = scm.register(license_key: key, instance_identity: identity)
224
+
225
+ unless result.success?
226
+ case result.error_message
227
+ when /Invalid license key/i
228
+ # Customer entered wrong key
229
+ flash[:error] = "The license key is invalid. Please check and try again."
230
+ when /Error contacting/i
231
+ # Network issue during registration
232
+ flash[:error] = "Cannot reach the license server. Please check your connection."
233
+ else
234
+ # Other registration errors (product disabled, activation pending, etc.)
235
+ flash[:error] = "Registration failed: #{result.error_message}"
236
+ end
237
+ end
238
+ ```
239
+
240
+ ### Validation Failures
241
+
242
+ The `invalid_reason` on `SessionValidity` and `InstanceValidity` tells you exactly what went wrong:
243
+
244
+ ```ruby
245
+ def handle_license_failure(validity)
246
+ case validity.invalid_reason
247
+ when :license_expired
248
+ redirect_to license_expired_path,
249
+ alert: "Your license has expired. Please renew to continue."
250
+
251
+ when :no_seats_available
252
+ redirect_to no_seats_path,
253
+ alert: "All licensed seats are in use. Please try again later or contact your admin."
254
+
255
+ when :session_revoked
256
+ redirect_to session_revoked_path,
257
+ alert: "Your session was revoked by an administrator."
258
+
259
+ when :instance_disabled, :user_disabled
260
+ redirect_to account_disabled_path,
261
+ alert: "Your license has been disabled. Please contact support."
262
+
263
+ when :inconsistent_registration, :inconsistent_user
264
+ # Hardware or identity changed — may need re-registration
265
+ redirect_to reregistration_path,
266
+ alert: "License identity mismatch detected. Re-registration may be required."
267
+
268
+ when :activation_pending
269
+ redirect_to activation_pending_path,
270
+ alert: "Your license is pending activation. Please check with your administrator."
271
+
272
+ when :user_tampering_detected
273
+ # System clock was manipulated
274
+ redirect_to tampering_detected_path,
275
+ alert: "A time inconsistency was detected. Please verify your system clock."
276
+
277
+ when :registration_required
278
+ redirect_to registration_path
279
+
280
+ when :service_unreachable
281
+ render_service_unavailable
282
+
283
+ else
284
+ redirect_to license_error_path,
285
+ alert: "A licensing error occurred. Please try again."
286
+ end
287
+ end
288
+ ```
289
+
290
+ ### Component Checkout Failures
291
+
292
+ ```ruby
293
+ result = ucm.check_out_components("AdvancedReporting")
294
+
295
+ unless result.success?
296
+ case result.failure_reason
297
+ when :insufficient_tokens
298
+ flash[:error] = "Not enough tokens to use this feature."
299
+ when :invalid_component
300
+ flash[:error] = "This component is not available in your license."
301
+ else
302
+ flash[:error] = "Component checkout failed: #{result.failure_reason}"
303
+ end
304
+ end
305
+ ```
306
+
307
+ ## Grace Period Configuration
308
+
309
+ Grace periods allow your application to continue working when the license server is temporarily unreachable:
310
+
311
+ ```ruby
312
+ strategy = EvoleapLicensing::ControlStrategy.new(
313
+ # Allow unregistered products to run for 7 days (trial period)
314
+ grace_period_for_unregistered_product: 7 * 24 * 3600,
315
+
316
+ # Automatically start a new session when the current one expires
317
+ start_session_for_expired_session: false
318
+ )
319
+
320
+ scm = EvoleapLicensing::ServerControlManager.new(
321
+ product_id: "your-product-guid",
322
+ version: "1.0.0",
323
+ public_key: public_key,
324
+ strategy: strategy
325
+ )
326
+ ```
327
+
328
+ The **validation failure grace period** is set server-side when you register your product. It determines how long a previously-validated instance can operate without contacting the license server.
329
+
330
+ ## Example Rails Middleware
331
+
332
+ For applications that need to check licensing on every request:
333
+
334
+ ```ruby
335
+ # app/middleware/license_check_middleware.rb
336
+ class LicenseCheckMiddleware
337
+ SKIP_PATHS = %w[/license /health /assets].freeze
338
+
339
+ def initialize(app)
340
+ @app = app
341
+ end
342
+
343
+ def call(env)
344
+ request = ActionDispatch::Request.new(env)
345
+
346
+ # Skip licensing for certain paths
347
+ return @app.call(env) if SKIP_PATHS.any? { |p| request.path.start_with?(p) }
348
+
349
+ # Skip if no authenticated user
350
+ return @app.call(env) unless request.session[:user_id]
351
+
352
+ user = User.find_by(id: request.session[:user_id])
353
+ return @app.call(env) unless user
354
+
355
+ ucm = build_user_control_manager(user, request.session)
356
+ user_identity = EvoleapLicensing::UserIdentity.new("uid" => user.id.to_s)
357
+ validity = ucm.validate_session(user_identity: user_identity)
358
+
359
+ if validity.valid?
360
+ # Persist updated state
361
+ user.update_column(:elm_user_state, ucm.user_state.to_h.to_json)
362
+ request.session[:elm_session_state] = ucm.session_state.to_h
363
+ @app.call(env)
364
+ else
365
+ [403, { "Content-Type" => "text/html" },
366
+ ["License validation failed: #{validity.invalid_reason}"]]
367
+ end
368
+ end
369
+
370
+ private
371
+
372
+ def build_user_control_manager(user, session)
373
+ public_key = Rails.application.credentials.elm_public_key
374
+ server_state = JSON.parse(File.read(Rails.root.join("config", "server_state.json")))
375
+
376
+ EvoleapLicensing::UserControlManager.new(
377
+ product_id: Rails.application.credentials.elm_product_id,
378
+ version: Rails.application.config.app_version,
379
+ instance_id: server_state["instance_guid"],
380
+ public_key: public_key,
381
+ user_state: EvoleapLicensing::UserState.new(
382
+ user.elm_user_state.is_a?(String) ? JSON.parse(user.elm_user_state) : user.elm_user_state
383
+ ),
384
+ session_state: EvoleapLicensing::SessionState.new(session[:elm_session_state])
385
+ )
386
+ end
387
+ end
388
+ ```
389
+
390
+ Add to `config/application.rb`:
391
+
392
+ ```ruby
393
+ config.middleware.use LicenseCheckMiddleware
394
+ ```
395
+
396
+ ## Feature Flags
397
+
398
+ Features returned from session validation can be used to gate functionality:
399
+
400
+ ```ruby
401
+ # After validate_session succeeds:
402
+ features = ucm.session_state.features
403
+
404
+ if features.include?("advanced_reporting")
405
+ # Show advanced reporting UI
406
+ end
407
+
408
+ if features.include?("api_access")
409
+ # Allow API access
410
+ end
411
+ ```
412
+
413
+ ## Staging vs. Production
414
+
415
+ ELM provides a free staging server for development. To switch between environments:
416
+
417
+ ```ruby
418
+ # config/initializers/evoleap_licensing.rb
419
+ EvoleapLicensing.configure do |config|
420
+ if Rails.env.production?
421
+ config.host = "https://elm.evoleap.com"
422
+ else
423
+ config.host = "https://staging.elm.evoleap.com"
424
+ end
425
+ end
426
+ ```
427
+
428
+ You'll need separate product IDs and public keys for staging and production. Store them in Rails credentials:
429
+
430
+ ```yaml
431
+ # config/credentials.yml.enc
432
+ elm:
433
+ staging:
434
+ product_id: "staging-product-guid"
435
+ public_key: |
436
+ -----BEGIN PUBLIC KEY-----
437
+ ...staging key...
438
+ -----END PUBLIC KEY-----
439
+ production:
440
+ product_id: "production-product-guid"
441
+ public_key: |
442
+ -----BEGIN PUBLIC KEY-----
443
+ ...production key...
444
+ -----END PUBLIC KEY-----
445
+ ```
446
+
447
+ ## API Reference
448
+
449
+ ### ServerControlManager
450
+
451
+ | Method | Returns | Description |
452
+ |--------|---------|-------------|
453
+ | `register(license_key:, instance_identity:)` | `RegistrationResult` | Register server instance |
454
+ | `validate_instance(instance_identity:)` | `InstanceValidity` | Validate instance license |
455
+
456
+ ### UserControlManager
457
+
458
+ | Method | Returns | Description |
459
+ |--------|---------|-------------|
460
+ | `register(user_info:, user_identity:)` | `RegistrationResult` | Register a user |
461
+ | `validate_session(user_identity:, requested_duration:)` | `SessionValidity` | Begin or extend session |
462
+ | `end_session` | `Boolean` | End active session |
463
+ | `check_out_components(*names)` | `ComponentCheckoutResult` | Check out components |
464
+ | `check_in_components(*names)` | `ComponentCheckinResult` | Check in components |
465
+ | `components_status` | `ComponentsStatus` | Get all component statuses |
466
+ | `license_info` | `LicenseInfo` | Get license owner info |
467
+
468
+ ### Result Types
469
+
470
+ - **`RegistrationResult`** — `success?`, `error_message`
471
+ - **`SessionValidity`** — `valid?`, `invalid_reason`, `validity_duration`, `in_validation_failure_grace_period?`, `in_unregistered_grace_period?`, `grace_period_expiration`
472
+ - **`InstanceValidity`** — `valid?`, `invalid_reason`, `in_validation_failure_grace_period?`, `in_unregistered_grace_period?`, `grace_period_expiration`
473
+ - **`ComponentCheckoutResult`** — `success?`, `failure_reason`, `components`, `component_entitlements`
474
+ - **`ComponentCheckinResult`** — `success?`, `failure_reason`
475
+ - **`ComponentsStatus`** — `success?`, `components`, `component_entitlements`, `error_message`
476
+ - **`LicenseInfo`** — `success?`, `owner_name`, `owner_logo_url`, `expiry`, `error_message`
477
+
478
+ ## License
479
+
480
+ MIT
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EvoleapLicensing
4
+ class Configuration
5
+ attr_accessor :host, :timeout, :api_version, :user_agent
6
+
7
+ def initialize
8
+ @host = "https://elm.evoleap.com"
9
+ @timeout = 60
10
+ @api_version = "v3.4"
11
+ @user_agent = "evoleap-licensing-ruby/#{EvoleapLicensing::VERSION}"
12
+ end
13
+
14
+ def api_base_url
15
+ "#{@host}/api/#{@api_version}/auth"
16
+ end
17
+ end
18
+
19
+ class << self
20
+ attr_writer :configuration
21
+
22
+ def configuration
23
+ @configuration ||= Configuration.new
24
+ end
25
+
26
+ def configure
27
+ yield(configuration)
28
+ end
29
+
30
+ def reset_configuration!
31
+ @configuration = Configuration.new
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EvoleapLicensing
4
+ module ControlLogic
5
+ def self.first_launch_time_valid?(first_launch_time, current_time)
6
+ current_time >= first_launch_time
7
+ end
8
+
9
+ def self.time_inconsistency_detected?(last_trusted_time, previous_times, current_time, current_time_is_trusted)
10
+ return false if last_trusted_time.nil?
11
+ return false if current_time_is_trusted
12
+
13
+ time_to_check = get_time_to_check(previous_times, last_trusted_time)
14
+ current_time < time_to_check
15
+ end
16
+
17
+ def self.in_grace_period_for_unregistered_product?(strategy, first_launch_time, current_time)
18
+ return [false, nil] if strategy.grace_period_for_unregistered_product <= 0
19
+
20
+ expiration = first_launch_time + strategy.grace_period_for_unregistered_product
21
+ ok = current_time < expiration
22
+ [ok, expiration]
23
+ end
24
+
25
+ def self.in_grace_period_for_validation_failures?(grace_period, number_of_failed_attempts, last_successful_time, current_time)
26
+ return [false, nil] if last_successful_time.nil? || grace_period.nil? || grace_period <= 0
27
+
28
+ expiration = last_successful_time + grace_period
29
+ ok = number_of_failed_attempts == 0 || current_time < expiration
30
+ [ok, expiration]
31
+ end
32
+
33
+ def self.get_time_to_check(times, fallback)
34
+ times.length <= 1 ? fallback : times[1]
35
+ end
36
+ private_class_method :get_time_to_check
37
+ end
38
+ end