familia 2.0.0.pre14 → 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 (276) 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 +66 -6
  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 +4 -4
  40. data/docs/migrating/v2.0.0-pre12.md +2 -2
  41. data/docs/migrating/v2.0.0-pre13.md +1 -1
  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 +623 -19
  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 +6 -6
  54. data/examples/single_connection_transaction_confusions.rb +379 -0
  55. data/lib/familia/base.rb +49 -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/commands.rb +53 -51
  65. data/lib/familia/data_type/serialization.rb +108 -107
  66. data/lib/familia/data_type/types/counter.rb +1 -1
  67. data/lib/familia/data_type/types/hashkey.rb +13 -10
  68. data/lib/familia/data_type/types/{list.rb → listkey.rb} +13 -5
  69. data/lib/familia/data_type/types/lock.rb +3 -2
  70. data/lib/familia/data_type/types/sorted_set.rb +26 -15
  71. data/lib/familia/data_type/types/{string.rb → stringkey.rb} +7 -5
  72. data/lib/familia/data_type/types/unsorted_set.rb +20 -27
  73. data/lib/familia/data_type.rb +75 -47
  74. data/lib/familia/distinguisher.rb +85 -0
  75. data/lib/familia/encryption/encrypted_data.rb +15 -24
  76. data/lib/familia/encryption/manager.rb +6 -4
  77. data/lib/familia/encryption/providers/aes_gcm_provider.rb +1 -1
  78. data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +7 -9
  79. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +4 -5
  80. data/lib/familia/encryption/request_cache.rb +7 -7
  81. data/lib/familia/encryption.rb +2 -3
  82. data/lib/familia/errors.rb +9 -3
  83. data/lib/familia/{autoloader.rb → features/autoloader.rb} +49 -23
  84. data/lib/familia/features/encrypted_fields/concealed_string.rb +3 -4
  85. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +13 -14
  86. data/lib/familia/features/encrypted_fields.rb +68 -66
  87. data/lib/familia/features/expiration/extensions.rb +61 -0
  88. data/lib/familia/features/expiration.rb +35 -87
  89. data/lib/familia/features/external_identifier.rb +11 -12
  90. data/lib/familia/features/object_identifier.rb +58 -20
  91. data/lib/familia/features/quantization.rb +17 -22
  92. data/lib/familia/features/relationships/README.md +97 -0
  93. data/lib/familia/features/relationships/collection_operations.rb +104 -0
  94. data/lib/familia/features/relationships/indexing/multi_index_generators.rb +202 -0
  95. data/lib/familia/features/relationships/indexing/unique_index_generators.rb +301 -0
  96. data/lib/familia/features/relationships/indexing.rb +176 -256
  97. data/lib/familia/features/relationships/indexing_relationship.rb +35 -0
  98. data/lib/familia/features/relationships/participation/participant_methods.rb +160 -0
  99. data/lib/familia/features/relationships/participation/target_methods.rb +225 -0
  100. data/lib/familia/features/relationships/participation.rb +656 -0
  101. data/lib/familia/features/relationships/participation_relationship.rb +31 -0
  102. data/lib/familia/features/relationships/score_encoding.rb +20 -20
  103. data/lib/familia/features/relationships.rb +69 -271
  104. data/lib/familia/features/safe_dump.rb +127 -132
  105. data/lib/familia/features/transient_fields/redacted_string.rb +6 -6
  106. data/lib/familia/features/transient_fields/transient_field_type.rb +5 -5
  107. data/lib/familia/features/transient_fields.rb +5 -5
  108. data/lib/familia/features.rb +21 -21
  109. data/lib/familia/field_type.rb +24 -4
  110. data/lib/familia/horreum/core/connection.rb +229 -26
  111. data/lib/familia/horreum/core/database_commands.rb +27 -17
  112. data/lib/familia/horreum/core/serialization.rb +40 -20
  113. data/lib/familia/horreum/core/utils.rb +2 -1
  114. data/lib/familia/horreum/shared/settings.rb +2 -1
  115. data/lib/familia/horreum/subclass/definition.rb +33 -45
  116. data/lib/familia/horreum/subclass/management.rb +72 -24
  117. data/lib/familia/horreum/subclass/related_fields_management.rb +82 -21
  118. data/lib/familia/horreum.rb +196 -114
  119. data/lib/familia/json_serializer.rb +0 -1
  120. data/lib/familia/logging.rb +11 -114
  121. data/lib/familia/refinements/dear_json.rb +122 -0
  122. data/lib/familia/refinements/logger_trace.rb +20 -17
  123. data/lib/familia/refinements/stylize_words.rb +65 -0
  124. data/lib/familia/refinements/time_literals.rb +60 -52
  125. data/lib/familia/refinements.rb +2 -1
  126. data/lib/familia/secure_identifier.rb +60 -28
  127. data/lib/familia/settings.rb +83 -7
  128. data/lib/familia/utils.rb +5 -87
  129. data/lib/familia/verifiable_identifier.rb +4 -4
  130. data/lib/familia/version.rb +1 -1
  131. data/lib/familia.rb +72 -15
  132. data/lib/middleware/database_middleware.rb +56 -14
  133. data/lib/{familia/multi_result.rb → multi_result.rb} +23 -16
  134. data/try/configuration/scenarios_try.rb +1 -1
  135. data/try/connection/fiber_context_preservation_try.rb +250 -0
  136. data/try/connection/handler_constraints_try.rb +59 -0
  137. data/try/connection/operation_mode_guards_try.rb +208 -0
  138. data/try/connection/pipeline_fallback_integration_try.rb +128 -0
  139. data/try/connection/responsibility_chain_tracking_try.rb +72 -0
  140. data/try/connection/transaction_fallback_integration_try.rb +288 -0
  141. data/try/connection/transaction_mode_permissive_try.rb +153 -0
  142. data/try/connection/transaction_mode_strict_try.rb +98 -0
  143. data/try/connection/transaction_mode_warn_try.rb +131 -0
  144. data/try/connection/transaction_modes_try.rb +249 -0
  145. data/try/core/autoloader_try.rb +129 -11
  146. data/try/core/connection_try.rb +7 -7
  147. data/try/core/conventional_inheritance_try.rb +130 -0
  148. data/try/core/create_method_try.rb +15 -23
  149. data/try/core/database_consistency_try.rb +10 -10
  150. data/try/core/errors_try.rb +8 -11
  151. data/try/core/familia_extended_try.rb +2 -2
  152. data/try/core/familia_members_methods_try.rb +76 -0
  153. data/try/core/isolated_dbclient_try.rb +165 -0
  154. data/try/core/middleware_try.rb +16 -16
  155. data/try/core/persistence_operations_try.rb +4 -4
  156. data/try/core/pools_try.rb +42 -26
  157. data/try/core/secure_identifier_try.rb +28 -24
  158. data/try/core/time_utils_try.rb +10 -10
  159. data/try/core/tools_try.rb +1 -1
  160. data/try/core/utils_try.rb +2 -2
  161. data/try/data_types/boolean_try.rb +4 -4
  162. data/try/data_types/datatype_base_try.rb +0 -2
  163. data/try/data_types/list_try.rb +10 -10
  164. data/try/data_types/sorted_set_try.rb +5 -5
  165. data/try/data_types/string_try.rb +12 -12
  166. data/try/data_types/unsortedset_try.rb +33 -0
  167. data/try/debugging/cache_behavior_tracer.rb +7 -7
  168. data/try/debugging/debug_aad_process.rb +1 -1
  169. data/try/debugging/debug_concealed_internal.rb +1 -1
  170. data/try/debugging/debug_cross_context.rb +1 -1
  171. data/try/debugging/debug_fresh_cross_context.rb +1 -1
  172. data/try/debugging/encryption_method_tracer.rb +10 -10
  173. data/try/edge_cases/hash_symbolization_try.rb +1 -1
  174. data/try/edge_cases/ttl_side_effects_try.rb +1 -1
  175. data/try/encryption/config_persistence_try.rb +2 -2
  176. data/try/encryption/encryption_core_try.rb +19 -19
  177. data/try/encryption/instance_variable_scope_try.rb +1 -1
  178. data/try/encryption/module_loading_try.rb +2 -2
  179. data/try/encryption/providers/aes_gcm_provider_try.rb +1 -1
  180. data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +1 -1
  181. data/try/encryption/secure_memory_handling_try.rb +1 -1
  182. data/try/features/encrypted_fields/concealed_string_core_try.rb +11 -7
  183. data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
  184. data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +3 -3
  185. data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +10 -10
  186. data/try/features/encrypted_fields/encrypted_fields_security_try.rb +14 -14
  187. data/try/features/encrypted_fields/error_conditions_try.rb +7 -7
  188. data/try/features/encrypted_fields/fresh_key_try.rb +1 -1
  189. data/try/features/encrypted_fields/nonce_uniqueness_try.rb +1 -1
  190. data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +7 -7
  191. data/try/features/encrypted_fields/universal_serialization_safety_try.rb +13 -20
  192. data/try/features/external_identifier/external_identifier_try.rb +1 -1
  193. data/try/features/feature_dependencies_try.rb +3 -3
  194. data/try/features/object_identifier/object_identifier_integration_try.rb +28 -34
  195. data/try/features/object_identifier/object_identifier_try.rb +10 -0
  196. data/try/features/quantization/quantization_try.rb +1 -1
  197. data/try/features/relationships/indexing_commands_verification_try.rb +136 -0
  198. data/try/features/relationships/indexing_try.rb +433 -0
  199. data/try/features/relationships/participation_commands_verification_spec.rb +102 -0
  200. data/try/features/relationships/participation_commands_verification_try.rb +105 -0
  201. data/try/features/relationships/participation_performance_improvements_try.rb +124 -0
  202. data/try/features/relationships/participation_reverse_index_try.rb +196 -0
  203. data/try/features/relationships/relationships_api_changes_try.rb +72 -71
  204. data/try/features/relationships/relationships_edge_cases_try.rb +15 -18
  205. data/try/features/relationships/relationships_performance_minimal_try.rb +2 -2
  206. data/try/features/relationships/relationships_performance_simple_try.rb +8 -8
  207. data/try/features/relationships/relationships_performance_try.rb +20 -20
  208. data/try/features/relationships/relationships_try.rb +27 -38
  209. data/try/features/safe_dump/safe_dump_advanced_try.rb +2 -2
  210. data/try/features/transient_fields/refresh_reset_try.rb +1 -1
  211. data/try/features/transient_fields/simple_refresh_test.rb +1 -1
  212. data/try/helpers/test_cleanup.rb +86 -0
  213. data/try/helpers/test_helpers.rb +3 -3
  214. data/try/horreum/base_try.rb +3 -2
  215. data/try/horreum/commands_try.rb +1 -1
  216. data/try/horreum/destroy_related_fields_cleanup_try.rb +330 -0
  217. data/try/horreum/initialization_try.rb +11 -7
  218. data/try/horreum/relations_try.rb +21 -13
  219. data/try/horreum/serialization_try.rb +12 -11
  220. data/try/integration/cross_component_try.rb +3 -3
  221. data/try/memory/memory_basic_test.rb +1 -1
  222. data/try/memory/memory_docker_ruby_dump.sh +1 -1
  223. data/try/models/customer_safe_dump_try.rb +1 -1
  224. data/try/models/customer_try.rb +8 -10
  225. data/try/models/datatype_base_try.rb +3 -3
  226. data/try/models/familia_object_try.rb +9 -8
  227. data/try/performance/benchmarks_try.rb +2 -2
  228. data/try/prototypes/atomic_saves_v1_context_proxy.rb +2 -2
  229. data/try/prototypes/atomic_saves_v3_connection_pool.rb +3 -3
  230. data/try/prototypes/atomic_saves_v4.rb +1 -1
  231. data/try/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +4 -4
  232. data/try/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
  233. data/try/prototypes/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
  234. data/try/prototypes/pooling/lib/connection_pool_metrics.rb +5 -5
  235. data/try/prototypes/pooling/lib/connection_pool_stress_test.rb +26 -26
  236. data/try/prototypes/pooling/lib/connection_pool_threading_models.rb +7 -7
  237. data/try/prototypes/pooling/lib/visualize_stress_results.rb +1 -1
  238. data/try/prototypes/pooling/pool_siege.rb +11 -11
  239. data/try/prototypes/pooling/run_stress_tests.rb +7 -7
  240. data/try/refinements/dear_json_array_methods_try.rb +53 -0
  241. data/try/refinements/dear_json_hash_methods_try.rb +54 -0
  242. data/try/refinements/logger_trace_methods_try.rb +44 -0
  243. data/try/refinements/time_literals_numeric_methods_try.rb +141 -0
  244. data/try/refinements/time_literals_string_methods_try.rb +80 -0
  245. metadata +77 -45
  246. data/.rubocop_todo.yml +0 -208
  247. data/docs/connection_pooling.md +0 -192
  248. data/docs/guides/Connection-Pooling-Guide.md +0 -437
  249. data/docs/guides/Encrypted-Fields-Overview.md +0 -101
  250. data/docs/guides/Feature-System-Autoloading.md +0 -228
  251. data/docs/guides/Home.md +0 -116
  252. data/docs/guides/Relationships-Guide.md +0 -737
  253. data/docs/guides/relationships-methods.md +0 -266
  254. data/docs/reference/auditing_database_commands.rb +0 -228
  255. data/examples/permissions.rb +0 -240
  256. data/lib/familia/features/autoloadable.rb +0 -113
  257. data/lib/familia/features/relationships/cascading.rb +0 -437
  258. data/lib/familia/features/relationships/membership.rb +0 -497
  259. data/lib/familia/features/relationships/permission_management.rb +0 -264
  260. data/lib/familia/features/relationships/querying.rb +0 -615
  261. data/lib/familia/features/relationships/redis_operations.rb +0 -274
  262. data/lib/familia/features/relationships/tracking.rb +0 -418
  263. data/lib/familia/refinements/snake_case.rb +0 -40
  264. data/lib/familia/validation/command_recorder.rb +0 -336
  265. data/lib/familia/validation/expectations.rb +0 -519
  266. data/lib/familia/validation/validation_helpers.rb +0 -443
  267. data/lib/familia/validation/validator.rb +0 -412
  268. data/lib/familia/validation.rb +0 -140
  269. data/try/data_types/set_try.rb +0 -33
  270. data/try/features/autoloadable/autoloadable_try.rb +0 -61
  271. data/try/features/relationships/categorical_permissions_try.rb +0 -515
  272. data/try/features/safe_dump/safe_dump_autoloading_try.rb +0 -111
  273. data/try/validation/atomic_operations_try.rb.disabled +0 -320
  274. data/try/validation/command_validation_try.rb.disabled +0 -207
  275. data/try/validation/performance_validation_try.rb.disabled +0 -324
  276. 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
177
+ # vault.save
178
+ #
179
+ # # Step 3: Re-encrypt existing records
180
+ # vault.re_encrypt_fields! # Uses current key version
178
181
  # vault.save
179
182
  #
180
- # # Step 3: After all data is re-encrypted, remove legacy key
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,15 +259,17 @@ module Familia
255
259
  # - Insider threats with application access
256
260
  #
257
261
  module EncryptedFields
262
+ Familia::Base.add_feature self, :encrypted_fields
263
+
258
264
  def self.included(base)
259
- Familia.trace :LOADED, self, base, caller(1..1) if Familia.debug?
260
- base.extend ClassMethods
265
+ Familia.trace :LOADED, self, base if Familia.debug?
266
+ base.extend ModelClassMethods
261
267
 
262
268
  # Initialize encrypted fields tracking
263
269
  base.instance_variable_set(:@encrypted_fields, []) unless base.instance_variable_defined?(:@encrypted_fields)
264
270
  end
265
271
 
266
- module ClassMethods
272
+ module ModelClassMethods
267
273
  # Define an encrypted field that transparently encrypts/decrypts values
268
274
  #
269
275
  # Encrypted fields are stored as JSON objects containing the encrypted
@@ -287,13 +293,13 @@ module Familia
287
293
  # encrypted_field :content, aad_fields: [:doc_id, :owner_id]
288
294
  # end
289
295
  #
290
- def encrypted_field(name, aad_fields: [], **kwargs)
296
+ def encrypted_field(name, aad_fields: [], **)
291
297
  @encrypted_fields ||= []
292
298
  @encrypted_fields << name unless @encrypted_fields.include?(name)
293
299
 
294
300
  require_relative 'encrypted_fields/encrypted_field_type'
295
301
 
296
- field_type = EncryptedFieldType.new(name, aad_fields: aad_fields, **kwargs)
302
+ field_type = EncryptedFieldType.new(name, aad_fields: aad_fields, **)
297
303
  register_field_type(field_type)
298
304
  end
299
305
 
@@ -324,7 +330,7 @@ module Familia
324
330
  algorithm: provider.algorithm_name,
325
331
  key_size: provider.key_size,
326
332
  nonce_size: provider.nonce_size,
327
- tag_size: provider.tag_size
333
+ tag_size: provider.tag_size,
328
334
  }
329
335
  end
330
336
  end
@@ -357,9 +363,7 @@ module Familia
357
363
  def clear_encrypted_fields!
358
364
  self.class.encrypted_fields.each do |field_name|
359
365
  field_value = instance_variable_get("@#{field_name}")
360
- if field_value.respond_to?(:clear!)
361
- field_value.clear!
362
- end
366
+ field_value.clear! if field_value.respond_to?(:clear!)
363
367
  end
364
368
  end
365
369
 
@@ -419,19 +423,17 @@ module Familia
419
423
  self.class.encrypted_fields.each_with_object({}) do |field_name, status|
420
424
  field_value = instance_variable_get("@#{field_name}")
421
425
 
422
- if field_value.nil?
423
- status[field_name] = { encrypted: false, value: nil }
424
- elsif field_value.respond_to?(:cleared?) && field_value.cleared?
425
- status[field_name] = { encrypted: true, cleared: true }
426
- elsif field_value.respond_to?(:concealed?) && field_value.concealed?
427
- status[field_name] = { encrypted: true, algorithm: "unknown", cleared: false }
428
- else
429
- status[field_name] = { encrypted: false, value: "[CONCEALED]" }
430
- 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
431
435
  end
432
436
  end
433
-
434
- Familia::Base.add_feature self, :encrypted_fields
435
437
  end
436
438
  end
437
439
  end
@@ -0,0 +1,61 @@
1
+ # lib/familia/features/expiration/extensions.rb
2
+
3
+ module Familia
4
+ # Add a default update_expiration method for all classes that include
5
+ # Familia::Base. Since expiration is a core feature, we can confidently
6
+ # call `horreum_instance.update_expiration` without defensive programming
7
+ # even when expiration is not enabled for the horreum_instance class.
8
+ module Base
9
+ # Base implementation of update_expiration that maintains API compatibility
10
+ # with the :expiration feature's implementation.
11
+ #
12
+ # This is a no-op implementation that gets overridden by the :expiration
13
+ # feature. It accepts an optional default_expiration parameter to maintain
14
+ # interface compatibility with the overriding implementations.
15
+ #
16
+ # @param default_expiration [Numeric, nil] Time To Live in seconds
17
+ # @return [nil] Always returns nil for the base implementation
18
+ #
19
+ # @note This is a no-op implementation. Classes that need expiration
20
+ # functionality should include the :expiration feature.
21
+ #
22
+ # @example Enable expiration feature
23
+ # class MyModel < Familia::Horreum
24
+ # feature :expiration
25
+ # default_expiration 1.hour
26
+ # end
27
+ #
28
+ def update_expiration(default_expiration: nil)
29
+ Familia.ld <<~LOG
30
+ [update_expiration] Expiration feature not enabled for #{self.class}.
31
+ Key: #{dbkey} Arg: #{default_expiration} (caller: #{caller(1..1)})
32
+ LOG
33
+ nil
34
+ end
35
+
36
+ # Base implementation of ttl that returns -1 (no expiration set)
37
+ #
38
+ # @return [Integer] Always returns -1 for the base implementation
39
+ #
40
+ def ttl
41
+ -1
42
+ end
43
+
44
+ # Base implementation of expires? that returns false
45
+ #
46
+ # @return [Boolean] Always returns false for the base implementation
47
+ #
48
+ def expires?
49
+ false
50
+ end
51
+
52
+ # Base implementation of expired? that returns false
53
+ #
54
+ # @param _threshold [Numeric] Ignored in base implementation
55
+ # @return [Boolean] Always returns false for the base implementation
56
+ #
57
+ def expired?(_threshold = 0)
58
+ false
59
+ end
60
+ end
61
+ end
@@ -1,13 +1,15 @@
1
1
  # lib/familia/features/expiration.rb
2
2
 
3
+ require_relative 'expiration/extensions'
4
+
3
5
  module Familia
4
6
  module Features
5
7
  # Expiration is a feature that provides Time To Live (TTL) management for Familia
6
- # objects and their associated Redis/Valkey data structures. It enables automatic
8
+ # objects and their associated Valkey/Redis data structures. It enables automatic
7
9
  # data cleanup and supports cascading expiration across related objects.
8
10
  #
9
11
  # This feature allows you to:
10
- # - Set default expiration times at the class level
12
+ # - UnsortedSet default expiration times at the class level
11
13
  # - Update expiration times for individual objects
12
14
  # - Cascade expiration settings to related data structures
13
15
  # - Query remaining TTL for objects
@@ -33,7 +35,7 @@ module Familia
33
35
  # session.update_expiration(30.minutes)
34
36
  # session.ttl # => 1799
35
37
  #
36
- # # Set custom expiration for new objects
38
+ # # UnsortedSet custom expiration for new objects
37
39
  # session.update_expiration(default_expiration: 2.hours)
38
40
  #
39
41
  # Class-Level Configuration:
@@ -141,33 +143,37 @@ module Familia
141
143
  #
142
144
  # Performance Considerations:
143
145
  #
144
- # - TTL operations are performed on Redis/Valkey side with minimal overhead
146
+ # - TTL operations are performed on Valkey/Redis side with minimal overhead
145
147
  # - Cascading expiration uses pipelining for efficiency when possible
146
- # - Zero expiration values skip Redis EXPIRE calls entirely
147
- # - 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)
148
150
  #
149
151
  module Expiration
150
152
  @default_expiration = nil
151
153
 
154
+ Familia::Base.add_feature self, :expiration
155
+
152
156
  using Familia::Refinements::TimeLiterals
153
157
 
154
158
  def self.included(base)
155
- Familia.trace :LOADED, self, base, caller(1..1) if Familia.debug?
156
- base.extend ClassMethods
159
+ Familia.trace :LOADED, self, base if Familia.debug?
160
+ base.extend ModelClassMethods
157
161
 
158
162
  # Initialize default_expiration instance variable if not already defined
159
163
  # This ensures the class has a place to store its default expiration setting
160
164
  return if base.instance_variable_defined?(:@default_expiration)
161
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).
162
168
  base.instance_variable_set(:@default_expiration, @default_expiration)
163
169
  end
164
170
 
165
- # Familia::Expiration::ClassMethods
171
+ # Familia::Expiration::ModelClassMethods
166
172
  #
167
- module ClassMethods
168
- # Set the default expiration time for instances of this class
173
+ module ModelClassMethods
174
+ # UnsortedSet the default expiration time for instances of this class
169
175
  #
170
- # @param expiration [Numeric] Time in seconds (can be fractional)
176
+ # @param value [Numeric] Time in seconds (can be fractional)
171
177
  #
172
178
  attr_writer :default_expiration
173
179
 
@@ -180,7 +186,7 @@ module Familia
180
186
  # @param num [Numeric, nil] Expiration time in seconds
181
187
  # @return [Float] The default expiration in seconds
182
188
  #
183
- # @example Set default expiration
189
+ # @example UnsortedSet default expiration
184
190
  # class MyModel < Familia::Horreum
185
191
  # feature :expiration
186
192
  # default_expiration 1.hour
@@ -195,7 +201,7 @@ module Familia
195
201
  end
196
202
  end
197
203
 
198
- # Set the default expiration time for this instance
204
+ # UnsortedSet the default expiration time for this instance
199
205
  #
200
206
  # @param num [Numeric] Expiration time in seconds
201
207
  #
@@ -214,9 +220,9 @@ module Familia
214
220
  @default_expiration || self.class.default_expiration
215
221
  end
216
222
 
217
- # 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
218
224
  #
219
- # 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,
220
226
  # after which it will be automatically removed. The method also handles
221
227
  # cascading expiration to related data structures when applicable.
222
228
  #
@@ -262,15 +268,17 @@ module Familia
262
268
  # don't want to silently fail at setting expirations and cause data
263
269
  # retention issues (e.g. not removed in a timely fashion).
264
270
  unless default_expiration.is_a?(Numeric)
265
- 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})"
266
273
  end
267
274
 
268
275
  unless default_expiration >= 0
269
- 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})"
270
278
  end
271
279
 
272
280
  # If zero, simply skip setting an expiry for this key. If we were to set
273
- # 0, Redis would drop the key immediately.
281
+ # 0, Valkey/Redis would drop the key immediately.
274
282
  if default_expiration.zero?
275
283
  Familia.ld "[update_expiration] No expiration for #{self.class} (#{dbkey})"
276
284
  return true
@@ -278,8 +286,9 @@ module Familia
278
286
 
279
287
  Familia.ld "[update_expiration] Expires #{dbkey} in #{default_expiration} seconds"
280
288
 
281
- # Redis' EXPIRE command returns 1 if the timeout was set, 0 if key does
282
- # 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.
283
292
  expire(default_expiration)
284
293
  end
285
294
 
@@ -295,7 +304,7 @@ module Familia
295
304
  # expired_session.ttl # => -1
296
305
  #
297
306
  def ttl
298
- redis.ttl(dbkey)
307
+ dbclient.ttl(dbkey)
299
308
  end
300
309
 
301
310
  # Check if this object's data will expire
@@ -303,7 +312,7 @@ module Familia
303
312
  # @return [Boolean] true if TTL is set, false if data persists indefinitely
304
313
  #
305
314
  def expires?
306
- ttl > 0
315
+ ttl.positive?
307
316
  end
308
317
 
309
318
  # Check if this object's data has expired or will expire soon
@@ -321,6 +330,7 @@ module Familia
321
330
  current_ttl = ttl
322
331
  return false if current_ttl == -1 # no expiration set
323
332
  return true if current_ttl == -2 # key does not exist
333
+
324
334
  current_ttl <= threshold
325
335
  end
326
336
 
@@ -337,7 +347,7 @@ module Familia
337
347
  #
338
348
  def extend_expiration(duration)
339
349
  current_ttl = ttl
340
- return false if current_ttl < 0 # No current expiration set
350
+ return false unless current_ttl.positive? # no current expiration set
341
351
 
342
352
  new_ttl = current_ttl + duration.to_f
343
353
  expire(new_ttl)
@@ -351,70 +361,8 @@ module Familia
351
361
  # session.persist!
352
362
  #
353
363
  def persist!
354
- redis.persist(dbkey)
364
+ dbclient.persist(dbkey)
355
365
  end
356
-
357
- Familia::Base.add_feature self, :expiration
358
- end
359
- end
360
- end
361
-
362
- module Familia
363
- # Add a default update_expiration method for all classes that include
364
- # Familia::Base. Since expiration is a core feature, we can confidently
365
- # call `horreum_instance.update_expiration` without defensive programming
366
- # even when expiration is not enabled for the horreum_instance class.
367
- module Base
368
- # Base implementation of update_expiration that maintains API compatibility
369
- # with the :expiration feature's implementation.
370
- #
371
- # This is a no-op implementation that gets overridden by the :expiration
372
- # feature. It accepts an optional default_expiration parameter to maintain
373
- # interface compatibility with the overriding implementations.
374
- #
375
- # @param default_expiration [Numeric, nil] Time To Live in seconds
376
- # @return [nil] Always returns nil for the base implementation
377
- #
378
- # @note This is a no-op implementation. Classes that need expiration
379
- # functionality should include the :expiration feature.
380
- #
381
- # @example Enable expiration feature
382
- # class MyModel < Familia::Horreum
383
- # feature :expiration
384
- # default_expiration 1.hour
385
- # end
386
- #
387
- def update_expiration(default_expiration: nil)
388
- Familia.ld <<~LOG
389
- [update_expiration] Expiration feature not enabled for #{self.class}.
390
- Key: #{dbkey} Arg: #{default_expiration} (caller: #{caller(1..1)})
391
- LOG
392
- nil
393
- end
394
-
395
- # Base implementation of ttl that returns -1 (no expiration set)
396
- #
397
- # @return [Integer] Always returns -1 for the base implementation
398
- #
399
- def ttl
400
- -1
401
- end
402
-
403
- # Base implementation of expires? that returns false
404
- #
405
- # @return [Boolean] Always returns false for the base implementation
406
- #
407
- def expires?
408
- false
409
- end
410
-
411
- # Base implementation of expired? that returns false
412
- #
413
- # @param threshold [Numeric] Ignored in base implementation
414
- # @return [Boolean] Always returns false for the base implementation
415
- #
416
- def expired?(_threshold = 0)
417
- false
418
366
  end
419
367
  end
420
368
  end
@@ -5,9 +5,11 @@ module Familia
5
5
  # Familia::Features::ExternalIdentifier
6
6
  #
7
7
  module ExternalIdentifier
8
+ Familia::Base.add_feature self, :external_identifier, depends_on: [:object_identifier]
9
+
8
10
  def self.included(base)
9
- Familia.trace :LOADED, self, base, caller(1..1) if Familia.debug?
10
- base.extend ClassMethods
11
+ Familia.trace :LOADED, self, base if Familia.debug?
12
+ base.extend ModelClassMethods
11
13
 
12
14
  # Ensure default prefix is set in feature options
13
15
  base.add_feature_options(:external_identifier, prefix: 'ext')
@@ -41,11 +43,9 @@ module Familia
41
43
  # feature :external_identifier
42
44
  # field :email
43
45
  # end
44
- #
45
46
  # user = User.new(email: 'user@example.com')
46
47
  # user.objid # => "01234567-89ab-7def-8000-123456789abc"
47
48
  # user.extid # => "ext_abc123def456ghi789" (deterministic from objid)
48
- #
49
49
  # # Same objid always produces same extid
50
50
  # user2 = User.new(objid: user.objid, email: 'user@example.com')
51
51
  # user2.extid # => "ext_abc123def456ghi789" (identical to user.extid)
@@ -75,7 +75,7 @@ module Familia
75
75
 
76
76
  instance_variable_set(:"@#{field_name}", derived_extid)
77
77
 
78
- # Update mapping if we have an identifier
78
+ # Update mapping if we have an identifier (objid)
79
79
  self.class.extid_lookup[derived_extid] = identifier if respond_to?(:identifier) && identifier
80
80
 
81
81
  derived_extid
@@ -86,7 +86,7 @@ module Familia
86
86
  # Override setter to preserve values during initialization
87
87
  #
88
88
  # This ensures that values passed during object initialization
89
- # (e.g., when loading from Redis) are preserved and not overwritten
89
+ # (e.g., when loading from Valkey/Redis) are preserved and not overwritten
90
90
  # by the lazy generation logic.
91
91
  #
92
92
  # @param klass [Class] The class to define the method on
@@ -129,9 +129,9 @@ module Familia
129
129
  end
130
130
  end
131
131
 
132
- # ExternalIdentifier::ClassMethods
132
+ # ExternalIdentifier::ModelClassMethods
133
133
  #
134
- module ClassMethods
134
+ module ModelClassMethods
135
135
  # Find an object by its external identifier
136
136
  #
137
137
  # @param extid [String] The external identifier to search for
@@ -142,7 +142,7 @@ module Familia
142
142
 
143
143
  if Familia.debug?
144
144
  reference = caller(1..1).first
145
- Familia.trace :FIND_BY_EXTID, Familia.dbclient, extid, reference
145
+ Familia.trace :FIND_BY_EXTID, nil, extid, reference
146
146
  end
147
147
 
148
148
  # Look up the primary ID from the external ID mapping
@@ -153,7 +153,8 @@ module Familia
153
153
  find_by_id(primary_id)
154
154
  rescue Familia::NotFound
155
155
  # If the object was deleted but mapping wasn't cleaned up
156
- extid_lookup.remove_field(extid)
156
+ # we could autoclean here, as long as we log it.
157
+ # extid_lookup.remove_field(extid)
157
158
  nil
158
159
  end
159
160
  end
@@ -303,8 +304,6 @@ module Familia
303
304
  normalized
304
305
  end
305
306
  end
306
-
307
- Familia::Base.add_feature self, :external_identifier, depends_on: [:object_identifier]
308
307
  end
309
308
  end
310
309
  end