familia 2.0.0.pre5 → 2.0.0.pre7

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.
Files changed (151) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/claude-code-review.yml +57 -0
  3. data/.github/workflows/claude.yml +71 -0
  4. data/.gitignore +5 -1
  5. data/.rubocop.yml +3 -0
  6. data/CLAUDE.md +32 -10
  7. data/Gemfile +2 -2
  8. data/Gemfile.lock +4 -3
  9. data/docs/wiki/API-Reference.md +95 -18
  10. data/docs/wiki/Connection-Pooling-Guide.md +437 -0
  11. data/docs/wiki/Encrypted-Fields-Overview.md +40 -3
  12. data/docs/wiki/Expiration-Feature-Guide.md +596 -0
  13. data/docs/wiki/Feature-System-Guide.md +631 -0
  14. data/docs/wiki/Features-System-Developer-Guide.md +892 -0
  15. data/docs/wiki/Field-System-Guide.md +784 -0
  16. data/docs/wiki/Home.md +82 -15
  17. data/docs/wiki/Implementation-Guide.md +126 -33
  18. data/docs/wiki/Quantization-Feature-Guide.md +721 -0
  19. data/docs/wiki/Relationships-Guide.md +684 -0
  20. data/docs/wiki/Security-Model.md +65 -25
  21. data/docs/wiki/Transient-Fields-Guide.md +280 -0
  22. data/examples/bit_encoding_integration.rb +237 -0
  23. data/examples/redis_command_validation_example.rb +231 -0
  24. data/examples/relationships_basic.rb +273 -0
  25. data/lib/familia/base.rb +1 -1
  26. data/lib/familia/connection.rb +3 -3
  27. data/lib/familia/data_type/types/counter.rb +38 -0
  28. data/lib/familia/data_type/types/hashkey.rb +18 -0
  29. data/lib/familia/data_type/types/lock.rb +43 -0
  30. data/lib/familia/data_type/types/string.rb +9 -2
  31. data/lib/familia/data_type.rb +9 -6
  32. data/lib/familia/encryption/encrypted_data.rb +137 -0
  33. data/lib/familia/encryption/manager.rb +21 -4
  34. data/lib/familia/encryption/providers/aes_gcm_provider.rb +20 -0
  35. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +20 -0
  36. data/lib/familia/encryption.rb +1 -1
  37. data/lib/familia/errors.rb +17 -3
  38. data/lib/familia/features/encrypted_fields/concealed_string.rb +293 -0
  39. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +94 -26
  40. data/lib/familia/features/encrypted_fields.rb +413 -4
  41. data/lib/familia/features/expiration.rb +319 -33
  42. data/lib/familia/features/quantization.rb +385 -44
  43. data/lib/familia/features/relationships/cascading.rb +438 -0
  44. data/lib/familia/features/relationships/indexing.rb +370 -0
  45. data/lib/familia/features/relationships/membership.rb +503 -0
  46. data/lib/familia/features/relationships/permission_management.rb +264 -0
  47. data/lib/familia/features/relationships/querying.rb +620 -0
  48. data/lib/familia/features/relationships/redis_operations.rb +274 -0
  49. data/lib/familia/features/relationships/score_encoding.rb +442 -0
  50. data/lib/familia/features/relationships/tracking.rb +379 -0
  51. data/lib/familia/features/relationships.rb +466 -0
  52. data/lib/familia/features/safe_dump.rb +1 -1
  53. data/lib/familia/features/transient_fields/redacted_string.rb +1 -1
  54. data/lib/familia/features/transient_fields.rb +192 -10
  55. data/lib/familia/features.rb +2 -1
  56. data/lib/familia/field_type.rb +5 -2
  57. data/lib/familia/horreum/{connection.rb → core/connection.rb} +2 -8
  58. data/lib/familia/horreum/{database_commands.rb → core/database_commands.rb} +14 -3
  59. data/lib/familia/horreum/core/serialization.rb +535 -0
  60. data/lib/familia/horreum/{utils.rb → core/utils.rb} +0 -2
  61. data/lib/familia/horreum/core.rb +21 -0
  62. data/lib/familia/horreum/{settings.rb → shared/settings.rb} +0 -2
  63. data/lib/familia/horreum/{definition_methods.rb → subclass/definition.rb} +45 -29
  64. data/lib/familia/horreum/{management_methods.rb → subclass/management.rb} +9 -8
  65. data/lib/familia/horreum/{related_fields_management.rb → subclass/related_fields_management.rb} +15 -10
  66. data/lib/familia/horreum.rb +17 -17
  67. data/lib/familia/validation/command_recorder.rb +336 -0
  68. data/lib/familia/validation/expectations.rb +519 -0
  69. data/lib/familia/validation/test_helpers.rb +443 -0
  70. data/lib/familia/validation/validator.rb +412 -0
  71. data/lib/familia/validation.rb +140 -0
  72. data/lib/familia/version.rb +1 -1
  73. data/lib/familia.rb +1 -1
  74. data/try/core/create_method_try.rb +240 -0
  75. data/try/core/database_consistency_try.rb +299 -0
  76. data/try/core/errors_try.rb +25 -4
  77. data/try/core/familia_try.rb +1 -1
  78. data/try/core/persistence_operations_try.rb +297 -0
  79. data/try/data_types/counter_try.rb +93 -0
  80. data/try/data_types/lock_try.rb +133 -0
  81. data/try/debugging/debug_aad_process.rb +82 -0
  82. data/try/debugging/debug_concealed_internal.rb +59 -0
  83. data/try/debugging/debug_concealed_reveal.rb +61 -0
  84. data/try/debugging/debug_context_aad.rb +68 -0
  85. data/try/debugging/debug_context_simple.rb +80 -0
  86. data/try/debugging/debug_cross_context.rb +62 -0
  87. data/try/debugging/debug_database_load.rb +64 -0
  88. data/try/debugging/debug_encrypted_json_check.rb +53 -0
  89. data/try/debugging/debug_encrypted_json_step_by_step.rb +62 -0
  90. data/try/debugging/debug_exists_lifecycle.rb +54 -0
  91. data/try/debugging/debug_field_decrypt.rb +74 -0
  92. data/try/debugging/debug_fresh_cross_context.rb +73 -0
  93. data/try/debugging/debug_load_path.rb +66 -0
  94. data/try/debugging/debug_method_definition.rb +46 -0
  95. data/try/debugging/debug_method_resolution.rb +41 -0
  96. data/try/debugging/debug_minimal.rb +24 -0
  97. data/try/debugging/debug_provider.rb +68 -0
  98. data/try/debugging/debug_secure_behavior.rb +73 -0
  99. data/try/debugging/debug_string_class.rb +46 -0
  100. data/try/debugging/debug_test.rb +46 -0
  101. data/try/debugging/debug_test_design.rb +80 -0
  102. data/try/edge_cases/hash_symbolization_try.rb +1 -0
  103. data/try/edge_cases/reserved_keywords_try.rb +1 -0
  104. data/try/edge_cases/string_coercion_try.rb +2 -0
  105. data/try/encryption/encryption_core_try.rb +6 -4
  106. data/try/features/categorical_permissions_try.rb +515 -0
  107. data/try/features/encrypted_fields_core_try.rb +19 -11
  108. data/try/features/encrypted_fields_integration_try.rb +66 -70
  109. data/try/features/encrypted_fields_no_cache_security_try.rb +22 -8
  110. data/try/features/encrypted_fields_security_try.rb +151 -144
  111. data/try/features/encryption_fields/aad_protection_try.rb +108 -23
  112. data/try/features/encryption_fields/concealed_string_core_try.rb +253 -0
  113. data/try/features/encryption_fields/context_isolation_try.rb +30 -8
  114. data/try/features/encryption_fields/error_conditions_try.rb +6 -6
  115. data/try/features/encryption_fields/fresh_key_derivation_try.rb +20 -14
  116. data/try/features/encryption_fields/fresh_key_try.rb +27 -22
  117. data/try/features/encryption_fields/key_rotation_try.rb +16 -10
  118. data/try/features/encryption_fields/nonce_uniqueness_try.rb +15 -13
  119. data/try/features/encryption_fields/secure_by_default_behavior_try.rb +310 -0
  120. data/try/features/encryption_fields/thread_safety_try.rb +6 -6
  121. data/try/features/encryption_fields/universal_serialization_safety_try.rb +174 -0
  122. data/try/features/feature_dependencies_try.rb +3 -3
  123. data/try/features/relationships_edge_cases_try.rb +145 -0
  124. data/try/features/relationships_performance_minimal_try.rb +132 -0
  125. data/try/features/relationships_performance_simple_try.rb +155 -0
  126. data/try/features/relationships_performance_try.rb +420 -0
  127. data/try/features/relationships_performance_working_try.rb +144 -0
  128. data/try/features/relationships_try.rb +237 -0
  129. data/try/features/safe_dump_try.rb +3 -0
  130. data/try/features/transient_fields/redacted_string_try.rb +2 -0
  131. data/try/features/transient_fields/single_use_redacted_string_try.rb +2 -0
  132. data/try/features/transient_fields_core_try.rb +1 -1
  133. data/try/features/transient_fields_integration_try.rb +1 -1
  134. data/try/helpers/test_helpers.rb +26 -1
  135. data/try/horreum/base_try.rb +14 -8
  136. data/try/horreum/enhanced_conflict_handling_try.rb +3 -1
  137. data/try/horreum/initialization_try.rb +1 -1
  138. data/try/horreum/relations_try.rb +2 -2
  139. data/try/horreum/serialization_persistent_fields_try.rb +8 -8
  140. data/try/horreum/serialization_try.rb +39 -4
  141. data/try/models/customer_safe_dump_try.rb +1 -1
  142. data/try/models/customer_try.rb +1 -1
  143. data/try/validation/atomic_operations_try.rb.disabled +320 -0
  144. data/try/validation/command_validation_try.rb.disabled +207 -0
  145. data/try/validation/performance_validation_try.rb.disabled +324 -0
  146. data/try/validation/real_world_scenarios_try.rb.disabled +390 -0
  147. metadata +81 -12
  148. data/TEST_COVERAGE.md +0 -40
  149. data/lib/familia/features/relatable_objects.rb +0 -125
  150. data/lib/familia/horreum/serialization.rb +0 -473
  151. data/try/features/relatable_objects_try.rb +0 -220
@@ -4,23 +4,120 @@ require_relative 'transient_fields/redacted_string'
4
4
 
5
5
  module Familia
6
6
  module Features
7
- # Familia::Features::TransientFields
7
+ # TransientFields is a feature that provides secure handling of sensitive runtime data
8
+ # that should never be persisted to Redis/Valkey. Unlike encrypted fields, transient
9
+ # fields exist only in memory and are automatically wrapped in RedactedString objects
10
+ # for security.
8
11
  #
9
- # Provides secure transient fields that wrap sensitive values in RedactedString
10
- # objects. These fields are excluded from serialization operations and provide
11
- # automatic memory wiping for security.
12
+ # Transient fields are ideal for:
13
+ # - API keys and tokens that change frequently
14
+ # - Temporary passwords or passphrases
15
+ # - Session-specific secrets
16
+ # - Any sensitive data that should never touch persistent storage
17
+ # - Debug or development secrets that need secure handling
18
+ #
19
+ # All transient field values are automatically wrapped in RedactedString instances
20
+ # which provide:
21
+ # - Automatic redaction in logs and string representations
22
+ # - Secure memory management with explicit cleanup
23
+ # - Safe access patterns through expose blocks
24
+ # - Protection against accidental value exposure
25
+ #
26
+ # Example:
27
+ #
28
+ # class ApiClient < Familia::Horreum
29
+ # feature :transient_fields
30
+ #
31
+ # field :endpoint # Regular persistent field
32
+ # transient_field :token # Transient field (not persisted)
33
+ # transient_field :secret, as: :api_secret # Custom accessor name
34
+ # end
35
+ #
36
+ # client = ApiClient.new(
37
+ # endpoint: 'https://api.example.com',
38
+ # token: ENV['API_TOKEN'],
39
+ # secret: ENV['API_SECRET']
40
+ # )
41
+ #
42
+ # # Regular field persists
43
+ # client.save
44
+ # client.endpoint # => "https://api.example.com"
45
+ #
46
+ # # Transient fields are RedactedString instances
47
+ # puts client.token # => "[REDACTED]"
48
+ #
49
+ # # Access the actual value safely
50
+ # client.token.expose do |token|
51
+ # response = HTTP.post(client.endpoint,
52
+ # headers: { 'Authorization' => "Bearer #{token}" }
53
+ # )
54
+ # # Token value is only available within this block
55
+ # end
56
+ #
57
+ # # Explicit cleanup when done
58
+ # client.token.clear!
59
+ #
60
+ # Security Features:
61
+ #
62
+ # RedactedString automatically protects sensitive values:
63
+ # - String representation shows "[REDACTED]" instead of actual value
64
+ # - Inspect output shows "[REDACTED]" instead of actual value
65
+ # - Hash values are constant to prevent value inference
66
+ # - Equality checks work only on object identity
67
+ #
68
+ # Safe Access Patterns:
69
+ #
70
+ # # ✅ Recommended: Use .expose block
71
+ # client.token.expose do |token|
72
+ # # Use token directly without creating copies
73
+ # HTTP.auth("Bearer #{token}") # Safe
74
+ # end
75
+ #
76
+ # # ✅ Direct access (use carefully)
77
+ # raw_token = client.token.value
78
+ # # Remember to clear original source if needed
79
+ #
80
+ # # ❌ Avoid: These create uncontrolled copies
81
+ # token_copy = client.token.value.dup # Creates copy in memory
82
+ # interpolated = "Bearer #{client.token}" # Creates copy via to_s
83
+ #
84
+ # Memory Management:
85
+ #
86
+ # # Clear individual fields
87
+ # client.token.clear!
88
+ #
89
+ # # Check if cleared
90
+ # client.token.cleared? # => true
91
+ #
92
+ # # Accessing cleared values raises error
93
+ # client.token.value # => SecurityError: Value already cleared
94
+ #
95
+ # ⚠️ Important Security Limitations:
96
+ #
97
+ # Ruby provides NO memory safety guarantees for cryptographic secrets:
98
+ # - No secure wiping: .clear! is best-effort only
99
+ # - GC copying: Garbage collector may duplicate secrets
100
+ # - String operations: Every manipulation creates copies
101
+ # - Memory persistence: Secrets may remain in memory indefinitely
102
+ #
103
+ # For highly sensitive applications, consider external secrets management
104
+ # (HashiCorp Vault, AWS Secrets Manager) or languages with secure memory handling.
12
105
  #
13
106
  module TransientFields
14
107
  def self.included(base)
15
- Familia.ld "[#{base}] Loaded #{self}"
108
+ Familia.trace :LOADED, self, base, caller(1..1) if Familia.debug?
16
109
  base.extend ClassMethods
110
+
111
+ # Initialize transient fields tracking
112
+ base.instance_variable_set(:@transient_fields, []) unless base.instance_variable_defined?(:@transient_fields)
17
113
  end
18
114
 
19
- # ClassMethods
20
- #
21
115
  module ClassMethods
22
116
  # Define a transient field that automatically wraps values in RedactedString
23
117
  #
118
+ # Transient fields are not persisted to Redis/Valkey and exist only in memory.
119
+ # All values are automatically wrapped in RedactedString for security.
120
+ #
24
121
  # @param name [Symbol] The field name
25
122
  # @param as [Symbol] The method name (defaults to field name)
26
123
  # @param kwargs [Hash] Additional field options
@@ -31,14 +128,99 @@ module Familia
31
128
  # transient_field :api_key
32
129
  # end
33
130
  #
131
+ # @example Define a transient field with custom accessor name
132
+ # class Service < Familia::Horreum
133
+ # feature :transient_fields
134
+ # transient_field :secret_key, as: :api_secret
135
+ # end
136
+ #
137
+ # service = Service.new(secret_key: 'secret123')
138
+ # service.api_secret.expose { |key| use_api_key(key) }
139
+ #
34
140
  def transient_field(name, as: name, **kwargs)
35
- # Use the field type system - much cleaner than alias_method approach!
36
- # We can now remove the transient_field method from this feature entirely
37
- # since it's built into DefinitionMethods using TransientFieldType
141
+ @transient_fields ||= []
142
+ @transient_fields << name unless @transient_fields.include?(name)
143
+
144
+ # Use the field type system for proper integration
38
145
  require_relative 'transient_fields/transient_field_type'
39
146
  field_type = TransientFieldType.new(name, as: as, **kwargs.merge(fast_method: false))
40
147
  register_field_type(field_type)
41
148
  end
149
+
150
+ # Returns list of transient field names defined on this class
151
+ #
152
+ # @return [Array<Symbol>] Array of transient field names
153
+ #
154
+ def transient_fields
155
+ @transient_fields || []
156
+ end
157
+
158
+ # Check if a field is transient
159
+ #
160
+ # @param field_name [Symbol] The field name to check
161
+ # @return [Boolean] true if field is transient, false otherwise
162
+ #
163
+ def transient_field?(field_name)
164
+ transient_fields.include?(field_name.to_sym)
165
+ end
166
+ end
167
+
168
+ # Clear all transient fields for this instance
169
+ #
170
+ # This method iterates through all defined transient fields and calls
171
+ # clear! on each RedactedString instance. Use this for cleanup when
172
+ # the object is no longer needed.
173
+ #
174
+ # @return [void]
175
+ #
176
+ # @example Clear all secrets when done
177
+ # client = ApiClient.new(token: 'secret', api_key: 'key123')
178
+ # # ... use client ...
179
+ # client.clear_transient_fields!
180
+ # client.token.cleared? # => true
181
+ #
182
+ def clear_transient_fields!
183
+ self.class.transient_fields.each do |field_name|
184
+ field_value = instance_variable_get("@#{field_name}")
185
+ if field_value.respond_to?(:clear!)
186
+ field_value.clear!
187
+ end
188
+ end
189
+ end
190
+
191
+ # Check if all transient fields have been cleared
192
+ #
193
+ # @return [Boolean] true if all transient fields are cleared, false otherwise
194
+ #
195
+ def transient_fields_cleared?
196
+ self.class.transient_fields.all? do |field_name|
197
+ field_value = instance_variable_get("@#{field_name}")
198
+ field_value.nil? || (field_value.respond_to?(:cleared?) && field_value.cleared?)
199
+ end
200
+ end
201
+
202
+ # Returns a hash of transient field names and their redacted representations
203
+ #
204
+ # This method is useful for debugging and logging as it shows which transient
205
+ # fields are defined without exposing their actual values.
206
+ #
207
+ # @return [Hash] Hash with field names as keys and "[REDACTED]" as values
208
+ #
209
+ # @example Check transient field status
210
+ # client.transient_fields_summary
211
+ # # => { token: "[REDACTED]", api_key: "[REDACTED]" }
212
+ #
213
+ def transient_fields_summary
214
+ self.class.transient_fields.each_with_object({}) do |field_name, summary|
215
+ field_value = instance_variable_get("@#{field_name}")
216
+ if field_value.nil?
217
+ summary[field_name] = nil
218
+ elsif field_value.respond_to?(:cleared?) && field_value.cleared?
219
+ summary[field_name] = "[CLEARED]"
220
+ else
221
+ summary[field_name] = "[REDACTED]"
222
+ end
223
+ end
42
224
  end
43
225
 
44
226
  Familia::Base.add_feature self, :transient_fields, depends_on: nil
@@ -60,13 +60,14 @@ module Familia
60
60
  end
61
61
 
62
62
  end
63
-
64
63
  end
65
64
 
66
65
  # Load all feature files from the features directory
67
66
  features_dir = File.join(__dir__, 'features')
67
+ Familia.ld "[DEBUG] Loading features from #{features_dir}"
68
68
  if Dir.exist?(features_dir)
69
69
  Dir.glob(File.join(features_dir, '*.rb')).each do |feature_file|
70
+ Familia.ld "[DEBUG] Loading feature #{feature_file}"
70
71
  require_relative feature_file
71
72
  end
72
73
  end
@@ -27,7 +27,7 @@ module Familia
27
27
  # end
28
28
  #
29
29
  class FieldType
30
- attr_reader :name, :options, :method_name, :fast_method_name, :on_conflict
30
+ attr_reader :name, :options, :method_name, :fast_method_name, :on_conflict, :loggable
31
31
 
32
32
  # Initialize a new field type
33
33
  #
@@ -38,9 +38,11 @@ module Familia
38
38
  # (defaults to "#{name}!"). If false, no fast method is created
39
39
  # @param on_conflict [Symbol] Conflict resolution strategy when method
40
40
  # already exists (:raise, :skip, :warn, :overwrite)
41
+ # @param loggable [Boolean] Whether this field should be included in
42
+ # serialization and logging operations (default: true)
41
43
  # @param options [Hash] Additional options for the field type
42
44
  #
43
- def initialize(name, as: name, fast_method: :"#{name}!", on_conflict: :raise, **options)
45
+ def initialize(name, as: name, fast_method: :"#{name}!", on_conflict: :raise, loggable: true, **options)
44
46
  @name = name.to_sym
45
47
  @method_name = as == false ? nil : as.to_sym
46
48
  @fast_method_name = fast_method == false ? nil : fast_method&.to_sym
@@ -51,6 +53,7 @@ module Familia
51
53
  end
52
54
 
53
55
  @on_conflict = on_conflict
56
+ @loggable = loggable
54
57
  @options = options
55
58
  end
56
59
 
@@ -2,8 +2,8 @@
2
2
 
3
3
  module Familia
4
4
  class Horreum
5
- # Familia::Horreum::Connection
6
- #
5
+ # Connection: Valkey connection management for Horreum instances
6
+ # Provides both instance and class-level connection methods
7
7
  module Connection
8
8
  attr_reader :uri
9
9
 
@@ -69,11 +69,5 @@ module Familia
69
69
  block_result
70
70
  end
71
71
  end
72
-
73
- # include for instance methods after it's loaded. Note that Horreum::Utils
74
- # are also included and at one time also has a uri method. This connection
75
- # module is also extended for the class level methods. It will require some
76
- # disambiguation at some point.
77
- include Familia::Horreum::Connection
78
72
  end
79
73
  end
@@ -37,7 +37,7 @@ module Familia
37
37
  # @note The default behavior maintains backward compatibility by treating empty hashes
38
38
  # as non-existent. Use `check_size: false` for pure key existence checking.
39
39
  def exists?(check_size: true)
40
- key_exists = self.class.dbclient.exists?(dbkey)
40
+ key_exists = self.class.exists?(identifier)
41
41
  return key_exists unless check_size
42
42
 
43
43
  key_exists && !size.zero?
@@ -106,6 +106,19 @@ module Familia
106
106
  dbclient.hset dbkey, field, value
107
107
  end
108
108
 
109
+ # Sets field in the hash stored at key to value, only if field does not yet exist.
110
+ # If key does not exist, a new key holding a hash is created. If field already exists,
111
+ # this operation has no effect.
112
+ #
113
+ # @param field [String] The field to set in the hash
114
+ # @param value [String] The value to set for the field
115
+ # @return [Integer] 1 if the field is a new field in the hash and the value was set,
116
+ # 0 if the field already exists in the hash and no operation was performed
117
+ def hsetnx(field, value)
118
+ Familia.trace :HSETNX, dbclient, field, caller(1..1) if Familia.debug?
119
+ dbclient.hsetnx dbkey, field, value
120
+ end
121
+
109
122
  def hmset(hsh = {})
110
123
  hsh ||= to_h
111
124
  Familia.trace :HMSET, dbclient, hsh, caller(1..1) if Familia.debug?
@@ -165,7 +178,5 @@ module Familia
165
178
  end
166
179
  alias clear delete!
167
180
  end
168
-
169
- include DatabaseCommands # these become Familia::Horreum instance methods
170
181
  end
171
182
  end