licentivo-launch 0.1.2 → 0.1.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f25a673aa0fa381137469b0364787bf62cf2a08ff5d07056a135d0c06da72dc1
4
- data.tar.gz: 173f9331cb936efd93cba33cbfba77810c5d3483e71d9d202af05edbfb98a6af
3
+ metadata.gz: a541ba332c2a6d9adb50c359917ace42ef33495b30af65407e84a521ea081343
4
+ data.tar.gz: 868bda7f957341cc376c8b82377d7af7bab54b500c3cc3432fa190dfb2fe9029
5
5
  SHA512:
6
- metadata.gz: d58be5da9f7c2430b11c09b90ed26fd463a77b9f1e4e07314df71f7918acb3bbcfc8e82df5ee13a72f5d085510c54c01e9285938ed50a917c16453a91d759558
7
- data.tar.gz: b120504fc4bbe8de9cea230770d5f398a6ffff5960ed6883d7c962997b713f47d3f270f832ca934e6d2b8639aff637ff681a237a56b534741df06479388dad72
6
+ metadata.gz: 917ab9655d0e640d9e459d96e262521e4d711bbd62806e58bd521c241aedaefad3356e35d9f81f509b3ebcf15e24465cb7168b390519bd75d84a577b5c3dc205
7
+ data.tar.gz: 7bbc9bf5c8cb4a59094d44dc07a2a116be40344720ce1787b946ed94945161564fa3a944ec2773fe5ddd72f876ece9aa3ace604678a20964578ae270bcfb3fa8
data/README.md CHANGED
@@ -1,12 +1,14 @@
1
- # Launch Ruby 0.1.2
1
+ # Launch Ruby 0.1.3
2
+
3
+ > Local account-exchange changes are unpublished. Login uses a short-lived identity assertion; Launch calculates purchased dates and Licentivo owns runtime permission. See the repository guide docs/account-purchase-boundary.md.
2
4
 
3
5
  Ruby 3.1+. Install only the Launch gem matching the platform:
4
6
 
5
7
  ```sh
6
- gem install licentivo-launch --version 0.1.2
8
+ gem install licentivo-launch --version 0.1.3
7
9
  ```
8
10
 
9
- SDK 0.11.1 Ruby code, its private native core and Windows curl DLL are included. RubyGems resolves the ordinary base64 dependency. The generic source gem builds its own embedded core automatically and needs normal C11/curl/OpenSSL tools for the platform. No official SDK gem or external native-library override is needed.
11
+ SDK 0.11.2 Ruby code, its private native core and Windows curl DLL are included. RubyGems resolves the ordinary base64 dependency. The generic source gem builds its own embedded core automatically and needs normal C11/curl/OpenSSL tools for the platform. No official SDK gem or external native-library override is needed.
10
12
 
11
13
  ## Direct initialization and manual lifecycle
12
14
 
@@ -26,10 +28,20 @@ restore, logout and release_device must handle errors. CancellationToken is coop
26
28
 
27
29
  Launch automatically supplies the current merchant URL, existing product mapping and signing public keys when you select the software in its integration page. Copy its connection function, or use the values already shown there when writing code manually. Do not ask the integrator to type two internal product IDs. No launch-config.json or product.json file is required. The merchant URL is mandatory; the official licensing origin is fixed inside the package to https://www.licentivo.com.
28
30
 
29
- The connection function initializes an application; it does not authorize business work. Login obtains only the signed-in customer's credential. Restore checks account expiry, identity, product and environment. Put every protected operation inside the feature guard. Do not substitute merchant order state or a locally saved boolean for the SDK's signed checks.
31
+ The connection function creates the account adapter. Login obtains a short-lived identity assertion, which is passed only in memory to the official SDK with the connection and environment. Remembered account records contain public routing context and the Launch account session; they never store the assertion, license key or signed runtime permission. Every protected action still passes the official SDK.
30
32
 
31
- Normal close stops the runtime. Logout clears local account/cache state and follows this package's remote-session contract. Device release is a separate explicit operation, and failure must be reported. Sessions default to memory. Remembered login needs a protected store; never persist merchant passwords. Scope private SDK cache directories by origin, product, environment and account, and exclude them from installers and logs. Session encryption does not encrypt every official SDK device file.
33
+ Normal close preserves remembered login and device binding. Explicit Logout first marks the protected account record as signed out, delegates device logout to the official SDK and stops local use. Until Launch confirms sign-out (HTTP 200 or an already-invalid session via HTTP 401), a protected account logout task is retained. Login, Restore and subsequent Logout retry that task; they never restore it as a login. Process-restart recovery requires a persistent protected Store; MemoryStore lasts only for the current process. ReleaseDevice follows the same flow. Keep the official SDK cache and .logout.json journal intact; that SDK alone manages its device-release tickets.
32
34
 
33
- Only transport failures or HTTP 5xx may attempt an existing cryptographically verified offline grant. Explicit 401/403/404, session/account mismatch, expiration or suspension deny access without offline fallback. Pending retries are finite and honor Retry-After; cancellation and rate limiting never authorize work. First activation and mandatory-online policies retain their network requirements.
35
+ Launch Restore confirms the account online and obtains a new identity assertion. It never selects offline permission during a Launch outage. Applications requiring offline startup use the official SDK recovery contract directly. The official SDK alone owns signed permission, device credentials, offline expiry and logout recovery.
34
36
 
35
37
  Manual development is supported without AI. The combined source bundle includes docs/manual-integration.md with all nine language flows, secure storage, error handling and acceptance procedures. Local package/runtime checks are distinct from external payment and licensing acceptance. Check the current publication status at https://github.com/spf86/licentivo-launch-sdk/blob/main/docs/registry-status.json before using public registry installation commands.
38
+
39
+
40
+ <!-- account-store-ownership-2026-10-24 -->
41
+ ## Local account logout and session ownership (unpublished)
42
+
43
+ Account logout tasks use a separate key in the configured secure store. Custom stores must support multiple opaque keys, atomic value replacement and isolation to the current OS user and host. Account lifecycle operations are coordinated across local instances/processes, independently of the licensing cache directory.
44
+
45
+ Logout also records a local, session-specific account exit intent using hashes of the account scope and bearer; it stores no bearer or licensing grant there. Restore checks that intent before requesting identity, so a failed vault write or lifecycle-lock timeout cannot turn an interrupted logout into a remembered login. A storage failure is reported explicitly. If both vault persistence and local intent storage are unavailable, handle the failure and retry before terminating; the SDK does not claim durable completion.
46
+
47
+ An old instance only changes its own account record and logout task. Repeating a completed logout does not adopt a newer remembered account. Retrying other sessions' account tasks never opens their licensing runtime. Licentivo owns device revocation, signed leases, offline permission and its shared cache files; ordinary close still preserves remembered login.
@@ -4,7 +4,7 @@ require 'json'
4
4
  require 'monitor'
5
5
 
6
6
  module Licentivo
7
- VERSION = '0.11.1'
7
+ VERSION = '0.11.2'
8
8
 
9
9
  class Error < StandardError
10
10
  attr_reader :code, :http_status, :request_id, :retryable, :retry_after_seconds
@@ -71,6 +71,7 @@ module Licentivo
71
71
  resolve_metered_feature: ['ln_resolve_metered_feature', [p,p,i], i],
72
72
  validate: ['ln_validate', [p], i], heartbeat: ['ln_heartbeat', [p], i],
73
73
  refresh_online: ['ln_refresh_online', [p], i], deactivate: ['ln_deactivate', [p], i],
74
+ logout: ['ln_logout', [p], i], retry_pending_logouts: ['ln_retry_pending_logouts', [p], i], logout_pending: ['ln_logout_pending', [p], i],
74
75
  reserve: ['ln_reserve', [p,p,q,p], p], consume: ['ln_consume', [p,p,q,p], p],
75
76
  commit: ['ln_commit', [p,p,p], p], cancel: ['ln_cancel', [p,p,p], p],
76
77
  offline_request: ['ln_offline_request', [p,p], p], import_offline: ['ln_import_offline', [p,p,p], i],
@@ -154,9 +155,15 @@ module Licentivo
154
155
  def validate = checked(:validate)
155
156
  def heartbeat = checked(:heartbeat)
156
157
  def refresh_online = checked(:refresh_online)
157
- def deactivate = checked(:deactivate)
158
- def status = read_json(:status)
159
- def feature_status(feature) = read_json(:feature_status, Native.input(feature))
158
+ def deactivate = logout
159
+ def logout
160
+ @signed_out = true
161
+ checked(:logout)
162
+ end
163
+ def retry_pending_logouts = checked(:retry_pending_logouts)
164
+ def logout_pending? = @lock.synchronize { Native.call(:logout_pending, active) != 0 }
165
+ def status = @signed_out ? signed_out_status : read_json(:status)
166
+ def feature_status(feature) = @signed_out ? signed_out_status : read_json(:feature_status, Native.input(feature))
160
167
  def reserve(feature, quantity, job_id) = read_json(:reserve, Native.input(feature), integer(quantity), Native.input(job_id))
161
168
  def consume(feature, quantity, job_id) = read_json(:consume, Native.input(feature), integer(quantity), Native.input(job_id))
162
169
  def commit(reservation_id, job_id) = read_json(:commit, Native.input(reservation_id), Native.input(job_id))
@@ -190,6 +197,7 @@ module Licentivo
190
197
  end
191
198
 
192
199
  private
200
+ def signed_out_status = { 'state' => 'released', 'allowed' => false, 'code' => 'LAUNCH_LOGIN_REQUIRED', 'request_id' => '', 'retryable' => false, 'lease_valid_until' => 0 }
193
201
  def integer(value)
194
202
  raise ArgumentError, 'Quantity must be a positive signed 64-bit integer' unless value.is_a?(Integer) && value.positive? && value <= 0x7fff_ffff_ffff_ffff
195
203
  value
@@ -210,7 +218,10 @@ module Licentivo
210
218
  retryable: info[212,4].unpack1('i') != 0, retry_after_seconds: info[216,4].unpack1('i'))
211
219
  end
212
220
  def checked(name, *arguments)
213
- @lock.synchronize { raise error if Native.call(name, active, *arguments) == 0 }
221
+ @lock.synchronize do
222
+ raise Error.new('Complete a fresh login', code: 'LAUNCH_LOGIN_REQUIRED') if @signed_out && ![:logout, :deactivate, :retry_pending_logouts].include?(name)
223
+ raise error if Native.call(name, active, *arguments) == 0
224
+ end
214
225
  true
215
226
  end
216
227
  def read_json(name, *arguments)
@@ -221,6 +232,7 @@ module Licentivo
221
232
  end
222
233
  end
223
234
  def run_callback(name, *arguments)
235
+ raise Error.new('Complete a fresh login', code: 'LAUNCH_LOGIN_REQUIRED') if @signed_out
224
236
  raise ArgumentError, 'Supply your business operation as a block' unless block_given?
225
237
  @lock.synchronize do
226
238
  raise Error.new('A business callback is already running', code: 'WORKFLOW_BUSY') if @in_callback
@@ -11,7 +11,7 @@ require 'time'
11
11
  require 'monitor'
12
12
 
13
13
  module LicentivoLaunch
14
- VERSION = '0.1.2'
14
+ VERSION = '0.1.3'
15
15
  LIMIT = 1 << 20
16
16
  UUID = /\A[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}\z/
17
17
  class Error < StandardError
@@ -163,12 +163,13 @@ module LicentivoLaunch
163
163
  @store, @transport = store, transport || LicentivoLaunch.method(:default_transport)
164
164
  @factory = runtime_factory || lambda do |config|
165
165
  require_relative 'licentivo_launch/vendor/licentivo'
166
- raise Error.new('SDK_VERSION_MISMATCH') unless Licentivo::VERSION == '0.11.1'
166
+ raise Error.new('SDK_VERSION_MISMATCH') unless Licentivo::VERSION == '0.11.2'
167
167
  Licentivo::Client.new(config)
168
168
  end
169
169
  @root = File.expand_path(cache_dir || File.join(Dir.home, '.cache', 'LicentivoLaunch'))
170
- @scope = ['launch-session-v1', @launch['merchant_origin'], @launch['launch_product_id'], @launch['licentivo_product_id'], @launch['environment']].join("\n")
170
+ @scope = ['launch-account-v2', @launch['merchant_origin'], @launch['launch_product_id'], @launch['licentivo_product_id'], @launch['environment']].join("\n")
171
171
  @lock, @closed, @in_callback, @attempts = Monitor.new, false, false, pending_attempts
172
+ @account_cleared = false
172
173
  @wait = sleep || lambda { |seconds, cancellation| (seconds * 10).ceil.times { cancellation&.check!; Kernel.sleep(0.1) }; cancellation&.check! }
173
174
  end
174
175
  def self.connect(merchant_origin, product_id, licentivo_product_id, trusted_keys, environment: 'live', **options)
@@ -177,23 +178,33 @@ module LicentivoLaunch
177
178
  end
178
179
  def self.from_files(launch_path, product_path, **options) = new(*LicentivoLaunch.load_config(launch_path, product_path), **options)
179
180
  def login(email, password, cancellation: nil)
180
- serial do
181
- cancellation&.check!; logout_internal(cancellation)
181
+ account_serial do
182
+ cancellation&.check!; raise Error.new('LOGOUT_PENDING') unless logout_internal(cancellation)
182
183
  grant = call('/auth/login', method: 'POST', body: { email: email, password: password, kind: 'desktop' }, cancellation: cancellation)
183
- @record = { 'version' => 1, 'scope' => @scope, 'user' => grant['user'], 'token' => grant['access_token'], 'expires_at' => grant['expires_at'] }
184
+ @record = { 'version' => 2, 'scope' => @scope, 'user' => grant['user'], 'token' => grant['access_token'], 'expires_at' => grant['expires_at'] }
185
+ @account_cleared = false
184
186
  begin
185
187
  raise Error.new('INVALID_SESSION') unless valid_identity?(@record) && expiry(@record) > Time.now
186
188
  @record['credential'] = credential(cancellation)
187
- state = start_runtime(false, cancellation); @store.save(@scope, @record); state
188
- rescue StandardError
189
- logout_internal(nil); raise
189
+ state = start_runtime(false, cancellation); @store.save(@scope, @record.merge('credential' => @record['credential'].select { |key, _| %w[product_id licentivo_product_id launch_connection_id environment expires_at].include?(key) })); state
190
+ rescue StandardError => original
191
+ begin; logout_internal(nil); rescue StandardError; end
192
+ raise original
190
193
  end
191
194
  end
192
195
  end
193
196
  def restore(cancellation: nil)
194
- serial do
195
- cancellation&.check!; stop; @record = saved
197
+ account_serial do
198
+ cancellation&.check!; stop; @record = @record&.dig('logout_pending') ? @record : saved
199
+ task = pending_accounts.find { |record| !@record || record['token'] == @record['token'] }
200
+ @record = task if task
196
201
  next nil unless @record
202
+ @record = @record.merge('version'=>3,'logout_pending'=>true) if has_intent?(@record)
203
+ if @record['logout_pending']
204
+ raise Error.new('ACCOUNT_LOGOUT_PENDING') unless logout_internal(cancellation)
205
+ next nil
206
+ end
207
+ retry_other_accounts(@record['token'], cancellation)
197
208
  if expiry(@record) <= Time.now
198
209
  clear; raise Error.new('SESSION_EXPIRED')
199
210
  end
@@ -203,12 +214,12 @@ module LicentivoLaunch
203
214
  @record['credential'] = credential(cancellation); @record['user'] = me['user']
204
215
  rescue Error => e
205
216
  cancellation&.check!
206
- next start_runtime(true, cancellation) if e.code == 'NETWORK_UNAVAILABLE' || (500..599).cover?(e.status)
217
+
207
218
  clear if e.code != 'ENTITLEMENT_PENDING' && ([401, 403, 404].include?(e.status) || %w[SESSION_EXPIRED SESSION_MISMATCH ENTITLEMENT_EXPIRED ENTITLEMENT_SUSPENDED ENTITLEMENT_NOT_PURCHASED].include?(e.code))
208
219
  raise
209
220
  end
210
221
  begin
211
- state = start_runtime(false, cancellation); @store.save(@scope, @record); state
222
+ state = start_runtime(false, cancellation); @store.save(@scope, @record.merge('credential' => @record['credential'].select { |key, _| %w[product_id licentivo_product_id launch_connection_id environment expires_at].include?(key) })); state
212
223
  rescue StandardError
213
224
  clear; raise
214
225
  end
@@ -226,28 +237,50 @@ module LicentivoLaunch
226
237
  end
227
238
  end
228
239
  end
229
- def logout(cancellation: nil) = serial { logout_internal(cancellation) }
240
+ def logout(cancellation: nil)
241
+ account_serial(true) { if @account_cleared && !@record; retry_other_accounts(nil,cancellation);true;else;logout_internal(cancellation);end }
242
+ end
230
243
  def release_device(cancellation: nil)
231
- serial do
232
- cancellation&.check!
233
- raise Error.new('LOGIN_REQUIRED') unless @runtime
234
- begin
235
- @runtime.deactivate; cancellation&.check!
236
- ensure
237
- logout_internal(cancellation)
238
- end
239
- end
244
+ logout(cancellation: cancellation)
240
245
  end
241
246
  def close
247
+ pending = false
242
248
  @lock.synchronize do
243
249
  raise Error.new('WORKFLOW_BUSY') if @in_callback
244
250
  return if @closed
245
251
  @closed = true
252
+ pending = @record&.dig('logout_pending')
246
253
  begin; stop; ensure; @record = nil; end
247
254
  end
248
255
  end
249
256
 
250
257
  private
258
+ def account_serial(logout = false)
259
+ serial do
260
+ begin
261
+ begin; remember_intent(@record) if logout && @record; rescue SystemCallError, IOError; end
262
+ root = File.join(Dir.home, '.cache', 'LicentivoLaunch', 'account-locks', 'ruby')
263
+ FileUtils.mkdir_p(root, mode: 0o700)
264
+ File.open(File.join(root, Digest::SHA256.hexdigest(@scope) + '.lock'), File::RDWR | File::CREAT, 0o600) do |file|
265
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 5
266
+ until file.flock(File::LOCK_EX | File::LOCK_NB)
267
+ raise Error.new('ACCOUNT_LOCK_BUSY') if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
268
+ Kernel.sleep(0.02)
269
+ end
270
+ begin; yield; ensure; file.flock(File::LOCK_UN); end
271
+ end
272
+ rescue StandardError
273
+ if logout
274
+ if @runtime
275
+ begin; @runtime.respond_to?(:logout) ? @runtime.logout : @runtime.deactivate; rescue StandardError; end
276
+ end
277
+ stop
278
+ @record = @record.merge('version' => 3, 'logout_pending' => true) if @record
279
+ end
280
+ raise
281
+ end
282
+ end
283
+ end
251
284
  def serial
252
285
  @lock.synchronize do
253
286
  raise Error.new('APPLICATION_CLOSED') if @closed
@@ -258,18 +291,19 @@ module LicentivoLaunch
258
291
  def cache(record) = File.join(@root, Digest::SHA256.hexdigest(@scope), Digest::SHA256.hexdigest(record.dig('user', 'id')), 'lease.json')
259
292
  def expiry(record) = Time.iso8601(record['expires_at'])
260
293
  def valid_identity?(record)
261
- record.is_a?(Hash) && record['version'] == 1 && record['scope'] == @scope && record['user'].is_a?(Hash) && UUID.match?(record.dig('user', 'id').to_s) && record['token'].is_a?(String) && record['token'].match?(/\A[A-Za-z0-9_-]{43}\z/) && record['expires_at'].is_a?(String) && record['expires_at'].match?(/(?:Z|[+-]\d\d:\d\d)\z/) && expiry(record)
294
+ record.is_a?(Hash) && record['version'] == 2 && record['scope'] == @scope && record['user'].is_a?(Hash) && UUID.match?(record.dig('user', 'id').to_s) && record['token'].is_a?(String) && record['token'].match?(/\A[A-Za-z0-9_-]{43}\z/) && record['expires_at'].is_a?(String) && record['expires_at'].match?(/(?:Z|[+-]\d\d:\d\d)\z/) && expiry(record)
262
295
  rescue ArgumentError, TypeError
263
296
  false
264
297
  end
265
298
  def saved
266
299
  record = @store.load(@scope)
267
- if record && !(valid_identity?(record) && record['credential'].is_a?(Hash) && record.dig('credential', 'product_id') == @launch['launch_product_id'] && record.dig('credential', 'license_key').is_a?(String) && !record.dig('credential', 'license_key').empty?)
300
+ if record.is_a?(Hash) && record['logout_pending'] == true && [2, 3].include?(record['version']) && record['scope'] == @scope && record['token'].is_a?(String) && !record['token'].empty?
301
+ return record
302
+ end
303
+ if record && !(valid_identity?(record) && record['credential'].is_a?(Hash) && record.dig('credential', 'product_id') == @launch['launch_product_id'] && UUID.match?(record.dig('credential', 'launch_connection_id').to_s) && !record.dig('credential', 'identity_token'))
268
304
  clear(record); raise Error.new('INVALID_SAVED_SESSION')
269
305
  end
270
306
  record
271
- rescue StandardError
272
- @store.delete(@scope); raise
273
307
  end
274
308
  def call(path, method: 'GET', body: nil, cancellation: nil, token: @record&.fetch('token', nil))
275
309
  cancellation&.check!
@@ -295,29 +329,18 @@ module LicentivoLaunch
295
329
  data['data']
296
330
  end
297
331
  def credential(cancellation)
298
- @attempts.times do |attempt|
299
- begin
300
- value = call("/software/products/#{@launch['launch_product_id']}/credential", method: 'POST', body: { environment: @launch['environment'] }, cancellation: cancellation)
301
- raise Error.new('CREDENTIAL_MISMATCH') unless value['product_id'] == @launch['launch_product_id'] && value['license_key'].is_a?(String) && !value['license_key'].empty? && value['version'].is_a?(Integer) && value['version'] >= 1
302
- return value
303
- rescue Error => e
304
- raise unless e.status == 403 && e.code == 'ENTITLEMENT_PENDING' && attempt + 1 < @attempts && e.retry_after_seconds <= 30
305
- @wait.call([3, e.retry_after_seconds].max, cancellation); cancellation&.check!
306
- end
307
- end
332
+ value = call("/software/products/#{@launch['launch_product_id']}/identity", method: 'POST', body: { environment: @launch['environment'] }, cancellation: cancellation)
333
+ raise Error.new('IDENTITY_MISMATCH') unless value['product_id'] == @launch['launch_product_id'] && value['licentivo_product_id'] == @product['product_id'] && value['environment'] == @launch['environment'] && UUID.match?(value['launch_connection_id'].to_s) && value['identity_token'].is_a?(String) && value['identity_token'].length.between?(32, 12288)
334
+ value
308
335
  end
309
336
  def start_runtime(offline, cancellation)
310
337
  cancellation&.check!
311
338
  path = cache(@record); FileUtils.mkdir_p(File.dirname(path), mode: 0o700)
312
- runtime = @factory.call(@product.merge('license_key' => @record.dig('credential', 'license_key'), 'cache_path' => path))
339
+ context = @record.fetch('credential')
340
+ runtime = @factory.call(@product.merge('license_key' => '', 'identity_token' => context.fetch('identity_token', ''), 'launch_connection_id' => context.fetch('launch_connection_id'), 'environment' => @launch['environment'], 'cache_path' => path))
313
341
  begin
314
- cancellation&.check!
315
- if offline
316
- state = runtime.status
317
- raise Error.new('OFFLINE_UNAVAILABLE') unless state['allowed'] == true && state['state'] == 'offline_valid'
318
- end
319
- runtime.start; cancellation&.check!; @runtime = runtime
320
- { user: JSON.parse(JSON.generate(@record['user'])), expires_at: @record['expires_at'], offline_fallback: offline }
342
+ runtime.start; cancellation&.check!; @runtime = runtime; @account_cleared=false
343
+ { user: JSON.parse(JSON.generate(@record['user'])), expires_at: @record['expires_at'] }
321
344
  rescue StandardError
322
345
  runtime.close; raise
323
346
  end
@@ -327,26 +350,109 @@ module LicentivoLaunch
327
350
  runtime&.close
328
351
  end
329
352
  def clear(record = @record)
330
- begin
331
- stop
332
- ensure
333
- @record = nil
353
+ begin; stop; ensure; @record = nil; @account_cleared=true;delete_owned(record) if record; end
354
+ end
355
+ def queue_key = @scope + "\naccount-logout-queue-v1"
356
+ def intent_path(record)=File.join(Dir.home,'.cache','LicentivoLaunch','account-intents','ruby',Digest::SHA256.hexdigest(@scope)+'.'+Digest::SHA256.hexdigest(record['token'])+'.signedout')
357
+ def remember_intent(record)
358
+ path=intent_path(record);FileUtils.mkdir_p(File.dirname(path),mode:0o700)
359
+ File.open(path,File::WRONLY|File::CREAT,0o600){|file|file.write('1');file.flush;file.fsync}
360
+ end
361
+ def has_intent?(record)
362
+ File.stat(intent_path(record));true
363
+ rescue Errno::ENOENT
364
+ false
365
+ end
366
+ def pending_accounts
367
+ queue = @store.load(queue_key)
368
+ return [] unless queue
369
+ unless queue.is_a?(Hash) && queue['version'] == 1 && queue['records'].is_a?(Array) && queue['records'].length <= 64 && queue['records'].all? { |record| record.is_a?(Hash) && record['logout_pending'] == true && record['version'] == 3 && record['scope'] == @scope && record['token'].is_a?(String) && !record['token'].empty? && !record.dig('credential', 'identity_token') }
370
+ raise Error.new('INVALID_SAVED_SESSION')
371
+ end
372
+ queue['records']
373
+ end
374
+ def update_pending(record, remove: false)
375
+ records = pending_accounts.reject { |value| value['token'] == record['token'] }
376
+ unless remove
377
+ public_record = record.merge('version' => 3, 'logout_pending' => true, 'credential' => (record['credential'] || {}).select { |key, _| %w[product_id licentivo_product_id launch_connection_id environment expires_at].include?(key) })
378
+ records << public_record
379
+ end
380
+ raise Error.new('ACCOUNT_LOGOUT_QUEUE_FULL') if records.length > 64 || JSON.generate({ 'version' => 1, 'records' => records }).bytesize > 60000
381
+ records.empty? ? @store.delete(queue_key) : @store.save(queue_key, { 'version' => 1, 'records' => records })
382
+ end
383
+ def owns?(record)
384
+ active = @store.load(@scope)
385
+ record && active && active['token'] == record['token']
386
+ end
387
+ def delete_owned(record)
388
+ @store.delete(@scope) if owns?(record)
389
+ end
390
+ def mark_logout(record)
391
+ begin;remember_intent(record);rescue SystemCallError, IOError;end
392
+ queue_failure = nil
393
+ begin; update_pending(record); rescue StandardError => error; queue_failure = error; end
394
+ if owns?(record)
334
395
  begin
335
- @store.delete(@scope)
336
- ensure
337
- FileUtils.rm_rf(File.dirname(cache(record))) if record.is_a?(Hash) && record['user'].is_a?(Hash) && UUID.match?(record.dig('user', 'id').to_s)
396
+ stored = record.merge('credential' => (record['credential'] || {}).select { |key, _| %w[product_id licentivo_product_id launch_connection_id environment expires_at].include?(key) })
397
+ @store.save(@scope, stored)
398
+ return
399
+ rescue StandardError
400
+ begin; delete_owned(record); rescue StandardError; end
338
401
  end
339
402
  end
403
+ raise Error.new('ACCOUNT_LOGOUT_STORAGE_FAILED') if queue_failure
404
+ end
405
+ def finish_account(record)
406
+ delete_owned(record)
407
+ update_pending(record, remove: true)
408
+ begin;File.unlink(intent_path(record));rescue Errno::ENOENT;end
409
+ end
410
+ def retry_other_accounts(skip, cancellation)
411
+ pending_accounts.each do |record|
412
+ next if record['token'] == skip
413
+ begin; call('/auth/logout', method: 'POST', body: {}, cancellation: cancellation, token: record['token'])
414
+ rescue StandardError => error
415
+ raise Error.new('ACCOUNT_LOGOUT_PENDING') unless error.is_a?(Error) && error.status == 401
416
+ end
417
+ finish_account(record)
418
+ end
340
419
  end
341
420
  def logout_internal(cancellation)
342
- record = @record || saved
343
- clear(record)
344
- return true unless record
421
+ record = @record || saved || pending_accounts.first
422
+ failure, persist_failure, other_failure, pending, confirmed = nil, nil, nil, false, true
423
+ begin; retry_other_accounts(record && record['token'], cancellation); rescue StandardError => error; other_failure = error; end
424
+ if record
425
+ @record = record.merge('version' => 3, 'logout_pending' => true)
426
+ record = @record
427
+ begin; mark_logout(record); rescue StandardError => error; persist_failure = error; end
428
+ end
345
429
  begin
346
- call('/auth/logout', method: 'POST', body: {}, cancellation: cancellation, token: record['token']); true
347
- rescue StandardError
348
- false
430
+ active = @store.load(@scope)
431
+ if !@runtime && record && (!active || active['token'] == record['token']) && record['credential'].is_a?(Hash) && record['credential']['launch_connection_id']
432
+ context = record.fetch('credential')
433
+ @runtime = @factory.call(@product.merge('license_key' => '', 'identity_token' => '', 'launch_connection_id' => context.fetch('launch_connection_id'), 'environment' => @launch['environment'], 'cache_path' => cache(record)))
434
+ end
435
+ if @runtime
436
+ result = @runtime.respond_to?(:logout) ? @runtime.logout : @runtime.deactivate
437
+ pending = (@runtime.respond_to?(:logout_pending?) && @runtime.logout_pending?) || (result.is_a?(Hash) && (result[:revocation_pending] || result['revocation_pending']))
438
+ raise Error.new('SDK_LOGOUT_STORAGE_FAILED') if result.is_a?(Hash) && (result[:storage_error] || result['storage_error'])
439
+ end
440
+ rescue StandardError => error
441
+ failure = error
442
+ ensure
443
+ stop
444
+ end
445
+ if record
446
+ begin; call('/auth/logout', method: 'POST', body: {}, cancellation: cancellation, token: record['token']); rescue StandardError => error; confirmed = error.is_a?(Error) && error.status == 401; end
447
+ end
448
+ if confirmed
449
+ finish_account(record) if record
450
+ clear(record)
349
451
  end
452
+ raise persist_failure if !confirmed && persist_failure
453
+ raise failure if failure
454
+ raise other_failure if other_failure
455
+ confirmed && !pending
350
456
  end
351
457
  end
352
458
  end
data/vendor/VERSION CHANGED
@@ -1 +1 @@
1
- 0.11.1
1
+ 0.11.2
data/vendor/c/demo.c CHANGED
@@ -28,6 +28,18 @@ int main(int argc, char **argv) {
28
28
  for (int i = 1; i < argc; i++) if (!strcmp(argv[i], "--cache-test")) cache_test = 1;
29
29
  ln_client *client = ln_init_file(path);
30
30
  if (!client) { fprintf(stderr, "C: invalid configuration\n"); return 1; }
31
+ for(int i=1;i<argc;i++)if(!strcmp(argv[i],"--logout-wait")){
32
+ char ready[4096],gate[4096];snprintf(ready,sizeof(ready),"%s.ready",path);snprintf(gate,sizeof(gate),"%s.continue",path);FILE*f=ln_check_feature(client,"export")?fopen(ready,"wb"):NULL;if(!f){ln_destroy(client);return 1;}fputs("ready",f);fclose(f);double deadline=clock_seconds()+30;
33
+ for(;;){f=fopen(gate,"rb");if(f){fclose(f);break;}if(clock_seconds()>deadline){ln_destroy(client);return 1;}
34
+ #ifdef _WIN32
35
+ Sleep(10);
36
+ #else
37
+ struct timespec pause={0,10000000L};nanosleep(&pause,NULL);
38
+ #endif
39
+ }
40
+ int ok=ln_logout(client)&&!ln_check_feature(client,"export");ln_destroy(client);return ok?0:1;
41
+ }
42
+ for(int i=1;i<argc;i++)if(!strcmp(argv[i],"--logout-retry")){int ok=ln_retry_pending_logouts(client)&&!ln_check_feature(client,"export");ln_destroy(client);if(ok)puts("C: pending logout completed");return ok?0:1;}
31
43
  for(int i=1;i<argc;i++)if(!strcmp(argv[i],"--improvements-test")){int ok=improvements(client,path);if(!ok)fprintf(stderr,"C: %s\n",ln_last_error(client));ln_destroy(client);if(ok)puts("C: credential / reservation / typed status OK");return ok?0:1;}
32
44
  for(int i=1;i<argc;i++)if(!strcmp(argv[i],"--startup-test")){double start=clock_seconds();int ok=ln_start(client)&&clock_seconds()-start<.3;start=clock_seconds();ok=ok&&ln_check_feature(client,"export")&&clock_seconds()-start<.3;ln_destroy(client);if(ok)puts("C: cached startup and local feature checks stay responsive");return ok?0:1;}
33
45
  int advanced=0,offline_export=0,offline_import=0,floating=0;
@@ -37,7 +49,7 @@ int main(int argc, char **argv) {
37
49
  int ok=1;if(!strcmp(argv[i],"--transfer-import")){char file[4096];snprintf(file,sizeof(file),"%s.request.json",path);char*req=read_text(file);snprintf(file,sizeof(file),"%s.response.json",path);char*res=read_text(file);ok=req&&res&&ln_import_offline(client,req,res);free(req);free(res);}else ok=ln_start(client);
38
50
  ok=ok&&ln_validate(client)&&ln_heartbeat(client)&&ln_check_feature(client,"export");if(!ok)fprintf(stderr,"C: %s\n",ln_last_error(client));ln_destroy(client);if(ok)puts("C: transferred device credential OK");return ok?0:1;
39
51
  }
40
- if(floating){int ok=ln_start(client);ln_client*other=ln_init_file(path);ok=ok&&other&&!ln_check_feature(other,"export")&&!ln_start(other);ln_destroy(client);ok=ok&&ln_start(other)&&ln_check_feature(other,"export");ln_destroy(other);if(ok)puts("C: floating seat release OK");return ok?0:1;}
52
+ if(floating){int ok=ln_start(client);ln_client*other=ln_init_file(path);ok=ok&&other&&!ln_check_feature(other,"export")&&ln_start(other);ln_destroy(client);ok=ok&&ln_start(other)&&ln_check_feature(other,"export");ln_destroy(other);if(ok)puts("C: floating seat release OK");return ok?0:1;}
41
53
  if(advanced||offline_import){int ok=1;
42
54
  if(offline_import){char file[4096];snprintf(file,sizeof(file),"%s.request.json",path);char*req=read_text(file);snprintf(file,sizeof(file),"%s.response.json",path);char*res=read_text(file);ok=req&&res&&ln_import_offline(client,req,res);free(req);free(res);}else ok=ln_start(client);
43
55
  ok=ok&&ln_check_feature(client,"export");char*r=ok?ln_consume(client,"export",1,"business-job-1"):NULL;ok=ok&&used_is(r,1,0);ln_free_string(r);
@@ -0,0 +1,92 @@
1
+ /* Internal implementation, included after the transport helpers in licentivo.c. */
2
+ static char*logout_path(ln_client*c){const char*p=text(c->cfg,"cache_path");if(!p)return NULL;char*out=malloc(strlen(p)+13);if(out)sprintf(out,"%s.logout.json",p);return out;}
3
+ static cJSON*pending_logouts(ln_client*c){return (cJSON*)get(c->logout_state,"pending");}
4
+ static int has_pending_logouts(ln_client*c){return cJSON_GetArraySize(pending_logouts(c))>0;}
5
+ static void new_logout_state(ln_client*c){c->logout_state=cJSON_CreateObject();cJSON_AddNumberToObject(c->logout_state,"version",1);cJSON_AddStringToObject(c->logout_state,"source_hash","");cJSON_AddBoolToObject(c->logout_state,"signed_out",0);cJSON_AddArrayToObject(c->logout_state,"pending");}
6
+ static const char*logout_text(const cJSON*s,const char*key){const char*v=text(s,key);return v?v:"";}
7
+ static void logout_binding(const cJSON*t,char out[80]){int64_t generation=0;const char*a=text(t,"activation_id");if(a&&num(t,"expected_generation",&generation))snprintf(out,80,"%s:%lld",a,(long long)generation);else out[0]=0;}
8
+ static char*logout_directory(ln_client*c){
9
+ const char*root=getenv("LOCALAPPDATA");if(!root||!root[0])root=getenv("XDG_CACHE_HOME");char*owned=NULL;
10
+ if(!root||!root[0]){const char*home=getenv("HOME");if(!home||!home[0])return NULL;owned=malloc(strlen(home)+8);if(!owned)return NULL;sprintf(owned,"%s/.cache",home);root=owned;}
11
+ const char*cache=text(c->cfg,"cache_path");char*absolute=NULL;
12
+ #ifdef _WIN32
13
+ wchar_t*wide=state_windows_path(cache);if(wide){const wchar_t*start=!wcsncmp(wide,L"\\\\?\\",4)?wide+4:wide;int n=WideCharToMultiByte(CP_UTF8,0,start,-1,NULL,0,NULL,NULL);absolute=malloc((size_t)n);if(absolute)WideCharToMultiByte(CP_UTF8,0,start,-1,absolute,n,NULL,NULL);free(wide);}
14
+ #else
15
+ if(cache[0]=='/')absolute=strdup(cache);else{char*cwd=getcwd(NULL,0);if(cwd){absolute=malloc(strlen(cwd)+strlen(cache)+2);if(absolute)sprintf(absolute,"%s/%s",cwd,cache);free(cwd);}}
16
+ #endif
17
+ if(!absolute){free(owned);return NULL;}size_t n=strlen(absolute)+strlen(text(c->cfg,"base_url"))+strlen(text(c->cfg,"product_id"))+strlen(text(c->cfg,"device_id"))+4;char*context=malloc(n);char scope[65];
18
+ if(!context){free(absolute);free(owned);return NULL;}snprintf(context,n,"%s\n%s\n%s\n%s",absolute,text(c->cfg,"base_url"),text(c->cfg,"product_id"),text(c->cfg,"device_id"));int ok=hash(context,scope);free(absolute);free(context);char*out=ok?malloc(strlen(root)+strlen(scope)+20):NULL;if(out)sprintf(out,"%s/Licentivo/logout/%s",root,scope);free(owned);return out;
19
+ }
20
+ static char*logout_scope(const cJSON*s){const char*a=logout_text(s,"source_hash"),*b=logout_text(s,"binding_key");char*out=malloc(strlen(a)+strlen(b)+2);if(out)sprintf(out,"%s\n%s",a,b);return out;}
21
+ static int save_logout_state(ln_client*c){
22
+ int64_t revision=0;num(c->logout_state,"revision",&revision);int64_t now=(int64_t)time(NULL)*1000;revision=revision+1>now?revision+1:now;cJSON_DeleteItemFromObjectCaseSensitive(c->logout_state,"revision");cJSON_AddNumberToObject(c->logout_state,"revision",(double)revision);
23
+ char*dir=logout_directory(c),*scope=logout_scope(c->logout_state),h[65];int fallback=0;if(dir&&scope&&hash(scope,h)){char*p=malloc(strlen(dir)+72);if(p){sprintf(p,"%s/%s.json",dir,h);fallback=write_state(p,c->logout_state);free(p);}}free(dir);free(scope);
24
+ char*p=logout_path(c);int primary=p&&write_state(p,c->logout_state);free(p);return fallback||(cJSON_IsTrue(get(c->logout_state,"signed_out"))&&primary);
25
+ }
26
+ static void read_logout_record(ln_client*c,cJSON*records,const char*path){
27
+ errno=0;char*b=readfile(path,65536);int absent=errno==ENOENT;if(!b){if(!absent)set_denied(c);return;}cJSON*s=ln_json(b);free(b);int64_t version;
28
+ if(!s||!num(s,"version",&version)||version!=1||!cJSON_IsArray(get(s,"pending"))){cJSON_Delete(s);set_denied(c);return;}char*scope=logout_scope(s);if(scope){const cJSON*old=get(records,scope);int64_t before=0,after=0;num(old,"revision",&before);num(s,"revision",&after);if(!old||after>before||(after==before&&(cJSON_IsTrue(get(s,"signed_out"))||!cJSON_IsTrue(get(old,"signed_out"))))){cJSON_DeleteItemFromObjectCaseSensitive(records,scope);cJSON_AddItemToObject(records,scope,s);}else cJSON_Delete(s);free(scope);}else cJSON_Delete(s);
29
+ }
30
+ static cJSON*logout_records(ln_client*c){
31
+ cJSON*records=cJSON_CreateObject();char*p=logout_path(c);if(p){read_logout_record(c,records,p);free(p);}else set_denied(c);char*dir=logout_directory(c);
32
+ if(dir){
33
+ #ifdef _WIN32
34
+ char*pattern=malloc(strlen(dir)+8);if(pattern){sprintf(pattern,"%s/*.json",dir);wchar_t*wide=state_windows_path(pattern);free(pattern);WIN32_FIND_DATAW data;HANDLE search=wide?FindFirstFileW(wide,&data):INVALID_HANDLE_VALUE;free(wide);if(search!=INVALID_HANDLE_VALUE){do{char name[256];int n=WideCharToMultiByte(CP_UTF8,0,data.cFileName,-1,name,sizeof(name),NULL,NULL);if(n>0){p=malloc(strlen(dir)+strlen(name)+2);if(p){sprintf(p,"%s/%s",dir,name);read_logout_record(c,records,p);free(p);}}}while(FindNextFileW(search,&data));FindClose(search);}else if(GetLastError()!=ERROR_FILE_NOT_FOUND&&GetLastError()!=ERROR_PATH_NOT_FOUND)set_denied(c);}
35
+ #else
36
+ DIR*entries=opendir(dir);if(entries){struct dirent*entry;while((entry=readdir(entries))){size_t n=strlen(entry->d_name);if(n>5&&!strcmp(entry->d_name+n-5,".json")){p=malloc(strlen(dir)+n+2);if(p){sprintf(p,"%s/%s",dir,entry->d_name);read_logout_record(c,records,p);free(p);}}}closedir(entries);}else if(errno!=ENOENT)set_denied(c);
37
+ #endif
38
+ free(dir);
39
+ }
40
+ return records;
41
+ }
42
+ static void load_logout_state(ln_client*c){
43
+ new_logout_state(c);c->logout_bindings=cJSON_CreateObject();cJSON*records=logout_records(c);
44
+ char h[65]="",binding[80];source_hash(c,h);logout_binding(get(c->auth,"logout_ticket"),binding);cJSON*tickets=cJSON_CreateArray();
45
+ for(cJSON*s=records->child;s;s=s->next){const char*b=logout_text(s,"binding_key");if(b[0]&&cJSON_IsTrue(get(s,"signed_out"))&&!get(c->logout_bindings,b))cJSON_AddBoolToObject(c->logout_bindings,b,1);}
46
+ for(cJSON*s=records->child;s;s=s->next){const char*b=logout_text(s,"binding_key");if((!strcmp(h,logout_text(s,"source_hash"))||(logout_text(c->cfg,"launch_connection_id")[0]&&binding[0]&&!strcmp(b,binding))||(!c->auth&&!source_input(c)[0]))&&(!b[0]||!binding[0]||!strcmp(b,binding))){cJSON_Delete(c->logout_state);c->logout_state=cJSON_Duplicate(s,1);if(cJSON_IsTrue(get(s,"signed_out"))&&!text(c->cfg,"license_key")[0])set_denied(c);}for(const cJSON*t=get(s,"pending")->child;t;t=t->next){int found=0;for(cJSON*x=tickets->child;x;x=x->next)if(!strcmp(logout_text(t,"operation_id"),logout_text(x,"operation_id")))found=1;if(!found)cJSON_AddItemToArray(tickets,cJSON_Duplicate(t,1));}}
47
+ cJSON_ReplaceItemInObjectCaseSensitive(c->logout_state,"pending",tickets);cJSON_Delete(records);
48
+ if(is_denied(c)){cJSON_Delete(c->auth);c->auth=NULL;}
49
+ }
50
+ static int acknowledge_logout(ln_client*c,const char*operation){
51
+ cJSON*records=logout_records(c);char*dir=logout_directory(c);int ok=dir!=NULL;
52
+ for(cJSON*s=records->child;s&&ok;s=s->next){cJSON*pending=(cJSON*)get(s,"pending");int changed=0;for(int i=cJSON_GetArraySize(pending)-1;i>=0;i--){if(!strcmp(logout_text(cJSON_GetArrayItem(pending,i),"operation_id"),operation)){cJSON_DeleteItemFromArray(pending,i);changed=1;}}
53
+ if(changed){char*scope=logout_scope(s),h[65];ok=scope&&hash(scope,h);if(ok){char*p=malloc(strlen(dir)+72);ok=p!=NULL;if(p){sprintf(p,"%s/%s.json",dir,h);ok=write_state(p,s);free(p);}}free(scope);}
54
+ }
55
+ free(dir);cJSON_Delete(records);return ok;
56
+ }
57
+ static int revocation_error(const char*code){const char*codes[]={"LICENSE_REVOKED","LICENSE_SUSPENDED","LICENSE_EXPIRED","DEVICE_RELEASED","DEVICE_NOT_ACTIVE","DEVICE_CREDENTIAL_REVOKED","DEVICE_CREDENTIAL_INVALID","AUTHORIZATION_SESSION_REVOKED","AUTHORIZATION_SESSION_EXPIRED","LAUNCH_LOGIN_REQUIRED","LAUNCH_ENTITLEMENT_REVOKED","LAUNCH_ENTITLEMENT_EXPIRED"};for(size_t i=0;i<sizeof(codes)/sizeof(codes[0]);i++)if(!strcmp(code,codes[i]))return 1;return 0;}
58
+ static int deny_runtime(ln_client*c){
59
+ set_denied(c);if(!c->logout_state)new_logout_state(c);char h[65];
60
+ if((source_input(c)[0]||c->auth)&&source_hash(c,h))cJSON_ReplaceItemInObjectCaseSensitive(c->logout_state,"source_hash",cJSON_CreateString(h));
61
+ cJSON_ReplaceItemInObjectCaseSensitive(c->logout_state,"signed_out",cJSON_CreateBool(1));
62
+ const cJSON*ticket=get(c->auth,"logout_ticket");const char*operation=text(ticket,"operation_id");int found=0;
63
+ char binding[80];logout_binding(ticket,binding);if(binding[0]){cJSON_DeleteItemFromObjectCaseSensitive(c->logout_state,"binding_key");cJSON_AddStringToObject(c->logout_state,"binding_key",binding);if(c->logout_bindings&&!get(c->logout_bindings,binding))cJSON_AddBoolToObject(c->logout_bindings,binding,1);}
64
+ if(operation){for(cJSON*p=pending_logouts(c)->child;p;p=p->next)if(text(p,"operation_id")&&!strcmp(operation,text(p,"operation_id")))found=1;if(!found)cJSON_AddItemToArray(pending_logouts(c),cJSON_Duplicate(ticket,1));}
65
+ int ok=save_logout_state(c);invalidate(c);cJSON_Delete(c->auth);c->auth=NULL;c->auth_dirty=0;
66
+ /* Keep the immutable configuration graph stable for concurrent local checks. */
67
+ const char*private_names[]={"license_key","identity_token"};for(int i=0;i<2;i++){char*secret=(char*)text(c->cfg,private_names[i]);if(secret){size_t count=strlen(secret);volatile unsigned char*p=(volatile unsigned char*)secret;while(count--)*p++=0;}}return ok;
68
+ }
69
+ static int logout_binding_blocked(ln_client*c,const cJSON*result){
70
+ const char*connection=text(c->cfg,"launch_connection_id");char binding[80];const cJSON*ticket=get(result,"logout_ticket");logout_binding(ticket,binding);
71
+ if(!connection||!connection[0]||!cJSON_IsTrue(get(c->logout_bindings,binding)))return 0;
72
+ if(c->auth){cJSON_DeleteItemFromObjectCaseSensitive(c->auth,"logout_ticket");cJSON_AddItemToObject(c->auth,"logout_ticket",cJSON_Duplicate(ticket,1));}deny_runtime(c);strcpy(c->error,"LAUNCH_LOGIN_REQUIRED");return 1;
73
+ }
74
+ static int retry_logouts_unlocked(ln_client*c){
75
+ while(has_pending_logouts(c)){
76
+ cJSON*ticket=cJSON_GetArrayItem(pending_logouts(c),0);char*body=cJSON_PrintUnformatted(ticket);const char*base=text(c->cfg,"base_url");char*url=malloc(strlen(base)+32);if(!body||!url){free(body);free(url);return 0;}sprintf(url,"%s/api/v1/sessions/logout",base);
77
+ CURL*curl=curl_easy_init();if(!curl){free(body);free(url);return 0;}struct buffer b={{0},0,0};struct curl_slist*headers=curl_slist_append(NULL,"Content-Type: application/json");
78
+ curl_easy_setopt(curl,CURLOPT_URL,url);curl_easy_setopt(curl,CURLOPT_POSTFIELDS,body);curl_easy_setopt(curl,CURLOPT_HTTPHEADER,headers);curl_easy_setopt(curl,CURLOPT_WRITEFUNCTION,receive);curl_easy_setopt(curl,CURLOPT_WRITEDATA,&b);curl_easy_setopt(curl,CURLOPT_TIMEOUT,5L);curl_easy_setopt(curl,CURLOPT_CONNECTTIMEOUT,5L);curl_easy_setopt(curl,CURLOPT_NOSIGNAL,1L);curl_easy_setopt(curl,CURLOPT_FOLLOWLOCATION,0L);curl_easy_setopt(curl,CURLOPT_SSL_VERIFYPEER,1L);curl_easy_setopt(curl,CURLOPT_SSL_VERIFYHOST,2L);
79
+ const char*proxy=text(c->cfg,"proxy_url"),*ca=getenv("LICENTIVO_CA_BUNDLE");if(proxy&&proxy[0])curl_easy_setopt(curl,CURLOPT_PROXY,proxy);if(ca&&ca[0])curl_easy_setopt(curl,CURLOPT_CAINFO,ca);
80
+ CURLcode error=curl_easy_perform(curl);long status=0;curl_easy_getinfo(curl,CURLINFO_RESPONSE_CODE,&status);curl_easy_cleanup(curl);curl_slist_free_all(headers);free(body);free(url);
81
+ cJSON*envelope=error==CURLE_OK&&!b.oversized?ln_json(b.data):NULL;const cJSON*ack=get(envelope,"data");const char*state=text(ack,"status"),*operation=text(ack,"operation_id"),*activation=text(ack,"activation_id");int64_t generation=0,expected=0;
82
+ int ok=status==200&&state&&(!strcmp(state,"succeeded")||!strcmp(state,"stale"))&&operation&&activation&&text(ticket,"operation_id")&&text(ticket,"activation_id")&&!strcmp(operation,text(ticket,"operation_id"))&&!strcmp(activation,text(ticket,"activation_id"))&&num(ack,"expected_generation",&generation)&&num(ticket,"expected_generation",&expected)&&generation==expected;cJSON_Delete(envelope);if(!ok)return 0;
83
+ char completed[128];snprintf(completed,sizeof(completed),"%s",logout_text(ticket,"operation_id"));cJSON_DeleteItemFromArray(pending_logouts(c),0);if(!acknowledge_logout(c,completed)||!save_logout_state(c))return 0;
84
+ }
85
+ return 1;
86
+ }
87
+ int ln_retry_pending_logouts(ln_client*c){if(!c)return 0;lock(c);int ok=retry_logouts_unlocked(c);unlock(c);return ok;}
88
+ int ln_logout_pending(ln_client*c){if(!c)return 0;lock(c);int pending=has_pending_logouts(c);unlock(c);return pending;}
89
+ int ln_logout(ln_client*c){
90
+ if(!c)return 0;
91
+ set_denied(c);lock(c);int ok=deny_runtime(c);retry_logouts_unlocked(c);int pending=has_pending_logouts(c);strcpy(c->error,ok?"DEVICE_RELEASED":"LOCAL_CREDENTIAL_CLEANUP_FAILED");publish(c,0);unlock(c);notify(c);if(pending)ln_start_heartbeat(c);return ok;
92
+ }
data/vendor/c/licentivo.c CHANGED
@@ -15,17 +15,51 @@
15
15
  #include <math.h>
16
16
  #include <errno.h>
17
17
  #include <sys/stat.h>
18
+ #include <wchar.h>
18
19
  #ifdef _WIN32
19
20
  #include <windows.h>
20
21
  #include <bcrypt.h>
21
22
  #include <direct.h>
22
- #define mkdir_private(p) _mkdir(p)
23
+ #include <io.h>
24
+ /* State journals can exceed MAX_PATH under a caller's cache directory. Use
25
+ * absolute UTF-16 paths with the extended prefix for every state-file operation. */
26
+ static wchar_t*state_windows_path(const char*path){
27
+ int size=MultiByteToWideChar(CP_UTF8,MB_ERR_INVALID_CHARS,path,-1,NULL,0);if(!size){errno=EINVAL;return NULL;}
28
+ wchar_t*input=malloc((size_t)size*sizeof(wchar_t));if(!input){errno=ENOMEM;return NULL;}
29
+ if(!MultiByteToWideChar(CP_UTF8,MB_ERR_INVALID_CHARS,path,-1,input,size)){free(input);errno=EINVAL;return NULL;}
30
+ for(wchar_t*p=input;*p;p++)if(*p==L'/')*p=L'\\';
31
+ if(!wcsncmp(input,L"\\\\?\\",4))return input;
32
+ DWORD count=GetFullPathNameW(input,0,NULL,NULL);if(!count||count>32760){free(input);errno=EINVAL;return NULL;}
33
+ wchar_t*absolute=malloc((size_t)count*sizeof(wchar_t));if(!absolute){free(input);errno=ENOMEM;return NULL;}
34
+ DWORD written=GetFullPathNameW(input,count,absolute,NULL);free(input);if(!written||written>=count){free(absolute);errno=EINVAL;return NULL;}
35
+ int unc=absolute[0]==L'\\'&&absolute[1]==L'\\';
36
+ wchar_t*result=malloc(((size_t)written+9)*sizeof(wchar_t));if(!result){free(absolute);errno=ENOMEM;return NULL;}
37
+ wcscpy(result,unc?L"\\\\?\\UNC\\":L"\\\\?\\");wcscat(result,absolute+(unc?2:0));free(absolute);return result;
38
+ }
39
+ static FILE*state_windows_open(const char*path,const char*mode){
40
+ wchar_t*p=state_windows_path(path),wide_mode[8];if(!p)return NULL;size_t length=strlen(mode);if(length>=8){free(p);errno=EINVAL;return NULL;}
41
+ for(size_t i=0;i<=length;i++){wide_mode[i]=(wchar_t)(unsigned char)mode[i];}
42
+ FILE*f=_wfopen(p,wide_mode);free(p);return f;
43
+ }
44
+ static int state_windows_remove(const char*path){wchar_t*p=state_windows_path(path);if(!p)return -1;int result=_wremove(p);free(p);return result;}
45
+ static int state_windows_mkdir(const char*path){wchar_t*p=state_windows_path(path);if(!p)return -1;int result=_wmkdir(p);free(p);return result;}
46
+ static int state_windows_stat(const char*path,struct stat*info){
47
+ wchar_t*p=state_windows_path(path);if(!p)return -1;WIN32_FILE_ATTRIBUTE_DATA data;BOOL ok=GetFileAttributesExW(p,GetFileExInfoStandard,&data);DWORD error=GetLastError();free(p);
48
+ if(!ok){errno=error==ERROR_FILE_NOT_FOUND||error==ERROR_PATH_NOT_FOUND?ENOENT:EACCES;return -1;}
49
+ memset(info,0,sizeof(*info));info->st_size=data.nFileSizeHigh||data.nFileSizeLow>2147483647?2147483647:(long)data.nFileSizeLow;return 0;
50
+ }
51
+ static int state_windows_move(const char*from,const char*to){wchar_t*a=state_windows_path(from),*b=state_windows_path(to);int ok=a&&b&&MoveFileExW(a,b,MOVEFILE_REPLACE_EXISTING|MOVEFILE_WRITE_THROUGH);free(a);free(b);return ok;}
52
+ #define fopen(path,mode) state_windows_open(path,mode)
53
+ #define remove(path) state_windows_remove(path)
54
+ #define stat(path,info) state_windows_stat(path,info)
55
+ #define mkdir_private(p) state_windows_mkdir(p)
23
56
  #else
24
57
  #include <openssl/evp.h>
25
58
  #include <openssl/rand.h>
26
59
  #include <pthread.h>
27
60
  #include <stdatomic.h>
28
61
  #include <unistd.h>
62
+ #include <dirent.h>
29
63
  #define mkdir_private(p) mkdir(p,0700)
30
64
  #endif
31
65
  #define LIMIT 16384
@@ -48,14 +82,29 @@ static int ensure_curl(void) { return pthread_once(&curl_once, initialize_curl)
48
82
  #endif
49
83
 
50
84
  struct ln_client {
51
- cJSON *cfg,*cached,*auth;int auth_dirty;char session[37];int interval;int offline;int64_t wall;double tick;char error[80];int started;int finished;int refresh_pending;int online;int retryable;long http_status;int retry_after;char request_id[128];int64_t retry_at;
85
+ cJSON *cfg,*cached,*auth,*logout_state,*logout_bindings;int auth_dirty;char session[37];int interval;int offline;int64_t wall;double tick;char error[80];int started;int finished;int refresh_pending;int online;int retryable;long http_status;int retry_after;char request_id[128];int64_t retry_at;
52
86
  cJSON *view;int64_t view_wall;double view_tick;int view_online;ln_error_info view_error;ln_status_callback callback;void *callback_data;int task_busy;
53
87
  #ifdef _WIN32
54
- CRITICAL_SECTION mutex,view_mutex;HANDLE thread;volatile LONG stop;
88
+ CRITICAL_SECTION mutex,view_mutex;HANDLE thread;volatile LONG stop,denied;
55
89
  #else
56
- pthread_mutex_t mutex,view_mutex;pthread_t thread;atomic_int stop;
90
+ pthread_mutex_t mutex,view_mutex;pthread_t thread;atomic_int stop,denied;
57
91
  #endif
58
92
  };
93
+ static int is_denied(ln_client*c){
94
+ #ifdef _WIN32
95
+ return InterlockedCompareExchange(&c->denied,0,0)!=0;
96
+ #else
97
+ return atomic_load(&c->denied);
98
+ #endif
99
+ }
100
+ static void set_denied(ln_client*c){
101
+ #ifdef _WIN32
102
+ InterlockedExchange(&c->denied,1);
103
+ #else
104
+ atomic_store(&c->denied,1);
105
+ #endif
106
+ }
107
+ static void load_logout_state(ln_client*c);static int deny_runtime(ln_client*c);static int revocation_error(const char*code);static int retry_logouts_unlocked(ln_client*c);static int has_pending_logouts(ln_client*c);
59
108
  static const cJSON *get(const cJSON *o,const char*k){return cJSON_GetObjectItemCaseSensitive(o,k);}
60
109
  static const char *text(const cJSON *o,const char*k){const cJSON*v=get(o,k);return cJSON_IsString(v)?v->valuestring:NULL;}
61
110
  static int num(const cJSON*o,const char*k,int64_t*out){const cJSON*v=get(o,k);if(!cJSON_IsNumber(v)||!isfinite(v->valuedouble)||v->valuedouble!=floor(v->valuedouble)||fabs(v->valuedouble)>9007199254740991.0)return 0;*out=(int64_t)v->valuedouble;return 1;}
@@ -65,13 +114,17 @@ static int fields(const cJSON*v,const char*const*names,int n){if(!cJSON_IsObject
65
114
  static int uuid(const char*s){if(!s||strlen(s)!=36)return 0;for(int i=0;i<36;i++){if(i==8||i==13||i==18||i==23){if(s[i]!='-')return 0;}else if(!isxdigit((unsigned char)s[i]))return 0;}return 1;}
66
115
  static int device(const char*s){if(!s||strlen(s)!=64)return 0;for(int i=0;i<64;i++)if(!((s[i]>='a'&&s[i]<='f')||(s[i]>='0'&&s[i]<='9')))return 0;return 1;}
67
116
  static int credential_secret(const char*s,const char*prefix){if(!s||strlen(s)!=50||strncmp(s,prefix,7))return 0;for(int i=7;i<50;i++)if(!isalnum((unsigned char)s[i])&&s[i]!='_'&&s[i]!='-')return 0;return 1;}
68
- static int temporary(ln_client*c){return c->auth!=NULL||text(c->cfg,"license_key")[0]!=0;}
117
+ static const char*source_input(ln_client*c){const char*identity=text(c->cfg,"identity_token");return identity&&identity[0]?identity:text(c->cfg,"license_key");}
118
+ static int source_hash(ln_client*c,char out[65]);
119
+ static int temporary(ln_client*c){return c->auth!=NULL||source_input(c)[0]!=0;}
69
120
  static const char*runtime_key(ln_client*c){const char*k=text(c->auth,"device_key");return k&&k[0]?k:text(c->cfg,"license_key");}
70
121
  static void publish(ln_client*c,int online);
71
122
  static void notify(ln_client*c);
72
123
  static void load_credential(ln_client*c);
73
124
  static int prepare_credential(ln_client*c);
74
125
  static int accept_credential(ln_client*c,const cJSON*result);
126
+ static int save_logout_state(ln_client*c);
127
+ static int logout_binding_blocked(ln_client*c,const cJSON*result);
75
128
  static const char b64chars[]="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
76
129
  static int b64(const char*s,unsigned char*out,size_t capacity){if(!s||!s[0])return -1;unsigned int acc=0,bits=0;size_t used=0;for(size_t i=0;s[i];i++){const char*p=strchr(b64chars,s[i]);if(!p||s[i]=='=')return -1;acc=(acc<<6)|(unsigned int)(p-b64chars);bits+=6;if(bits>=8){bits-=8;if(used>=capacity)return -1;out[used++]=(unsigned char)(acc>>bits);}}if(bits==6||(bits&&(acc&((1u<<bits)-1u))))return -1;return (int)used;}
77
130
  static int64_t policy_span(const cJSON*c,int64_t issued){
@@ -95,11 +148,12 @@ static int app_version(const char*s){if(!s)return 0;const char*p=s;for(int i=0;i
95
148
  static int advanced_claims(const cJSON*c){
96
149
  const cJSON*request=get(c,"offline_request_id");if(request&&!uuid(text(c,"offline_request_id")))return 0;const cJSON*x=get(c,"capabilities");if(!x)return 1;if(!cJSON_IsObject(x))return 0;
97
150
  const char*model=text(x,"license_model");if(!model||(strcmp(model,"node_locked")&&strcmp(model,"floating")))return 0;
98
- const char*const allowed[]={"license_model","session_id","session_expires_at","app_version","version_released_at","maintenance_until"};for(const cJSON*v=x->child;v;v=v->next){int ok=0;for(int i=0;i<6;i++)if(!strcmp(v->string,allowed[i]))ok=1;if(!ok)return 0;}
151
+ const char*const allowed[]={"license_model","session_id","session_expires_at","app_version","version_released_at","maintenance_until","launch_connection_id","environment","binding_version"};for(const cJSON*v=x->child;v;v=v->next){int ok=0;for(int i=0;i<9;i++)if(!strcmp(v->string,allowed[i]))ok=1;if(!ok)return 0;}
99
152
  int64_t session=0,released=0,maintenance=0,issued=0,expires=0;const char*const dates[]={"session_expires_at","version_released_at","maintenance_until"};int64_t*values[]={&session,&released,&maintenance};for(int i=0;i<3;i++)if(get(x,dates[i])&&(!num(x,dates[i],values[i])||*values[i]<0||*values[i]>253402300799LL))return 0;
100
153
  if(get(x,"app_version")&&!app_version(text(x,"app_version")))return 0;
101
154
  if(!num(c,"issued_at",&issued)||!num(c,"expires_at",&expires))return 0;
102
155
  const cJSON*policy=get(c,"runtime_policy");int64_t heartbeat=0;if(!strcmp(model,"floating")&&(!uuid(text(x,"session_id"))||session<=issued||expires>session||!num(policy,"heartbeat_seconds",&heartbeat)||heartbeat==0||!strcmp(text(policy,"offline_mode")?text(policy,"offline_mode"):"","permanent")))return 0;
156
+ if(get(x,"launch_connection_id")||get(x,"environment")||get(x,"binding_version")){int64_t binding=0,h=0,o=0;const char*env=text(x,"environment"),*mode=text(policy,"offline_mode");if(!uuid(text(x,"launch_connection_id"))||!env||(strcmp(env,"live")&&strcmp(env,"sandbox"))||!num(x,"binding_version",&binding)||binding<1||strcmp(model,"node_locked")||!uuid(text(x,"session_id"))||session<=issued||expires>session||!num(policy,"heartbeat_seconds",&h)||h<600||!mode||!strcmp(mode,"permanent")||!num(policy,"offline_seconds",&o)||o>86400)return 0;}
103
157
  if(released>issued||(maintenance>0&&(!text(x,"app_version")||released==0||released>maintenance)))return 0;
104
158
  return 1;
105
159
  }
@@ -196,9 +250,10 @@ static int origin(const cJSON*cfg){CURLU*u=curl_url();if(!u)return 0;int valid=0
196
250
  valid=!strcmp(scheme,"https")||(!strcmp(scheme,"http")&&cJSON_IsTrue(get(cfg,"allow_http"))&&(!strcmp(host,"localhost")||!strcmp(host,"127.0.0.1")||!strcmp(host,"[::1]")));
197
251
  done:curl_free(scheme);curl_free(host);curl_free(path);curl_url_cleanup(u);return valid;}
198
252
  static cJSON*claims_from(const cJSON*envelope){unsigned char raw[LIMIT+1];int n=b64(text(envelope,"payload"),raw,LIMIT);if(n<0)return NULL;raw[n]=0;return ln_json((char*)raw);}
199
- static int client_context(ln_client*c,const cJSON*envelope){cJSON*claims=claims_from(envelope);const cJSON*x=get(claims,"capabilities");int ok=1;if(x){const char*version=text(x,"app_version"),*expected=text(c->cfg,"app_version"),*model=text(x,"license_model");if(strcmp(version?version:"",expected?expected:""))ok=0;if(model&&!strcmp(model,"floating")&&(!text(x,"session_id")||strcmp(text(x,"session_id"),c->session)))ok=0;}cJSON_Delete(claims);return ok;}
253
+ static int client_context(ln_client*c,const cJSON*envelope){cJSON*claims=claims_from(envelope);const cJSON*x=get(claims,"capabilities");int ok=1;const char*connection=text(c->cfg,"launch_connection_id"),*environment=text(c->cfg,"environment");if(!x&&connection&&connection[0])ok=0;if(x){const char*signed_connection=text(x,"launch_connection_id"),*signed_environment=text(x,"environment");if(strcmp(signed_connection?signed_connection:"",connection?connection:"")||strcmp(signed_environment?signed_environment:"",environment?environment:""))ok=0;const char*version=text(x,"app_version"),*expected=text(c->cfg,"app_version"),*model=text(x,"license_model");if(strcmp(version?version:"",expected?expected:""))ok=0;if(model&&!strcmp(model,"floating")&&(!text(x,"session_id")||strcmp(text(x,"session_id"),c->session)))ok=0;}cJSON_Delete(claims);return ok;}
200
254
  static int floating(ln_client*c){cJSON*claims=claims_from(c->cached);const char*model=text(get(claims,"capabilities"),"license_model");int result=model&&!strcmp(model,"floating");cJSON_Delete(claims);return result;}
201
- static int cache_valid(ln_client*c){return c->cached&&client_context(c,c->cached)&&(!c->wall||time(NULL)>=c->wall+(monotonic()-c->tick)-300)&&ln_verify(c->cached,get(c->cfg,"trusted_keys"),text(c->cfg,"product_id"),text(c->cfg,"device_id"),time(NULL));}
255
+ static int source_hash(ln_client*c,char out[65]){const char*source=source_input(c);if(source&&source[0])return hash(source,out);const char*h=text(c->auth,"source_hash");if(!device(h))return 0;strcpy(out,h);return 1;}
256
+ static int cache_valid(ln_client*c){return !is_denied(c)&&c->cached&&client_context(c,c->cached)&&(!c->wall||time(NULL)>=c->wall+(monotonic()-c->tick)-300)&&ln_verify(c->cached,get(c->cfg,"trusted_keys"),text(c->cfg,"product_id"),text(c->cfg,"device_id"),time(NULL));}
202
257
  static void view_lock(ln_client*c){
203
258
  #ifdef _WIN32
204
259
  EnterCriticalSection(&c->view_mutex);
@@ -214,7 +269,7 @@ static void view_unlock(ln_client*c){
214
269
  #endif
215
270
  }
216
271
  static void publish(ln_client*c,int online){view_lock(c);cJSON_Delete(c->view);c->view=cJSON_Duplicate(c->cached,1);c->view_wall=c->wall;c->view_tick=c->tick;c->view_online=online;snprintf(c->view_error.code,sizeof(c->view_error.code),"%s",c->error);snprintf(c->view_error.request_id,sizeof(c->view_error.request_id),"%s",c->request_id);c->view_error.http_status=(int)c->http_status;c->view_error.retryable=c->retryable;c->view_error.retry_after_seconds=c->retry_after;view_unlock(c);}
217
- static cJSON*local_claims(ln_client*c){view_lock(c);cJSON*envelope=cJSON_Duplicate(c->view,1);int64_t wall=c->view_wall;double tick=c->view_tick;view_unlock(c);int valid=envelope&&client_context(c,envelope)&&(!wall||time(NULL)>=wall+monotonic()-tick-300)&&ln_verify(envelope,get(c->cfg,"trusted_keys"),text(c->cfg,"product_id"),text(c->cfg,"device_id"),time(NULL));cJSON*claims=valid?claims_from(envelope):NULL;cJSON_Delete(envelope);return claims;}
272
+ static cJSON*local_claims(ln_client*c){if(is_denied(c))return NULL;view_lock(c);cJSON*envelope=cJSON_Duplicate(c->view,1);int64_t wall=c->view_wall;double tick=c->view_tick;view_unlock(c);int valid=envelope&&client_context(c,envelope)&&(!wall||time(NULL)>=wall+monotonic()-tick-300)&&ln_verify(envelope,get(c->cfg,"trusted_keys"),text(c->cfg,"product_id"),text(c->cfg,"device_id"),time(NULL));cJSON*claims=valid?claims_from(envelope):NULL;cJSON_Delete(envelope);return claims;}
218
273
  int ln_get_error_info(ln_client*c,ln_error_info*out){if(!c||!out)return 0;view_lock(c);*out=c->view_error;view_unlock(c);return 1;}
219
274
  char*ln_status(ln_client*c){if(!c)return NULL;cJSON*claims=local_claims(c),*status=cJSON_CreateObject();view_lock(c);ln_error_info e=c->view_error;int online=c->view_online;view_unlock(c);const char*state=claims?(online?"active":"offline_valid"):"verification_required";if(!strcmp(e.code,"LICENSE_EXPIRED"))state="expired";if(!strcmp(e.code,"LICENSE_REVOKED")||!strcmp(e.code,"LICENSE_SUSPENDED"))state="revoked";if(!strcmp(e.code,"DEVICE_RELEASED")||!strcmp(e.code,"DEVICE_CREDENTIAL_REVOKED"))state="released";if(!strcmp(e.code,"FEATURE_QUOTA_EXCEEDED"))state="quota_exhausted";int64_t expires=0;num(claims,"expires_at",&expires);cJSON_AddStringToObject(status,"state",state);cJSON_AddBoolToObject(status,"allowed",claims!=NULL);cJSON_AddStringToObject(status,"code",e.code);cJSON_AddStringToObject(status,"request_id",e.request_id);cJSON_AddBoolToObject(status,"retryable",e.retryable);cJSON_AddNumberToObject(status,"lease_valid_until",(double)expires);char*out=cJSON_PrintUnformatted(status);cJSON_Delete(status);cJSON_Delete(claims);return out;}
220
275
  char*ln_feature_status(ln_client*c,const char*feature){
@@ -242,9 +297,9 @@ ln_client*ln_init(const char*config){if(!ensure_curl())return NULL;cJSON*cfg=ln_
242
297
  #ifdef _WIN32
243
298
  InitializeCriticalSection(&c->mutex);InitializeCriticalSection(&c->view_mutex);
244
299
  #else
245
- pthread_mutex_init(&c->mutex,NULL);pthread_mutex_init(&c->view_mutex,NULL);atomic_init(&c->stop,0);
300
+ pthread_mutex_init(&c->mutex,NULL);pthread_mutex_init(&c->view_mutex,NULL);atomic_init(&c->stop,0);atomic_init(&c->denied,0);
246
301
  #endif
247
- load_credential(c);if(!secret[0]&&!c->auth){ln_destroy(c);return NULL;}const char*path=text(cfg,"cache_path");char*b=path&&path[0]?readfile(path,LIMIT):NULL;if(b){cJSON*s=ln_json(b);int64_t version,observed;char h[65];int hash_ok=secret[0]?hash(secret,h):device(text(c->auth,"source_hash"));if(!secret[0]&&hash_ok)strcpy(h,text(c->auth,"source_hash"));if(hash_ok&&s&&num(s,"version",&version)&&version==1&&text(s,"license_hash")&&!strcmp(text(s,"license_hash"),h)&&num(s,"observed_at",&observed)&&time(NULL)+300>=observed&&ln_verify(get(s,"lease"),keys,text(cfg,"product_id"),text(cfg,"device_id"),time(NULL))){int interval=3600,mode=envelope_policy(get(s,"lease"),&interval);if(mode!=1&&client_context(c,get(s,"lease"))){c->cached=cJSON_Duplicate(get(s,"lease"),1);c->wall=observed;c->interval=interval;c->offline=mode;}}cJSON_Delete(s);free(b);}publish(c,0);return c;}
302
+ load_credential(c);load_logout_state(c);if(!source_input(c)[0]&&!c->auth&&!is_denied(c)){ln_destroy(c);return NULL;}const char*path=text(cfg,"cache_path");char*b=!is_denied(c)&&path&&path[0]?readfile(path,LIMIT):NULL;if(b){cJSON*s=ln_json(b);int64_t version,observed;char h[65];int hash_ok=source_hash(c,h);if(hash_ok&&s&&num(s,"version",&version)&&version==1&&text(s,"license_hash")&&!strcmp(text(s,"license_hash"),h)&&num(s,"observed_at",&observed)&&time(NULL)+300>=observed&&ln_verify(get(s,"lease"),keys,text(cfg,"product_id"),text(cfg,"device_id"),time(NULL))){int interval=3600,mode=envelope_policy(get(s,"lease"),&interval);if(mode!=1&&client_context(c,get(s,"lease"))){c->cached=cJSON_Duplicate(get(s,"lease"),1);c->wall=observed;c->interval=interval;c->offline=mode;}}cJSON_Delete(s);free(b);}publish(c,0);if(has_pending_logouts(c))ln_start_heartbeat(c);return c;}
248
303
  ln_client*ln_init_file(const char*p){char*b=readfile(p,LIMIT);if(!b)return NULL;ln_client*c=ln_init(b);free(b);return c;}
249
304
  ln_client*ln_init_product(const char*product_id,const char*trusted_keys_json,const char*license_key){
250
305
  if(!product_id||!trusted_keys_json||!license_key)return NULL;
@@ -272,38 +327,48 @@ ln_client*ln_init_file_with_license(const char*path,const char*license_key){
272
327
  ln_client*c=ln_init(b);free(b);return c;
273
328
  }
274
329
  const char*ln_last_error(const ln_client*c){return c?c->error:"INVALID_CONFIGURATION";}
275
- static void invalidate(ln_client*c){cJSON_Delete(c->cached);c->cached=NULL;c->offline=0;c->interval=3600;publish(c,0);const char*p=text(c->cfg,"cache_path");if(p&&p[0])remove(p);}
330
+ static void invalidate(ln_client*c){cJSON_Delete(c->cached);c->cached=NULL;c->offline=0;c->interval=3600;publish(c,0);const char*p=text(c->cfg,"cache_path");if(p&&p[0]&&!is_denied(c))remove(p);}
276
331
  static void mkdirs(const char*p){char*copy=malloc(strlen(p)+1);if(!copy)return;strcpy(copy,p);for(char*s=copy+1;*s;s++)if(*s=='/'||*s=='\\'){char old=*s;*s=0;mkdir_private(copy);*s=old;}free(copy);}
277
332
  static int write_state(const char*p,const cJSON*s){
278
- char*b=cJSON_PrintUnformatted(s);char*tmp=malloc(strlen(p)+40);if(!tmp||!b){free(tmp);free(b);return 0;}unsigned char random[8];if(!random_bytes(random,8)){free(tmp);free(b);return 0;}sprintf(tmp,"%s.%02x%02x%02x%02x.tmp",p,random[0],random[1],random[2],random[3]);mkdirs(p);FILE*f=fopen(tmp,"wb");int ok=0;if(f){
333
+ char*b=cJSON_PrintUnformatted(s);char*tmp=malloc(strlen(p)+40);if(!tmp||!b){free(tmp);free(b);return 0;}unsigned char random[8];if(!random_bytes(random,8)){free(tmp);free(b);return 0;}
334
+ /* Keep temporary filenames short even when a task journal has a long name. */
335
+ const char*slash=strrchr(p,'/'),*backslash=strrchr(p,'\\');if(backslash&&(!slash||backslash>slash))slash=backslash;size_t prefix=slash?(size_t)(slash-p)+1:0;memcpy(tmp,p,prefix);sprintf(tmp+prefix,".state-%02x%02x%02x%02x%02x%02x%02x%02x.tmp",random[0],random[1],random[2],random[3],random[4],random[5],random[6],random[7]);mkdirs(p);FILE*f=fopen(tmp,"wb");int ok=0;if(f){
279
336
  #ifndef _WIN32
280
337
  fchmod(fileno(f),0600);
281
338
  #endif
282
- ok=fwrite(b,1,strlen(b),f)==strlen(b);if(fclose(f))ok=0;
339
+ ok=fwrite(b,1,strlen(b),f)==strlen(b);if(fflush(f))ok=0;
340
+ #ifdef _WIN32
341
+ if(ok&&_commit(_fileno(f)))ok=0;
342
+ #else
343
+ if(ok&&fsync(fileno(f)))ok=0;
344
+ #endif
345
+ if(fclose(f))ok=0;
283
346
  #ifdef _WIN32
284
- if(ok)ok=MoveFileExA(tmp,p,MOVEFILE_REPLACE_EXISTING|MOVEFILE_WRITE_THROUGH)!=0;
347
+ if(ok)ok=state_windows_move(tmp,p);
285
348
  #else
286
349
  if(ok)ok=rename(tmp,p)==0;
287
350
  #endif
288
351
  }remove(tmp);free(tmp);free(b);return ok;
289
352
  }
290
- static int save(ln_client*c){const char*p=text(c->cfg,"cache_path");if(!p||!p[0])return 1;char h[65];if(!text(c->cfg,"license_key")[0])strcpy(h,text(c->auth,"source_hash"));else if(!hash(text(c->cfg,"license_key"),h))return 0;cJSON*s=cJSON_CreateObject();cJSON_AddNumberToObject(s,"version",1);cJSON_AddStringToObject(s,"license_hash",h);cJSON_AddItemToObject(s,"lease",cJSON_Duplicate(c->cached,1));cJSON_AddNumberToObject(s,"observed_at",(double)c->wall);int ok=write_state(p,s);cJSON_Delete(s);return ok;}
353
+ static int save(ln_client*c){const char*p=text(c->cfg,"cache_path");if(!p||!p[0])return 1;char h[65];if(!source_hash(c,h))return 0;cJSON*s=cJSON_CreateObject();cJSON_AddNumberToObject(s,"version",1);cJSON_AddStringToObject(s,"license_hash",h);cJSON_AddItemToObject(s,"lease",cJSON_Duplicate(c->cached,1));cJSON_AddNumberToObject(s,"observed_at",(double)c->wall);int ok=write_state(p,s);cJSON_Delete(s);return ok;}
291
354
  static char*credential_path(ln_client*c){const char*p=text(c->cfg,"cache_path");if(!p||!p[0])return NULL;char*out=malloc(strlen(p)+18);if(out)sprintf(out,"%s.credential.json",p);return out;}
292
355
  static void load_credential(ln_client*c){
293
356
 
294
- char*p=credential_path(c);if(!p)return;char*b=readfile(p,LIMIT);free(p);if(!b)return;cJSON*s=ln_json(b);free(b);char h[65];int64_t version;int ok=hash(text(c->cfg,"license_key"),h)&&s&&num(s,"version",&version)&&version==1&&text(s,"origin")&&!strcmp(text(s,"origin"),text(c->cfg,"base_url"))&&device(text(s,"source_hash"))&&(!text(c->cfg,"license_key")[0]||!strcmp(text(s,"source_hash"),h))&&uuid(text(s,"request_id"))&&uuid(text(s,"session_id"));
357
+ char*p=credential_path(c);if(!p)return;char*b=readfile(p,LIMIT);free(p);if(!b)return;cJSON*s=ln_json(b);free(b);char h[65];int64_t version;int ok=hash(source_input(c),h)&&s&&num(s,"version",&version)&&version==1&&text(s,"origin")&&!strcmp(text(s,"origin"),text(c->cfg,"base_url"))&&device(text(s,"source_hash"))&&(!source_input(c)[0]||!strcmp(text(s,"source_hash"),h))&&uuid(text(s,"request_id"))&&uuid(text(s,"session_id"));
295
358
  const char*ctx[]={"product_id","device_id"};for(int i=0;i<2;i++){const char*a=text(s,ctx[i]),*b=text(c->cfg,ctx[i]);if(!a||!b||strcmp(a,b))ok=0;}const char*k=text(s,"device_key");if(!k||(k[0]&&!credential_secret(k,"lv_dev_"))||(!text(c->cfg,"license_key")[0]&&!k[0]))ok=0;
296
359
  if(ok){c->auth=s;if(!k[0])strcpy(c->session,text(s,"session_id"));}else cJSON_Delete(s);
297
360
  }
298
361
  static int save_credential(ln_client*c){char*p=credential_path(c);if(!p){strcpy(c->error,"CREDENTIAL_PATH_REQUIRED");return 0;}int ok=write_state(p,c->auth);free(p);if(ok)c->auth_dirty=0;else strcpy(c->error,"CREDENTIAL_WRITE_FAILED");return ok;}
299
362
  static int prepare_credential(ln_client*c){
300
- if(!temporary(c))return 1;
301
- if(!c->auth){char h[65],id[37];if(!hash(text(c->cfg,"license_key"),h)||!request_uuid(id))return 0;c->auth=cJSON_CreateObject();cJSON_AddNumberToObject(c->auth,"version",1);cJSON_AddStringToObject(c->auth,"origin",text(c->cfg,"base_url"));cJSON_AddStringToObject(c->auth,"source_hash",h);cJSON_AddStringToObject(c->auth,"product_id",text(c->cfg,"product_id"));cJSON_AddStringToObject(c->auth,"device_id",text(c->cfg,"device_id"));cJSON_AddStringToObject(c->auth,"device_key","");cJSON_AddStringToObject(c->auth,"request_id",id);cJSON_AddStringToObject(c->auth,"session_id",c->session);c->auth_dirty=1;}
363
+ if(is_denied(c)){strcpy(c->error,"LAUNCH_LOGIN_REQUIRED");return 0;}if(!temporary(c))return 1;
364
+ if(!c->auth){char h[65],id[37];if(!source_hash(c,h)||!request_uuid(id))return 0;c->auth=cJSON_CreateObject();cJSON_AddNumberToObject(c->auth,"version",1);cJSON_AddStringToObject(c->auth,"origin",text(c->cfg,"base_url"));cJSON_AddStringToObject(c->auth,"source_hash",h);cJSON_AddStringToObject(c->auth,"product_id",text(c->cfg,"product_id"));cJSON_AddStringToObject(c->auth,"device_id",text(c->cfg,"device_id"));cJSON_AddStringToObject(c->auth,"device_key","");cJSON_AddStringToObject(c->auth,"request_id",id);cJSON_AddStringToObject(c->auth,"session_id",c->session);c->auth_dirty=1;}
302
365
  return !c->auth_dirty||save_credential(c);
303
366
  }
304
367
  static int accept_credential(ln_client*c,const cJSON*result){
305
- if(!temporary(c)||(text(c->auth,"device_key")&&text(c->auth,"device_key")[0]))return 1;
306
- const char*k=text(result,"device_key");if(!credential_secret(k,"lv_dev_")){if(strncmp(text(c->cfg,"license_key"),"lv_tmp_",7))return 1;strcpy(c->error,"DEVICE_CREDENTIAL_MISSING");return 0;}if(!prepare_credential(c))return 0;cJSON_ReplaceItemInObjectCaseSensitive(c->auth,"device_key",cJSON_CreateString(k));c->auth_dirty=1;return save_credential(c);
368
+ if(logout_binding_blocked(c,result))return 0;
369
+ if(cJSON_IsTrue(get(c->logout_state,"signed_out"))){cJSON_ReplaceItemInObjectCaseSensitive(c->logout_state,"signed_out",cJSON_CreateBool(0));if(!save_logout_state(c))return 0;}
370
+ if(get(result,"logout_ticket")){if(!prepare_credential(c))return 0;cJSON_DeleteItemFromObjectCaseSensitive(c->auth,"logout_ticket");cJSON_AddItemToObject(c->auth,"logout_ticket",cJSON_Duplicate(get(result,"logout_ticket"),1));c->auth_dirty=1;if(!save_credential(c))return 0;}if(!temporary(c)||(text(c->auth,"device_key")&&text(c->auth,"device_key")[0]))return 1;
371
+ const char*k=text(result,"device_key");if(!credential_secret(k,"lv_dev_")){if(strncmp(text(c->cfg,"license_key"),"lv_tmp_",7)&&!(text(c->cfg,"identity_token")&&text(c->cfg,"identity_token")[0]))return 1;strcpy(c->error,"DEVICE_CREDENTIAL_MISSING");return 0;}if(!prepare_credential(c))return 0;cJSON_ReplaceItemInObjectCaseSensitive(c->auth,"device_key",cJSON_CreateString(k));c->auth_dirty=1;return save_credential(c);
307
372
  }
308
373
  struct buffer{char data[LIMIT+1];size_t size;int oversized;};
309
374
  static size_t receive(void*data,size_t size,size_t n,void*arg){struct buffer*b=arg;size_t amount=size*n;if(amount>LIMIT-b->size){b->oversized=1;return 0;}memcpy(b->data+b->size,data,amount);b->size+=amount;b->data[b->size]=0;return amount;}
@@ -315,19 +380,20 @@ static void sleepms(int ms){
315
380
  #endif
316
381
  }
317
382
  static size_t receive_header(char*data,size_t size,size_t n,void*arg){size_t length=size*n;ln_client*c=arg;if(length>12&&!strncmp(data,"Retry-After:",12)){char value[32];size_t count=length-12;if(count>31)count=31;memcpy(value,data+12,count);value[count]=0;long seconds=strtol(value,NULL,10);if(seconds>0)c->retry_after=seconds>86400?86400:(int)seconds;}return length;}
318
- static cJSON*request_extra(ln_client*c,const char*op,const char*activation,int*transient,const char*operation_id,const cJSON*extra){*transient=0;c->error[0]=0;c->request_id[0]=0;c->retryable=0;c->http_status=0;c->retry_after=0;if(!prepare_credential(c))return NULL;if(!strcmp(op,"activate")&&temporary(c)&&!text(c->auth,"device_key")[0]&&!c->cached)operation_id=text(c->auth,"request_id");unsigned char random[16];if(!random_bytes(random,16)){strcpy(c->error,"RANDOM_SOURCE_FAILED");return NULL;}char idem[120]="Idempotency-Key: ";size_t used=strlen(idem);for(int i=0;i<16;i++)sprintf(idem+used+i*2,"%02x",random[i]);if(operation_id){if(strlen(operation_id)>80||strpbrk(operation_id," \t\r\n")){strcpy(c->error,"INVALID_OPERATION_ID");return NULL;}snprintf(idem,sizeof(idem),"Idempotency-Key: %s",operation_id);}cJSON*body=cJSON_CreateObject();const char*names[]={"product_id","license_key","device_id","device_name","app_version"};for(int i=0;i<5;i++){const char*s=text(c->cfg,names[i]);cJSON_AddStringToObject(body,names[i],s?s:"");}cJSON_ReplaceItemInObjectCaseSensitive(body,"license_key",cJSON_CreateString(runtime_key(c)));cJSON_AddStringToObject(body,"activation_id",activation);cJSON_AddStringToObject(body,"session_id",c->session);cJSON_AddItemToObject(body,"device_profile",cJSON_Duplicate(get(c->cfg,"device_profile"),1));if(extra)for(const cJSON*x=extra->child;x;x=x->next)cJSON_AddItemToObject(body,x->string,cJSON_Duplicate(x,1));char*json=cJSON_PrintUnformatted(body);cJSON_Delete(body);const char*base=text(c->cfg,"base_url");char*url=malloc(strlen(base)+strlen(op)+20);if(!url||!json){free(url);free(json);return NULL;}strcpy(url,base);size_t len=strlen(url);if(len&&url[len-1]=='/')url[len-1]=0;strcat(url,"/api/v1/");strcat(url,op);struct curl_slist*headers=NULL;headers=curl_slist_append(headers,"Content-Type: application/json");headers=curl_slist_append(headers,idem);cJSON*result=NULL;
383
+ static cJSON*request_extra(ln_client*c,const char*op,const char*activation,int*transient,const char*operation_id,const cJSON*extra){*transient=0;c->error[0]=0;c->request_id[0]=0;c->retryable=0;c->http_status=0;c->retry_after=0;if(!prepare_credential(c))return NULL;if(!strcmp(op,"activate")&&temporary(c)&&!text(c->auth,"device_key")[0]&&!c->cached)operation_id=text(c->auth,"request_id");unsigned char random[16];if(!random_bytes(random,16)){strcpy(c->error,"RANDOM_SOURCE_FAILED");return NULL;}char idem[120]="Idempotency-Key: ";size_t used=strlen(idem);for(int i=0;i<16;i++)sprintf(idem+used+i*2,"%02x",random[i]);if(operation_id){if(strlen(operation_id)>80||strpbrk(operation_id," \t\r\n")){strcpy(c->error,"INVALID_OPERATION_ID");return NULL;}snprintf(idem,sizeof(idem),"Idempotency-Key: %s",operation_id);}cJSON*body=cJSON_CreateObject();const char*names[]={"product_id","license_key","device_id","device_name","app_version"};for(int i=0;i<5;i++){const char*s=text(c->cfg,names[i]);cJSON_AddStringToObject(body,names[i],s?s:"");}cJSON_ReplaceItemInObjectCaseSensitive(body,"license_key",cJSON_CreateString(runtime_key(c)));cJSON_AddStringToObject(body,"activation_id",activation);cJSON_AddStringToObject(body,"session_id",c->session);cJSON_AddItemToObject(body,"device_profile",cJSON_Duplicate(get(c->cfg,"device_profile"),1));if(extra)for(const cJSON*x=extra->child;x;x=x->next)cJSON_AddItemToObject(body,x->string,cJSON_Duplicate(x,1));if(!get(c->auth,"logout_ticket"))cJSON_AddBoolToObject(body,"request_logout_ticket",1);if(text(c->cfg,"launch_connection_id")){cJSON_AddStringToObject(body,"launch_connection_id",text(c->cfg,"launch_connection_id"));cJSON_AddStringToObject(body,"environment",text(c->cfg,"environment")?text(c->cfg,"environment"):"");}if(!runtime_key(c)[0]&&text(c->cfg,"identity_token")){const char*identity_names[]={"identity_token","launch_connection_id","environment"};for(int i=0;i<3;i++){const char*v=text(c->cfg,identity_names[i]);cJSON_DeleteItemFromObjectCaseSensitive(body,identity_names[i]);cJSON_AddStringToObject(body,identity_names[i],v?v:"");}op="launch/exchange";}char*json=cJSON_PrintUnformatted(body);cJSON_Delete(body);const char*base=text(c->cfg,"base_url");char*url=malloc(strlen(base)+strlen(op)+20);if(!url||!json){free(url);free(json);return NULL;}strcpy(url,base);size_t len=strlen(url);if(len&&url[len-1]=='/')url[len-1]=0;strcat(url,"/api/v1/");strcat(url,op);struct curl_slist*headers=NULL;headers=curl_slist_append(headers,"Content-Type: application/json");headers=curl_slist_append(headers,idem);cJSON*result=NULL;
319
384
  for(int attempt=0;attempt<2;attempt++){if(attempt)sleepms(200);struct buffer b={{0},0,0};CURL*curl=curl_easy_init();if(!curl)break;curl_easy_setopt(curl,CURLOPT_URL,url);curl_easy_setopt(curl,CURLOPT_POSTFIELDS,json);curl_easy_setopt(curl,CURLOPT_HTTPHEADER,headers);curl_easy_setopt(curl,CURLOPT_WRITEFUNCTION,receive);curl_easy_setopt(curl,CURLOPT_WRITEDATA,&b);int64_t timeout=10;num(c->cfg,"timeout_seconds",&timeout);curl_easy_setopt(curl,CURLOPT_TIMEOUT,(long)timeout);const char*proxy=text(c->cfg,"proxy_url");if(proxy&&proxy[0])curl_easy_setopt(curl,CURLOPT_PROXY,proxy);curl_easy_setopt(curl,CURLOPT_HEADERFUNCTION,receive_header);curl_easy_setopt(curl,CURLOPT_HEADERDATA,c);curl_easy_setopt(curl,CURLOPT_CONNECTTIMEOUT,5L);curl_easy_setopt(curl,CURLOPT_NOSIGNAL,1L);curl_easy_setopt(curl,CURLOPT_FOLLOWLOCATION,0L);curl_easy_setopt(curl,CURLOPT_SSL_VERIFYPEER,1L);curl_easy_setopt(curl,CURLOPT_SSL_VERIFYHOST,2L);const char*ca=getenv("LICENTIVO_CA_BUNDLE");if(ca&&ca[0])curl_easy_setopt(curl,CURLOPT_CAINFO,ca);CURLcode err=curl_easy_perform(curl);long status=0;curl_easy_getinfo(curl,CURLINFO_RESPONSE_CODE,&status);curl_easy_cleanup(curl);if(b.oversized){strcpy(c->error,"RESPONSE_TOO_LARGE");*transient=0;break;}c->http_status=status;if(err!=CURLE_OK){*transient=1;c->retryable=1;strcpy(c->error,"SERVER_UNAVAILABLE");continue;}*transient=0;if(memchr(b.data,0,b.size)){strcpy(c->error,"INVALID_JSON");break;}cJSON*env=ln_json(b.data);if(!env){strcpy(c->error,"UNEXPECTED_RESPONSE");c->retryable=status==429||status>=500;*transient=c->retryable;if(c->retry_after>0)c->retry_at=(int64_t)time(NULL)+c->retry_after;if(status>=500)continue;break;}const char*trace=text(env,"request_id");snprintf(c->request_id,sizeof(c->request_id),"%s",trace?trace:"");if(status!=200||get(env,"error")){const char*code=text(get(env,"error"),"code");snprintf(c->error,sizeof(c->error),"%s",code?code:"UNEXPECTED_RESPONSE");c->retryable=status==429||status>=500;*transient=c->retryable;if(c->retry_after>0)c->retry_at=(int64_t)time(NULL)+c->retry_after;cJSON_Delete(env);if(status>=500)continue;break;}result=cJSON_Duplicate(get(env,"data"),1);cJSON_Delete(env);break;}
320
- curl_slist_free_all(headers);free(url);free(json);return result;
385
+ curl_slist_free_all(headers);free(url);free(json);if(!result&&revocation_error(c->error))deny_runtime(c);return result;
321
386
  }
322
387
  static cJSON*request(ln_client*c,const char*op,const char*activation,int*transient){return request_extra(c,op,activation,transient,NULL,NULL);}
388
+ #include "device_logout.h"
323
389
  static char*activation_id(ln_client*c){if(!c->cached)return NULL;unsigned char raw[LIMIT+1];int n=b64(text(c->cached,"payload"),raw,LIMIT);if(n<0)return NULL;raw[n]=0;cJSON*claims=ln_json((char*)raw);const char*s=text(claims,"activation_id");char*out=s?malloc(strlen(s)+1):NULL;if(out)strcpy(out,s);cJSON_Delete(claims);return out;}
324
- static int call(ln_client*c,const char*op,int force_online){if(!c)return 0;lock(c);if(!force_online&&!strcmp(op,"activate")&&c->offline==2&&c->interval==0&&cache_valid(c)){unlock(c);return 1;}char*id=NULL;if(strcmp(op,"activate")){id=activation_id(c);if(!id){strcpy(c->error,"ACTIVATE_FIRST");unlock(c);return 0;}}int temporary=0;cJSON*data=request(c,op,id?id:"",&temporary);free(id);if(!data){int ok=!force_online&&temporary&&c->offline!=1&&cache_valid(c);if(!temporary||c->offline==1)invalidate(c);publish(c,0);unlock(c);notify(c);return ok;}
325
- const cJSON*e=get(data,"lease");int ok=client_context(c,e)&&ln_verify(e,get(c->cfg,"trusted_keys"),text(c->cfg,"product_id"),text(c->cfg,"device_id"),time(NULL));if(ok){cJSON_Delete(c->cached);c->cached=cJSON_Duplicate(e,1);char*activation=activation_id(c);const char*expected=text(data,"activation_id");ok=activation&&expected&&!strcmp(activation,expected);free(activation);}if(ok){c->wall=time(NULL);c->tick=monotonic();int64_t interval;if(num(data,"heartbeat_seconds",&interval)&&interval>=10&&interval<=86400)c->interval=(int)interval;c->offline=envelope_policy(c->cached,&c->interval);ok=accept_credential(c,data)&&save(c);if(ok)c->error[0]=0;else if(c->error[0]==0)strcpy(c->error,"CACHE_WRITE_FAILED");}else{strcpy(c->error,"INVALID_LEASE");invalidate(c);}cJSON_Delete(data);publish(c,ok);unlock(c);notify(c);return ok;}
390
+ static int call(ln_client*c,const char*op,int force_online){if(!c)return 0;lock(c);if(is_denied(c)){strcpy(c->error,"LAUNCH_LOGIN_REQUIRED");unlock(c);return 0;}if(!force_online&&!strcmp(op,"activate")&&c->offline==2&&c->interval==0&&cache_valid(c)){unlock(c);return 1;}char*id=NULL;if(strcmp(op,"activate")){id=activation_id(c);if(!id){strcpy(c->error,"ACTIVATE_FIRST");unlock(c);return 0;}}int temporary=0;cJSON*data=request(c,op,id?id:"",&temporary);free(id);if(!data){int ok=!force_online&&temporary&&c->offline!=1&&cache_valid(c);if(!temporary||c->offline==1)invalidate(c);publish(c,0);unlock(c);notify(c);return ok;}
391
+ if(is_denied(c)){if(c->auth&&get(data,"logout_ticket")){cJSON_DeleteItemFromObjectCaseSensitive(c->auth,"logout_ticket");cJSON_AddItemToObject(c->auth,"logout_ticket",cJSON_Duplicate(get(data,"logout_ticket"),1));}cJSON_Delete(data);unlock(c);return 0;}const cJSON*e=get(data,"lease");int ok=client_context(c,e)&&ln_verify(e,get(c->cfg,"trusted_keys"),text(c->cfg,"product_id"),text(c->cfg,"device_id"),time(NULL));if(ok){cJSON_Delete(c->cached);c->cached=cJSON_Duplicate(e,1);char*activation=activation_id(c);const char*expected=text(data,"activation_id");ok=activation&&expected&&!strcmp(activation,expected);free(activation);}if(ok){c->wall=time(NULL);c->tick=monotonic();int64_t interval;if(num(data,"heartbeat_seconds",&interval)&&interval>=10&&interval<=86400)c->interval=(int)interval;c->offline=envelope_policy(c->cached,&c->interval);ok=accept_credential(c,data)&&save(c);if(ok)c->error[0]=0;else if(c->error[0]==0)strcpy(c->error,"CACHE_WRITE_FAILED");}else{strcpy(c->error,"INVALID_LEASE");invalidate(c);}cJSON_Delete(data);publish(c,ok);unlock(c);notify(c);return ok;}
326
392
  int ln_activate(ln_client*c){return call(c,"activate",0);}int ln_validate(ln_client*c){return call(c,"validate",0);}int ln_heartbeat(ln_client*c){return call(c,"heartbeat",0);}
327
393
  int ln_check_feature(ln_client*c,const char*feature){if(!c||!feature)return 0;cJSON*claims=local_claims(c);int ok=0;const cJSON*fs=get(claims,"features");for(const cJSON*f=fs?fs->child:NULL;f;f=f->next)if(cJSON_IsString(f)&&!strcmp(f->valuestring,feature))ok=1;cJSON_Delete(claims);return ok;}
328
394
  int ln_run_feature(ln_client*c,const char*feature,ln_feature_action action,void*context){if(!action||!ln_check_feature(c,feature))return 0;return action(context)!=0;}
329
395
  int ln_refresh_online(ln_client*c){return call(c,"activate",1);}
330
- int ln_deactivate(ln_client*c){if(!c)return 0;lock(c);int is_floating=floating(c);char*id=activation_id(c);invalidate(c);int ok=1;if(id){int temp;cJSON*result=request(c,"deactivate",id,&temp);ok=result!=NULL;cJSON_Delete(result);free(id);}if(ok&&!is_floating){char*path=credential_path(c);if(path){remove(path);free(path);}cJSON_Delete(c->auth);c->auth=NULL;strcpy(c->error,"DEVICE_RELEASED");}publish(c,0);unlock(c);notify(c);return ok;}
396
+ int ln_deactivate(ln_client*c){return ln_logout(c);}
331
397
  static int stopped(ln_client*c){
332
398
  #ifdef _WIN32
333
399
  return InterlockedCompareExchange(&c->stop,0,0)!=0;
@@ -340,7 +406,7 @@ static DWORD WINAPI heartbeat_run(LPVOID arg){
340
406
  #else
341
407
  static void*heartbeat_run(void*arg){
342
408
  #endif
343
- ln_client*c=arg;lock(c);int refresh=c->refresh_pending;c->refresh_pending=0;unlock(c);if(refresh&&!stopped(c))ln_refresh_online(c);while(!stopped(c)){unsigned char r=0;random_bytes(&r,1);lock(c);double wait=(c->interval<600?600:c->interval)*(1+r/255.0*.1);double retry=(double)c->retry_at-time(NULL);if(retry>wait)wait=retry;int disabled=c->interval==0;unlock(c);if(disabled)break;double end=monotonic()+wait;while(!stopped(c)&&monotonic()<end)sleepms(100);if(!stopped(c))ln_heartbeat(c);}lock(c);c->finished=1;unlock(c);return 0;}
409
+ ln_client*c=arg;lock(c);int refresh=c->refresh_pending;c->refresh_pending=0;unlock(c);if(refresh&&!stopped(c))ln_refresh_online(c);while(!stopped(c)){unsigned char r=0;random_bytes(&r,1);lock(c);double wait=has_pending_logouts(c)?2:(c->interval<600?600:c->interval)*(1+r/255.0*.1);double retry=(double)c->retry_at-time(NULL);if(retry>wait)wait=retry;int disabled=(c->interval==0||is_denied(c))&&!has_pending_logouts(c);unlock(c);if(disabled)break;double end=monotonic()+wait;while(!stopped(c)&&monotonic()<end)sleepms(100);if(!stopped(c)){if(is_denied(c)||ln_logout_pending(c))ln_retry_pending_logouts(c);if(!is_denied(c))ln_heartbeat(c);}}lock(c);c->finished=1;unlock(c);return 0;}
344
410
  int ln_start_heartbeat(ln_client*c){if(!c)return 0;if(c->started){lock(c);int finished=c->finished;unlock(c);if(!finished)return 1;ln_stop_heartbeat(c);}lock(c);int disabled=c->interval==0;c->finished=0;unlock(c);if(disabled&&!c->refresh_pending)return 1;
345
411
  #ifdef _WIN32
346
412
  InterlockedExchange(&c->stop,0);c->thread=CreateThread(NULL,0,heartbeat_run,c,0,NULL);if(!c->thread)return 0;
@@ -355,7 +421,7 @@ void ln_stop_heartbeat(ln_client*c){if(!c||!c->started)return;
355
421
  atomic_store(&c->stop,1);pthread_join(c->thread,NULL);
356
422
  #endif
357
423
  c->started=0;}
358
- void ln_destroy(ln_client*c){if(!c)return;ln_stop_heartbeat(c);if(floating(c))ln_deactivate(c);cJSON_Delete(c->cfg);cJSON_Delete(c->cached);cJSON_Delete(c->auth);cJSON_Delete(c->view);
424
+ void ln_destroy(ln_client*c){if(!c)return;ln_stop_heartbeat(c);if(floating(c)&&!is_denied(c)){char*id=activation_id(c);if(id){int transient;cJSON*result=request(c,"deactivate",id,&transient);cJSON_Delete(result);free(id);}}cJSON_Delete(c->logout_bindings);cJSON_Delete(c->logout_state);cJSON_Delete(c->cfg);cJSON_Delete(c->cached);cJSON_Delete(c->auth);cJSON_Delete(c->view);
359
425
  #ifdef _WIN32
360
426
  DeleteCriticalSection(&c->mutex);DeleteCriticalSection(&c->view_mutex);
361
427
  #else
@@ -363,13 +429,13 @@ void ln_destroy(ln_client*c){if(!c)return;ln_stop_heartbeat(c);if(floating(c))ln
363
429
  #endif
364
430
  free(c);}
365
431
 
366
- int ln_start(ln_client*c){if(!c)return 0;lock(c);int fast=cache_valid(c)&&c->offline!=1&&!floating(c);if(fast)c->refresh_pending=1;unlock(c);return (fast||ln_activate(c))&&ln_start_heartbeat(c);}
432
+ int ln_start(ln_client*c){if(!c)return 0;if(is_denied(c)&&ln_logout_pending(c))ln_retry_pending_logouts(c);lock(c);int fast=cache_valid(c)&&c->offline!=1&&!floating(c);if(fast)c->refresh_pending=1;unlock(c);return (fast||ln_activate(c))&&ln_start_heartbeat(c);}
367
433
  ln_client*ln_open(const char*path){ln_client*c=ln_init_file(path);if(c&&!ln_start(c)){ln_destroy(c);return NULL;}return c;}
368
434
  ln_client*ln_open_with_license(const char*path,const char*license_key){ln_client*c=ln_init_file_with_license(path,license_key);if(c&&!ln_start(c)){ln_destroy(c);return NULL;}return c;}
369
435
  void ln_free_string(char*s){free(s);}
370
436
  char*ln_consume(ln_client*c,const char*feature,int64_t quantity,const char*operation_id){if(!c||!feature||!operation_id||!operation_id[0])return NULL;lock(c);if(!cache_valid(c)){strcpy(c->error,"LICENSE_CHECK_REQUIRED");unlock(c);return NULL;}char*activation=activation_id(c);cJSON*extra=cJSON_CreateObject();cJSON_AddStringToObject(extra,"feature",feature);cJSON_AddNumberToObject(extra,"quantity",(double)quantity);int transient=0;cJSON*result=request_extra(c,"consume",activation,&transient,operation_id,extra);char*out=result?cJSON_PrintUnformatted(get(result,"consumption")):NULL;cJSON_Delete(extra);free(activation);if(!result&&(!strcmp(c->error,"LICENSE_REVOKED")||!strcmp(c->error,"LICENSE_SUSPENDED")||!strcmp(c->error,"DEVICE_RELEASED")||!strcmp(c->error,"DEVICE_CREDENTIAL_REVOKED")||!strcmp(c->error,"LICENSE_EXPIRED")))invalidate(c);int success=result!=NULL;cJSON_Delete(result);publish(c,success);unlock(c);notify(c);return out;}
371
- char*ln_offline_request(ln_client*c,const char*operation){if(!c||!operation||(strcmp(operation,"activate")&&strcmp(operation,"deactivate")))return NULL;lock(c);if(!prepare_credential(c)){unlock(c);return NULL;}char*activation=NULL;if(!strcmp(operation,"deactivate")){if(!cache_valid(c)){unlock(c);return NULL;}activation=activation_id(c);invalidate(c);}char id[37];if(!request_uuid(id)){free(activation);unlock(c);return NULL;}cJSON*body=cJSON_CreateObject();const char*names[]={"product_id","license_key","device_id","device_name","app_version"};for(int i=0;i<5;i++){const char*v=text(c->cfg,names[i]);cJSON_AddStringToObject(body,names[i],v?v:"");}cJSON_ReplaceItemInObjectCaseSensitive(body,"license_key",cJSON_CreateString(runtime_key(c)));cJSON_AddStringToObject(body,"activation_id",activation?activation:"");cJSON_AddStringToObject(body,"session_id",c->session);cJSON_AddItemToObject(body,"device_profile",cJSON_Duplicate(get(c->cfg,"device_profile"),1));cJSON_AddNumberToObject(body,"version",1);cJSON_AddStringToObject(body,"request_id",id);cJSON_AddNumberToObject(body,"created_at",(double)time(NULL));cJSON_AddStringToObject(body,"operation",operation);char*out=cJSON_PrintUnformatted(body);cJSON_Delete(body);free(activation);unlock(c);return out;}
372
- int ln_import_offline(ln_client*c,const char*request_json,const char*response_json){if(!c||!request_json||!response_json||strlen(request_json)>LIMIT||strlen(response_json)>LIMIT)return 0;lock(c);cJSON*req=ln_json(request_json),*res=ln_json(response_json);int64_t version=0;int ok=req&&res&&num(res,"version",&version)&&version==1;const char*fields[]={"request_id","operation"};for(int i=0;i<2;i++){const char*a=text(req,fields[i]),*b=text(res,fields[i]);if(!a||!b||strcmp(a,b))ok=0;}if(!text(req,"operation")||strcmp(text(req,"operation"),"activate"))ok=0;const char*context[]={"product_id","device_id","license_key"};for(int i=0;i<3;i++){const char*a=text(req,context[i]),*b=!strcmp(context[i],"license_key")?runtime_key(c):text(c->cfg,context[i]);if(!a||!b||strcmp(a,b))ok=0;}
437
+ char*ln_offline_request(ln_client*c,const char*operation){if(!c||is_denied(c)||!operation||(strcmp(operation,"activate")&&strcmp(operation,"deactivate")))return NULL;lock(c);if(!prepare_credential(c)){unlock(c);return NULL;}char*activation=NULL;if(!strcmp(operation,"deactivate")){if(!cache_valid(c)){unlock(c);return NULL;}activation=activation_id(c);invalidate(c);}char id[37];if(!request_uuid(id)){free(activation);unlock(c);return NULL;}cJSON*body=cJSON_CreateObject();const char*names[]={"product_id","license_key","device_id","device_name","app_version"};for(int i=0;i<5;i++){const char*v=text(c->cfg,names[i]);cJSON_AddStringToObject(body,names[i],v?v:"");}cJSON_ReplaceItemInObjectCaseSensitive(body,"license_key",cJSON_CreateString(runtime_key(c)));cJSON_AddStringToObject(body,"activation_id",activation?activation:"");cJSON_AddStringToObject(body,"session_id",c->session);cJSON_AddItemToObject(body,"device_profile",cJSON_Duplicate(get(c->cfg,"device_profile"),1));cJSON_AddBoolToObject(body,"request_logout_ticket",1);cJSON_AddNumberToObject(body,"version",1);cJSON_AddStringToObject(body,"request_id",id);cJSON_AddNumberToObject(body,"created_at",(double)time(NULL));cJSON_AddStringToObject(body,"operation",operation);char*out=cJSON_PrintUnformatted(body);cJSON_Delete(body);free(activation);unlock(c);return out;}
438
+ int ln_import_offline(ln_client*c,const char*request_json,const char*response_json){if(!c||is_denied(c)||!request_json||!response_json||strlen(request_json)>LIMIT||strlen(response_json)>LIMIT)return 0;lock(c);cJSON*req=ln_json(request_json),*res=ln_json(response_json);int64_t version=0;int ok=req&&res&&num(res,"version",&version)&&version==1;const char*fields[]={"request_id","operation"};for(int i=0;i<2;i++){const char*a=text(req,fields[i]),*b=text(res,fields[i]);if(!a||!b||strcmp(a,b))ok=0;}if(!text(req,"operation")||strcmp(text(req,"operation"),"activate"))ok=0;const char*context[]={"product_id","device_id","license_key"};for(int i=0;i<3;i++){const char*a=text(req,context[i]),*b=!strcmp(context[i],"license_key")?runtime_key(c):text(c->cfg,context[i]);if(!a||!b||strcmp(a,b))ok=0;}
373
439
  const cJSON*result=get(res,"result"),*envelope=get(result,"lease");if(!client_context(c,envelope)||!ln_verify(envelope,get(c->cfg,"trusted_keys"),text(c->cfg,"product_id"),text(c->cfg,"device_id"),time(NULL)))ok=0;cJSON*claims=claims_from(envelope);const char*id=text(claims,"offline_request_id"),*expected=text(req,"request_id"),*activation=text(claims,"activation_id"),*response_id=text(result,"activation_id");if(!id||!expected||strcmp(id,expected)||!activation||!response_id||strcmp(activation,response_id)||envelope_policy(envelope,NULL)==1)ok=0;
374
440
  if(ok){cJSON_Delete(c->cached);c->cached=cJSON_Duplicate(envelope,1);c->wall=time(NULL);c->tick=monotonic();c->offline=envelope_policy(c->cached,&c->interval);ok=accept_credential(c,result)&&save(c);}if(!ok)strcpy(c->error,"INVALID_OFFLINE_RESPONSE");cJSON_Delete(claims);cJSON_Delete(req);cJSON_Delete(res);publish(c,0);unlock(c);notify(c);return ok;
375
441
  }
data/vendor/c/licentivo.h CHANGED
@@ -53,6 +53,10 @@ int ln_heartbeat(ln_client *client);
53
53
  int ln_refresh_online(ln_client *client);
54
54
  int ln_check_feature(ln_client *client,const char *feature);
55
55
  int ln_deactivate(ln_client *client);
56
+ /* Logout denies local use immediately. Network failure leaves an unlink-only retry. */
57
+ int ln_logout(ln_client *client);
58
+ int ln_retry_pending_logouts(ln_client *client);
59
+ int ln_logout_pending(ln_client *client);
56
60
  int ln_start_heartbeat(ln_client *client);
57
61
  void ln_stop_heartbeat(ln_client *client);
58
62
  void ln_destroy(ln_client *client);
Binary file
@@ -1135,7 +1135,7 @@ typedef CURLSTScode (*curl_hstswrite_callback)(CURL *easy,
1135
1135
 
1136
1136
  /* CURLOPT aliases that make no runtime difference */
1137
1137
 
1138
- /* 'char *' argument to a string with a null-terminator */
1138
+ /* 'char *' argument to a string with a trailing zero */
1139
1139
  #define CURLOPTTYPE_STRINGPOINT CURLOPTTYPE_OBJECTPOINT
1140
1140
 
1141
1141
  /* 'struct curl_slist *' argument */
@@ -3042,9 +3042,8 @@ typedef enum {
3042
3042
  } curl_closepolicy;
3043
3043
 
3044
3044
  #define CURL_GLOBAL_SSL (1 << 0) /* no purpose since 7.57.0 */
3045
- #define CURL_GLOBAL_WINSOCK (1 << 1)
3046
- #define CURL_GLOBAL_WIN32 CURL_GLOBAL_WINSOCK /* for compatibility */
3047
- #define CURL_GLOBAL_ALL (CURL_GLOBAL_SSL | CURL_GLOBAL_WINSOCK)
3045
+ #define CURL_GLOBAL_WIN32 (1 << 1)
3046
+ #define CURL_GLOBAL_ALL (CURL_GLOBAL_SSL | CURL_GLOBAL_WIN32)
3048
3047
  #define CURL_GLOBAL_NOTHING 0
3049
3048
  #define CURL_GLOBAL_DEFAULT CURL_GLOBAL_ALL
3050
3049
  #define CURL_GLOBAL_ACK_EINTR (1 << 2)
@@ -32,12 +32,12 @@
32
32
 
33
33
  /* This is the version number of the libcurl package from which this header
34
34
  file origins: */
35
- #define LIBCURL_VERSION "8.23.0"
35
+ #define LIBCURL_VERSION "8.22.0"
36
36
 
37
37
  /* The numeric version number is also available "in parts" by using these
38
38
  defines: */
39
39
  #define LIBCURL_VERSION_MAJOR 8
40
- #define LIBCURL_VERSION_MINOR 23
40
+ #define LIBCURL_VERSION_MINOR 22
41
41
  #define LIBCURL_VERSION_PATCH 0
42
42
  /* This is the numeric version of the libcurl version number, meant for easier
43
43
  parsing and comparisons by programs. The LIBCURL_VERSION_NUM define always
@@ -57,7 +57,7 @@
57
57
  Note: This define is the full hex number and _does not_ use the
58
58
  CURL_VERSION_BITS() macro since curl's own configure script greps for it
59
59
  and needs it to contain the full number. */
60
- #define LIBCURL_VERSION_NUM 0x081700
60
+ #define LIBCURL_VERSION_NUM 0x081600
61
61
 
62
62
  /*
63
63
  * This is the date and time when the full source package was created. The
@@ -68,7 +68,7 @@
68
68
  *
69
69
  * "2007-11-23"
70
70
  */
71
- #define LIBCURL_TIMESTAMP "2026-10-14"
71
+ #define LIBCURL_TIMESTAMP "2026-09-02"
72
72
 
73
73
  #define CURL_VERSION_BITS(x, y, z) ((x) << 16 | (y) << 8 | (z))
74
74
  #define CURL_AT_LEAST_VERSION(x, y, z) \
@@ -538,7 +538,6 @@ CURL_EXTERN CURLMcode curl_multi_waitfds(CURLM *m,
538
538
  */
539
539
  #define CURLMNOTIFY_INFO_READ 0
540
540
  #define CURLMNOTIFY_EASY_DONE 1
541
- #define CURLMNOTIFY_LAST 2 /* last, not used */
542
541
 
543
542
  /*
544
543
  * Callback to install via CURLMOPT_NOTIFYFUNCTION.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: licentivo-launch
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Licentivo Launch contributors
@@ -46,6 +46,7 @@ files:
46
46
  - vendor/VERSION
47
47
  - vendor/c/app.c
48
48
  - vendor/c/demo.c
49
+ - vendor/c/device_logout.h
49
50
  - vendor/c/feature_tasks.h
50
51
  - vendor/c/licentivo.c
51
52
  - vendor/c/licentivo.h