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 +4 -4
- data/README.md +3 -3
- data/lib/liteguard/client.rb +85 -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: c5639cf9bfedbd8a5410e0d33addc0c5d2668ef4048fd53562698af2d23b47d7
|
|
4
|
+
data.tar.gz: 4f5160aae968869e16db59db5b687b9e3e314073cb48557c54c34e78dad80fb7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
"
|
|
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(
|
|
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
|
|
65
|
+
Apache 2.0 see [LICENSE](https://github.com/liteguard/liteguard/blob/main/LICENSE).
|
|
66
66
|
|
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,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 #{@
|
|
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 #{@
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1175
|
-
|
|
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
|
|
1315
|
+
# Return pending unadopted-guard observations for tests.
|
|
1262
1316
|
#
|
|
1263
|
-
# @return [Array<
|
|
1317
|
+
# @return [Array<UnadoptedGuardObservation>] pending unadopted guard observations
|
|
1264
1318
|
def pending_unadopted_guards_for_testing
|
|
1265
|
-
@monitor.synchronize
|
|
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.
|
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.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-
|
|
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
|
|
43
|
+
at startup. Every open? check is sub-millisecond with no network round-trip.
|
|
44
44
|
email:
|
|
45
45
|
executables: []
|
|
46
46
|
extensions: []
|