liteguard 0.2.20260314
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 +7 -0
- data/LICENSE +158 -0
- data/README.md +66 -0
- data/lib/liteguard/client.rb +1282 -0
- data/lib/liteguard/evaluation.rb +104 -0
- data/lib/liteguard/scope.rb +159 -0
- data/lib/liteguard/types.rb +134 -0
- data/lib/liteguard.rb +19 -0
- metadata +85 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
module Liteguard
|
|
2
|
+
# Local rule evaluation engine — all evaluation is done without network calls.
|
|
3
|
+
module Evaluation
|
|
4
|
+
# Evaluate a guard against a property hash.
|
|
5
|
+
#
|
|
6
|
+
# Rules are processed in order and the first matching rule determines the
|
|
7
|
+
# result. If no enabled rule matches, the guard's configured default value
|
|
8
|
+
# is returned.
|
|
9
|
+
#
|
|
10
|
+
# @param guard [Guard] guard definition to evaluate
|
|
11
|
+
# @param properties [Hash{String => Object}] normalized evaluation properties
|
|
12
|
+
# @return [Boolean] the resolved open/closed result
|
|
13
|
+
def self.evaluate_guard(guard, properties)
|
|
14
|
+
guard.rules.each do |rule|
|
|
15
|
+
next unless rule.enabled
|
|
16
|
+
return rule.result if matches_rule?(rule, properties)
|
|
17
|
+
end
|
|
18
|
+
guard.default_value
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Determine whether a single rule matches the provided properties.
|
|
22
|
+
#
|
|
23
|
+
# @param rule [Rule] rule to evaluate
|
|
24
|
+
# @param props [Hash{String => Object}] normalized evaluation properties
|
|
25
|
+
# @return [Boolean] `true` when the rule matches
|
|
26
|
+
def self.matches_rule?(rule, props)
|
|
27
|
+
raw = props[rule.property_name]
|
|
28
|
+
return false if raw.nil?
|
|
29
|
+
return false if rule.values.empty?
|
|
30
|
+
|
|
31
|
+
first = rule.values.first
|
|
32
|
+
|
|
33
|
+
case rule.operator
|
|
34
|
+
when :equals then values_equal?(raw, first)
|
|
35
|
+
when :not_equals then !values_equal?(raw, first)
|
|
36
|
+
when :in then rule.values.any? { |v| values_equal?(raw, v) }
|
|
37
|
+
when :not_in then rule.values.none? { |v| values_equal?(raw, v) }
|
|
38
|
+
when :regex
|
|
39
|
+
pattern = first.to_s
|
|
40
|
+
begin
|
|
41
|
+
!!(raw.to_s =~ Regexp.new(pattern))
|
|
42
|
+
rescue RegexpError
|
|
43
|
+
false
|
|
44
|
+
end
|
|
45
|
+
when :gt then compare_ordered_values(raw, first)&.positive? || false
|
|
46
|
+
when :gte then (comparison = compare_ordered_values(raw, first)) && comparison >= 0 || false
|
|
47
|
+
when :lt then compare_ordered_values(raw, first)&.negative? || false
|
|
48
|
+
when :lte then (comparison = compare_ordered_values(raw, first)) && comparison <= 0 || false
|
|
49
|
+
else false
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
private_class_method :matches_rule?
|
|
53
|
+
|
|
54
|
+
# Determine whether two raw property values are equal without mixed-type coercion.
|
|
55
|
+
#
|
|
56
|
+
# @param a [Object] left-hand value
|
|
57
|
+
# @param b [Object] right-hand value
|
|
58
|
+
# @return [Boolean] `true` when values are equal and of the same kind
|
|
59
|
+
def self.values_equal?(a, b)
|
|
60
|
+
case a
|
|
61
|
+
when TrueClass, FalseClass
|
|
62
|
+
b == a
|
|
63
|
+
when String
|
|
64
|
+
b.is_a?(String) && a == b
|
|
65
|
+
else
|
|
66
|
+
na = to_numeric(a)
|
|
67
|
+
nb = to_numeric(b)
|
|
68
|
+
!na.nil? && !nb.nil? && na == nb
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
private_class_method :values_equal?
|
|
72
|
+
|
|
73
|
+
# Compare ordered values without mixed-type coercion.
|
|
74
|
+
#
|
|
75
|
+
# @param a [Object] left-hand value
|
|
76
|
+
# @param b [Object] right-hand value
|
|
77
|
+
# @return [-1, 0, 1, nil] comparison result compatible with `<=>`
|
|
78
|
+
def self.compare_ordered_values(a, b)
|
|
79
|
+
if a.is_a?(String) || b.is_a?(String)
|
|
80
|
+
return nil unless a.is_a?(String) && b.is_a?(String)
|
|
81
|
+
|
|
82
|
+
return a <=> b
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
na = to_numeric(a)
|
|
86
|
+
nb = to_numeric(b)
|
|
87
|
+
return nil if na.nil? || nb.nil?
|
|
88
|
+
|
|
89
|
+
na <=> nb
|
|
90
|
+
end
|
|
91
|
+
private_class_method :compare_ordered_values
|
|
92
|
+
|
|
93
|
+
# Coerce supported numeric values into a comparable form.
|
|
94
|
+
#
|
|
95
|
+
# @param v [Object] raw property value
|
|
96
|
+
# @return [Float, nil] numeric representation, or `nil` if coercion fails
|
|
97
|
+
def self.to_numeric(v)
|
|
98
|
+
case v
|
|
99
|
+
when Numeric then v.to_f
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
private_class_method :to_numeric
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
module Liteguard
|
|
2
|
+
# Immutable request scope for Liteguard evaluations.
|
|
3
|
+
class Scope
|
|
4
|
+
# @return [String] cache key for the guard bundle backing this scope
|
|
5
|
+
attr_reader :bundle_key
|
|
6
|
+
|
|
7
|
+
# Create a new immutable scope.
|
|
8
|
+
#
|
|
9
|
+
# Application code typically uses `Liteguard::Client#create_scope` instead of
|
|
10
|
+
# calling this constructor directly.
|
|
11
|
+
#
|
|
12
|
+
# @param client [Client] owning client instance
|
|
13
|
+
# @param properties [Hash{String => Object}] normalized scope properties
|
|
14
|
+
# @param bundle_key [String] cache key for the bundle used by this scope
|
|
15
|
+
# @param protected_context [ProtectedContext, nil] optional signed protected
|
|
16
|
+
# context associated with the scope
|
|
17
|
+
# @return [void]
|
|
18
|
+
def initialize(client, properties, bundle_key, protected_context)
|
|
19
|
+
@client = client
|
|
20
|
+
@properties = properties.dup.freeze
|
|
21
|
+
@bundle_key = bundle_key
|
|
22
|
+
@protected_context = protected_context ? ProtectedContext.new(
|
|
23
|
+
properties: protected_context.properties.dup,
|
|
24
|
+
signature: protected_context.signature.dup
|
|
25
|
+
) : nil
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Return a new scope with merged properties.
|
|
29
|
+
#
|
|
30
|
+
# Existing properties are preserved unless overwritten by the new values.
|
|
31
|
+
#
|
|
32
|
+
# @param properties [Hash] properties to merge into the scope
|
|
33
|
+
# @return [Scope] a derived immutable scope
|
|
34
|
+
def with_properties(properties)
|
|
35
|
+
self.class.new(@client, @properties.merge(normalize_properties(properties)), @bundle_key, @protected_context)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Alias-friendly wrapper for {#with_properties}.
|
|
39
|
+
#
|
|
40
|
+
# @param properties [Hash] properties to merge into the scope
|
|
41
|
+
# @return [Scope] a derived immutable scope
|
|
42
|
+
def add_properties(properties)
|
|
43
|
+
with_properties(properties)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Return a new scope with the named properties removed.
|
|
47
|
+
#
|
|
48
|
+
# @param names [Array<String, Symbol>] property names to remove
|
|
49
|
+
# @return [Scope] a derived immutable scope
|
|
50
|
+
def clear_properties(names)
|
|
51
|
+
next_properties = @properties.dup
|
|
52
|
+
Array(names).each { |name| next_properties.delete(name.to_s) }
|
|
53
|
+
self.class.new(@client, next_properties, @bundle_key, @protected_context)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Return a new scope with all properties cleared.
|
|
57
|
+
#
|
|
58
|
+
# @return [Scope] an empty derived scope
|
|
59
|
+
def reset_properties
|
|
60
|
+
self.class.new(@client, {}, @bundle_key, @protected_context)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Return a new scope bound to a protected-context bundle.
|
|
64
|
+
#
|
|
65
|
+
# @param protected_context [ProtectedContext, Hash] signed protected context
|
|
66
|
+
# @return [Scope] a derived immutable scope
|
|
67
|
+
def bind_protected_context(protected_context)
|
|
68
|
+
@client.bind_protected_context_to_scope(self, protected_context)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Return a new scope using the public guard bundle.
|
|
72
|
+
#
|
|
73
|
+
# @return [Scope] a derived immutable scope without protected context
|
|
74
|
+
def clear_protected_context
|
|
75
|
+
@client.ensure_public_bundle_ready
|
|
76
|
+
self.class.new(@client, @properties, Client::PUBLIC_BUNDLE_KEY, nil)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Evaluate a guard within this scope and emit telemetry.
|
|
80
|
+
#
|
|
81
|
+
# @param name [String] guard name to evaluate
|
|
82
|
+
# @param options [Hash, nil] optional per-call overrides
|
|
83
|
+
# @return [Boolean] `true` when the guard resolves open
|
|
84
|
+
def is_open(name, options = nil, **legacy_options)
|
|
85
|
+
@client.is_open_in_scope(self, name, options, **legacy_options)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Evaluate a guard within this scope without emitting telemetry.
|
|
89
|
+
#
|
|
90
|
+
# @param name [String] guard name to evaluate
|
|
91
|
+
# @param options [Hash, nil] optional per-call overrides
|
|
92
|
+
# @return [Boolean] `true` when the guard resolves open
|
|
93
|
+
def peek_is_open(name, options = nil, **legacy_options)
|
|
94
|
+
@client.peek_is_open_in_scope(self, name, options, **legacy_options)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Evaluate a guard and run the block only when it is open.
|
|
98
|
+
#
|
|
99
|
+
# @param name [String] guard name to evaluate
|
|
100
|
+
# @param options [Hash, nil] optional per-call overrides
|
|
101
|
+
# @yield Runs only when the guard resolves open
|
|
102
|
+
# @return [Object, nil] the block return value, or `nil` when the guard is
|
|
103
|
+
# closed
|
|
104
|
+
def execute_if_open(name, options = nil, **legacy_options, &block)
|
|
105
|
+
@client.execute_if_open_in_scope(self, name, options, **legacy_options, &block)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Bind this scope as the active scope for the duration of a block.
|
|
109
|
+
#
|
|
110
|
+
# @yield Runs with this scope bound as active
|
|
111
|
+
# @return [Object] the block return value
|
|
112
|
+
def run(&block)
|
|
113
|
+
@client.with_scope(self, &block)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Run a block in this scope and attach a shared execution identifier.
|
|
117
|
+
#
|
|
118
|
+
# @yield Runs inside this scope and a correlated execution scope
|
|
119
|
+
# @return [Object] the block return value
|
|
120
|
+
def with_execution(&block)
|
|
121
|
+
@client.with_scope(self) { @client.with_execution(&block) }
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Return a defensive copy of the scope properties.
|
|
125
|
+
#
|
|
126
|
+
# @return [Hash] scope properties keyed by string
|
|
127
|
+
def properties
|
|
128
|
+
@properties.dup
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Return a defensive copy of the protected context, if present.
|
|
132
|
+
#
|
|
133
|
+
# @return [ProtectedContext, nil] copied protected context or `nil`
|
|
134
|
+
def protected_context
|
|
135
|
+
return nil unless @protected_context
|
|
136
|
+
|
|
137
|
+
ProtectedContext.new(
|
|
138
|
+
properties: @protected_context.properties.dup,
|
|
139
|
+
signature: @protected_context.signature.dup
|
|
140
|
+
)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Determine whether this scope belongs to the provided client.
|
|
144
|
+
#
|
|
145
|
+
# @param client [Client] client to compare against
|
|
146
|
+
# @return [Boolean] `true` when both references point to the same client
|
|
147
|
+
def belongs_to?(client)
|
|
148
|
+
@client.equal?(client)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
private
|
|
152
|
+
|
|
153
|
+
def normalize_properties(properties)
|
|
154
|
+
return {} if properties.nil?
|
|
155
|
+
|
|
156
|
+
properties.each_with_object({}) { |(key, value), acc| acc[key.to_s] = value }
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# Code generated by tools/src/proto-codegen.ts — DO NOT EDIT.
|
|
2
|
+
# Source of truth: proto/liteguard.proto Run: make proto
|
|
3
|
+
|
|
4
|
+
# Data-plane types — generated from proto/liteguard.proto.
|
|
5
|
+
module Liteguard
|
|
6
|
+
|
|
7
|
+
OPERATORS = %i[equals not_equals in not_in regex gt gte lt lte].freeze
|
|
8
|
+
|
|
9
|
+
# A single rule condition within a Guard.
|
|
10
|
+
Rule = Data.define(
|
|
11
|
+
:property_name,
|
|
12
|
+
:operator,
|
|
13
|
+
:values,
|
|
14
|
+
:result,
|
|
15
|
+
:enabled
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
# A named feature guard with an ordered rule set.
|
|
19
|
+
Guard = Data.define(
|
|
20
|
+
:name,
|
|
21
|
+
:rules,
|
|
22
|
+
:default_value,
|
|
23
|
+
:adopted,
|
|
24
|
+
:rate_limit_per_minute,
|
|
25
|
+
:rate_limit_properties,
|
|
26
|
+
:disable_measurement
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# Keys accepted by the options hash passed to {Liteguard::Client#is_open}.
|
|
30
|
+
CHECK_OPTION_KEYS = %i[properties fallback disable_measurement].freeze
|
|
31
|
+
|
|
32
|
+
# ProtectedContext (mirrors proto message).
|
|
33
|
+
ProtectedContext = Data.define(
|
|
34
|
+
:properties,
|
|
35
|
+
:signature
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# GetGuardsRequest (mirrors proto message).
|
|
39
|
+
GetGuardsRequest = Data.define(
|
|
40
|
+
:project_client_key_id,
|
|
41
|
+
:environment,
|
|
42
|
+
:protected_context
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Parsed response from the guards endpoint.
|
|
46
|
+
GuardsResponse = Data.define(
|
|
47
|
+
:guards,
|
|
48
|
+
:refresh_rate_seconds,
|
|
49
|
+
:etag
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# OpenTelemetry trace correlation attached to a Signal.
|
|
53
|
+
TraceContext = Data.define(
|
|
54
|
+
:trace_id,
|
|
55
|
+
:span_id,
|
|
56
|
+
:parent_span_id
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# GuardCheckPerformance (mirrors proto message).
|
|
60
|
+
GuardCheckPerformance = Data.define(
|
|
61
|
+
:rss_bytes,
|
|
62
|
+
:heap_used_bytes,
|
|
63
|
+
:heap_total_bytes,
|
|
64
|
+
:cpu_time_ns,
|
|
65
|
+
:gc_count,
|
|
66
|
+
:thread_count
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# GuardExecutionPerformance (mirrors proto message).
|
|
70
|
+
GuardExecutionPerformance = Data.define(
|
|
71
|
+
:duration_ns,
|
|
72
|
+
:rss_end_bytes,
|
|
73
|
+
:heap_used_end_bytes,
|
|
74
|
+
:heap_total_end_bytes,
|
|
75
|
+
:cpu_time_end_ns,
|
|
76
|
+
:gc_count_end,
|
|
77
|
+
:thread_count_end,
|
|
78
|
+
:completed,
|
|
79
|
+
:error_class
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# SignalPerformance (mirrors proto message).
|
|
83
|
+
SignalPerformance = Data.define(
|
|
84
|
+
:guard_check,
|
|
85
|
+
:guard_execution
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# A buffered guard-check record for upload to the data plane.
|
|
89
|
+
Signal = Data.define(
|
|
90
|
+
:guard_name,
|
|
91
|
+
:result,
|
|
92
|
+
:properties,
|
|
93
|
+
:timestamp_ms,
|
|
94
|
+
:trace,
|
|
95
|
+
:signal_id,
|
|
96
|
+
:execution_id,
|
|
97
|
+
:parent_signal_id,
|
|
98
|
+
:sequence_number,
|
|
99
|
+
:callsite_id,
|
|
100
|
+
:kind,
|
|
101
|
+
:dropped_signals_since_last,
|
|
102
|
+
:measurement
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# SendUnadoptedGuardsRequest (mirrors proto message).
|
|
106
|
+
SendUnadoptedGuardsRequest = Data.define(
|
|
107
|
+
:project_client_key_id,
|
|
108
|
+
:environment,
|
|
109
|
+
:guard_names
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# SendUnadoptedGuardsResponse (mirrors proto message).
|
|
113
|
+
SendUnadoptedGuardsResponse = Data.define(
|
|
114
|
+
:accepted
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Default values for {Liteguard::Client} initialization options.
|
|
118
|
+
CLIENT_OPTION_DEFAULTS = {
|
|
119
|
+
project_client_key_id: nil,
|
|
120
|
+
environment: nil,
|
|
121
|
+
fallback: false,
|
|
122
|
+
refresh_rate_seconds: 30,
|
|
123
|
+
flush_rate_seconds: 10,
|
|
124
|
+
flush_size: 500,
|
|
125
|
+
backend_url: "https://api.liteguard.io",
|
|
126
|
+
quiet: true,
|
|
127
|
+
http_timeout_seconds: 4,
|
|
128
|
+
flush_buffer_multiplier:4,
|
|
129
|
+
disable_measurement: false,
|
|
130
|
+
}.freeze
|
|
131
|
+
|
|
132
|
+
CLIENT_OPTION_KEYS = CLIENT_OPTION_DEFAULTS.keys.freeze
|
|
133
|
+
|
|
134
|
+
end
|
data/lib/liteguard.rb
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
require_relative "liteguard/types"
|
|
2
|
+
require_relative "liteguard/evaluation"
|
|
3
|
+
require_relative "liteguard/scope"
|
|
4
|
+
require_relative "liteguard/client"
|
|
5
|
+
|
|
6
|
+
# Liteguard feature guard SDK for Ruby.
|
|
7
|
+
#
|
|
8
|
+
# require 'liteguard'
|
|
9
|
+
#
|
|
10
|
+
# client = Liteguard::Client.new('pckid-...', environment: 'production')
|
|
11
|
+
# client.start
|
|
12
|
+
# scope = client.create_scope(user_id: 'user-123', plan: 'pro')
|
|
13
|
+
#
|
|
14
|
+
# if scope.is_open('payments.checkout')
|
|
15
|
+
# # feature is enabled
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
module Liteguard
|
|
19
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: liteguard
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.2.20260314
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Liteguard
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-03-15 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: rspec
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '3.13'
|
|
20
|
+
type: :development
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '3.13'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: webmock
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '3.23'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '3.23'
|
|
41
|
+
description: Liteguard gives you feature guards, observability, and CVE auto-disable
|
|
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.
|
|
44
|
+
email:
|
|
45
|
+
executables: []
|
|
46
|
+
extensions: []
|
|
47
|
+
extra_rdoc_files: []
|
|
48
|
+
files:
|
|
49
|
+
- LICENSE
|
|
50
|
+
- README.md
|
|
51
|
+
- lib/liteguard.rb
|
|
52
|
+
- lib/liteguard/client.rb
|
|
53
|
+
- lib/liteguard/evaluation.rb
|
|
54
|
+
- lib/liteguard/scope.rb
|
|
55
|
+
- lib/liteguard/types.rb
|
|
56
|
+
homepage: https://liteguard.io
|
|
57
|
+
licenses:
|
|
58
|
+
- Apache-2.0
|
|
59
|
+
metadata:
|
|
60
|
+
homepage_uri: https://liteguard.io
|
|
61
|
+
source_code_uri: https://github.com/liteguard/liteguard/tree/main/sdk/ruby
|
|
62
|
+
documentation_uri: https://github.com/liteguard/liteguard/tree/main/sdk/ruby#readme
|
|
63
|
+
changelog_uri: https://github.com/liteguard/liteguard/releases
|
|
64
|
+
bug_tracker_uri: https://github.com/liteguard/liteguard/issues
|
|
65
|
+
rubygems_mfa_required: 'true'
|
|
66
|
+
post_install_message:
|
|
67
|
+
rdoc_options: []
|
|
68
|
+
require_paths:
|
|
69
|
+
- lib
|
|
70
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '3.1'
|
|
75
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
76
|
+
requirements:
|
|
77
|
+
- - ">="
|
|
78
|
+
- !ruby/object:Gem::Version
|
|
79
|
+
version: '0'
|
|
80
|
+
requirements: []
|
|
81
|
+
rubygems_version: 3.5.22
|
|
82
|
+
signing_key:
|
|
83
|
+
specification_version: 4
|
|
84
|
+
summary: Feature guards, observability, and security response in a single import
|
|
85
|
+
test_files: []
|