familia 2.0.0.pre15 → 2.0.0.pre16

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 (274) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/code-quality.yml +138 -0
  3. data/.github/workflows/code-smellage.yml +145 -0
  4. data/.github/workflows/docs.yml +31 -8
  5. data/.gitignore +1 -1
  6. data/.pre-commit-config.yaml +7 -1
  7. data/.reek.yml +98 -0
  8. data/.rubocop.yml +48 -10
  9. data/.talismanrc +9 -0
  10. data/.yardopts +18 -13
  11. data/CHANGELOG.rst +64 -4
  12. data/CLAUDE.md +1 -1
  13. data/Gemfile +6 -5
  14. data/Gemfile.lock +99 -23
  15. data/LICENSE.txt +1 -1
  16. data/README.md +285 -85
  17. data/changelog.d/README.md +2 -2
  18. data/docs/archive/FAMILIA_RELATIONSHIPS.md +22 -22
  19. data/docs/archive/FAMILIA_TECHNICAL.md +41 -41
  20. data/docs/archive/FAMILIA_UPDATE.md +3 -3
  21. data/docs/archive/README.md +3 -2
  22. data/docs/{guides/API-Reference.md → archive/api-reference.md} +87 -101
  23. data/docs/conf.py +29 -0
  24. data/docs/guides/{Field-System-Guide.md → core-field-system.md} +9 -9
  25. data/docs/guides/feature-encrypted-fields.md +785 -0
  26. data/docs/guides/{Expiration-Feature-Guide.md → feature-expiration.md} +11 -2
  27. data/docs/guides/feature-external-identifiers.md +637 -0
  28. data/docs/guides/feature-object-identifiers.md +435 -0
  29. data/docs/guides/{Quantization-Feature-Guide.md → feature-quantization.md} +94 -29
  30. data/docs/guides/feature-relationships-methods.md +684 -0
  31. data/docs/guides/feature-relationships.md +200 -0
  32. data/docs/guides/{Features-System-Developer-Guide.md → feature-system-devs.md} +4 -4
  33. data/docs/guides/{Feature-System-Guide.md → feature-system.md} +5 -5
  34. data/docs/guides/{Transient-Fields-Guide.md → feature-transient-fields.md} +2 -2
  35. data/docs/guides/{Implementation-Guide.md → implementation.md} +3 -3
  36. data/docs/guides/index.md +176 -0
  37. data/docs/guides/{Security-Model.md → security-model.md} +1 -1
  38. data/docs/migrating/v2.0.0-pre.md +1 -1
  39. data/docs/migrating/v2.0.0-pre11.md +2 -2
  40. data/docs/migrating/v2.0.0-pre12.md +2 -2
  41. data/docs/migrating/v2.0.0-pre5.md +33 -12
  42. data/docs/migrating/v2.0.0-pre6.md +2 -2
  43. data/docs/migrating/v2.0.0-pre7.md +8 -8
  44. data/docs/overview.md +623 -19
  45. data/docs/reference/api-technical.md +1365 -0
  46. data/examples/autoloader/mega_customer/features/deprecated_fields.rb +7 -0
  47. data/examples/autoloader/mega_customer/safe_dump_fields.rb +1 -1
  48. data/examples/autoloader/mega_customer.rb +3 -1
  49. data/examples/encrypted_fields.rb +378 -0
  50. data/examples/json_usage_patterns.rb +144 -0
  51. data/examples/relationships.rb +13 -13
  52. data/examples/safe_dump.rb +6 -6
  53. data/examples/single_connection_transaction_confusions.rb +379 -0
  54. data/lib/familia/base.rb +49 -10
  55. data/lib/familia/connection/handlers.rb +223 -0
  56. data/lib/familia/connection/individual_command_proxy.rb +64 -0
  57. data/lib/familia/connection/middleware.rb +75 -0
  58. data/lib/familia/connection/operation_core.rb +93 -0
  59. data/lib/familia/connection/operations.rb +277 -0
  60. data/lib/familia/connection/pipeline_core.rb +87 -0
  61. data/lib/familia/connection/transaction_core.rb +100 -0
  62. data/lib/familia/connection.rb +60 -186
  63. data/lib/familia/data_type/commands.rb +53 -51
  64. data/lib/familia/data_type/serialization.rb +108 -107
  65. data/lib/familia/data_type/types/counter.rb +1 -1
  66. data/lib/familia/data_type/types/hashkey.rb +13 -10
  67. data/lib/familia/data_type/types/{list.rb → listkey.rb} +13 -5
  68. data/lib/familia/data_type/types/lock.rb +3 -2
  69. data/lib/familia/data_type/types/sorted_set.rb +26 -15
  70. data/lib/familia/data_type/types/{string.rb → stringkey.rb} +7 -5
  71. data/lib/familia/data_type/types/unsorted_set.rb +20 -27
  72. data/lib/familia/data_type.rb +75 -47
  73. data/lib/familia/distinguisher.rb +85 -0
  74. data/lib/familia/encryption/encrypted_data.rb +15 -24
  75. data/lib/familia/encryption/manager.rb +6 -4
  76. data/lib/familia/encryption/providers/aes_gcm_provider.rb +1 -1
  77. data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +7 -9
  78. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +4 -5
  79. data/lib/familia/encryption/request_cache.rb +7 -7
  80. data/lib/familia/encryption.rb +2 -3
  81. data/lib/familia/errors.rb +9 -3
  82. data/lib/familia/features/autoloader.rb +30 -12
  83. data/lib/familia/features/encrypted_fields/concealed_string.rb +3 -4
  84. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +13 -14
  85. data/lib/familia/features/encrypted_fields.rb +66 -64
  86. data/lib/familia/features/expiration/extensions.rb +1 -1
  87. data/lib/familia/features/expiration.rb +31 -26
  88. data/lib/familia/features/external_identifier.rb +9 -12
  89. data/lib/familia/features/object_identifier.rb +56 -19
  90. data/lib/familia/features/quantization.rb +16 -21
  91. data/lib/familia/features/relationships/README.md +97 -0
  92. data/lib/familia/features/relationships/collection_operations.rb +104 -0
  93. data/lib/familia/features/relationships/indexing/multi_index_generators.rb +202 -0
  94. data/lib/familia/features/relationships/indexing/unique_index_generators.rb +301 -0
  95. data/lib/familia/features/relationships/indexing.rb +176 -256
  96. data/lib/familia/features/relationships/indexing_relationship.rb +35 -0
  97. data/lib/familia/features/relationships/participation/participant_methods.rb +160 -0
  98. data/lib/familia/features/relationships/participation/target_methods.rb +225 -0
  99. data/lib/familia/features/relationships/participation.rb +656 -0
  100. data/lib/familia/features/relationships/participation_relationship.rb +31 -0
  101. data/lib/familia/features/relationships/score_encoding.rb +20 -20
  102. data/lib/familia/features/relationships.rb +65 -266
  103. data/lib/familia/features/safe_dump.rb +127 -130
  104. data/lib/familia/features/transient_fields/redacted_string.rb +6 -6
  105. data/lib/familia/features/transient_fields/transient_field_type.rb +5 -5
  106. data/lib/familia/features/transient_fields.rb +3 -5
  107. data/lib/familia/features.rb +4 -13
  108. data/lib/familia/field_type.rb +24 -4
  109. data/lib/familia/horreum/core/connection.rb +229 -26
  110. data/lib/familia/horreum/core/database_commands.rb +27 -17
  111. data/lib/familia/horreum/core/serialization.rb +40 -20
  112. data/lib/familia/horreum/core/utils.rb +2 -1
  113. data/lib/familia/horreum/shared/settings.rb +2 -1
  114. data/lib/familia/horreum/subclass/definition.rb +33 -45
  115. data/lib/familia/horreum/subclass/management.rb +72 -24
  116. data/lib/familia/horreum/subclass/related_fields_management.rb +82 -21
  117. data/lib/familia/horreum.rb +196 -114
  118. data/lib/familia/json_serializer.rb +0 -1
  119. data/lib/familia/logging.rb +11 -114
  120. data/lib/familia/refinements/dear_json.rb +122 -0
  121. data/lib/familia/refinements/logger_trace.rb +20 -17
  122. data/lib/familia/refinements/stylize_words.rb +65 -0
  123. data/lib/familia/refinements/time_literals.rb +60 -52
  124. data/lib/familia/refinements.rb +2 -1
  125. data/lib/familia/secure_identifier.rb +60 -28
  126. data/lib/familia/settings.rb +83 -7
  127. data/lib/familia/utils.rb +5 -87
  128. data/lib/familia/verifiable_identifier.rb +4 -4
  129. data/lib/familia/version.rb +1 -1
  130. data/lib/familia.rb +72 -14
  131. data/lib/middleware/database_middleware.rb +56 -14
  132. data/lib/{familia/multi_result.rb → multi_result.rb} +23 -16
  133. data/try/configuration/scenarios_try.rb +1 -1
  134. data/try/connection/fiber_context_preservation_try.rb +250 -0
  135. data/try/connection/handler_constraints_try.rb +59 -0
  136. data/try/connection/operation_mode_guards_try.rb +208 -0
  137. data/try/connection/pipeline_fallback_integration_try.rb +128 -0
  138. data/try/connection/responsibility_chain_tracking_try.rb +72 -0
  139. data/try/connection/transaction_fallback_integration_try.rb +288 -0
  140. data/try/connection/transaction_mode_permissive_try.rb +153 -0
  141. data/try/connection/transaction_mode_strict_try.rb +98 -0
  142. data/try/connection/transaction_mode_warn_try.rb +131 -0
  143. data/try/connection/transaction_modes_try.rb +249 -0
  144. data/try/core/autoloader_try.rb +120 -2
  145. data/try/core/connection_try.rb +7 -7
  146. data/try/core/conventional_inheritance_try.rb +130 -0
  147. data/try/core/create_method_try.rb +15 -23
  148. data/try/core/database_consistency_try.rb +10 -10
  149. data/try/core/errors_try.rb +8 -11
  150. data/try/core/familia_extended_try.rb +2 -2
  151. data/try/core/familia_members_methods_try.rb +76 -0
  152. data/try/core/isolated_dbclient_try.rb +165 -0
  153. data/try/core/middleware_try.rb +16 -16
  154. data/try/core/persistence_operations_try.rb +4 -4
  155. data/try/core/pools_try.rb +42 -26
  156. data/try/core/secure_identifier_try.rb +28 -24
  157. data/try/core/time_utils_try.rb +10 -10
  158. data/try/core/tools_try.rb +1 -1
  159. data/try/core/utils_try.rb +2 -2
  160. data/try/data_types/boolean_try.rb +4 -4
  161. data/try/data_types/datatype_base_try.rb +0 -2
  162. data/try/data_types/list_try.rb +10 -10
  163. data/try/data_types/sorted_set_try.rb +5 -5
  164. data/try/data_types/string_try.rb +12 -12
  165. data/try/data_types/unsortedset_try.rb +33 -0
  166. data/try/debugging/cache_behavior_tracer.rb +7 -7
  167. data/try/debugging/debug_aad_process.rb +1 -1
  168. data/try/debugging/debug_concealed_internal.rb +1 -1
  169. data/try/debugging/debug_cross_context.rb +1 -1
  170. data/try/debugging/debug_fresh_cross_context.rb +1 -1
  171. data/try/debugging/encryption_method_tracer.rb +10 -10
  172. data/try/edge_cases/hash_symbolization_try.rb +1 -1
  173. data/try/edge_cases/ttl_side_effects_try.rb +1 -1
  174. data/try/encryption/config_persistence_try.rb +2 -2
  175. data/try/encryption/encryption_core_try.rb +19 -19
  176. data/try/encryption/instance_variable_scope_try.rb +1 -1
  177. data/try/encryption/module_loading_try.rb +2 -2
  178. data/try/encryption/providers/aes_gcm_provider_try.rb +1 -1
  179. data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +1 -1
  180. data/try/encryption/secure_memory_handling_try.rb +1 -1
  181. data/try/features/encrypted_fields/concealed_string_core_try.rb +11 -7
  182. data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
  183. data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +3 -3
  184. data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +10 -10
  185. data/try/features/encrypted_fields/encrypted_fields_security_try.rb +14 -14
  186. data/try/features/encrypted_fields/error_conditions_try.rb +7 -7
  187. data/try/features/encrypted_fields/fresh_key_try.rb +1 -1
  188. data/try/features/encrypted_fields/nonce_uniqueness_try.rb +1 -1
  189. data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +7 -7
  190. data/try/features/encrypted_fields/universal_serialization_safety_try.rb +13 -20
  191. data/try/features/external_identifier/external_identifier_try.rb +1 -1
  192. data/try/features/feature_dependencies_try.rb +3 -3
  193. data/try/features/object_identifier/object_identifier_integration_try.rb +28 -34
  194. data/try/features/object_identifier/object_identifier_try.rb +10 -0
  195. data/try/features/quantization/quantization_try.rb +1 -1
  196. data/try/features/relationships/indexing_commands_verification_try.rb +136 -0
  197. data/try/features/relationships/indexing_try.rb +433 -0
  198. data/try/features/relationships/participation_commands_verification_spec.rb +102 -0
  199. data/try/features/relationships/participation_commands_verification_try.rb +105 -0
  200. data/try/features/relationships/participation_performance_improvements_try.rb +124 -0
  201. data/try/features/relationships/participation_reverse_index_try.rb +196 -0
  202. data/try/features/relationships/relationships_api_changes_try.rb +72 -71
  203. data/try/features/relationships/relationships_edge_cases_try.rb +15 -18
  204. data/try/features/relationships/relationships_performance_minimal_try.rb +2 -2
  205. data/try/features/relationships/relationships_performance_simple_try.rb +8 -8
  206. data/try/features/relationships/relationships_performance_try.rb +20 -20
  207. data/try/features/relationships/relationships_try.rb +27 -38
  208. data/try/features/safe_dump/safe_dump_advanced_try.rb +2 -2
  209. data/try/features/transient_fields/refresh_reset_try.rb +1 -1
  210. data/try/features/transient_fields/simple_refresh_test.rb +1 -1
  211. data/try/helpers/test_cleanup.rb +86 -0
  212. data/try/helpers/test_helpers.rb +3 -3
  213. data/try/horreum/base_try.rb +3 -2
  214. data/try/horreum/commands_try.rb +1 -1
  215. data/try/horreum/destroy_related_fields_cleanup_try.rb +330 -0
  216. data/try/horreum/initialization_try.rb +11 -7
  217. data/try/horreum/relations_try.rb +21 -13
  218. data/try/horreum/serialization_try.rb +12 -11
  219. data/try/integration/cross_component_try.rb +3 -3
  220. data/try/memory/memory_basic_test.rb +1 -1
  221. data/try/memory/memory_docker_ruby_dump.sh +1 -1
  222. data/try/models/customer_safe_dump_try.rb +1 -1
  223. data/try/models/customer_try.rb +8 -10
  224. data/try/models/datatype_base_try.rb +3 -3
  225. data/try/models/familia_object_try.rb +9 -8
  226. data/try/performance/benchmarks_try.rb +2 -2
  227. data/try/prototypes/atomic_saves_v1_context_proxy.rb +2 -2
  228. data/try/prototypes/atomic_saves_v3_connection_pool.rb +3 -3
  229. data/try/prototypes/atomic_saves_v4.rb +1 -1
  230. data/try/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +4 -4
  231. data/try/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
  232. data/try/prototypes/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
  233. data/try/prototypes/pooling/lib/connection_pool_metrics.rb +5 -5
  234. data/try/prototypes/pooling/lib/connection_pool_stress_test.rb +26 -26
  235. data/try/prototypes/pooling/lib/connection_pool_threading_models.rb +7 -7
  236. data/try/prototypes/pooling/lib/visualize_stress_results.rb +1 -1
  237. data/try/prototypes/pooling/pool_siege.rb +11 -11
  238. data/try/prototypes/pooling/run_stress_tests.rb +7 -7
  239. data/try/refinements/dear_json_array_methods_try.rb +53 -0
  240. data/try/refinements/dear_json_hash_methods_try.rb +54 -0
  241. data/try/refinements/logger_trace_methods_try.rb +44 -0
  242. data/try/refinements/time_literals_numeric_methods_try.rb +141 -0
  243. data/try/refinements/time_literals_string_methods_try.rb +80 -0
  244. metadata +75 -43
  245. data/.rubocop_todo.yml +0 -208
  246. data/docs/connection_pooling.md +0 -192
  247. data/docs/guides/Connection-Pooling-Guide.md +0 -437
  248. data/docs/guides/Encrypted-Fields-Overview.md +0 -101
  249. data/docs/guides/Feature-System-Autoloading.md +0 -198
  250. data/docs/guides/Home.md +0 -116
  251. data/docs/guides/Relationships-Guide.md +0 -737
  252. data/docs/guides/relationships-methods.md +0 -266
  253. data/docs/reference/auditing_database_commands.rb +0 -228
  254. data/examples/permissions.rb +0 -240
  255. data/lib/familia/features/relationships/cascading.rb +0 -437
  256. data/lib/familia/features/relationships/membership.rb +0 -497
  257. data/lib/familia/features/relationships/permission_management.rb +0 -264
  258. data/lib/familia/features/relationships/querying.rb +0 -615
  259. data/lib/familia/features/relationships/redis_operations.rb +0 -274
  260. data/lib/familia/features/relationships/tracking.rb +0 -418
  261. data/lib/familia/refinements/snake_case.rb +0 -40
  262. data/lib/familia/validation/command_recorder.rb +0 -336
  263. data/lib/familia/validation/expectations.rb +0 -519
  264. data/lib/familia/validation/validation_helpers.rb +0 -443
  265. data/lib/familia/validation/validator.rb +0 -412
  266. data/lib/familia/validation.rb +0 -140
  267. data/try/data_types/set_try.rb +0 -33
  268. data/try/features/relationships/categorical_permissions_try.rb +0 -515
  269. data/try/features/safe_dump/module_based_extensions_try.rb +0 -100
  270. data/try/features/safe_dump/safe_dump_autoloading_try.rb +0 -107
  271. data/try/validation/atomic_operations_try.rb.disabled +0 -320
  272. data/try/validation/command_validation_try.rb.disabled +0 -207
  273. data/try/validation/performance_validation_try.rb.disabled +0 -324
  274. data/try/validation/real_world_scenarios_try.rb.disabled +0 -390
@@ -5,7 +5,7 @@ require_relative 'encrypted_fields/encrypted_field_type'
5
5
  module Familia
6
6
  module Features
7
7
  # EncryptedFields is a feature that provides transparent encryption and decryption
8
- # of sensitive data stored in Redis/Valkey. It uses strong cryptographic algorithms
8
+ # of sensitive data stored in Valkey/Redis. It uses strong cryptographic algorithms
9
9
  # with field-specific key derivation to protect data at rest while maintaining
10
10
  # easy access patterns for authorized applications.
11
11
  #
@@ -46,7 +46,7 @@ module Familia
46
46
  # Security Features:
47
47
  #
48
48
  # Each encrypted field uses a unique encryption key derived from:
49
- # - Master encryption key (from Familia.encryption_key)
49
+ # - Master encryption key (from Familia.encryption_keys[current_key_version])
50
50
  # - Field name (cryptographic domain separation)
51
51
  # - Record identifier (per-record key derivation)
52
52
  # - Class name (per-class key derivation)
@@ -97,27 +97,22 @@ module Familia
97
97
  # # The content can only be decrypted if doc_id, owner_id, and classification
98
98
  # # values match those used during encryption
99
99
  #
100
- # Passphrase Protection:
100
+ # Request-Level Caching:
101
101
  #
102
- # For ultra-sensitive fields, require user passphrases for decryption:
102
+ # For performance optimization, enable key derivation caching per request:
103
103
  #
104
- # class PersonalVault < Familia::Horreum
105
- # feature :encrypted_fields
106
- #
107
- # field :user_id
108
- # encrypted_field :diary_entry # Ultra-sensitive
109
- # encrypted_field :photos # Ultra-sensitive
104
+ # Familia::Encryption.with_request_cache do
105
+ # vault.secret_key = "value1"
106
+ # vault.api_token = "value2"
107
+ # vault.save # Reuses derived keys within this block
110
108
  # end
111
109
  #
112
- # vault = PersonalVault.new(user_id: 123, diary_entry: "Dear diary...")
113
- # vault.save
114
- #
115
- # # Passphrase required for decryption
116
- # diary = vault.diary_entry(passphrase_value: user_passphrase)
110
+ # # Cache is automatically cleared when block exits
111
+ # # Or manually: Familia::Encryption.clear_request_cache!
117
112
  #
118
- # Memory Safety:
113
+ # Preventing Accidental Leakage:
119
114
  #
120
- # Encrypted fields return ConcealedString objects that provide memory protection:
115
+ # Encrypted fields return ConcealedString objects to help prevent exposure.
121
116
  #
122
117
  # secret = vault.secret_key
123
118
  # secret.class # => ConcealedString
@@ -125,13 +120,11 @@ module Familia
125
120
  # secret.inspect # => "[CONCEALED]" (automatic redaction)
126
121
  #
127
122
  # # Safe access pattern
128
- # secret.expose do |value|
129
- # # Use value directly without creating copies
130
- # api_call(authorization: "Bearer #{value}")
131
- # end
123
+ # raw_value = secret.reveal # Returns actual decrypted string
124
+ # # Use raw_value carefully - avoid creating copies
132
125
  #
133
- # # Direct access (use carefully)
134
- # raw_value = secret.value # Returns actual decrypted string
126
+ # # Check if cleared from memory available to Ruby runtime process.
127
+ # secret.cleared? # Returns true if wiped
135
128
  #
136
129
  # # Explicit cleanup
137
130
  # secret.clear! # Best-effort memory wiping
@@ -143,41 +136,51 @@ module Familia
143
136
  # # Invalid ciphertext or tampering
144
137
  # vault.secret_key # => Familia::EncryptionError: Authentication failed
145
138
  #
146
- # # Wrong passphrase
147
- # vault.diary_entry(passphrase_value: "wrong")
148
- # # => Familia::EncryptionError: Invalid passphrase
139
+ # # Missing encryption configuration
140
+ # Familia.config.encryption_keys = {}
141
+ # vault.secret_key # => Familia::EncryptionError: No encryption keys configured
149
142
  #
150
- # # Missing encryption key
151
- # Familia.encryption_key = nil
152
- # vault.secret_key # => Familia::EncryptionError: No encryption key configured
143
+ # # Invalid key version
144
+ # # Key exists in storage but not in current configuration
145
+ # vault.secret_key # => Familia::EncryptionError: Key version not found: v1
153
146
  #
154
147
  # Configuration:
155
148
  #
156
- # # Set master encryption key (required)
149
+ # # Configure versioned encryption keys (required)
157
150
  # Familia.configure do |config|
158
- # config.encryption_key = ENV['FAMILIA_ENCRYPTION_KEY']
159
- # config.encryption_personalization = 'MyApp-2024' # Optional customization
151
+ # config.encryption_keys = {
152
+ # v1: ENV['FAMILIA_ENCRYPTION_KEY'],
153
+ # v2: ENV['FAMILIA_ENCRYPTION_KEY_V2']
154
+ # }
155
+ # config.current_key_version = :v2
156
+ # config.encryption_personalization = 'MyApp-2024' # Optional (XChaCha20 only)
160
157
  # end
161
158
  #
162
- # # Generate a new encryption key
163
- # key = Familia::Encryption.generate_key
164
- # puts key # => "base64-encoded-32-byte-key"
159
+ # # Validate configuration before use
160
+ # Familia::Encryption.validate_configuration!
165
161
  #
166
162
  # Key Rotation:
167
163
  #
168
164
  # The feature supports key versioning for seamless key rotation:
169
165
  #
170
- # # Step 1: Add new key while keeping old key
166
+ # # Step 1: Add new key version while keeping old keys
171
167
  # Familia.configure do |config|
172
- # config.encryption_key = new_key
173
- # config.legacy_encryption_keys = { 'v1' => old_key }
168
+ # config.encryption_keys = {
169
+ # v1: old_key,
170
+ # v2: new_key
171
+ # }
172
+ # config.current_key_version = :v2
174
173
  # end
175
174
  #
176
- # # Step 2: Objects decrypt with old key, encrypt with new key
177
- # vault.secret_key = "new-secret" # Encrypted with new key
175
+ # # Step 2: Objects decrypt with any valid key, encrypt with current key
176
+ # vault.secret_key = "new-secret" # Encrypted with v2 key
178
177
  # vault.save
179
178
  #
180
- # # Step 3: After all data is re-encrypted, remove legacy key
179
+ # # Step 3: Re-encrypt existing records
180
+ # vault.re_encrypt_fields! # Uses current key version
181
+ # vault.save
182
+ #
183
+ # # Step 4: After all data is re-encrypted, remove old key
181
184
  #
182
185
  # Integration Patterns:
183
186
  #
@@ -208,10 +211,9 @@ module Familia
208
211
  # user = User.find(user_id)
209
212
  #
210
213
  # # Access encrypted field safely
211
- # user.credit_card_number.expose do |cc_number|
212
- # # Process payment without storing plaintext
213
- # payment_gateway.charge(cc_number, amount)
214
- # end
214
+ # cc_number = user.credit_card_number.reveal
215
+ # # Process payment without storing plaintext
216
+ # payment_gateway.charge(cc_number, amount)
215
217
  #
216
218
  # # Clear sensitive data from memory
217
219
  # user.credit_card_number.clear!
@@ -221,10 +223,12 @@ module Familia
221
223
  # Performance Considerations:
222
224
  #
223
225
  # - Encryption/decryption adds ~1-5ms overhead per field
224
- # - Key derivation is cached per field/record combination
226
+ # - Key derivation is NOT cached by default for security
227
+ # - Use request-level caching for performance: with_request_cache { ... }
225
228
  # - XChaCha20-Poly1305 is ~2x faster than AES-256-GCM
226
229
  # - Memory allocation increases due to ciphertext expansion
227
230
  # - Consider batching operations for high-throughput scenarios
231
+ # - Personalization only affects XChaCha20-Poly1305 BLAKE2b derivation
228
232
  #
229
233
  # Security Limitations:
230
234
  #
@@ -258,14 +262,14 @@ module Familia
258
262
  Familia::Base.add_feature self, :encrypted_fields
259
263
 
260
264
  def self.included(base)
261
- Familia.trace :LOADED, self, base, caller(1..1) if Familia.debug?
262
- base.extend ClassMethods
265
+ Familia.trace :LOADED, self, base if Familia.debug?
266
+ base.extend ModelClassMethods
263
267
 
264
268
  # Initialize encrypted fields tracking
265
269
  base.instance_variable_set(:@encrypted_fields, []) unless base.instance_variable_defined?(:@encrypted_fields)
266
270
  end
267
271
 
268
- module ClassMethods
272
+ module ModelClassMethods
269
273
  # Define an encrypted field that transparently encrypts/decrypts values
270
274
  #
271
275
  # Encrypted fields are stored as JSON objects containing the encrypted
@@ -289,13 +293,13 @@ module Familia
289
293
  # encrypted_field :content, aad_fields: [:doc_id, :owner_id]
290
294
  # end
291
295
  #
292
- def encrypted_field(name, aad_fields: [], **kwargs)
296
+ def encrypted_field(name, aad_fields: [], **)
293
297
  @encrypted_fields ||= []
294
298
  @encrypted_fields << name unless @encrypted_fields.include?(name)
295
299
 
296
300
  require_relative 'encrypted_fields/encrypted_field_type'
297
301
 
298
- field_type = EncryptedFieldType.new(name, aad_fields: aad_fields, **kwargs)
302
+ field_type = EncryptedFieldType.new(name, aad_fields: aad_fields, **)
299
303
  register_field_type(field_type)
300
304
  end
301
305
 
@@ -326,7 +330,7 @@ module Familia
326
330
  algorithm: provider.algorithm_name,
327
331
  key_size: provider.key_size,
328
332
  nonce_size: provider.nonce_size,
329
- tag_size: provider.tag_size
333
+ tag_size: provider.tag_size,
330
334
  }
331
335
  end
332
336
  end
@@ -359,9 +363,7 @@ module Familia
359
363
  def clear_encrypted_fields!
360
364
  self.class.encrypted_fields.each do |field_name|
361
365
  field_value = instance_variable_get("@#{field_name}")
362
- if field_value.respond_to?(:clear!)
363
- field_value.clear!
364
- end
366
+ field_value.clear! if field_value.respond_to?(:clear!)
365
367
  end
366
368
  end
367
369
 
@@ -421,15 +423,15 @@ module Familia
421
423
  self.class.encrypted_fields.each_with_object({}) do |field_name, status|
422
424
  field_value = instance_variable_get("@#{field_name}")
423
425
 
424
- if field_value.nil?
425
- status[field_name] = { encrypted: false, value: nil }
426
- elsif field_value.respond_to?(:cleared?) && field_value.cleared?
427
- status[field_name] = { encrypted: true, cleared: true }
428
- elsif field_value.respond_to?(:concealed?) && field_value.concealed?
429
- status[field_name] = { encrypted: true, algorithm: "unknown", cleared: false }
430
- else
431
- status[field_name] = { encrypted: false, value: "[CONCEALED]" }
432
- end
426
+ status[field_name] = if field_value.nil?
427
+ { encrypted: false, value: nil }
428
+ elsif field_value.respond_to?(:cleared?) && field_value.cleared?
429
+ { encrypted: true, cleared: true }
430
+ elsif field_value.respond_to?(:concealed?) && field_value.concealed?
431
+ { encrypted: true, algorithm: 'unknown', cleared: false }
432
+ else
433
+ { encrypted: false, value: '[CONCEALED]' }
434
+ end
433
435
  end
434
436
  end
435
437
  end
@@ -51,7 +51,7 @@ module Familia
51
51
 
52
52
  # Base implementation of expired? that returns false
53
53
  #
54
- # @param threshold [Numeric] Ignored in base implementation
54
+ # @param _threshold [Numeric] Ignored in base implementation
55
55
  # @return [Boolean] Always returns false for the base implementation
56
56
  #
57
57
  def expired?(_threshold = 0)
@@ -5,11 +5,11 @@ require_relative 'expiration/extensions'
5
5
  module Familia
6
6
  module Features
7
7
  # Expiration is a feature that provides Time To Live (TTL) management for Familia
8
- # objects and their associated Redis/Valkey data structures. It enables automatic
8
+ # objects and their associated Valkey/Redis data structures. It enables automatic
9
9
  # data cleanup and supports cascading expiration across related objects.
10
10
  #
11
11
  # This feature allows you to:
12
- # - Set default expiration times at the class level
12
+ # - UnsortedSet default expiration times at the class level
13
13
  # - Update expiration times for individual objects
14
14
  # - Cascade expiration settings to related data structures
15
15
  # - Query remaining TTL for objects
@@ -35,7 +35,7 @@ module Familia
35
35
  # session.update_expiration(30.minutes)
36
36
  # session.ttl # => 1799
37
37
  #
38
- # # Set custom expiration for new objects
38
+ # # UnsortedSet custom expiration for new objects
39
39
  # session.update_expiration(default_expiration: 2.hours)
40
40
  #
41
41
  # Class-Level Configuration:
@@ -143,10 +143,10 @@ module Familia
143
143
  #
144
144
  # Performance Considerations:
145
145
  #
146
- # - TTL operations are performed on Redis/Valkey side with minimal overhead
146
+ # - TTL operations are performed on Valkey/Redis side with minimal overhead
147
147
  # - Cascading expiration uses pipelining for efficiency when possible
148
- # - Zero expiration values skip Redis EXPIRE calls entirely
149
- # - TTL queries are direct Redis operations (very fast)
148
+ # - Zero expiration values skip Valkey/Redis EXPIRE calls entirely
149
+ # - TTL queries are direct db operations (very fast)
150
150
  #
151
151
  module Expiration
152
152
  @default_expiration = nil
@@ -156,22 +156,24 @@ module Familia
156
156
  using Familia::Refinements::TimeLiterals
157
157
 
158
158
  def self.included(base)
159
- Familia.trace :LOADED, self, base, caller(1..1) if Familia.debug?
160
- base.extend ClassMethods
159
+ Familia.trace :LOADED, self, base if Familia.debug?
160
+ base.extend ModelClassMethods
161
161
 
162
162
  # Initialize default_expiration instance variable if not already defined
163
163
  # This ensures the class has a place to store its default expiration setting
164
164
  return if base.instance_variable_defined?(:@default_expiration)
165
165
 
166
+ # The instance var here will return the value from the implementing
167
+ # model class (or nil if it's not set, as you'd expect).
166
168
  base.instance_variable_set(:@default_expiration, @default_expiration)
167
169
  end
168
170
 
169
- # Familia::Expiration::ClassMethods
171
+ # Familia::Expiration::ModelClassMethods
170
172
  #
171
- module ClassMethods
172
- # Set the default expiration time for instances of this class
173
+ module ModelClassMethods
174
+ # UnsortedSet the default expiration time for instances of this class
173
175
  #
174
- # @param expiration [Numeric] Time in seconds (can be fractional)
176
+ # @param value [Numeric] Time in seconds (can be fractional)
175
177
  #
176
178
  attr_writer :default_expiration
177
179
 
@@ -184,7 +186,7 @@ module Familia
184
186
  # @param num [Numeric, nil] Expiration time in seconds
185
187
  # @return [Float] The default expiration in seconds
186
188
  #
187
- # @example Set default expiration
189
+ # @example UnsortedSet default expiration
188
190
  # class MyModel < Familia::Horreum
189
191
  # feature :expiration
190
192
  # default_expiration 1.hour
@@ -199,7 +201,7 @@ module Familia
199
201
  end
200
202
  end
201
203
 
202
- # Set the default expiration time for this instance
204
+ # UnsortedSet the default expiration time for this instance
203
205
  #
204
206
  # @param num [Numeric] Expiration time in seconds
205
207
  #
@@ -218,9 +220,9 @@ module Familia
218
220
  @default_expiration || self.class.default_expiration
219
221
  end
220
222
 
221
- # Sets an expiration time for the Redis/Valkey data associated with this object
223
+ # Sets an expiration time for the Valkey/Redis data associated with this object
222
224
  #
223
- # This method allows setting a Time To Live (TTL) for the data in Redis,
225
+ # This method allows setting a Time To Live (TTL) for the data in Valkey/Redis,
224
226
  # after which it will be automatically removed. The method also handles
225
227
  # cascading expiration to related data structures when applicable.
226
228
  #
@@ -266,15 +268,17 @@ module Familia
266
268
  # don't want to silently fail at setting expirations and cause data
267
269
  # retention issues (e.g. not removed in a timely fashion).
268
270
  unless default_expiration.is_a?(Numeric)
269
- raise Familia::Problem, "Default expiration must be a number (#{default_expiration.class} given for #{self.class})"
271
+ raise Familia::Problem,
272
+ "Default expiration must be a number (#{default_expiration.class} given for #{self.class})"
270
273
  end
271
274
 
272
275
  unless default_expiration >= 0
273
- raise Familia::Problem, "Default expiration must be non-negative (#{default_expiration} given for #{self.class})"
276
+ raise Familia::Problem,
277
+ "Default expiration must be non-negative (#{default_expiration} given for #{self.class})"
274
278
  end
275
279
 
276
280
  # If zero, simply skip setting an expiry for this key. If we were to set
277
- # 0, Redis would drop the key immediately.
281
+ # 0, Valkey/Redis would drop the key immediately.
278
282
  if default_expiration.zero?
279
283
  Familia.ld "[update_expiration] No expiration for #{self.class} (#{dbkey})"
280
284
  return true
@@ -282,8 +286,9 @@ module Familia
282
286
 
283
287
  Familia.ld "[update_expiration] Expires #{dbkey} in #{default_expiration} seconds"
284
288
 
285
- # Redis' EXPIRE command returns 1 if the timeout was set, 0 if key does
286
- # not exist or the timeout could not be set. Via redis-rb, it's a boolean.
289
+ # The Valkey/Redis' EXPIRE command returns 1 if the timeout was set, 0
290
+ # if key does not exist or the timeout could not be set. Via redis-rb,
291
+ # it's a boolean.
287
292
  expire(default_expiration)
288
293
  end
289
294
 
@@ -299,7 +304,7 @@ module Familia
299
304
  # expired_session.ttl # => -1
300
305
  #
301
306
  def ttl
302
- redis.ttl(dbkey)
307
+ dbclient.ttl(dbkey)
303
308
  end
304
309
 
305
310
  # Check if this object's data will expire
@@ -307,7 +312,7 @@ module Familia
307
312
  # @return [Boolean] true if TTL is set, false if data persists indefinitely
308
313
  #
309
314
  def expires?
310
- ttl > 0
315
+ ttl.positive?
311
316
  end
312
317
 
313
318
  # Check if this object's data has expired or will expire soon
@@ -325,6 +330,7 @@ module Familia
325
330
  current_ttl = ttl
326
331
  return false if current_ttl == -1 # no expiration set
327
332
  return true if current_ttl == -2 # key does not exist
333
+
328
334
  current_ttl <= threshold
329
335
  end
330
336
 
@@ -341,7 +347,7 @@ module Familia
341
347
  #
342
348
  def extend_expiration(duration)
343
349
  current_ttl = ttl
344
- return false if current_ttl < 0 # No current expiration set
350
+ return false unless current_ttl.positive? # no current expiration set
345
351
 
346
352
  new_ttl = current_ttl + duration.to_f
347
353
  expire(new_ttl)
@@ -355,9 +361,8 @@ module Familia
355
361
  # session.persist!
356
362
  #
357
363
  def persist!
358
- redis.persist(dbkey)
364
+ dbclient.persist(dbkey)
359
365
  end
360
-
361
366
  end
362
367
  end
363
368
  end
@@ -5,12 +5,11 @@ module Familia
5
5
  # Familia::Features::ExternalIdentifier
6
6
  #
7
7
  module ExternalIdentifier
8
-
9
8
  Familia::Base.add_feature self, :external_identifier, depends_on: [:object_identifier]
10
9
 
11
10
  def self.included(base)
12
- Familia.trace :LOADED, self, base, caller(1..1) if Familia.debug?
13
- base.extend ClassMethods
11
+ Familia.trace :LOADED, self, base if Familia.debug?
12
+ base.extend ModelClassMethods
14
13
 
15
14
  # Ensure default prefix is set in feature options
16
15
  base.add_feature_options(:external_identifier, prefix: 'ext')
@@ -44,11 +43,9 @@ module Familia
44
43
  # feature :external_identifier
45
44
  # field :email
46
45
  # end
47
- #
48
46
  # user = User.new(email: 'user@example.com')
49
47
  # user.objid # => "01234567-89ab-7def-8000-123456789abc"
50
48
  # user.extid # => "ext_abc123def456ghi789" (deterministic from objid)
51
- #
52
49
  # # Same objid always produces same extid
53
50
  # user2 = User.new(objid: user.objid, email: 'user@example.com')
54
51
  # user2.extid # => "ext_abc123def456ghi789" (identical to user.extid)
@@ -78,7 +75,7 @@ module Familia
78
75
 
79
76
  instance_variable_set(:"@#{field_name}", derived_extid)
80
77
 
81
- # Update mapping if we have an identifier
78
+ # Update mapping if we have an identifier (objid)
82
79
  self.class.extid_lookup[derived_extid] = identifier if respond_to?(:identifier) && identifier
83
80
 
84
81
  derived_extid
@@ -89,7 +86,7 @@ module Familia
89
86
  # Override setter to preserve values during initialization
90
87
  #
91
88
  # This ensures that values passed during object initialization
92
- # (e.g., when loading from Redis) are preserved and not overwritten
89
+ # (e.g., when loading from Valkey/Redis) are preserved and not overwritten
93
90
  # by the lazy generation logic.
94
91
  #
95
92
  # @param klass [Class] The class to define the method on
@@ -132,9 +129,9 @@ module Familia
132
129
  end
133
130
  end
134
131
 
135
- # ExternalIdentifier::ClassMethods
132
+ # ExternalIdentifier::ModelClassMethods
136
133
  #
137
- module ClassMethods
134
+ module ModelClassMethods
138
135
  # Find an object by its external identifier
139
136
  #
140
137
  # @param extid [String] The external identifier to search for
@@ -145,7 +142,7 @@ module Familia
145
142
 
146
143
  if Familia.debug?
147
144
  reference = caller(1..1).first
148
- Familia.trace :FIND_BY_EXTID, Familia.dbclient, extid, reference
145
+ Familia.trace :FIND_BY_EXTID, nil, extid, reference
149
146
  end
150
147
 
151
148
  # Look up the primary ID from the external ID mapping
@@ -156,7 +153,8 @@ module Familia
156
153
  find_by_id(primary_id)
157
154
  rescue Familia::NotFound
158
155
  # If the object was deleted but mapping wasn't cleaned up
159
- extid_lookup.remove_field(extid)
156
+ # we could autoclean here, as long as we log it.
157
+ # extid_lookup.remove_field(extid)
160
158
  nil
161
159
  end
162
160
  end
@@ -306,7 +304,6 @@ module Familia
306
304
  normalized
307
305
  end
308
306
  end
309
-
310
307
  end
311
308
  end
312
309
  end