liteguard 0.4.20260317 → 0.7.20260603
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 +14 -0
- data/lib/liteguard/client.rb +300 -94
- data/lib/liteguard/decision.rb +34 -0
- data/lib/liteguard/evaluation.rb +15 -0
- data/lib/liteguard/scope.rb +30 -2
- data/lib/liteguard/types.rb +50 -6
- data/lib/liteguard.rb +1 -0
- metadata +7 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 674b74c3afa87054d07083210c6f894afad8bc53cb2b6ae8a4e8bd045b30c14c
|
|
4
|
+
data.tar.gz: 6cf6d9d1fceb2760404e65a0b26faa05ff3687318492d4e124f35eced843e9f5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 51eec265fceb91ea7994cf97d64878afb7312ce5bf72abe7732d584da009a8b7ce82efa163144110054254d01ad935cbe22a77ba3ca9f8a01ca5963f5d28d0ef
|
|
7
|
+
data.tar.gz: d998303a8c1bfa5156aabee64afe9d8b8e174c7da199004491b5cf5e94376a157644d459605fca2c2bbde7d87f9e1381993265f8d2fe0c636057acb60e4d4dc3
|
data/README.md
CHANGED
|
@@ -47,16 +47,30 @@ client.shutdown
|
|
|
47
47
|
- `client.start` fetches the initial bundle and starts refresh and flush workers.
|
|
48
48
|
- `client.create_scope(**properties)` creates an immutable scope.
|
|
49
49
|
- `scope.open?(name, **options)` evaluates locally.
|
|
50
|
+
- `scope.evaluate(name, **options)` returns a `GuardDecision` with the full evaluation result.
|
|
50
51
|
- `scope.execute_if_open(name, **options) { ... }` measures guarded work.
|
|
52
|
+
- `scope.start_execution` creates an execution correlation handle.
|
|
51
53
|
- `scope.bind_protected_context(**protected_context)` derives a protected scope.
|
|
54
|
+
- `scope.properties` returns the current property bag for transport.
|
|
52
55
|
- `client.flush` flushes buffered telemetry.
|
|
53
56
|
- `client.shutdown` flushes and stops background work.
|
|
54
57
|
|
|
58
|
+
## Convenience helpers
|
|
59
|
+
|
|
60
|
+
For server applications that want request-scoped context propagation:
|
|
61
|
+
|
|
62
|
+
- `client.with_scope(scope) { ... }` runs a block with `scope` as the active scope, isolated via `Thread.current`.
|
|
63
|
+
- `client.active_scope` returns the scope bound to the current thread.
|
|
64
|
+
- `client.with_properties(properties) { ... }` derives and activates a scope with additional properties.
|
|
65
|
+
- `client.with_protected_context(protected_context) { ... }` derives and activates a protected scope.
|
|
66
|
+
|
|
55
67
|
## Notes
|
|
56
68
|
|
|
57
69
|
- Evaluation is local after the initial bundle fetch.
|
|
70
|
+
- Protected contexts use `properties`, `signature`, and optional `issued_at` / `expires_at` fields.
|
|
58
71
|
- Prefer explicit client and scope usage.
|
|
59
72
|
- Unadopted guards default open and emit no signals.
|
|
73
|
+
- The convenience helpers above also serve as infrastructure for auto-instrumentation of third-party dependencies. See the [Liteguard CLI](https://github.com/liteguard/liteguard/tree/main/cli) for build-time instrumentation.
|
|
60
74
|
|
|
61
75
|
## Development
|
|
62
76
|
|
data/lib/liteguard/client.rb
CHANGED
|
@@ -10,8 +10,8 @@ module Liteguard
|
|
|
10
10
|
# This class is the primary Ruby SDK entrypoint.
|
|
11
11
|
class Client
|
|
12
12
|
DEFAULT_BACKEND_URL = "https://api.liteguard.io"
|
|
13
|
-
DEFAULT_REFRESH_RATE =
|
|
14
|
-
DEFAULT_FLUSH_RATE =
|
|
13
|
+
DEFAULT_REFRESH_RATE = 60
|
|
14
|
+
DEFAULT_FLUSH_RATE = 60
|
|
15
15
|
DEFAULT_FLUSH_SIZE = 500
|
|
16
16
|
DEFAULT_HTTP_TIMEOUT = 4
|
|
17
17
|
DEFAULT_FLUSH_BUFFER_MULTIPLIER = 4
|
|
@@ -138,6 +138,25 @@ module Liteguard
|
|
|
138
138
|
end
|
|
139
139
|
end
|
|
140
140
|
|
|
141
|
+
# Start a new execution correlation context and return a handle.
|
|
142
|
+
#
|
|
143
|
+
# Returns a lightweight {Execution} handle that groups telemetry signals
|
|
144
|
+
# under a shared execution ID. Call {Execution#end_execution} when done.
|
|
145
|
+
# For block-based usage, prefer {#with_execution} instead.
|
|
146
|
+
#
|
|
147
|
+
# @return [Execution] a correlation handle
|
|
148
|
+
def start_execution
|
|
149
|
+
exec_id = next_signal_id
|
|
150
|
+
previous = Thread.current[:liteguard_execution_state]
|
|
151
|
+
Thread.current[:liteguard_execution_state] = {
|
|
152
|
+
execution_id: exec_id,
|
|
153
|
+
sequence_number: 0,
|
|
154
|
+
last_signal_id: nil,
|
|
155
|
+
}
|
|
156
|
+
cleanup = -> { Thread.current[:liteguard_execution_state] = previous }
|
|
157
|
+
Execution.new(exec_id, cleanup: cleanup)
|
|
158
|
+
end
|
|
159
|
+
|
|
141
160
|
# ---------------------------------------------------------------------
|
|
142
161
|
# Scope API
|
|
143
162
|
# ---------------------------------------------------------------------
|
|
@@ -152,6 +171,13 @@ module Liteguard
|
|
|
152
171
|
|
|
153
172
|
# Return the active scope for the current thread.
|
|
154
173
|
#
|
|
174
|
+
# This method serves two roles:
|
|
175
|
+
# 1. Developer convenience for retrieving the current request scope.
|
|
176
|
+
# 2. Required infrastructure for auto-instrumentation. Instrumented
|
|
177
|
+
# third-party code calls this method to discover the evaluation
|
|
178
|
+
# context established by application-level middleware via
|
|
179
|
+
# {#with_scope}.
|
|
180
|
+
#
|
|
155
181
|
# @return [Scope] the active scope, or the default scope when none is bound
|
|
156
182
|
def active_scope
|
|
157
183
|
scope = Thread.current[@active_scope_key]
|
|
@@ -162,6 +188,13 @@ module Liteguard
|
|
|
162
188
|
|
|
163
189
|
# Bind a scope for the duration of a block.
|
|
164
190
|
#
|
|
191
|
+
# This method serves two roles:
|
|
192
|
+
# 1. Developer convenience for scoping guard evaluation to a request.
|
|
193
|
+
# 2. Required infrastructure for auto-instrumentation. When application
|
|
194
|
+
# middleware activates a scope with this method, instrumented
|
|
195
|
+
# third-party code running inside the block can discover the scope
|
|
196
|
+
# via {#active_scope}.
|
|
197
|
+
#
|
|
165
198
|
# @param scope [Scope, nil] scope to bind, or `nil` to reuse the current
|
|
166
199
|
# scope
|
|
167
200
|
# @yield Runs with the resolved scope bound as active
|
|
@@ -209,64 +242,6 @@ module Liteguard
|
|
|
209
242
|
with_scope(active_scope.bind_protected_context(protected_context)) { yield }
|
|
210
243
|
end
|
|
211
244
|
|
|
212
|
-
# Replace the active scope with one that includes merged properties.
|
|
213
|
-
#
|
|
214
|
-
# @param properties [Hash] properties to merge into the active scope
|
|
215
|
-
# @return [Scope] the derived active scope
|
|
216
|
-
def add_properties(properties)
|
|
217
|
-
replace_current_scope(active_scope.with_properties(properties))
|
|
218
|
-
end
|
|
219
|
-
|
|
220
|
-
# Replace the active scope with one that omits the named properties.
|
|
221
|
-
#
|
|
222
|
-
# @param names [Array<String, Symbol>] property names to remove
|
|
223
|
-
# @return [Scope] the derived active scope
|
|
224
|
-
def clear_properties(names)
|
|
225
|
-
replace_current_scope(active_scope.clear_properties(Array(names).map(&:to_s)))
|
|
226
|
-
end
|
|
227
|
-
|
|
228
|
-
# Replace the active scope with an empty property scope.
|
|
229
|
-
#
|
|
230
|
-
# @return [Scope] the derived active scope
|
|
231
|
-
def reset_properties
|
|
232
|
-
replace_current_scope(active_scope.reset_properties)
|
|
233
|
-
end
|
|
234
|
-
|
|
235
|
-
# Replace the active scope with a protected-context-derived scope.
|
|
236
|
-
#
|
|
237
|
-
# @param protected_context [ProtectedContext, Hash] signed protected context
|
|
238
|
-
# @return [Scope] the derived active scope
|
|
239
|
-
def bind_protected_context(protected_context)
|
|
240
|
-
replace_current_scope(active_scope.bind_protected_context(protected_context))
|
|
241
|
-
end
|
|
242
|
-
|
|
243
|
-
# Replace the active scope with one using the public bundle.
|
|
244
|
-
#
|
|
245
|
-
# @return [Scope] the derived active scope
|
|
246
|
-
def clear_protected_context
|
|
247
|
-
replace_current_scope(active_scope.clear_protected_context)
|
|
248
|
-
end
|
|
249
|
-
|
|
250
|
-
# Return the current thread's evaluation properties.
|
|
251
|
-
#
|
|
252
|
-
# @return [Hash] active scope properties
|
|
253
|
-
def context
|
|
254
|
-
active_scope.properties
|
|
255
|
-
end
|
|
256
|
-
|
|
257
|
-
# Replace the current thread's active scope.
|
|
258
|
-
#
|
|
259
|
-
# Mutation helpers are intentionally thread-local so request-scoped data
|
|
260
|
-
# cannot leak through the process-wide default scope.
|
|
261
|
-
#
|
|
262
|
-
# @param scope [Scope] scope to install
|
|
263
|
-
# @return [Scope] the resolved scope
|
|
264
|
-
def replace_current_scope(scope)
|
|
265
|
-
resolved = resolve_scope(scope)
|
|
266
|
-
Thread.current[@active_scope_key] = resolved
|
|
267
|
-
resolved
|
|
268
|
-
end
|
|
269
|
-
|
|
270
245
|
# Resolve a scope argument and verify that it belongs to this client.
|
|
271
246
|
#
|
|
272
247
|
# @param scope [Scope, nil] candidate scope
|
|
@@ -347,6 +322,73 @@ module Liteguard
|
|
|
347
322
|
evaluate_guard_in_scope(scope, name.to_s, options, emit_signal: false)[:result]
|
|
348
323
|
end
|
|
349
324
|
|
|
325
|
+
# Evaluate a guard and return a {GuardDecision} with full reasoning.
|
|
326
|
+
#
|
|
327
|
+
# @param name [String] guard name to evaluate
|
|
328
|
+
# @param options [Hash, nil] optional per-call overrides
|
|
329
|
+
# @return [GuardDecision] the evaluation decision
|
|
330
|
+
def evaluate(name, options = nil, **legacy_options)
|
|
331
|
+
evaluate_in_scope(active_scope, name, options, **legacy_options)
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
# Evaluate a guard in the provided scope and return a {GuardDecision}.
|
|
335
|
+
#
|
|
336
|
+
# @param scope [Scope] scope to evaluate against
|
|
337
|
+
# @param name [String] guard name to evaluate
|
|
338
|
+
# @param options [Hash, nil] optional per-call overrides
|
|
339
|
+
# @return [GuardDecision] the evaluation decision
|
|
340
|
+
def evaluate_in_scope(scope, name, options = nil, **legacy_options)
|
|
341
|
+
options = normalize_is_open_options(options, legacy_options)
|
|
342
|
+
resolved_scope = resolve_scope(scope)
|
|
343
|
+
bundle = bundle_for_scope(resolved_scope)
|
|
344
|
+
effective_fallback = options[:fallback].nil? ? @fallback : options[:fallback]
|
|
345
|
+
|
|
346
|
+
unless bundle.ready
|
|
347
|
+
return GuardDecision.new(
|
|
348
|
+
name: name.to_s, is_open: effective_fallback, adopted: false,
|
|
349
|
+
reason: "fallback", matched_rule_index: nil, properties: {}
|
|
350
|
+
)
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
guard = bundle.guards[name.to_s]
|
|
354
|
+
if guard.nil?
|
|
355
|
+
record_unadopted_guard(name.to_s)
|
|
356
|
+
return GuardDecision.new(
|
|
357
|
+
name: name.to_s, is_open: true, adopted: false,
|
|
358
|
+
reason: "unadopted", matched_rule_index: nil, properties: {}
|
|
359
|
+
)
|
|
360
|
+
end
|
|
361
|
+
unless guard.adopted
|
|
362
|
+
record_unadopted_guard(name.to_s)
|
|
363
|
+
return GuardDecision.new(
|
|
364
|
+
name: name.to_s, is_open: true, adopted: false,
|
|
365
|
+
reason: "unadopted", matched_rule_index: nil, properties: {}
|
|
366
|
+
)
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
props = resolved_scope.properties
|
|
370
|
+
props = props.merge(options[:properties]) if options[:properties]
|
|
371
|
+
|
|
372
|
+
detailed = Evaluation.evaluate_guard_detailed(guard, props)
|
|
373
|
+
applied_rate_limit = apply_rate_limit(guard, name.to_s, detailed.result, props, emit_signal: true)
|
|
374
|
+
result = applied_rate_limit[:result]
|
|
375
|
+
|
|
376
|
+
reason = detailed.matched_rule_index >= 0 ? "matched_rule" : "default_value"
|
|
377
|
+
matched_idx = detailed.matched_rule_index >= 0 ? detailed.matched_rule_index : nil
|
|
378
|
+
|
|
379
|
+
buffer_signal(
|
|
380
|
+
name.to_s, result, props,
|
|
381
|
+
kind: "guard_check",
|
|
382
|
+
measurement: measurement_enabled?(guard, options) ? capture_guard_check_measurement : nil,
|
|
383
|
+
rate_limit_decisions: applied_rate_limit[:rate_limit_decisions]
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
GuardDecision.new(
|
|
387
|
+
name: name.to_s, is_open: result, adopted: guard.adopted,
|
|
388
|
+
reason: reason, matched_rule_index: matched_idx, properties: props.dup
|
|
389
|
+
)
|
|
390
|
+
end
|
|
391
|
+
|
|
350
392
|
# Evaluate a guard and execute the block only when it resolves open.
|
|
351
393
|
#
|
|
352
394
|
# @param name [String] guard name to evaluate
|
|
@@ -432,14 +474,8 @@ module Liteguard
|
|
|
432
474
|
props = props.merge(options[:properties])
|
|
433
475
|
end
|
|
434
476
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
result = if emit_signal
|
|
438
|
-
check_rate_limit(name, guard.rate_limit_per_minute, guard.rate_limit_properties, props)
|
|
439
|
-
else
|
|
440
|
-
would_pass_rate_limit(name, guard.rate_limit_per_minute, guard.rate_limit_properties, props)
|
|
441
|
-
end
|
|
442
|
-
end
|
|
477
|
+
applied_rate_limit = apply_rate_limit(guard, name, Evaluation.evaluate_guard(guard, props), props, emit_signal: emit_signal)
|
|
478
|
+
result = applied_rate_limit[:result]
|
|
443
479
|
|
|
444
480
|
signal = nil
|
|
445
481
|
if emit_signal
|
|
@@ -448,7 +484,8 @@ module Liteguard
|
|
|
448
484
|
result,
|
|
449
485
|
props,
|
|
450
486
|
kind: "guard_check",
|
|
451
|
-
measurement: measurement_enabled?(guard, options) ? capture_guard_check_measurement : nil
|
|
487
|
+
measurement: measurement_enabled?(guard, options) ? capture_guard_check_measurement : nil,
|
|
488
|
+
rate_limit_decisions: applied_rate_limit[:rate_limit_decisions]
|
|
452
489
|
)
|
|
453
490
|
end
|
|
454
491
|
|
|
@@ -490,6 +527,12 @@ module Liteguard
|
|
|
490
527
|
projectClientToken: @project_client_token,
|
|
491
528
|
environment: @environment,
|
|
492
529
|
signals: batch.map do |signal|
|
|
530
|
+
rate_limit_decisions_payload = if signal.rate_limit_decisions&.any?
|
|
531
|
+
{ rateLimitDecisions: signal.rate_limit_decisions.map { |decision| rate_limit_decision_payload(decision) } }
|
|
532
|
+
else
|
|
533
|
+
{}
|
|
534
|
+
end
|
|
535
|
+
|
|
493
536
|
{
|
|
494
537
|
guardName: signal.guard_name,
|
|
495
538
|
result: signal.result,
|
|
@@ -502,7 +545,8 @@ module Liteguard
|
|
|
502
545
|
kind: signal.kind,
|
|
503
546
|
droppedSignalsSinceLast: signal.dropped_signals_since_last,
|
|
504
547
|
**(signal.parent_signal_id ? { parentSignalId: signal.parent_signal_id } : {}),
|
|
505
|
-
**(signal.measurement ? { measurement: signal_measurement_payload(signal.measurement) } : {})
|
|
548
|
+
**(signal.measurement ? { measurement: signal_measurement_payload(signal.measurement) } : {}),
|
|
549
|
+
**rate_limit_decisions_payload
|
|
506
550
|
}
|
|
507
551
|
end
|
|
508
552
|
)
|
|
@@ -555,7 +599,7 @@ module Liteguard
|
|
|
555
599
|
# @param measurement [SignalPerformance, nil] optional measurement payload
|
|
556
600
|
# @param parent_signal_id_override [String, nil] explicit parent signal ID
|
|
557
601
|
# @return [Signal] buffered signal instance
|
|
558
|
-
def buffer_signal(guard_name, result, props, kind:, measurement: nil, parent_signal_id_override: nil)
|
|
602
|
+
def buffer_signal(guard_name, result, props, kind:, measurement: nil, parent_signal_id_override: nil, rate_limit_decisions: nil)
|
|
559
603
|
metadata = next_signal_metadata(parent_signal_id_override)
|
|
560
604
|
signal = Signal.new(
|
|
561
605
|
guard_name: guard_name,
|
|
@@ -570,7 +614,8 @@ module Liteguard
|
|
|
570
614
|
callsite_id: capture_callsite_id,
|
|
571
615
|
kind: kind,
|
|
572
616
|
dropped_signals_since_last: take_dropped_signals,
|
|
573
|
-
measurement: measurement
|
|
617
|
+
measurement: measurement,
|
|
618
|
+
rate_limit_decisions: Array(rate_limit_decisions).dup
|
|
574
619
|
)
|
|
575
620
|
should_flush = false
|
|
576
621
|
@monitor.synchronize do
|
|
@@ -729,10 +774,13 @@ module Liteguard
|
|
|
729
774
|
return PUBLIC_BUNDLE_KEY if protected_context.nil?
|
|
730
775
|
|
|
731
776
|
keys = protected_context.properties.keys.sort
|
|
732
|
-
parts = [protected_context.signature, ""]
|
|
777
|
+
parts = [protected_context.signature, "properties"]
|
|
733
778
|
keys.each do |key|
|
|
734
779
|
parts << "#{key}=#{protected_context.properties[key]}"
|
|
735
780
|
end
|
|
781
|
+
parts << ""
|
|
782
|
+
parts << "issuedAt=#{protected_context.issued_at || ''}"
|
|
783
|
+
parts << "expiresAt=#{protected_context.expires_at || ''}"
|
|
736
784
|
parts.join("\x00")
|
|
737
785
|
end
|
|
738
786
|
|
|
@@ -763,6 +811,8 @@ module Liteguard
|
|
|
763
811
|
properties: protected_context.properties,
|
|
764
812
|
signature: protected_context.signature
|
|
765
813
|
}
|
|
814
|
+
payload[:protectedContext][:issuedAt] = protected_context.issued_at unless protected_context.issued_at.nil?
|
|
815
|
+
payload[:protectedContext][:expiresAt] = protected_context.expires_at unless protected_context.expires_at.nil?
|
|
766
816
|
end
|
|
767
817
|
|
|
768
818
|
uri = URI("#{@backend_url}/api/v1/guards")
|
|
@@ -899,12 +949,31 @@ module Liteguard
|
|
|
899
949
|
rules: (raw["rules"] || []).map { |rule| parse_rule(rule) },
|
|
900
950
|
default_value: !!raw["defaultValue"],
|
|
901
951
|
adopted: !!raw["adopted"],
|
|
902
|
-
|
|
903
|
-
|
|
952
|
+
rate_limit: parse_rate_limit(raw["rateLimit"]),
|
|
953
|
+
dry_run_rate_limit: parse_dry_run_rate_limit(raw["dryRunRateLimit"]),
|
|
904
954
|
disable_measurement: raw.key?("disableMeasurement") ? raw["disableMeasurement"] : nil
|
|
905
955
|
)
|
|
906
956
|
end
|
|
907
957
|
|
|
958
|
+
def parse_rate_limit(raw)
|
|
959
|
+
return nil unless raw.is_a?(Hash)
|
|
960
|
+
|
|
961
|
+
GuardRateLimitConfig.new(
|
|
962
|
+
requests_per_minute: (raw["requestsPerMinute"] || 0).to_i,
|
|
963
|
+
partition_properties: Array(raw["partitionProperties"])
|
|
964
|
+
)
|
|
965
|
+
end
|
|
966
|
+
|
|
967
|
+
def parse_dry_run_rate_limit(raw)
|
|
968
|
+
return nil unless raw.is_a?(Hash)
|
|
969
|
+
|
|
970
|
+
GuardDryRunRateLimit.new(
|
|
971
|
+
dry_run_id: raw["dryRunId"].to_s,
|
|
972
|
+
requests_per_minute: (raw["requestsPerMinute"] || 0).to_i,
|
|
973
|
+
partition_properties: Array(raw["partitionProperties"])
|
|
974
|
+
)
|
|
975
|
+
end
|
|
976
|
+
|
|
908
977
|
# Parse a rule payload returned by the backend.
|
|
909
978
|
#
|
|
910
979
|
# @param raw [Hash] decoded rule payload
|
|
@@ -927,19 +996,118 @@ module Liteguard
|
|
|
927
996
|
# the bucket key
|
|
928
997
|
# @param props [Hash] evaluation properties
|
|
929
998
|
# @return [Boolean] `true` when the evaluation is within the limit
|
|
930
|
-
def
|
|
999
|
+
def evaluate_rate_limit(slot_key, limit_per_minute, rate_limit_properties, props, consume:)
|
|
931
1000
|
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
932
|
-
key = rate_limit_bucket_key(
|
|
1001
|
+
key = rate_limit_bucket_key(slot_key, rate_limit_properties, props)
|
|
933
1002
|
@monitor.synchronize do
|
|
934
1003
|
entry = @rate_limit_state[key] || { window_start: now, count: 0 }
|
|
935
1004
|
entry = { window_start: now, count: 0 } if now - entry[:window_start] >= 60.0
|
|
936
|
-
|
|
1005
|
+
count_in_window = entry[:count] + 1
|
|
1006
|
+
allowed = entry[:count] < limit_per_minute
|
|
1007
|
+
if consume
|
|
1008
|
+
entry = { window_start: entry[:window_start], count: count_in_window } if allowed
|
|
937
1009
|
@rate_limit_state[key] = entry
|
|
938
|
-
return false
|
|
939
1010
|
end
|
|
940
|
-
|
|
1011
|
+
{ allowed: allowed, count_in_window: count_in_window }
|
|
941
1012
|
end
|
|
942
|
-
|
|
1013
|
+
end
|
|
1014
|
+
|
|
1015
|
+
def consume_rate_limit(name, evaluation_target, dry_run_id, limit_per_minute, rate_limit_properties, props)
|
|
1016
|
+
evaluate_rate_limit(
|
|
1017
|
+
rate_limit_slot_key(name, evaluation_target, dry_run_id),
|
|
1018
|
+
limit_per_minute,
|
|
1019
|
+
rate_limit_properties,
|
|
1020
|
+
props,
|
|
1021
|
+
consume: true
|
|
1022
|
+
)
|
|
1023
|
+
end
|
|
1024
|
+
|
|
1025
|
+
def peek_rate_limit(name, evaluation_target, dry_run_id, limit_per_minute, rate_limit_properties, props)
|
|
1026
|
+
evaluate_rate_limit(
|
|
1027
|
+
rate_limit_slot_key(name, evaluation_target, dry_run_id),
|
|
1028
|
+
limit_per_minute,
|
|
1029
|
+
rate_limit_properties,
|
|
1030
|
+
props,
|
|
1031
|
+
consume: false
|
|
1032
|
+
)
|
|
1033
|
+
end
|
|
1034
|
+
|
|
1035
|
+
def rate_limit_slot_key(name, evaluation_target, dry_run_id)
|
|
1036
|
+
return "#{name}\x00active" if evaluation_target == :active
|
|
1037
|
+
|
|
1038
|
+
"#{name}\x00dry_run=#{dry_run_id}"
|
|
1039
|
+
end
|
|
1040
|
+
|
|
1041
|
+
def partition_values(rate_limit_properties, props)
|
|
1042
|
+
Array(rate_limit_properties).each_with_object({}) do |property, values|
|
|
1043
|
+
values[property] = props[property] if props.key?(property)
|
|
1044
|
+
end
|
|
1045
|
+
end
|
|
1046
|
+
|
|
1047
|
+
def build_rate_limit_decision(evaluation_target, dry_run_id, outcome, requests_per_minute, rate_limit_properties, props, count_in_window)
|
|
1048
|
+
RateLimitDecision.new(
|
|
1049
|
+
evaluation_target: evaluation_target,
|
|
1050
|
+
dry_run_id: dry_run_id,
|
|
1051
|
+
outcome: outcome,
|
|
1052
|
+
requests_per_minute: requests_per_minute,
|
|
1053
|
+
partition_properties: Array(rate_limit_properties).dup,
|
|
1054
|
+
partition_values: partition_values(rate_limit_properties, props),
|
|
1055
|
+
count_in_window: count_in_window
|
|
1056
|
+
)
|
|
1057
|
+
end
|
|
1058
|
+
|
|
1059
|
+
def apply_rate_limit(guard, name, initial_result, props, emit_signal:)
|
|
1060
|
+
rate_limit_decisions = []
|
|
1061
|
+
return { result: initial_result, rate_limit_decisions: rate_limit_decisions } unless initial_result
|
|
1062
|
+
|
|
1063
|
+
result = initial_result
|
|
1064
|
+
rate_limit = guard.rate_limit
|
|
1065
|
+
if rate_limit && rate_limit.requests_per_minute.to_i > 0
|
|
1066
|
+
evaluation = if emit_signal
|
|
1067
|
+
consume_rate_limit(name, :active, nil, rate_limit.requests_per_minute, rate_limit.partition_properties, props)
|
|
1068
|
+
else
|
|
1069
|
+
peek_rate_limit(name, :active, nil, rate_limit.requests_per_minute, rate_limit.partition_properties, props)
|
|
1070
|
+
end
|
|
1071
|
+
result = evaluation[:allowed]
|
|
1072
|
+
if emit_signal
|
|
1073
|
+
rate_limit_decisions << build_rate_limit_decision(
|
|
1074
|
+
:active,
|
|
1075
|
+
nil,
|
|
1076
|
+
evaluation[:allowed] ? :within_limit : :limited,
|
|
1077
|
+
rate_limit.requests_per_minute,
|
|
1078
|
+
rate_limit.partition_properties,
|
|
1079
|
+
props,
|
|
1080
|
+
evaluation[:count_in_window]
|
|
1081
|
+
)
|
|
1082
|
+
end
|
|
1083
|
+
end
|
|
1084
|
+
|
|
1085
|
+
dry_run_rate_limit = guard.dry_run_rate_limit
|
|
1086
|
+
if emit_signal && dry_run_rate_limit && dry_run_rate_limit.requests_per_minute.to_i > 0
|
|
1087
|
+
evaluation = consume_rate_limit(
|
|
1088
|
+
name,
|
|
1089
|
+
:dry_run,
|
|
1090
|
+
dry_run_rate_limit.dry_run_id,
|
|
1091
|
+
dry_run_rate_limit.requests_per_minute,
|
|
1092
|
+
dry_run_rate_limit.partition_properties,
|
|
1093
|
+
props
|
|
1094
|
+
)
|
|
1095
|
+
rate_limit_decisions << build_rate_limit_decision(
|
|
1096
|
+
:dry_run,
|
|
1097
|
+
dry_run_rate_limit.dry_run_id,
|
|
1098
|
+
evaluation[:allowed] ? :within_limit : :would_limit,
|
|
1099
|
+
dry_run_rate_limit.requests_per_minute,
|
|
1100
|
+
dry_run_rate_limit.partition_properties,
|
|
1101
|
+
props,
|
|
1102
|
+
evaluation[:count_in_window]
|
|
1103
|
+
)
|
|
1104
|
+
end
|
|
1105
|
+
|
|
1106
|
+
{ result: result, rate_limit_decisions: rate_limit_decisions }
|
|
1107
|
+
end
|
|
1108
|
+
|
|
1109
|
+
def check_rate_limit(name, limit_per_minute, rate_limit_properties, props)
|
|
1110
|
+
consume_rate_limit(name, :active, nil, limit_per_minute, rate_limit_properties, props)[:allowed]
|
|
943
1111
|
end
|
|
944
1112
|
|
|
945
1113
|
# Check whether an evaluation would pass rate limiting without consuming a
|
|
@@ -952,15 +1120,7 @@ module Liteguard
|
|
|
952
1120
|
# @param props [Hash] evaluation properties
|
|
953
1121
|
# @return [Boolean] `true` when the evaluation would pass the limit
|
|
954
1122
|
def would_pass_rate_limit(name, limit_per_minute, rate_limit_properties, props)
|
|
955
|
-
|
|
956
|
-
key = rate_limit_bucket_key(name, rate_limit_properties, props)
|
|
957
|
-
@monitor.synchronize do
|
|
958
|
-
entry = @rate_limit_state[key]
|
|
959
|
-
return true if entry.nil?
|
|
960
|
-
return true if now - entry[:window_start] >= 60.0
|
|
961
|
-
|
|
962
|
-
entry[:count] < limit_per_minute
|
|
963
|
-
end
|
|
1123
|
+
peek_rate_limit(name, :active, nil, limit_per_minute, rate_limit_properties, props)[:allowed]
|
|
964
1124
|
end
|
|
965
1125
|
|
|
966
1126
|
# Build the cache key used for rate-limit buckets.
|
|
@@ -1023,19 +1183,51 @@ module Liteguard
|
|
|
1023
1183
|
properties.each_with_object({}) { |(key, value), acc| acc[key.to_s] = value }
|
|
1024
1184
|
end
|
|
1025
1185
|
|
|
1186
|
+
# Normalize protected-context property keys and values to strings.
|
|
1187
|
+
#
|
|
1188
|
+
# @param properties [Hash, nil] raw protected property hash
|
|
1189
|
+
# @return [Hash] normalized protected property hash
|
|
1190
|
+
def normalize_protected_context_properties(properties)
|
|
1191
|
+
return {} if properties.nil?
|
|
1192
|
+
|
|
1193
|
+
properties.each_with_object({}) { |(key, value), acc| acc[key.to_s] = value.to_s }
|
|
1194
|
+
end
|
|
1195
|
+
|
|
1196
|
+
# Normalize an optional integer field from symbol or string keyed hashes.
|
|
1197
|
+
#
|
|
1198
|
+
# @param raw [Hash] source payload
|
|
1199
|
+
# @param snake_key [Symbol] snake_case key
|
|
1200
|
+
# @param camel_key [String] camelCase key
|
|
1201
|
+
# @return [Integer, nil] normalized integer value
|
|
1202
|
+
def normalize_optional_integer(raw, snake_key, camel_key)
|
|
1203
|
+
value = raw[snake_key] || raw[camel_key]
|
|
1204
|
+
return nil if value.nil?
|
|
1205
|
+
|
|
1206
|
+
Integer(value)
|
|
1207
|
+
end
|
|
1208
|
+
|
|
1026
1209
|
# Normalize a protected-context payload into a `ProtectedContext` object.
|
|
1027
1210
|
#
|
|
1028
1211
|
# @param protected_context [ProtectedContext, Hash] raw protected context
|
|
1029
1212
|
# @return [ProtectedContext] normalized protected context
|
|
1030
1213
|
def normalize_protected_context(protected_context)
|
|
1031
1214
|
raw = if protected_context.is_a?(ProtectedContext)
|
|
1032
|
-
{
|
|
1215
|
+
{
|
|
1216
|
+
properties: protected_context.properties,
|
|
1217
|
+
signature: protected_context.signature,
|
|
1218
|
+
issued_at: protected_context.issued_at,
|
|
1219
|
+
expires_at: protected_context.expires_at
|
|
1220
|
+
}
|
|
1033
1221
|
else
|
|
1034
1222
|
protected_context
|
|
1035
1223
|
end
|
|
1036
1224
|
ProtectedContext.new(
|
|
1037
|
-
properties:
|
|
1038
|
-
|
|
1225
|
+
properties: normalize_protected_context_properties(
|
|
1226
|
+
raw[:properties] || raw["properties"] || {}
|
|
1227
|
+
),
|
|
1228
|
+
signature: (raw[:signature] || raw["signature"]).to_s,
|
|
1229
|
+
issued_at: normalize_optional_integer(raw, :issued_at, "issuedAt"),
|
|
1230
|
+
expires_at: normalize_optional_integer(raw, :expires_at, "expiresAt")
|
|
1039
1231
|
)
|
|
1040
1232
|
end
|
|
1041
1233
|
|
|
@@ -1048,7 +1240,9 @@ module Liteguard
|
|
|
1048
1240
|
|
|
1049
1241
|
ProtectedContext.new(
|
|
1050
1242
|
properties: protected_context.properties.dup,
|
|
1051
|
-
signature: protected_context.signature.dup
|
|
1243
|
+
signature: protected_context.signature.dup,
|
|
1244
|
+
issued_at: protected_context.issued_at,
|
|
1245
|
+
expires_at: protected_context.expires_at
|
|
1052
1246
|
)
|
|
1053
1247
|
end
|
|
1054
1248
|
|
|
@@ -1173,6 +1367,18 @@ module Liteguard
|
|
|
1173
1367
|
payload
|
|
1174
1368
|
end
|
|
1175
1369
|
|
|
1370
|
+
def rate_limit_decision_payload(decision)
|
|
1371
|
+
{
|
|
1372
|
+
evaluationTarget: decision.evaluation_target.to_s,
|
|
1373
|
+
**(decision.dry_run_id ? { dryRunId: decision.dry_run_id } : {}),
|
|
1374
|
+
outcome: decision.outcome.to_s,
|
|
1375
|
+
requestsPerMinute: decision.requests_per_minute,
|
|
1376
|
+
partitionProperties: decision.partition_properties,
|
|
1377
|
+
partitionValues: decision.partition_values,
|
|
1378
|
+
countInWindow: decision.count_in_window,
|
|
1379
|
+
}
|
|
1380
|
+
end
|
|
1381
|
+
|
|
1176
1382
|
# Track an unadopted guard so it can be reported with observation metadata.
|
|
1177
1383
|
#
|
|
1178
1384
|
# @param name [String] guard name to record
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Liteguard
|
|
4
|
+
# Internal result from {Evaluation.evaluate_guard_detailed}.
|
|
5
|
+
DetailedEvalResult = Data.define(:result, :matched_rule_index)
|
|
6
|
+
|
|
7
|
+
# A lightweight execution correlation handle.
|
|
8
|
+
#
|
|
9
|
+
# Groups telemetry signals under a shared execution ID. Created by
|
|
10
|
+
# {Scope#start_execution} or {Client#start_execution}.
|
|
11
|
+
# Call {#end_execution} when the logical execution boundary is complete.
|
|
12
|
+
class Execution
|
|
13
|
+
# @return [String] the unique identifier for this execution
|
|
14
|
+
attr_reader :execution_id
|
|
15
|
+
|
|
16
|
+
# @api private
|
|
17
|
+
def initialize(execution_id, cleanup: nil)
|
|
18
|
+
@execution_id = execution_id
|
|
19
|
+
@cleanup = cleanup
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# End this execution.
|
|
23
|
+
#
|
|
24
|
+
# After calling +end_execution+, subsequent guard checks will no longer be
|
|
25
|
+
# correlated with this execution.
|
|
26
|
+
# @return [void]
|
|
27
|
+
def end_execution
|
|
28
|
+
if @cleanup
|
|
29
|
+
@cleanup.call
|
|
30
|
+
@cleanup = nil
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
data/lib/liteguard/evaluation.rb
CHANGED
|
@@ -18,6 +18,21 @@ module Liteguard
|
|
|
18
18
|
guard.default_value
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
+
# Evaluate a guard and return both the result and the matched rule index.
|
|
22
|
+
#
|
|
23
|
+
# @param guard [Guard] guard definition to evaluate
|
|
24
|
+
# @param properties [Hash{String => Object}] normalized evaluation properties
|
|
25
|
+
# @return [DetailedEvalResult] result with the matched rule index (-1 when default)
|
|
26
|
+
def self.evaluate_guard_detailed(guard, properties)
|
|
27
|
+
guard.rules.each_with_index do |rule, i|
|
|
28
|
+
next unless rule.enabled
|
|
29
|
+
if matches_rule?(rule, properties)
|
|
30
|
+
return DetailedEvalResult.new(result: rule.result, matched_rule_index: i)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
DetailedEvalResult.new(result: guard.default_value, matched_rule_index: -1)
|
|
34
|
+
end
|
|
35
|
+
|
|
21
36
|
# Determine whether a single rule matches the provided properties.
|
|
22
37
|
#
|
|
23
38
|
# @param rule [Rule] rule to evaluate
|
data/lib/liteguard/scope.rb
CHANGED
|
@@ -21,7 +21,9 @@ module Liteguard
|
|
|
21
21
|
@bundle_key = bundle_key
|
|
22
22
|
@protected_context = protected_context ? ProtectedContext.new(
|
|
23
23
|
properties: protected_context.properties.dup,
|
|
24
|
-
signature: protected_context.signature.dup
|
|
24
|
+
signature: protected_context.signature.dup,
|
|
25
|
+
issued_at: protected_context.issued_at,
|
|
26
|
+
expires_at: protected_context.expires_at
|
|
25
27
|
) : nil
|
|
26
28
|
end
|
|
27
29
|
|
|
@@ -105,6 +107,30 @@ module Liteguard
|
|
|
105
107
|
@client.execute_if_open_in_scope(self, name, options, **legacy_options, &block)
|
|
106
108
|
end
|
|
107
109
|
|
|
110
|
+
# Evaluate a guard and return a {GuardDecision} with full reasoning.
|
|
111
|
+
#
|
|
112
|
+
# Like {#is_open} but returns the full decision context including the
|
|
113
|
+
# reason, matched rule index, and the merged properties used. A
|
|
114
|
+
# +guard_check+ telemetry signal is buffered.
|
|
115
|
+
#
|
|
116
|
+
# @param name [String] guard name to evaluate
|
|
117
|
+
# @param options [Hash, nil] optional per-call overrides
|
|
118
|
+
# @return [GuardDecision] the evaluation decision
|
|
119
|
+
def evaluate(name, options = nil, **legacy_options)
|
|
120
|
+
@client.evaluate_in_scope(self, name, options, **legacy_options)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Start a new execution correlation context.
|
|
124
|
+
#
|
|
125
|
+
# Returns a lightweight {Execution} handle that groups telemetry signals
|
|
126
|
+
# under a shared execution ID. Call {Execution#end_execution} when the
|
|
127
|
+
# logical execution boundary is complete.
|
|
128
|
+
#
|
|
129
|
+
# @return [Execution] a correlation handle
|
|
130
|
+
def start_execution
|
|
131
|
+
@client.start_execution
|
|
132
|
+
end
|
|
133
|
+
|
|
108
134
|
# Bind this scope as the active scope for the duration of a block.
|
|
109
135
|
#
|
|
110
136
|
# @yield Runs with this scope bound as active
|
|
@@ -136,7 +162,9 @@ module Liteguard
|
|
|
136
162
|
|
|
137
163
|
ProtectedContext.new(
|
|
138
164
|
properties: @protected_context.properties.dup,
|
|
139
|
-
signature: @protected_context.signature.dup
|
|
165
|
+
signature: @protected_context.signature.dup,
|
|
166
|
+
issued_at: @protected_context.issued_at,
|
|
167
|
+
expires_at: @protected_context.expires_at
|
|
140
168
|
)
|
|
141
169
|
end
|
|
142
170
|
|
data/lib/liteguard/types.rb
CHANGED
|
@@ -6,6 +6,12 @@ module Liteguard
|
|
|
6
6
|
|
|
7
7
|
OPERATORS = %i[equals not_equals in not_in regex gt gte lt lte].freeze
|
|
8
8
|
|
|
9
|
+
GUARD_DECISION_REASONS = %i[matched_rule default_value unadopted fallback].freeze
|
|
10
|
+
|
|
11
|
+
RATE_LIMIT_EVALUATION_TARGETS = %i[active dry_run].freeze
|
|
12
|
+
|
|
13
|
+
RATE_LIMIT_DECISION_OUTCOMES = %i[within_limit limited would_limit].freeze
|
|
14
|
+
|
|
9
15
|
# A single rule condition within a Guard.
|
|
10
16
|
Rule = Data.define(
|
|
11
17
|
:property_name,
|
|
@@ -15,24 +21,49 @@ module Liteguard
|
|
|
15
21
|
:enabled
|
|
16
22
|
)
|
|
17
23
|
|
|
24
|
+
# GuardRateLimitConfig (mirrors proto message).
|
|
25
|
+
GuardRateLimitConfig = Data.define(
|
|
26
|
+
:requests_per_minute,
|
|
27
|
+
:partition_properties
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# GuardDryRunRateLimit (mirrors proto message).
|
|
31
|
+
GuardDryRunRateLimit = Data.define(
|
|
32
|
+
:dry_run_id,
|
|
33
|
+
:requests_per_minute,
|
|
34
|
+
:partition_properties
|
|
35
|
+
)
|
|
36
|
+
|
|
18
37
|
# A named feature guard with an ordered rule set.
|
|
19
38
|
Guard = Data.define(
|
|
20
39
|
:name,
|
|
21
40
|
:rules,
|
|
22
41
|
:default_value,
|
|
23
42
|
:adopted,
|
|
24
|
-
:
|
|
25
|
-
:
|
|
43
|
+
:rate_limit,
|
|
44
|
+
:dry_run_rate_limit,
|
|
26
45
|
:disable_measurement
|
|
27
46
|
)
|
|
28
47
|
|
|
48
|
+
# The detailed result of evaluating a guard.
|
|
49
|
+
GuardDecision = Data.define(
|
|
50
|
+
:name,
|
|
51
|
+
:is_open,
|
|
52
|
+
:adopted,
|
|
53
|
+
:reason,
|
|
54
|
+
:matched_rule_index,
|
|
55
|
+
:properties
|
|
56
|
+
)
|
|
57
|
+
|
|
29
58
|
# Keys accepted by the options hash passed to {Liteguard::Client#is_open}.
|
|
30
59
|
CHECK_OPTION_KEYS = %i[properties fallback disable_measurement].freeze
|
|
31
60
|
|
|
32
61
|
# ProtectedContext (mirrors proto message).
|
|
33
62
|
ProtectedContext = Data.define(
|
|
34
63
|
:properties,
|
|
35
|
-
:signature
|
|
64
|
+
:signature,
|
|
65
|
+
:issued_at,
|
|
66
|
+
:expires_at
|
|
36
67
|
)
|
|
37
68
|
|
|
38
69
|
# GetGuardsRequest (mirrors proto message).
|
|
@@ -45,6 +76,7 @@ module Liteguard
|
|
|
45
76
|
# Parsed response from the guards endpoint.
|
|
46
77
|
GuardsResponse = Data.define(
|
|
47
78
|
:guards,
|
|
79
|
+
:omitted_protected_property_names,
|
|
48
80
|
:refresh_rate_seconds,
|
|
49
81
|
:etag
|
|
50
82
|
)
|
|
@@ -85,6 +117,17 @@ module Liteguard
|
|
|
85
117
|
:guard_execution
|
|
86
118
|
)
|
|
87
119
|
|
|
120
|
+
# RateLimitDecision (mirrors proto message).
|
|
121
|
+
RateLimitDecision = Data.define(
|
|
122
|
+
:evaluation_target,
|
|
123
|
+
:dry_run_id,
|
|
124
|
+
:outcome,
|
|
125
|
+
:requests_per_minute,
|
|
126
|
+
:partition_properties,
|
|
127
|
+
:partition_values,
|
|
128
|
+
:count_in_window
|
|
129
|
+
)
|
|
130
|
+
|
|
88
131
|
# A buffered guard-check record for upload to the data plane.
|
|
89
132
|
Signal = Data.define(
|
|
90
133
|
:guard_name,
|
|
@@ -99,7 +142,8 @@ module Liteguard
|
|
|
99
142
|
:callsite_id,
|
|
100
143
|
:kind,
|
|
101
144
|
:dropped_signals_since_last,
|
|
102
|
-
:measurement
|
|
145
|
+
:measurement,
|
|
146
|
+
:rate_limit_decisions
|
|
103
147
|
)
|
|
104
148
|
|
|
105
149
|
# UnadoptedGuardObservation (mirrors proto message).
|
|
@@ -128,8 +172,8 @@ module Liteguard
|
|
|
128
172
|
project_client_token: nil,
|
|
129
173
|
environment: nil,
|
|
130
174
|
fallback: false,
|
|
131
|
-
refresh_rate_seconds:
|
|
132
|
-
flush_rate_seconds:
|
|
175
|
+
refresh_rate_seconds: 60,
|
|
176
|
+
flush_rate_seconds: 60,
|
|
133
177
|
flush_size: 500,
|
|
134
178
|
backend_url: "https://api.liteguard.io",
|
|
135
179
|
quiet: true,
|
data/lib/liteguard.rb
CHANGED
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.7.20260603
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Liteguard
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-06-04 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rspec
|
|
@@ -16,28 +16,28 @@ dependencies:
|
|
|
16
16
|
requirements:
|
|
17
17
|
- - "~>"
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
|
-
version:
|
|
19
|
+
version: 3.13.2
|
|
20
20
|
type: :development
|
|
21
21
|
prerelease: false
|
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
23
|
requirements:
|
|
24
24
|
- - "~>"
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
|
-
version:
|
|
26
|
+
version: 3.13.2
|
|
27
27
|
- !ruby/object:Gem::Dependency
|
|
28
28
|
name: webmock
|
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
|
30
30
|
requirements:
|
|
31
31
|
- - "~>"
|
|
32
32
|
- !ruby/object:Gem::Version
|
|
33
|
-
version:
|
|
33
|
+
version: 3.26.2
|
|
34
34
|
type: :development
|
|
35
35
|
prerelease: false
|
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
37
|
requirements:
|
|
38
38
|
- - "~>"
|
|
39
39
|
- !ruby/object:Gem::Version
|
|
40
|
-
version:
|
|
40
|
+
version: 3.26.2
|
|
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
43
|
at startup. Every open? check is sub-millisecond with no network round-trip.
|
|
@@ -50,6 +50,7 @@ files:
|
|
|
50
50
|
- README.md
|
|
51
51
|
- lib/liteguard.rb
|
|
52
52
|
- lib/liteguard/client.rb
|
|
53
|
+
- lib/liteguard/decision.rb
|
|
53
54
|
- lib/liteguard/evaluation.rb
|
|
54
55
|
- lib/liteguard/scope.rb
|
|
55
56
|
- lib/liteguard/types.rb
|