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 +4 -4
- data/README.md +8 -4
- data/lib/liteguard/client.rb +86 -27
- data/lib/liteguard/evaluation.rb +1 -1
- data/lib/liteguard/types.rb +15 -6
- data/lib/liteguard.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9c5c689f405afe521506111a670b47b74494ed547562a8e083f1c040bcf5b283
|
|
4
|
+
data.tar.gz: a873c10263763a6700a3fb7ac1ea355d829c927a885cfbc048abf9221d66559a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 36ffdc869fa486befba275c7a7683879b6ec0014071ba17d6339f28bf173980d4d8d6755f6c19d825b32f215f486cd62f9cbe64887bf1a4fe2c13ddc5401f763
|
|
7
|
+
data.tar.gz: 44cbf8a627c3edb19bbc1d0152d7670661279a728774dbf72def1e28329a6e6c8a7779b598eb37a23f12bf974bd956a4d778902af9b7d94e078b66efefeda12b
|
data/README.md
CHANGED
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
[](https://github.com/liteguard/liteguard/actions/workflows/test-ruby.yml)
|
|
4
4
|
[](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
|
-
"
|
|
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(
|
|
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
|
|
69
|
+
Apache 2.0 see [LICENSE](https://github.com/liteguard/liteguard/blob/main/LICENSE).
|
|
66
70
|
|
data/lib/liteguard/client.rb
CHANGED
|
@@ -31,7 +31,7 @@ module Liteguard
|
|
|
31
31
|
#
|
|
32
32
|
# The client remains idle until {#start} is called.
|
|
33
33
|
#
|
|
34
|
-
# @param
|
|
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(
|
|
55
|
-
@
|
|
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
|
|
462
|
+
# Flush all buffered telemetry and unadopted guard observations.
|
|
464
463
|
#
|
|
465
464
|
# @return [void]
|
|
466
465
|
def flush_signals
|
|
467
|
-
batch,
|
|
466
|
+
batch, unadopted_observations = @monitor.synchronize do
|
|
468
467
|
buffered = @signal_buffer.dup
|
|
469
468
|
@signal_buffer.clear
|
|
470
|
-
|
|
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,
|
|
473
|
+
[buffered, observations]
|
|
473
474
|
end
|
|
474
|
-
return if batch.empty? &&
|
|
475
|
+
return if batch.empty? && unadopted_observations.empty?
|
|
475
476
|
|
|
476
477
|
flush_signal_batch(batch) unless batch.empty?
|
|
477
|
-
flush_unadopted_guards(
|
|
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
|
-
|
|
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
|
|
522
|
+
# Upload unadopted guard observations discovered during evaluation.
|
|
522
523
|
#
|
|
523
|
-
# @param
|
|
524
|
+
# @param unadopted_observations [Array<UnadoptedGuardObservation>] observations to report
|
|
524
525
|
# @return [void]
|
|
525
|
-
def flush_unadopted_guards(
|
|
526
|
+
def flush_unadopted_guards(unadopted_observations)
|
|
526
527
|
payload = JSON.generate(
|
|
527
|
-
|
|
528
|
+
projectClientToken: @project_client_token,
|
|
528
529
|
environment: @environment,
|
|
529
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 #{@
|
|
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 #{@
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1175
|
-
|
|
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
|
|
1316
|
+
# Return pending unadopted-guard observations for tests.
|
|
1262
1317
|
#
|
|
1263
|
-
# @return [Array<
|
|
1318
|
+
# @return [Array<UnadoptedGuardObservation>] pending unadopted guard observations
|
|
1264
1319
|
def pending_unadopted_guards_for_testing
|
|
1265
|
-
@monitor.synchronize
|
|
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.
|
data/lib/liteguard/evaluation.rb
CHANGED
data/lib/liteguard/types.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
# Code generated by tools/src/proto-codegen.ts
|
|
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
|
|
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
|
-
:
|
|
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
|
-
:
|
|
116
|
+
:project_client_token,
|
|
108
117
|
:environment,
|
|
109
|
-
:
|
|
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
|
-
|
|
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('
|
|
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.
|
|
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-
|
|
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
|
|
43
|
+
at startup. Every open? check is sub-millisecond with no network round-trip.
|
|
44
44
|
email:
|
|
45
45
|
executables: []
|
|
46
46
|
extensions: []
|