liteguard 0.6.20260405 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 64fb509c369cbd43217407216706597424220bd7143e03d460b3ad4428b4ff7a
4
- data.tar.gz: b53c0c549c66353d3cb6f9aea811e5dffeb9e3b53f74f49a3cb315a17aa513e8
3
+ metadata.gz: 674b74c3afa87054d07083210c6f894afad8bc53cb2b6ae8a4e8bd045b30c14c
4
+ data.tar.gz: 6cf6d9d1fceb2760404e65a0b26faa05ff3687318492d4e124f35eced843e9f5
5
5
  SHA512:
6
- metadata.gz: 3488d12c50f5c9d3a6e48029ed450e2062f71efd24d06ebf217a88ee4742e0e3317c0731e4a2b13e38eebb33291bdc3b7745e8d5607451734c05a3378d58f383
7
- data.tar.gz: e5a64bcf4c70b0692a88841666e1eb1358859073ac2d1d51fcae2a672ecf9e5dd6895b1524e1827e5a61995b6fbb4186621cf451ada5bf8cdd4410193ed7979e
6
+ metadata.gz: 51eec265fceb91ea7994cf97d64878afb7312ce5bf72abe7732d584da009a8b7ce82efa163144110054254d01ad935cbe22a77ba3ca9f8a01ca5963f5d28d0ef
7
+ data.tar.gz: d998303a8c1bfa5156aabee64afe9d8b8e174c7da199004491b5cf5e94376a157644d459605fca2c2bbde7d87f9e1381993265f8d2fe0c636057acb60e4d4dc3
data/README.md CHANGED
@@ -67,6 +67,7 @@ For server applications that want request-scoped context propagation:
67
67
  ## Notes
68
68
 
69
69
  - Evaluation is local after the initial bundle fetch.
70
+ - Protected contexts use `properties`, `signature`, and optional `issued_at` / `expires_at` fields.
70
71
  - Prefer explicit client and scope usage.
71
72
  - Unadopted guards default open and emit no signals.
72
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.
@@ -370,10 +370,8 @@ module Liteguard
370
370
  props = props.merge(options[:properties]) if options[:properties]
371
371
 
372
372
  detailed = Evaluation.evaluate_guard_detailed(guard, props)
373
- result = detailed.result
374
- if result && guard.rate_limit_per_minute.to_i > 0
375
- result = check_rate_limit(name.to_s, guard.rate_limit_per_minute, guard.rate_limit_properties, props)
376
- end
373
+ applied_rate_limit = apply_rate_limit(guard, name.to_s, detailed.result, props, emit_signal: true)
374
+ result = applied_rate_limit[:result]
377
375
 
378
376
  reason = detailed.matched_rule_index >= 0 ? "matched_rule" : "default_value"
379
377
  matched_idx = detailed.matched_rule_index >= 0 ? detailed.matched_rule_index : nil
@@ -381,7 +379,8 @@ module Liteguard
381
379
  buffer_signal(
382
380
  name.to_s, result, props,
383
381
  kind: "guard_check",
384
- measurement: measurement_enabled?(guard, options) ? capture_guard_check_measurement : nil
382
+ measurement: measurement_enabled?(guard, options) ? capture_guard_check_measurement : nil,
383
+ rate_limit_decisions: applied_rate_limit[:rate_limit_decisions]
385
384
  )
386
385
 
387
386
  GuardDecision.new(
@@ -475,14 +474,8 @@ module Liteguard
475
474
  props = props.merge(options[:properties])
476
475
  end
477
476
 
478
- result = Evaluation.evaluate_guard(guard, props)
479
- if result && guard.rate_limit_per_minute.to_i > 0
480
- result = if emit_signal
481
- check_rate_limit(name, guard.rate_limit_per_minute, guard.rate_limit_properties, props)
482
- else
483
- would_pass_rate_limit(name, guard.rate_limit_per_minute, guard.rate_limit_properties, props)
484
- end
485
- 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]
486
479
 
487
480
  signal = nil
488
481
  if emit_signal
@@ -491,7 +484,8 @@ module Liteguard
491
484
  result,
492
485
  props,
493
486
  kind: "guard_check",
494
- 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]
495
489
  )
496
490
  end
497
491
 
@@ -533,6 +527,12 @@ module Liteguard
533
527
  projectClientToken: @project_client_token,
534
528
  environment: @environment,
535
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
+
536
536
  {
537
537
  guardName: signal.guard_name,
538
538
  result: signal.result,
@@ -545,7 +545,8 @@ module Liteguard
545
545
  kind: signal.kind,
546
546
  droppedSignalsSinceLast: signal.dropped_signals_since_last,
547
547
  **(signal.parent_signal_id ? { parentSignalId: signal.parent_signal_id } : {}),
548
- **(signal.measurement ? { measurement: signal_measurement_payload(signal.measurement) } : {})
548
+ **(signal.measurement ? { measurement: signal_measurement_payload(signal.measurement) } : {}),
549
+ **rate_limit_decisions_payload
549
550
  }
550
551
  end
551
552
  )
@@ -598,7 +599,7 @@ module Liteguard
598
599
  # @param measurement [SignalPerformance, nil] optional measurement payload
599
600
  # @param parent_signal_id_override [String, nil] explicit parent signal ID
600
601
  # @return [Signal] buffered signal instance
601
- 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)
602
603
  metadata = next_signal_metadata(parent_signal_id_override)
603
604
  signal = Signal.new(
604
605
  guard_name: guard_name,
@@ -613,7 +614,8 @@ module Liteguard
613
614
  callsite_id: capture_callsite_id,
614
615
  kind: kind,
615
616
  dropped_signals_since_last: take_dropped_signals,
616
- measurement: measurement
617
+ measurement: measurement,
618
+ rate_limit_decisions: Array(rate_limit_decisions).dup
617
619
  )
618
620
  should_flush = false
619
621
  @monitor.synchronize do
@@ -772,10 +774,13 @@ module Liteguard
772
774
  return PUBLIC_BUNDLE_KEY if protected_context.nil?
773
775
 
774
776
  keys = protected_context.properties.keys.sort
775
- parts = [protected_context.signature, ""]
777
+ parts = [protected_context.signature, "properties"]
776
778
  keys.each do |key|
777
779
  parts << "#{key}=#{protected_context.properties[key]}"
778
780
  end
781
+ parts << ""
782
+ parts << "issuedAt=#{protected_context.issued_at || ''}"
783
+ parts << "expiresAt=#{protected_context.expires_at || ''}"
779
784
  parts.join("\x00")
780
785
  end
781
786
 
@@ -806,6 +811,8 @@ module Liteguard
806
811
  properties: protected_context.properties,
807
812
  signature: protected_context.signature
808
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?
809
816
  end
810
817
 
811
818
  uri = URI("#{@backend_url}/api/v1/guards")
@@ -942,12 +949,31 @@ module Liteguard
942
949
  rules: (raw["rules"] || []).map { |rule| parse_rule(rule) },
943
950
  default_value: !!raw["defaultValue"],
944
951
  adopted: !!raw["adopted"],
945
- rate_limit_per_minute: (raw["rateLimitPerMinute"] || 0).to_i,
946
- rate_limit_properties: Array(raw["rateLimitProperties"]),
952
+ rate_limit: parse_rate_limit(raw["rateLimit"]),
953
+ dry_run_rate_limit: parse_dry_run_rate_limit(raw["dryRunRateLimit"]),
947
954
  disable_measurement: raw.key?("disableMeasurement") ? raw["disableMeasurement"] : nil
948
955
  )
949
956
  end
950
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
+
951
977
  # Parse a rule payload returned by the backend.
952
978
  #
953
979
  # @param raw [Hash] decoded rule payload
@@ -970,19 +996,118 @@ module Liteguard
970
996
  # the bucket key
971
997
  # @param props [Hash] evaluation properties
972
998
  # @return [Boolean] `true` when the evaluation is within the limit
973
- def check_rate_limit(name, limit_per_minute, rate_limit_properties, props)
999
+ def evaluate_rate_limit(slot_key, limit_per_minute, rate_limit_properties, props, consume:)
974
1000
  now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
975
- key = rate_limit_bucket_key(name, rate_limit_properties, props)
1001
+ key = rate_limit_bucket_key(slot_key, rate_limit_properties, props)
976
1002
  @monitor.synchronize do
977
1003
  entry = @rate_limit_state[key] || { window_start: now, count: 0 }
978
1004
  entry = { window_start: now, count: 0 } if now - entry[:window_start] >= 60.0
979
- if entry[:count] >= limit_per_minute
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
980
1009
  @rate_limit_state[key] = entry
981
- return false
982
1010
  end
983
- @rate_limit_state[key] = { window_start: entry[:window_start], count: entry[:count] + 1 }
1011
+ { allowed: allowed, count_in_window: count_in_window }
984
1012
  end
985
- true
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]
986
1111
  end
987
1112
 
988
1113
  # Check whether an evaluation would pass rate limiting without consuming a
@@ -995,15 +1120,7 @@ module Liteguard
995
1120
  # @param props [Hash] evaluation properties
996
1121
  # @return [Boolean] `true` when the evaluation would pass the limit
997
1122
  def would_pass_rate_limit(name, limit_per_minute, rate_limit_properties, props)
998
- now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
999
- key = rate_limit_bucket_key(name, rate_limit_properties, props)
1000
- @monitor.synchronize do
1001
- entry = @rate_limit_state[key]
1002
- return true if entry.nil?
1003
- return true if now - entry[:window_start] >= 60.0
1004
-
1005
- entry[:count] < limit_per_minute
1006
- end
1123
+ peek_rate_limit(name, :active, nil, limit_per_minute, rate_limit_properties, props)[:allowed]
1007
1124
  end
1008
1125
 
1009
1126
  # Build the cache key used for rate-limit buckets.
@@ -1066,19 +1183,51 @@ module Liteguard
1066
1183
  properties.each_with_object({}) { |(key, value), acc| acc[key.to_s] = value }
1067
1184
  end
1068
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
+
1069
1209
  # Normalize a protected-context payload into a `ProtectedContext` object.
1070
1210
  #
1071
1211
  # @param protected_context [ProtectedContext, Hash] raw protected context
1072
1212
  # @return [ProtectedContext] normalized protected context
1073
1213
  def normalize_protected_context(protected_context)
1074
1214
  raw = if protected_context.is_a?(ProtectedContext)
1075
- { properties: protected_context.properties, signature: protected_context.signature }
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
+ }
1076
1221
  else
1077
1222
  protected_context
1078
1223
  end
1079
1224
  ProtectedContext.new(
1080
- properties: normalize_properties(raw[:properties] || raw["properties"] || {}),
1081
- signature: (raw[:signature] || raw["signature"]).to_s
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")
1082
1231
  )
1083
1232
  end
1084
1233
 
@@ -1091,7 +1240,9 @@ module Liteguard
1091
1240
 
1092
1241
  ProtectedContext.new(
1093
1242
  properties: protected_context.properties.dup,
1094
- 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
1095
1246
  )
1096
1247
  end
1097
1248
 
@@ -1216,6 +1367,18 @@ module Liteguard
1216
1367
  payload
1217
1368
  end
1218
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
+
1219
1382
  # Track an unadopted guard so it can be reported with observation metadata.
1220
1383
  #
1221
1384
  # @param name [String] guard name to record
@@ -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
 
@@ -160,7 +162,9 @@ module Liteguard
160
162
 
161
163
  ProtectedContext.new(
162
164
  properties: @protected_context.properties.dup,
163
- 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
164
168
  )
165
169
  end
166
170
 
@@ -8,6 +8,10 @@ module Liteguard
8
8
 
9
9
  GUARD_DECISION_REASONS = %i[matched_rule default_value unadopted fallback].freeze
10
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
+
11
15
  # A single rule condition within a Guard.
12
16
  Rule = Data.define(
13
17
  :property_name,
@@ -17,14 +21,27 @@ module Liteguard
17
21
  :enabled
18
22
  )
19
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
+
20
37
  # A named feature guard with an ordered rule set.
21
38
  Guard = Data.define(
22
39
  :name,
23
40
  :rules,
24
41
  :default_value,
25
42
  :adopted,
26
- :rate_limit_per_minute,
27
- :rate_limit_properties,
43
+ :rate_limit,
44
+ :dry_run_rate_limit,
28
45
  :disable_measurement
29
46
  )
30
47
 
@@ -44,7 +61,9 @@ module Liteguard
44
61
  # ProtectedContext (mirrors proto message).
45
62
  ProtectedContext = Data.define(
46
63
  :properties,
47
- :signature
64
+ :signature,
65
+ :issued_at,
66
+ :expires_at
48
67
  )
49
68
 
50
69
  # GetGuardsRequest (mirrors proto message).
@@ -57,6 +76,7 @@ module Liteguard
57
76
  # Parsed response from the guards endpoint.
58
77
  GuardsResponse = Data.define(
59
78
  :guards,
79
+ :omitted_protected_property_names,
60
80
  :refresh_rate_seconds,
61
81
  :etag
62
82
  )
@@ -97,6 +117,17 @@ module Liteguard
97
117
  :guard_execution
98
118
  )
99
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
+
100
131
  # A buffered guard-check record for upload to the data plane.
101
132
  Signal = Data.define(
102
133
  :guard_name,
@@ -111,7 +142,8 @@ module Liteguard
111
142
  :callsite_id,
112
143
  :kind,
113
144
  :dropped_signals_since_last,
114
- :measurement
145
+ :measurement,
146
+ :rate_limit_decisions
115
147
  )
116
148
 
117
149
  # UnadoptedGuardObservation (mirrors proto message).
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.6.20260405
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-04-06 00:00:00.000000000 Z
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: '3.13'
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: '3.13'
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: '3.23'
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: '3.23'
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.