familia 2.0.0.pre15 → 2.0.0.pre17

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 (288) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +2 -2
  3. data/.github/workflows/code-quality.yml +138 -0
  4. data/.github/workflows/code-smells.yml +85 -0
  5. data/.github/workflows/docs.yml +31 -8
  6. data/.gitignore +3 -1
  7. data/.pre-commit-config.yaml +7 -1
  8. data/.reek.yml +98 -0
  9. data/.rubocop.yml +54 -10
  10. data/.talismanrc +9 -0
  11. data/.yardopts +18 -13
  12. data/CHANGELOG.rst +86 -4
  13. data/CLAUDE.md +39 -1
  14. data/Gemfile +6 -5
  15. data/Gemfile.lock +99 -23
  16. data/LICENSE.txt +1 -1
  17. data/README.md +285 -85
  18. data/changelog.d/README.md +2 -2
  19. data/docs/archive/FAMILIA_RELATIONSHIPS.md +22 -22
  20. data/docs/archive/FAMILIA_TECHNICAL.md +42 -42
  21. data/docs/archive/FAMILIA_UPDATE.md +3 -3
  22. data/docs/archive/README.md +3 -2
  23. data/docs/{guides/API-Reference.md → archive/api-reference.md} +87 -101
  24. data/docs/conf.py +29 -0
  25. data/docs/guides/{Field-System-Guide.md → core-field-system.md} +9 -9
  26. data/docs/guides/feature-encrypted-fields.md +785 -0
  27. data/docs/guides/{Expiration-Feature-Guide.md → feature-expiration.md} +11 -2
  28. data/docs/guides/feature-external-identifiers.md +637 -0
  29. data/docs/guides/feature-object-identifiers.md +435 -0
  30. data/docs/guides/{Quantization-Feature-Guide.md → feature-quantization.md} +94 -29
  31. data/docs/guides/feature-relationships-methods.md +684 -0
  32. data/docs/guides/feature-relationships.md +200 -0
  33. data/docs/guides/{Features-System-Developer-Guide.md → feature-system-devs.md} +4 -4
  34. data/docs/guides/{Feature-System-Guide.md → feature-system.md} +5 -5
  35. data/docs/guides/{Transient-Fields-Guide.md → feature-transient-fields.md} +2 -2
  36. data/docs/guides/{Implementation-Guide.md → implementation.md} +3 -3
  37. data/docs/guides/index.md +176 -0
  38. data/docs/guides/{Security-Model.md → security-model.md} +1 -1
  39. data/docs/migrating/v2.0.0-pre.md +1 -1
  40. data/docs/migrating/v2.0.0-pre11.md +2 -2
  41. data/docs/migrating/v2.0.0-pre12.md +2 -2
  42. data/docs/migrating/v2.0.0-pre5.md +33 -12
  43. data/docs/migrating/v2.0.0-pre6.md +2 -2
  44. data/docs/migrating/v2.0.0-pre7.md +8 -8
  45. data/docs/overview.md +624 -20
  46. data/docs/reference/api-technical.md +1365 -0
  47. data/examples/autoloader/mega_customer/features/deprecated_fields.rb +7 -0
  48. data/examples/autoloader/mega_customer/safe_dump_fields.rb +1 -1
  49. data/examples/autoloader/mega_customer.rb +3 -1
  50. data/examples/encrypted_fields.rb +378 -0
  51. data/examples/json_usage_patterns.rb +144 -0
  52. data/examples/relationships.rb +13 -13
  53. data/examples/safe_dump.rb +7 -7
  54. data/examples/single_connection_transaction_confusions.rb +379 -0
  55. data/lib/familia/base.rb +51 -10
  56. data/lib/familia/connection/handlers.rb +223 -0
  57. data/lib/familia/connection/individual_command_proxy.rb +64 -0
  58. data/lib/familia/connection/middleware.rb +75 -0
  59. data/lib/familia/connection/operation_core.rb +93 -0
  60. data/lib/familia/connection/operations.rb +277 -0
  61. data/lib/familia/connection/pipeline_core.rb +87 -0
  62. data/lib/familia/connection/transaction_core.rb +100 -0
  63. data/lib/familia/connection.rb +60 -186
  64. data/lib/familia/data_type/class_methods.rb +63 -0
  65. data/lib/familia/data_type/commands.rb +53 -51
  66. data/lib/familia/data_type/connection.rb +83 -0
  67. data/lib/familia/data_type/serialization.rb +108 -107
  68. data/lib/familia/data_type/settings.rb +96 -0
  69. data/lib/familia/data_type/types/counter.rb +1 -1
  70. data/lib/familia/data_type/types/hashkey.rb +15 -11
  71. data/lib/familia/data_type/types/{list.rb → listkey.rb} +13 -5
  72. data/lib/familia/data_type/types/lock.rb +3 -2
  73. data/lib/familia/data_type/types/sorted_set.rb +128 -14
  74. data/lib/familia/data_type/types/{string.rb → stringkey.rb} +7 -9
  75. data/lib/familia/data_type/types/unsorted_set.rb +20 -27
  76. data/lib/familia/data_type.rb +12 -171
  77. data/lib/familia/distinguisher.rb +85 -0
  78. data/lib/familia/encryption/encrypted_data.rb +15 -24
  79. data/lib/familia/encryption/manager.rb +6 -4
  80. data/lib/familia/encryption/providers/aes_gcm_provider.rb +1 -1
  81. data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +7 -9
  82. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +4 -5
  83. data/lib/familia/encryption/request_cache.rb +7 -7
  84. data/lib/familia/encryption.rb +2 -3
  85. data/lib/familia/errors.rb +9 -3
  86. data/lib/familia/features/autoloader.rb +30 -12
  87. data/lib/familia/features/encrypted_fields/concealed_string.rb +3 -4
  88. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +13 -14
  89. data/lib/familia/features/encrypted_fields.rb +71 -66
  90. data/lib/familia/features/expiration/extensions.rb +1 -1
  91. data/lib/familia/features/expiration.rb +31 -26
  92. data/lib/familia/features/external_identifier.rb +57 -19
  93. data/lib/familia/features/object_identifier.rb +134 -25
  94. data/lib/familia/features/quantization.rb +16 -21
  95. data/lib/familia/features/relationships/README.md +97 -0
  96. data/lib/familia/features/relationships/collection_operations.rb +104 -0
  97. data/lib/familia/features/relationships/indexing/multi_index_generators.rb +202 -0
  98. data/lib/familia/features/relationships/indexing/unique_index_generators.rb +306 -0
  99. data/lib/familia/features/relationships/indexing.rb +182 -256
  100. data/lib/familia/features/relationships/indexing_relationship.rb +35 -0
  101. data/lib/familia/features/relationships/participation/participant_methods.rb +164 -0
  102. data/lib/familia/features/relationships/participation/target_methods.rb +225 -0
  103. data/lib/familia/features/relationships/participation.rb +656 -0
  104. data/lib/familia/features/relationships/participation_relationship.rb +31 -0
  105. data/lib/familia/features/relationships/score_encoding.rb +20 -20
  106. data/lib/familia/features/relationships.rb +65 -266
  107. data/lib/familia/features/safe_dump.rb +127 -130
  108. data/lib/familia/features/transient_fields/redacted_string.rb +6 -6
  109. data/lib/familia/features/transient_fields/transient_field_type.rb +5 -5
  110. data/lib/familia/features/transient_fields.rb +10 -7
  111. data/lib/familia/features.rb +10 -14
  112. data/lib/familia/field_type.rb +6 -4
  113. data/lib/familia/horreum/connection.rb +297 -0
  114. data/lib/familia/horreum/{core/database_commands.rb → database_commands.rb} +27 -17
  115. data/lib/familia/horreum/{subclass/definition.rb → definition.rb} +139 -74
  116. data/lib/familia/horreum/{subclass/management.rb → management.rb} +73 -27
  117. data/lib/familia/horreum/{core/serialization.rb → persistence.rb} +108 -185
  118. data/lib/familia/horreum/{subclass/related_fields_management.rb → related_fields.rb} +104 -23
  119. data/lib/familia/horreum/serialization.rb +172 -0
  120. data/lib/familia/horreum/{shared/settings.rb → settings.rb} +2 -1
  121. data/lib/familia/horreum/{core/utils.rb → utils.rb} +2 -1
  122. data/lib/familia/horreum.rb +222 -119
  123. data/lib/familia/json_serializer.rb +0 -1
  124. data/lib/familia/logging.rb +11 -114
  125. data/lib/familia/refinements/dear_json.rb +122 -0
  126. data/lib/familia/refinements/logger_trace.rb +20 -17
  127. data/lib/familia/refinements/stylize_words.rb +65 -0
  128. data/lib/familia/refinements/time_literals.rb +60 -52
  129. data/lib/familia/refinements.rb +2 -1
  130. data/lib/familia/secure_identifier.rb +60 -28
  131. data/lib/familia/settings.rb +83 -7
  132. data/lib/familia/utils.rb +5 -87
  133. data/lib/familia/verifiable_identifier.rb +4 -4
  134. data/lib/familia/version.rb +1 -1
  135. data/lib/familia.rb +72 -14
  136. data/lib/middleware/database_middleware.rb +56 -14
  137. data/lib/{familia/multi_result.rb → multi_result.rb} +23 -16
  138. data/try/configuration/scenarios_try.rb +2 -2
  139. data/try/connection/fiber_context_preservation_try.rb +250 -0
  140. data/try/connection/handler_constraints_try.rb +59 -0
  141. data/try/connection/operation_mode_guards_try.rb +208 -0
  142. data/try/connection/pipeline_fallback_integration_try.rb +128 -0
  143. data/try/connection/responsibility_chain_tracking_try.rb +72 -0
  144. data/try/connection/transaction_fallback_integration_try.rb +288 -0
  145. data/try/connection/transaction_mode_permissive_try.rb +153 -0
  146. data/try/connection/transaction_mode_strict_try.rb +98 -0
  147. data/try/connection/transaction_mode_warn_try.rb +131 -0
  148. data/try/connection/transaction_modes_try.rb +249 -0
  149. data/try/core/autoloader_try.rb +120 -2
  150. data/try/core/connection_try.rb +10 -10
  151. data/try/core/conventional_inheritance_try.rb +130 -0
  152. data/try/core/create_method_try.rb +15 -23
  153. data/try/core/database_consistency_try.rb +11 -10
  154. data/try/core/errors_try.rb +11 -14
  155. data/try/core/familia_extended_try.rb +2 -2
  156. data/try/core/familia_members_methods_try.rb +76 -0
  157. data/try/core/familia_try.rb +1 -1
  158. data/try/core/isolated_dbclient_try.rb +165 -0
  159. data/try/core/middleware_try.rb +16 -16
  160. data/try/core/persistence_operations_try.rb +4 -4
  161. data/try/core/pools_try.rb +42 -26
  162. data/try/core/secure_identifier_try.rb +28 -24
  163. data/try/core/time_utils_try.rb +10 -10
  164. data/try/core/tools_try.rb +3 -3
  165. data/try/core/utils_try.rb +2 -2
  166. data/try/data_types/boolean_try.rb +4 -4
  167. data/try/data_types/datatype_base_try.rb +0 -2
  168. data/try/data_types/list_try.rb +10 -10
  169. data/try/data_types/sorted_set_try.rb +5 -5
  170. data/try/data_types/sorted_set_zadd_options_try.rb +625 -0
  171. data/try/data_types/string_try.rb +12 -12
  172. data/try/data_types/unsortedset_try.rb +33 -0
  173. data/try/debugging/cache_behavior_tracer.rb +7 -7
  174. data/try/debugging/debug_aad_process.rb +1 -1
  175. data/try/debugging/debug_concealed_internal.rb +1 -1
  176. data/try/debugging/debug_cross_context.rb +1 -1
  177. data/try/debugging/debug_fresh_cross_context.rb +1 -1
  178. data/try/debugging/encryption_method_tracer.rb +10 -10
  179. data/try/edge_cases/hash_symbolization_try.rb +1 -1
  180. data/try/edge_cases/ttl_side_effects_try.rb +1 -1
  181. data/try/encryption/config_persistence_try.rb +2 -2
  182. data/try/encryption/encryption_core_try.rb +19 -19
  183. data/try/encryption/instance_variable_scope_try.rb +1 -1
  184. data/try/encryption/module_loading_try.rb +2 -2
  185. data/try/encryption/providers/aes_gcm_provider_try.rb +1 -1
  186. data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +1 -1
  187. data/try/encryption/secure_memory_handling_try.rb +1 -1
  188. data/try/features/encrypted_fields/concealed_string_core_try.rb +11 -7
  189. data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
  190. data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +3 -3
  191. data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +10 -10
  192. data/try/features/encrypted_fields/encrypted_fields_security_try.rb +14 -14
  193. data/try/features/encrypted_fields/error_conditions_try.rb +7 -7
  194. data/try/features/encrypted_fields/fresh_key_try.rb +1 -1
  195. data/try/features/encrypted_fields/nonce_uniqueness_try.rb +1 -1
  196. data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +7 -7
  197. data/try/features/encrypted_fields/universal_serialization_safety_try.rb +13 -20
  198. data/try/features/external_identifier/external_identifier_try.rb +1 -1
  199. data/try/features/feature_dependencies_try.rb +3 -3
  200. data/try/features/field_groups_try.rb +244 -0
  201. data/try/features/object_identifier/object_identifier_integration_try.rb +28 -34
  202. data/try/features/object_identifier/object_identifier_try.rb +10 -0
  203. data/try/features/quantization/quantization_try.rb +1 -1
  204. data/try/features/relationships/indexing_commands_verification_try.rb +136 -0
  205. data/try/features/relationships/indexing_try.rb +443 -0
  206. data/try/features/relationships/participation_commands_verification_spec.rb +102 -0
  207. data/try/features/relationships/participation_commands_verification_try.rb +105 -0
  208. data/try/features/relationships/participation_performance_improvements_try.rb +124 -0
  209. data/try/features/relationships/participation_reverse_index_try.rb +196 -0
  210. data/try/features/relationships/relationships_api_changes_try.rb +72 -71
  211. data/try/features/relationships/relationships_edge_cases_try.rb +15 -18
  212. data/try/features/relationships/relationships_performance_minimal_try.rb +2 -2
  213. data/try/features/relationships/relationships_performance_simple_try.rb +8 -8
  214. data/try/features/relationships/relationships_performance_try.rb +20 -20
  215. data/try/features/relationships/relationships_try.rb +27 -38
  216. data/try/features/safe_dump/safe_dump_advanced_try.rb +2 -2
  217. data/try/features/transient_fields/refresh_reset_try.rb +3 -1
  218. data/try/features/transient_fields/simple_refresh_test.rb +1 -1
  219. data/try/helpers/test_cleanup.rb +86 -0
  220. data/try/helpers/test_helpers.rb +6 -7
  221. data/try/horreum/auto_indexing_on_save_try.rb +212 -0
  222. data/try/horreum/base_try.rb +3 -2
  223. data/try/horreum/commands_try.rb +3 -1
  224. data/try/horreum/defensive_initialization_try.rb +86 -0
  225. data/try/horreum/destroy_related_fields_cleanup_try.rb +332 -0
  226. data/try/horreum/initialization_try.rb +11 -7
  227. data/try/horreum/relations_try.rb +21 -13
  228. data/try/horreum/serialization_try.rb +12 -11
  229. data/try/horreum/settings_try.rb +2 -0
  230. data/try/integration/cross_component_try.rb +3 -3
  231. data/try/memory/memory_basic_test.rb +1 -1
  232. data/try/memory/memory_docker_ruby_dump.sh +2 -2
  233. data/try/models/customer_safe_dump_try.rb +1 -1
  234. data/try/models/customer_try.rb +13 -15
  235. data/try/models/datatype_base_try.rb +3 -3
  236. data/try/models/familia_object_try.rb +9 -8
  237. data/try/performance/benchmarks_try.rb +2 -2
  238. data/try/prototypes/atomic_saves_v1_context_proxy.rb +2 -2
  239. data/try/prototypes/atomic_saves_v3_connection_pool.rb +3 -3
  240. data/try/prototypes/atomic_saves_v4.rb +1 -1
  241. data/try/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +4 -4
  242. data/try/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
  243. data/try/prototypes/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
  244. data/try/prototypes/pooling/lib/connection_pool_metrics.rb +5 -5
  245. data/try/prototypes/pooling/lib/connection_pool_stress_test.rb +26 -26
  246. data/try/prototypes/pooling/lib/connection_pool_threading_models.rb +7 -7
  247. data/try/prototypes/pooling/lib/visualize_stress_results.rb +1 -1
  248. data/try/prototypes/pooling/pool_siege.rb +11 -11
  249. data/try/prototypes/pooling/run_stress_tests.rb +7 -7
  250. data/try/refinements/dear_json_array_methods_try.rb +53 -0
  251. data/try/refinements/dear_json_hash_methods_try.rb +54 -0
  252. data/try/refinements/logger_trace_methods_try.rb +44 -0
  253. data/try/refinements/time_literals_numeric_methods_try.rb +141 -0
  254. data/try/refinements/time_literals_string_methods_try.rb +80 -0
  255. data/try/valkey.conf +26 -0
  256. metadata +92 -52
  257. data/.rubocop_todo.yml +0 -208
  258. data/docs/connection_pooling.md +0 -192
  259. data/docs/guides/Connection-Pooling-Guide.md +0 -437
  260. data/docs/guides/Encrypted-Fields-Overview.md +0 -101
  261. data/docs/guides/Feature-System-Autoloading.md +0 -198
  262. data/docs/guides/Home.md +0 -116
  263. data/docs/guides/Relationships-Guide.md +0 -737
  264. data/docs/guides/relationships-methods.md +0 -266
  265. data/docs/reference/auditing_database_commands.rb +0 -228
  266. data/examples/permissions.rb +0 -240
  267. data/lib/familia/features/relationships/cascading.rb +0 -437
  268. data/lib/familia/features/relationships/membership.rb +0 -497
  269. data/lib/familia/features/relationships/permission_management.rb +0 -264
  270. data/lib/familia/features/relationships/querying.rb +0 -615
  271. data/lib/familia/features/relationships/redis_operations.rb +0 -274
  272. data/lib/familia/features/relationships/tracking.rb +0 -418
  273. data/lib/familia/horreum/core/connection.rb +0 -73
  274. data/lib/familia/horreum/core.rb +0 -21
  275. data/lib/familia/refinements/snake_case.rb +0 -40
  276. data/lib/familia/validation/command_recorder.rb +0 -336
  277. data/lib/familia/validation/expectations.rb +0 -519
  278. data/lib/familia/validation/validation_helpers.rb +0 -443
  279. data/lib/familia/validation/validator.rb +0 -412
  280. data/lib/familia/validation.rb +0 -140
  281. data/try/data_types/set_try.rb +0 -33
  282. data/try/features/relationships/categorical_permissions_try.rb +0 -515
  283. data/try/features/safe_dump/module_based_extensions_try.rb +0 -100
  284. data/try/features/safe_dump/safe_dump_autoloading_try.rb +0 -107
  285. data/try/validation/atomic_operations_try.rb.disabled +0 -320
  286. data/try/validation/command_validation_try.rb.disabled +0 -207
  287. data/try/validation/performance_validation_try.rb.disabled +0 -324
  288. 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
  #
@@ -255,17 +259,17 @@ module Familia
255
259
  # - Insider threats with application access
256
260
  #
257
261
  module EncryptedFields
258
- Familia::Base.add_feature self, :encrypted_fields
262
+ Familia::Base.add_feature self, :encrypted_fields, depends_on: nil, field_group: :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,16 @@ 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
- require_relative 'encrypted_fields/encrypted_field_type'
300
+ # Add to field_groups if the group exists
301
+ if field_groups&.key?(:encrypted_fields)
302
+ field_groups[:encrypted_fields] << name
303
+ end
297
304
 
298
- field_type = EncryptedFieldType.new(name, aad_fields: aad_fields, **kwargs)
305
+ field_type = EncryptedFieldType.new(name, aad_fields: aad_fields, **)
299
306
  register_field_type(field_type)
300
307
  end
301
308
 
@@ -326,7 +333,7 @@ module Familia
326
333
  algorithm: provider.algorithm_name,
327
334
  key_size: provider.key_size,
328
335
  nonce_size: provider.nonce_size,
329
- tag_size: provider.tag_size
336
+ tag_size: provider.tag_size,
330
337
  }
331
338
  end
332
339
  end
@@ -359,9 +366,7 @@ module Familia
359
366
  def clear_encrypted_fields!
360
367
  self.class.encrypted_fields.each do |field_name|
361
368
  field_value = instance_variable_get("@#{field_name}")
362
- if field_value.respond_to?(:clear!)
363
- field_value.clear!
364
- end
369
+ field_value.clear! if field_value.respond_to?(:clear!)
365
370
  end
366
371
  end
367
372
 
@@ -421,15 +426,15 @@ module Familia
421
426
  self.class.encrypted_fields.each_with_object({}) do |field_name, status|
422
427
  field_value = instance_variable_get("@#{field_name}")
423
428
 
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
429
+ status[field_name] = if field_value.nil?
430
+ { encrypted: false, value: nil }
431
+ elsif field_value.respond_to?(:cleared?) && field_value.cleared?
432
+ { encrypted: true, cleared: true }
433
+ elsif field_value.respond_to?(:concealed?) && field_value.concealed?
434
+ { encrypted: true, algorithm: 'unknown', cleared: false }
435
+ else
436
+ { encrypted: false, value: '[CONCEALED]' }
437
+ end
433
438
  end
434
439
  end
435
440
  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,12 @@ 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
13
+ base.include ModelInstanceMethods
14
14
 
15
15
  # Ensure default prefix is set in feature options
16
16
  base.add_feature_options(:external_identifier, prefix: 'ext')
@@ -44,11 +44,9 @@ module Familia
44
44
  # feature :external_identifier
45
45
  # field :email
46
46
  # end
47
- #
48
47
  # user = User.new(email: 'user@example.com')
49
48
  # user.objid # => "01234567-89ab-7def-8000-123456789abc"
50
49
  # user.extid # => "ext_abc123def456ghi789" (deterministic from objid)
51
- #
52
50
  # # Same objid always produces same extid
53
51
  # user2 = User.new(objid: user.objid, email: 'user@example.com')
54
52
  # user2.extid # => "ext_abc123def456ghi789" (identical to user.extid)
@@ -78,9 +76,6 @@ module Familia
78
76
 
79
77
  instance_variable_set(:"@#{field_name}", derived_extid)
80
78
 
81
- # Update mapping if we have an identifier
82
- self.class.extid_lookup[derived_extid] = identifier if respond_to?(:identifier) && identifier
83
-
84
79
  derived_extid
85
80
  end
86
81
  end
@@ -89,7 +84,7 @@ module Familia
89
84
  # Override setter to preserve values during initialization
90
85
  #
91
86
  # This ensures that values passed during object initialization
92
- # (e.g., when loading from Redis) are preserved and not overwritten
87
+ # (e.g., when loading from Valkey/Redis) are preserved and not overwritten
93
88
  # by the lazy generation logic.
94
89
  #
95
90
  # @param klass [Class] The class to define the method on
@@ -106,11 +101,6 @@ module Familia
106
101
 
107
102
  # Set the new value
108
103
  instance_variable_set(:"@#{field_name}", value)
109
-
110
- # Update mapping if we have both extid and identifier
111
- return unless value && respond_to?(:identifier) && identifier
112
-
113
- self.class.extid_lookup[value] = identifier
114
104
  end
115
105
  end
116
106
  end
@@ -132,9 +122,9 @@ module Familia
132
122
  end
133
123
  end
134
124
 
135
- # ExternalIdentifier::ClassMethods
125
+ # ExternalIdentifier::ModelClassMethods
136
126
  #
137
- module ClassMethods
127
+ module ModelClassMethods
138
128
  # Find an object by its external identifier
139
129
  #
140
130
  # @param extid [String] The external identifier to search for
@@ -145,7 +135,7 @@ module Familia
145
135
 
146
136
  if Familia.debug?
147
137
  reference = caller(1..1).first
148
- Familia.trace :FIND_BY_EXTID, Familia.dbclient, extid, reference
138
+ Familia.trace :FIND_BY_EXTID, nil, extid, reference
149
139
  end
150
140
 
151
141
  # Look up the primary ID from the external ID mapping
@@ -156,11 +146,60 @@ module Familia
156
146
  find_by_id(primary_id)
157
147
  rescue Familia::NotFound
158
148
  # If the object was deleted but mapping wasn't cleaned up
159
- extid_lookup.remove_field(extid)
149
+ # we could autoclean here, as long as we log it.
150
+ # extid_lookup.remove_field(extid)
160
151
  nil
161
152
  end
162
153
  end
163
154
 
155
+ # Instance methods for external identifier management
156
+ module ModelInstanceMethods
157
+ # Override save to update extid_lookup mapping
158
+ #
159
+ # This ensures the extid_lookup index is populated during save operations
160
+ # rather than during object initialization, preventing unwanted database
161
+ # writes when calling .new()
162
+ #
163
+ # @param update_expiration [Boolean] Whether to update key expiration
164
+ # @return [Boolean] True if save was successful
165
+ #
166
+ def save(update_expiration: true)
167
+ result = super
168
+
169
+ # Update extid_lookup mapping after successful save
170
+ if result && respond_to?(:extid) && respond_to?(:identifier)
171
+ current_extid = extid # Triggers lazy generation if needed
172
+ if current_extid && identifier
173
+ self.class.extid_lookup[current_extid] = identifier
174
+ end
175
+ end
176
+
177
+ result
178
+ end
179
+
180
+ # Override save_if_not_exists to update extid_lookup mapping
181
+ #
182
+ # This ensures the extid_lookup index is populated during create operations
183
+ # which use save_if_not_exists instead of save.
184
+ #
185
+ # @param update_expiration [Boolean] Whether to update key expiration
186
+ # @return [Boolean] True if save was successful
187
+ #
188
+ def save_if_not_exists(update_expiration: true)
189
+ result = super
190
+
191
+ # Update extid_lookup mapping after successful save
192
+ if result && respond_to?(:extid) && respond_to?(:identifier)
193
+ current_extid = extid # Triggers lazy generation if needed
194
+ if current_extid && identifier
195
+ self.class.extid_lookup[current_extid] = identifier
196
+ end
197
+ end
198
+
199
+ result
200
+ end
201
+ end
202
+
164
203
  # Derives a deterministic, public-facing external identifier from the object's
165
204
  # internal `objid`.
166
205
  #
@@ -306,7 +345,6 @@ module Familia
306
345
  normalized
307
346
  end
308
347
  end
309
-
310
348
  end
311
349
  end
312
350
  end