liteguard 0.2.20260314 → 0.4.20260317

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: 8f23791b98c679f5725e164d63279b9b4b9119c997a00239fdf7b2cdef79cc75
4
- data.tar.gz: e8747b2ff6273315d5ec0eb9c8e420f4ff7543b0c40e358cf45eeec39e6f2854
3
+ metadata.gz: 9c5c689f405afe521506111a670b47b74494ed547562a8e083f1c040bcf5b283
4
+ data.tar.gz: a873c10263763a6700a3fb7ac1ea355d829c927a885cfbc048abf9221d66559a
5
5
  SHA512:
6
- metadata.gz: e9555143d0eaaa6776dc6081efb0303a5fee8957a27bbc804f9464325debb5fd16d87e0cf91815194a043081128038becfee9620c453d00fbcaaab287fa60136
7
- data.tar.gz: afd0bfa6f21f799c2d5e04452c5711f7cce44af23ec8c8f061274f3c98edc362e0ec05ac0737b5b382898482649476c53dd7ad269b3266155536752c8c7bacdd
6
+ metadata.gz: 36ffdc869fa486befba275c7a7683879b6ec0014071ba17d6339f28bf173980d4d8d6755f6c19d825b32f215f486cd62f9cbe64887bf1a4fe2c13ddc5401f763
7
+ data.tar.gz: 44cbf8a627c3edb19bbc1d0152d7670661279a728774dbf72def1e28329a6e6c8a7779b598eb37a23f12bf974bd956a4d778902af9b7d94e078b66efefeda12b
data/README.md CHANGED
@@ -3,6 +3,10 @@
3
3
  [![Ruby SDK](https://github.com/liteguard/liteguard/actions/workflows/test-ruby.yml/badge.svg)](https://github.com/liteguard/liteguard/actions/workflows/test-ruby.yml)
4
4
  [![Gem Version](https://img.shields.io/gem/v/liteguard)](https://rubygems.org/gems/liteguard)
5
5
 
6
+ - Website: [liteguard.io](https://liteguard.io)
7
+ - Project Page: [https://github.com/liteguard/liteguard](https://github.com/liteguard/liteguard)
8
+ - Issues: [https://github.com/liteguard/liteguard/issues/new](https://github.com/liteguard/liteguard/issues/new)
9
+
6
10
  ## Installation
7
11
 
8
12
  ```bash
@@ -23,8 +27,8 @@ Requires Ruby 3.1+.
23
27
  require "liteguard"
24
28
 
25
29
  client = Liteguard::Client.new(
26
- "pckid-...",
27
- environment: "production"
30
+ "pct-...",
31
+ environment: "env-production"
28
32
  )
29
33
  client.start
30
34
 
@@ -39,7 +43,7 @@ client.shutdown
39
43
 
40
44
  ## Primary API
41
45
 
42
- - `Liteguard::Client.new(project_client_key_id, **options)` creates a client.
46
+ - `Liteguard::Client.new(project_client_token, **options)` creates a client.
43
47
  - `client.start` fetches the initial bundle and starts refresh and flush workers.
44
48
  - `client.create_scope(**properties)` creates an immutable scope.
45
49
  - `scope.open?(name, **options)` evaluates locally.
@@ -62,5 +66,5 @@ make test-ruby
62
66
 
63
67
  ## License
64
68
 
65
- Apache 2.0 see [LICENSE](https://github.com/liteguard/liteguard/blob/main/LICENSE).
69
+ Apache 2.0 see [LICENSE](https://github.com/liteguard/liteguard/blob/main/LICENSE).
66
70
 
@@ -31,7 +31,7 @@ module Liteguard
31
31
  #
32
32
  # The client remains idle until {#start} is called.
33
33
  #
34
- # @param project_client_key_id [String] project client key identifier from
34
+ # @param project_client_token [String] project client token from
35
35
  # the Liteguard control plane
36
36
  # @param opts [Hash] initialization options
37
37
  # @option opts [String, nil] :environment environment slug to send with API
@@ -51,8 +51,8 @@ module Liteguard
51
51
  # @option opts [Boolean] :quiet suppress warning output when `true`
52
52
  # @option opts [Boolean] :disable_measurement disable telemetry measurements
53
53
  # @return [void]
54
- def initialize(project_client_key_id, opts = {})
55
- @project_client_key_id = project_client_key_id
54
+ def initialize(project_client_token, opts = {})
55
+ @project_client_token = project_client_token
56
56
  @environment = opts.fetch(:environment, "").to_s
57
57
  @fallback = opts.fetch(:fallback, false)
58
58
  @refresh_rate = normalize_positive_option(opts[:refresh_rate_seconds], DEFAULT_REFRESH_RATE)
@@ -83,7 +83,6 @@ module Liteguard
83
83
 
84
84
  @signal_buffer = []
85
85
  @dropped_signals_pending = 0
86
- @reported_unadopted_guards = {}
87
86
  @pending_unadopted_guards = {}
88
87
  @rate_limit_state = {}
89
88
  end
@@ -460,21 +459,23 @@ module Liteguard
460
459
  # Signals
461
460
  # ---------------------------------------------------------------------
462
461
 
463
- # Flush all buffered telemetry and unadopted guard reports.
462
+ # Flush all buffered telemetry and unadopted guard observations.
464
463
  #
465
464
  # @return [void]
466
465
  def flush_signals
467
- batch, unadopted_guard_names = @monitor.synchronize do
466
+ batch, unadopted_observations = @monitor.synchronize do
468
467
  buffered = @signal_buffer.dup
469
468
  @signal_buffer.clear
470
- names = @pending_unadopted_guards.keys.sort
469
+ observations = @pending_unadopted_guards.keys.sort.map do |name|
470
+ finalize_unadopted_observation(@pending_unadopted_guards[name])
471
+ end
471
472
  @pending_unadopted_guards.clear
472
- [buffered, names]
473
+ [buffered, observations]
473
474
  end
474
- return if batch.empty? && unadopted_guard_names.empty?
475
+ return if batch.empty? && unadopted_observations.empty?
475
476
 
476
477
  flush_signal_batch(batch) unless batch.empty?
477
- flush_unadopted_guards(unadopted_guard_names) unless unadopted_guard_names.empty?
478
+ flush_unadopted_guards(unadopted_observations) unless unadopted_observations.empty?
478
479
  end
479
480
 
480
481
  # Upload a batch of buffered signals.
@@ -486,7 +487,7 @@ module Liteguard
486
487
  # @return [void]
487
488
  def flush_signal_batch(batch)
488
489
  payload = JSON.generate(
489
- projectClientKeyId: @project_client_key_id,
490
+ projectClientToken: @project_client_token,
490
491
  environment: @environment,
491
492
  signals: batch.map do |signal|
492
493
  {
@@ -518,21 +519,29 @@ module Liteguard
518
519
  end
519
520
  end
520
521
 
521
- # Upload unadopted guard names discovered during evaluation.
522
+ # Upload unadopted guard observations discovered during evaluation.
522
523
  #
523
- # @param unadopted_guard_names [Array<String>] guard names to report
524
+ # @param unadopted_observations [Array<UnadoptedGuardObservation>] observations to report
524
525
  # @return [void]
525
- def flush_unadopted_guards(unadopted_guard_names)
526
+ def flush_unadopted_guards(unadopted_observations)
526
527
  payload = JSON.generate(
527
- projectClientKeyId: @project_client_key_id,
528
+ projectClientToken: @project_client_token,
528
529
  environment: @environment,
529
- guardNames: unadopted_guard_names
530
+ observations: unadopted_observations.map do |observation|
531
+ {
532
+ guardName: observation.guard_name,
533
+ firstSeenMs: observation.first_seen_ms,
534
+ lastSeenMs: observation.last_seen_ms,
535
+ checkCount: observation.check_count,
536
+ estimatedChecksPerMinute: observation.estimated_checks_per_minute,
537
+ }
538
+ end
530
539
  )
531
540
  post_json("/api/v1/unadopted-guards", payload)
532
541
  rescue => e
533
542
  log "[liteguard] unadopted guard flush failed: #{e}"
534
543
  @monitor.synchronize do
535
- unadopted_guard_names.each { |name| @pending_unadopted_guards[name] = true }
544
+ unadopted_observations.each { |observation| merge_pending_unadopted_observation(observation) }
536
545
  end
537
546
  end
538
547
 
@@ -746,7 +755,7 @@ module Liteguard
746
755
  protected_context = bundle.protected_context ? copy_protected_context(bundle.protected_context) : nil
747
756
 
748
757
  payload = {
749
- projectClientKeyId: @project_client_key_id,
758
+ projectClientToken: @project_client_token,
750
759
  environment: @environment
751
760
  }
752
761
  if protected_context
@@ -758,8 +767,9 @@ module Liteguard
758
767
 
759
768
  uri = URI("#{@backend_url}/api/v1/guards")
760
769
  req = Net::HTTP::Post.new(uri)
761
- req["Authorization"] = "Bearer #{@project_client_key_id}"
770
+ req["Authorization"] = "Bearer #{@project_client_token}"
762
771
  req["Content-Type"] = "application/json"
772
+ req["X-Liteguard-Environment"] = @environment unless @environment.empty?
763
773
  req["If-None-Match"] = bundle.etag unless bundle.etag.to_s.empty?
764
774
  req.body = JSON.generate(payload)
765
775
 
@@ -864,7 +874,7 @@ module Liteguard
864
874
  def post_json(path, payload)
865
875
  uri = URI("#{@backend_url}#{path}")
866
876
  req = Net::HTTP::Post.new(uri)
867
- req["Authorization"] = "Bearer #{@project_client_key_id}"
877
+ req["Authorization"] = "Bearer #{@project_client_token}"
868
878
  req["Content-Type"] = "application/json"
869
879
  req["X-Liteguard-Environment"] = @environment unless @environment.empty?
870
880
  req.body = payload
@@ -1163,19 +1173,64 @@ module Liteguard
1163
1173
  payload
1164
1174
  end
1165
1175
 
1166
- # Track an unadopted guard so it can be reported once.
1176
+ # Track an unadopted guard so it can be reported with observation metadata.
1167
1177
  #
1168
1178
  # @param name [String] guard name to record
1169
1179
  # @return [void]
1170
1180
  def record_unadopted_guard(name)
1171
1181
  @monitor.synchronize do
1172
- return if @reported_unadopted_guards[name]
1182
+ now = (Time.now.to_f * 1000).to_i
1183
+ observation = @pending_unadopted_guards[name]
1184
+ if observation
1185
+ @pending_unadopted_guards[name] = observation.with(
1186
+ last_seen_ms: now,
1187
+ check_count: observation.check_count + 1
1188
+ )
1189
+ else
1190
+ @pending_unadopted_guards[name] = UnadoptedGuardObservation.new(
1191
+ guard_name: name,
1192
+ first_seen_ms: now,
1193
+ last_seen_ms: now,
1194
+ check_count: 1,
1195
+ estimated_checks_per_minute: 0.0
1196
+ )
1197
+ end
1198
+ end
1199
+ end
1200
+
1201
+ def finalize_unadopted_observation(observation)
1202
+ observation.with(
1203
+ estimated_checks_per_minute: estimate_checks_per_minute(
1204
+ observation.first_seen_ms,
1205
+ observation.last_seen_ms,
1206
+ observation.check_count
1207
+ )
1208
+ )
1209
+ end
1173
1210
 
1174
- @reported_unadopted_guards[name] = true
1175
- @pending_unadopted_guards[name] = true
1211
+ def merge_pending_unadopted_observation(observation)
1212
+ existing = @pending_unadopted_guards[observation.guard_name]
1213
+ if existing
1214
+ @pending_unadopted_guards[observation.guard_name] = existing.with(
1215
+ first_seen_ms: [existing.first_seen_ms, observation.first_seen_ms].min,
1216
+ last_seen_ms: [existing.last_seen_ms, observation.last_seen_ms].max,
1217
+ check_count: existing.check_count + observation.check_count,
1218
+ estimated_checks_per_minute: 0.0
1219
+ )
1220
+ else
1221
+ @pending_unadopted_guards[observation.guard_name] = observation.with(
1222
+ estimated_checks_per_minute: 0.0
1223
+ )
1176
1224
  end
1177
1225
  end
1178
1226
 
1227
+ def estimate_checks_per_minute(first_seen_ms, last_seen_ms, check_count)
1228
+ return 0.0 if check_count.to_i <= 0
1229
+
1230
+ window_ms = [last_seen_ms - first_seen_ms, 1000].max
1231
+ (check_count * 60_000.0) / window_ms
1232
+ end
1233
+
1179
1234
  # Capture a stable callsite identifier outside of Liteguard internals.
1180
1235
  #
1181
1236
  # @return [String] `path:line` identifier, or `unknown`
@@ -1258,11 +1313,15 @@ module Liteguard
1258
1313
  end
1259
1314
  end
1260
1315
 
1261
- # Return pending unadopted-guard names for tests.
1316
+ # Return pending unadopted-guard observations for tests.
1262
1317
  #
1263
- # @return [Array<String>] pending unadopted guard names
1318
+ # @return [Array<UnadoptedGuardObservation>] pending unadopted guard observations
1264
1319
  def pending_unadopted_guards_for_testing
1265
- @monitor.synchronize { @pending_unadopted_guards.keys.sort }
1320
+ @monitor.synchronize do
1321
+ @pending_unadopted_guards.keys.sort.map do |name|
1322
+ finalize_unadopted_observation(@pending_unadopted_guards[name])
1323
+ end
1324
+ end
1266
1325
  end
1267
1326
 
1268
1327
  # Return the number of cached bundles for tests.
@@ -1,5 +1,5 @@
1
1
  module Liteguard
2
- # Local rule evaluation engine all evaluation is done without network calls.
2
+ # Local rule evaluation engine. All evaluation is done without network calls.
3
3
  module Evaluation
4
4
  # Evaluate a guard against a property hash.
5
5
  #
@@ -1,7 +1,7 @@
1
- # Code generated by tools/src/proto-codegen.ts DO NOT EDIT.
1
+ # Code generated by tools/src/proto-codegen.ts DO NOT EDIT.
2
2
  # Source of truth: proto/liteguard.proto Run: make proto
3
3
 
4
- # Data-plane types generated from proto/liteguard.proto.
4
+ # Data-plane types. Generated from proto/liteguard.proto.
5
5
  module Liteguard
6
6
 
7
7
  OPERATORS = %i[equals not_equals in not_in regex gt gte lt lte].freeze
@@ -37,7 +37,7 @@ module Liteguard
37
37
 
38
38
  # GetGuardsRequest (mirrors proto message).
39
39
  GetGuardsRequest = Data.define(
40
- :project_client_key_id,
40
+ :project_client_token,
41
41
  :environment,
42
42
  :protected_context
43
43
  )
@@ -102,11 +102,20 @@ module Liteguard
102
102
  :measurement
103
103
  )
104
104
 
105
+ # UnadoptedGuardObservation (mirrors proto message).
106
+ UnadoptedGuardObservation = Data.define(
107
+ :guard_name,
108
+ :first_seen_ms,
109
+ :last_seen_ms,
110
+ :check_count,
111
+ :estimated_checks_per_minute
112
+ )
113
+
105
114
  # SendUnadoptedGuardsRequest (mirrors proto message).
106
115
  SendUnadoptedGuardsRequest = Data.define(
107
- :project_client_key_id,
116
+ :project_client_token,
108
117
  :environment,
109
- :guard_names
118
+ :observations
110
119
  )
111
120
 
112
121
  # SendUnadoptedGuardsResponse (mirrors proto message).
@@ -116,7 +125,7 @@ module Liteguard
116
125
 
117
126
  # Default values for {Liteguard::Client} initialization options.
118
127
  CLIENT_OPTION_DEFAULTS = {
119
- project_client_key_id: nil,
128
+ project_client_token: nil,
120
129
  environment: nil,
121
130
  fallback: false,
122
131
  refresh_rate_seconds: 30,
data/lib/liteguard.rb CHANGED
@@ -7,7 +7,7 @@ require_relative "liteguard/client"
7
7
  #
8
8
  # require 'liteguard'
9
9
  #
10
- # client = Liteguard::Client.new('pckid-...', environment: 'production')
10
+ # client = Liteguard::Client.new('pct-...', environment: 'production')
11
11
  # client.start
12
12
  # scope = client.create_scope(user_id: 'user-123', plan: 'pro')
13
13
  #
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: liteguard
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.20260314
4
+ version: 0.4.20260317
5
5
  platform: ruby
6
6
  authors:
7
7
  - Liteguard
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-03-15 00:00:00.000000000 Z
11
+ date: 2026-03-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec
@@ -40,7 +40,7 @@ dependencies:
40
40
  version: '3.23'
41
41
  description: Liteguard gives you feature guards, observability, and CVE auto-disable
42
42
  in one gem. Guards are evaluated entirely in-process against rules fetched once
43
- at startup every open? check is sub-millisecond with no network round-trip.
43
+ at startup. Every open? check is sub-millisecond with no network round-trip.
44
44
  email:
45
45
  executables: []
46
46
  extensions: []