liteguard 0.2.20260314 → 0.3.20260315

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: c5639cf9bfedbd8a5410e0d33addc0c5d2668ef4048fd53562698af2d23b47d7
4
+ data.tar.gz: 4f5160aae968869e16db59db5b687b9e3e314073cb48557c54c34e78dad80fb7
5
5
  SHA512:
6
- metadata.gz: e9555143d0eaaa6776dc6081efb0303a5fee8957a27bbc804f9464325debb5fd16d87e0cf91815194a043081128038becfee9620c453d00fbcaaab287fa60136
7
- data.tar.gz: afd0bfa6f21f799c2d5e04452c5711f7cce44af23ec8c8f061274f3c98edc362e0ec05ac0737b5b382898482649476c53dd7ad269b3266155536752c8c7bacdd
6
+ metadata.gz: 3da6c94e15e9638769a7a24608293ec1804d6267d99def6087614c4660808970a092aa4002c796717778fa638c6e7dcb63d0cbfc62fc5526fc5ad2c70f7b4f85
7
+ data.tar.gz: eb8674f429b011190e951feed26376f88f5f27baa68331823eb841d8c282b9de89ab068e2dc15b7b7d8ffada8e89fd1133fd939b214262b57d5bde77c208270e
data/README.md CHANGED
@@ -23,7 +23,7 @@ Requires Ruby 3.1+.
23
23
  require "liteguard"
24
24
 
25
25
  client = Liteguard::Client.new(
26
- "pckid-...",
26
+ "pct-...",
27
27
  environment: "production"
28
28
  )
29
29
  client.start
@@ -39,7 +39,7 @@ client.shutdown
39
39
 
40
40
  ## Primary API
41
41
 
42
- - `Liteguard::Client.new(project_client_key_id, **options)` creates a client.
42
+ - `Liteguard::Client.new(project_client_token, **options)` creates a client.
43
43
  - `client.start` fetches the initial bundle and starts refresh and flush workers.
44
44
  - `client.create_scope(**properties)` creates an immutable scope.
45
45
  - `scope.open?(name, **options)` evaluates locally.
@@ -62,5 +62,5 @@ make test-ruby
62
62
 
63
63
  ## License
64
64
 
65
- Apache 2.0 see [LICENSE](https://github.com/liteguard/liteguard/blob/main/LICENSE).
65
+ Apache 2.0 see [LICENSE](https://github.com/liteguard/liteguard/blob/main/LICENSE).
66
66
 
@@ -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,7 +767,7 @@ 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"
763
772
  req["If-None-Match"] = bundle.etag unless bundle.etag.to_s.empty?
764
773
  req.body = JSON.generate(payload)
@@ -864,7 +873,7 @@ module Liteguard
864
873
  def post_json(path, payload)
865
874
  uri = URI("#{@backend_url}#{path}")
866
875
  req = Net::HTTP::Post.new(uri)
867
- req["Authorization"] = "Bearer #{@project_client_key_id}"
876
+ req["Authorization"] = "Bearer #{@project_client_token}"
868
877
  req["Content-Type"] = "application/json"
869
878
  req["X-Liteguard-Environment"] = @environment unless @environment.empty?
870
879
  req.body = payload
@@ -1163,19 +1172,64 @@ module Liteguard
1163
1172
  payload
1164
1173
  end
1165
1174
 
1166
- # Track an unadopted guard so it can be reported once.
1175
+ # Track an unadopted guard so it can be reported with observation metadata.
1167
1176
  #
1168
1177
  # @param name [String] guard name to record
1169
1178
  # @return [void]
1170
1179
  def record_unadopted_guard(name)
1171
1180
  @monitor.synchronize do
1172
- return if @reported_unadopted_guards[name]
1181
+ now = (Time.now.to_f * 1000).to_i
1182
+ observation = @pending_unadopted_guards[name]
1183
+ if observation
1184
+ @pending_unadopted_guards[name] = observation.with(
1185
+ last_seen_ms: now,
1186
+ check_count: observation.check_count + 1
1187
+ )
1188
+ else
1189
+ @pending_unadopted_guards[name] = UnadoptedGuardObservation.new(
1190
+ guard_name: name,
1191
+ first_seen_ms: now,
1192
+ last_seen_ms: now,
1193
+ check_count: 1,
1194
+ estimated_checks_per_minute: 0.0
1195
+ )
1196
+ end
1197
+ end
1198
+ end
1173
1199
 
1174
- @reported_unadopted_guards[name] = true
1175
- @pending_unadopted_guards[name] = true
1200
+ def finalize_unadopted_observation(observation)
1201
+ observation.with(
1202
+ estimated_checks_per_minute: estimate_checks_per_minute(
1203
+ observation.first_seen_ms,
1204
+ observation.last_seen_ms,
1205
+ observation.check_count
1206
+ )
1207
+ )
1208
+ end
1209
+
1210
+ def merge_pending_unadopted_observation(observation)
1211
+ existing = @pending_unadopted_guards[observation.guard_name]
1212
+ if existing
1213
+ @pending_unadopted_guards[observation.guard_name] = existing.with(
1214
+ first_seen_ms: [existing.first_seen_ms, observation.first_seen_ms].min,
1215
+ last_seen_ms: [existing.last_seen_ms, observation.last_seen_ms].max,
1216
+ check_count: existing.check_count + observation.check_count,
1217
+ estimated_checks_per_minute: 0.0
1218
+ )
1219
+ else
1220
+ @pending_unadopted_guards[observation.guard_name] = observation.with(
1221
+ estimated_checks_per_minute: 0.0
1222
+ )
1176
1223
  end
1177
1224
  end
1178
1225
 
1226
+ def estimate_checks_per_minute(first_seen_ms, last_seen_ms, check_count)
1227
+ return 0.0 if check_count.to_i <= 0
1228
+
1229
+ window_ms = [last_seen_ms - first_seen_ms, 1000].max
1230
+ (check_count * 60_000.0) / window_ms
1231
+ end
1232
+
1179
1233
  # Capture a stable callsite identifier outside of Liteguard internals.
1180
1234
  #
1181
1235
  # @return [String] `path:line` identifier, or `unknown`
@@ -1258,11 +1312,15 @@ module Liteguard
1258
1312
  end
1259
1313
  end
1260
1314
 
1261
- # Return pending unadopted-guard names for tests.
1315
+ # Return pending unadopted-guard observations for tests.
1262
1316
  #
1263
- # @return [Array<String>] pending unadopted guard names
1317
+ # @return [Array<UnadoptedGuardObservation>] pending unadopted guard observations
1264
1318
  def pending_unadopted_guards_for_testing
1265
- @monitor.synchronize { @pending_unadopted_guards.keys.sort }
1319
+ @monitor.synchronize do
1320
+ @pending_unadopted_guards.keys.sort.map do |name|
1321
+ finalize_unadopted_observation(@pending_unadopted_guards[name])
1322
+ end
1323
+ end
1266
1324
  end
1267
1325
 
1268
1326
  # 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.3.20260315
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-16 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: []