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.
- checksums.yaml +4 -4
- data/.github/workflows/code-quality.yml +138 -0
- data/.github/workflows/code-smellage.yml +145 -0
- data/.github/workflows/docs.yml +31 -8
- data/.gitignore +1 -1
- data/.pre-commit-config.yaml +7 -1
- data/.reek.yml +98 -0
- data/.rubocop.yml +48 -10
- data/.talismanrc +9 -0
- data/.yardopts +18 -13
- data/CHANGELOG.rst +64 -4
- data/CLAUDE.md +1 -1
- data/Gemfile +6 -5
- data/Gemfile.lock +99 -23
- data/LICENSE.txt +1 -1
- data/README.md +285 -85
- data/changelog.d/README.md +2 -2
- data/docs/archive/FAMILIA_RELATIONSHIPS.md +22 -22
- data/docs/archive/FAMILIA_TECHNICAL.md +41 -41
- data/docs/archive/FAMILIA_UPDATE.md +3 -3
- data/docs/archive/README.md +3 -2
- data/docs/{guides/API-Reference.md → archive/api-reference.md} +87 -101
- data/docs/conf.py +29 -0
- data/docs/guides/{Field-System-Guide.md → core-field-system.md} +9 -9
- data/docs/guides/feature-encrypted-fields.md +785 -0
- data/docs/guides/{Expiration-Feature-Guide.md → feature-expiration.md} +11 -2
- data/docs/guides/feature-external-identifiers.md +637 -0
- data/docs/guides/feature-object-identifiers.md +435 -0
- data/docs/guides/{Quantization-Feature-Guide.md → feature-quantization.md} +94 -29
- data/docs/guides/feature-relationships-methods.md +684 -0
- data/docs/guides/feature-relationships.md +200 -0
- data/docs/guides/{Features-System-Developer-Guide.md → feature-system-devs.md} +4 -4
- data/docs/guides/{Feature-System-Guide.md → feature-system.md} +5 -5
- data/docs/guides/{Transient-Fields-Guide.md → feature-transient-fields.md} +2 -2
- data/docs/guides/{Implementation-Guide.md → implementation.md} +3 -3
- data/docs/guides/index.md +176 -0
- data/docs/guides/{Security-Model.md → security-model.md} +1 -1
- data/docs/migrating/v2.0.0-pre.md +1 -1
- data/docs/migrating/v2.0.0-pre11.md +2 -2
- data/docs/migrating/v2.0.0-pre12.md +2 -2
- data/docs/migrating/v2.0.0-pre5.md +33 -12
- data/docs/migrating/v2.0.0-pre6.md +2 -2
- data/docs/migrating/v2.0.0-pre7.md +8 -8
- data/docs/overview.md +623 -19
- data/docs/reference/api-technical.md +1365 -0
- data/examples/autoloader/mega_customer/features/deprecated_fields.rb +7 -0
- data/examples/autoloader/mega_customer/safe_dump_fields.rb +1 -1
- data/examples/autoloader/mega_customer.rb +3 -1
- data/examples/encrypted_fields.rb +378 -0
- data/examples/json_usage_patterns.rb +144 -0
- data/examples/relationships.rb +13 -13
- data/examples/safe_dump.rb +6 -6
- data/examples/single_connection_transaction_confusions.rb +379 -0
- data/lib/familia/base.rb +49 -10
- data/lib/familia/connection/handlers.rb +223 -0
- data/lib/familia/connection/individual_command_proxy.rb +64 -0
- data/lib/familia/connection/middleware.rb +75 -0
- data/lib/familia/connection/operation_core.rb +93 -0
- data/lib/familia/connection/operations.rb +277 -0
- data/lib/familia/connection/pipeline_core.rb +87 -0
- data/lib/familia/connection/transaction_core.rb +100 -0
- data/lib/familia/connection.rb +60 -186
- data/lib/familia/data_type/commands.rb +53 -51
- data/lib/familia/data_type/serialization.rb +108 -107
- data/lib/familia/data_type/types/counter.rb +1 -1
- data/lib/familia/data_type/types/hashkey.rb +13 -10
- data/lib/familia/data_type/types/{list.rb → listkey.rb} +13 -5
- data/lib/familia/data_type/types/lock.rb +3 -2
- data/lib/familia/data_type/types/sorted_set.rb +26 -15
- data/lib/familia/data_type/types/{string.rb → stringkey.rb} +7 -5
- data/lib/familia/data_type/types/unsorted_set.rb +20 -27
- data/lib/familia/data_type.rb +75 -47
- data/lib/familia/distinguisher.rb +85 -0
- data/lib/familia/encryption/encrypted_data.rb +15 -24
- data/lib/familia/encryption/manager.rb +6 -4
- data/lib/familia/encryption/providers/aes_gcm_provider.rb +1 -1
- data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +7 -9
- data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +4 -5
- data/lib/familia/encryption/request_cache.rb +7 -7
- data/lib/familia/encryption.rb +2 -3
- data/lib/familia/errors.rb +9 -3
- data/lib/familia/features/autoloader.rb +30 -12
- data/lib/familia/features/encrypted_fields/concealed_string.rb +3 -4
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +13 -14
- data/lib/familia/features/encrypted_fields.rb +66 -64
- data/lib/familia/features/expiration/extensions.rb +1 -1
- data/lib/familia/features/expiration.rb +31 -26
- data/lib/familia/features/external_identifier.rb +9 -12
- data/lib/familia/features/object_identifier.rb +56 -19
- data/lib/familia/features/quantization.rb +16 -21
- data/lib/familia/features/relationships/README.md +97 -0
- data/lib/familia/features/relationships/collection_operations.rb +104 -0
- data/lib/familia/features/relationships/indexing/multi_index_generators.rb +202 -0
- data/lib/familia/features/relationships/indexing/unique_index_generators.rb +301 -0
- data/lib/familia/features/relationships/indexing.rb +176 -256
- data/lib/familia/features/relationships/indexing_relationship.rb +35 -0
- data/lib/familia/features/relationships/participation/participant_methods.rb +160 -0
- data/lib/familia/features/relationships/participation/target_methods.rb +225 -0
- data/lib/familia/features/relationships/participation.rb +656 -0
- data/lib/familia/features/relationships/participation_relationship.rb +31 -0
- data/lib/familia/features/relationships/score_encoding.rb +20 -20
- data/lib/familia/features/relationships.rb +65 -266
- data/lib/familia/features/safe_dump.rb +127 -130
- data/lib/familia/features/transient_fields/redacted_string.rb +6 -6
- data/lib/familia/features/transient_fields/transient_field_type.rb +5 -5
- data/lib/familia/features/transient_fields.rb +3 -5
- data/lib/familia/features.rb +4 -13
- data/lib/familia/field_type.rb +24 -4
- data/lib/familia/horreum/core/connection.rb +229 -26
- data/lib/familia/horreum/core/database_commands.rb +27 -17
- data/lib/familia/horreum/core/serialization.rb +40 -20
- data/lib/familia/horreum/core/utils.rb +2 -1
- data/lib/familia/horreum/shared/settings.rb +2 -1
- data/lib/familia/horreum/subclass/definition.rb +33 -45
- data/lib/familia/horreum/subclass/management.rb +72 -24
- data/lib/familia/horreum/subclass/related_fields_management.rb +82 -21
- data/lib/familia/horreum.rb +196 -114
- data/lib/familia/json_serializer.rb +0 -1
- data/lib/familia/logging.rb +11 -114
- data/lib/familia/refinements/dear_json.rb +122 -0
- data/lib/familia/refinements/logger_trace.rb +20 -17
- data/lib/familia/refinements/stylize_words.rb +65 -0
- data/lib/familia/refinements/time_literals.rb +60 -52
- data/lib/familia/refinements.rb +2 -1
- data/lib/familia/secure_identifier.rb +60 -28
- data/lib/familia/settings.rb +83 -7
- data/lib/familia/utils.rb +5 -87
- data/lib/familia/verifiable_identifier.rb +4 -4
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +72 -14
- data/lib/middleware/database_middleware.rb +56 -14
- data/lib/{familia/multi_result.rb → multi_result.rb} +23 -16
- data/try/configuration/scenarios_try.rb +1 -1
- data/try/connection/fiber_context_preservation_try.rb +250 -0
- data/try/connection/handler_constraints_try.rb +59 -0
- data/try/connection/operation_mode_guards_try.rb +208 -0
- data/try/connection/pipeline_fallback_integration_try.rb +128 -0
- data/try/connection/responsibility_chain_tracking_try.rb +72 -0
- data/try/connection/transaction_fallback_integration_try.rb +288 -0
- data/try/connection/transaction_mode_permissive_try.rb +153 -0
- data/try/connection/transaction_mode_strict_try.rb +98 -0
- data/try/connection/transaction_mode_warn_try.rb +131 -0
- data/try/connection/transaction_modes_try.rb +249 -0
- data/try/core/autoloader_try.rb +120 -2
- data/try/core/connection_try.rb +7 -7
- data/try/core/conventional_inheritance_try.rb +130 -0
- data/try/core/create_method_try.rb +15 -23
- data/try/core/database_consistency_try.rb +10 -10
- data/try/core/errors_try.rb +8 -11
- data/try/core/familia_extended_try.rb +2 -2
- data/try/core/familia_members_methods_try.rb +76 -0
- data/try/core/isolated_dbclient_try.rb +165 -0
- data/try/core/middleware_try.rb +16 -16
- data/try/core/persistence_operations_try.rb +4 -4
- data/try/core/pools_try.rb +42 -26
- data/try/core/secure_identifier_try.rb +28 -24
- data/try/core/time_utils_try.rb +10 -10
- data/try/core/tools_try.rb +1 -1
- data/try/core/utils_try.rb +2 -2
- data/try/data_types/boolean_try.rb +4 -4
- data/try/data_types/datatype_base_try.rb +0 -2
- data/try/data_types/list_try.rb +10 -10
- data/try/data_types/sorted_set_try.rb +5 -5
- data/try/data_types/string_try.rb +12 -12
- data/try/data_types/unsortedset_try.rb +33 -0
- data/try/debugging/cache_behavior_tracer.rb +7 -7
- data/try/debugging/debug_aad_process.rb +1 -1
- data/try/debugging/debug_concealed_internal.rb +1 -1
- data/try/debugging/debug_cross_context.rb +1 -1
- data/try/debugging/debug_fresh_cross_context.rb +1 -1
- data/try/debugging/encryption_method_tracer.rb +10 -10
- data/try/edge_cases/hash_symbolization_try.rb +1 -1
- data/try/edge_cases/ttl_side_effects_try.rb +1 -1
- data/try/encryption/config_persistence_try.rb +2 -2
- data/try/encryption/encryption_core_try.rb +19 -19
- data/try/encryption/instance_variable_scope_try.rb +1 -1
- data/try/encryption/module_loading_try.rb +2 -2
- data/try/encryption/providers/aes_gcm_provider_try.rb +1 -1
- data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +1 -1
- data/try/encryption/secure_memory_handling_try.rb +1 -1
- data/try/features/encrypted_fields/concealed_string_core_try.rb +11 -7
- data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +3 -3
- data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +10 -10
- data/try/features/encrypted_fields/encrypted_fields_security_try.rb +14 -14
- data/try/features/encrypted_fields/error_conditions_try.rb +7 -7
- data/try/features/encrypted_fields/fresh_key_try.rb +1 -1
- data/try/features/encrypted_fields/nonce_uniqueness_try.rb +1 -1
- data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +7 -7
- data/try/features/encrypted_fields/universal_serialization_safety_try.rb +13 -20
- data/try/features/external_identifier/external_identifier_try.rb +1 -1
- data/try/features/feature_dependencies_try.rb +3 -3
- data/try/features/object_identifier/object_identifier_integration_try.rb +28 -34
- data/try/features/object_identifier/object_identifier_try.rb +10 -0
- data/try/features/quantization/quantization_try.rb +1 -1
- data/try/features/relationships/indexing_commands_verification_try.rb +136 -0
- data/try/features/relationships/indexing_try.rb +433 -0
- data/try/features/relationships/participation_commands_verification_spec.rb +102 -0
- data/try/features/relationships/participation_commands_verification_try.rb +105 -0
- data/try/features/relationships/participation_performance_improvements_try.rb +124 -0
- data/try/features/relationships/participation_reverse_index_try.rb +196 -0
- data/try/features/relationships/relationships_api_changes_try.rb +72 -71
- data/try/features/relationships/relationships_edge_cases_try.rb +15 -18
- data/try/features/relationships/relationships_performance_minimal_try.rb +2 -2
- data/try/features/relationships/relationships_performance_simple_try.rb +8 -8
- data/try/features/relationships/relationships_performance_try.rb +20 -20
- data/try/features/relationships/relationships_try.rb +27 -38
- data/try/features/safe_dump/safe_dump_advanced_try.rb +2 -2
- data/try/features/transient_fields/refresh_reset_try.rb +1 -1
- data/try/features/transient_fields/simple_refresh_test.rb +1 -1
- data/try/helpers/test_cleanup.rb +86 -0
- data/try/helpers/test_helpers.rb +3 -3
- data/try/horreum/base_try.rb +3 -2
- data/try/horreum/commands_try.rb +1 -1
- data/try/horreum/destroy_related_fields_cleanup_try.rb +330 -0
- data/try/horreum/initialization_try.rb +11 -7
- data/try/horreum/relations_try.rb +21 -13
- data/try/horreum/serialization_try.rb +12 -11
- data/try/integration/cross_component_try.rb +3 -3
- data/try/memory/memory_basic_test.rb +1 -1
- data/try/memory/memory_docker_ruby_dump.sh +1 -1
- data/try/models/customer_safe_dump_try.rb +1 -1
- data/try/models/customer_try.rb +8 -10
- data/try/models/datatype_base_try.rb +3 -3
- data/try/models/familia_object_try.rb +9 -8
- data/try/performance/benchmarks_try.rb +2 -2
- data/try/prototypes/atomic_saves_v1_context_proxy.rb +2 -2
- data/try/prototypes/atomic_saves_v3_connection_pool.rb +3 -3
- data/try/prototypes/atomic_saves_v4.rb +1 -1
- data/try/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +4 -4
- data/try/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
- data/try/prototypes/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
- data/try/prototypes/pooling/lib/connection_pool_metrics.rb +5 -5
- data/try/prototypes/pooling/lib/connection_pool_stress_test.rb +26 -26
- data/try/prototypes/pooling/lib/connection_pool_threading_models.rb +7 -7
- data/try/prototypes/pooling/lib/visualize_stress_results.rb +1 -1
- data/try/prototypes/pooling/pool_siege.rb +11 -11
- data/try/prototypes/pooling/run_stress_tests.rb +7 -7
- data/try/refinements/dear_json_array_methods_try.rb +53 -0
- data/try/refinements/dear_json_hash_methods_try.rb +54 -0
- data/try/refinements/logger_trace_methods_try.rb +44 -0
- data/try/refinements/time_literals_numeric_methods_try.rb +141 -0
- data/try/refinements/time_literals_string_methods_try.rb +80 -0
- metadata +75 -43
- data/.rubocop_todo.yml +0 -208
- data/docs/connection_pooling.md +0 -192
- data/docs/guides/Connection-Pooling-Guide.md +0 -437
- data/docs/guides/Encrypted-Fields-Overview.md +0 -101
- data/docs/guides/Feature-System-Autoloading.md +0 -198
- data/docs/guides/Home.md +0 -116
- data/docs/guides/Relationships-Guide.md +0 -737
- data/docs/guides/relationships-methods.md +0 -266
- data/docs/reference/auditing_database_commands.rb +0 -228
- data/examples/permissions.rb +0 -240
- data/lib/familia/features/relationships/cascading.rb +0 -437
- data/lib/familia/features/relationships/membership.rb +0 -497
- data/lib/familia/features/relationships/permission_management.rb +0 -264
- data/lib/familia/features/relationships/querying.rb +0 -615
- data/lib/familia/features/relationships/redis_operations.rb +0 -274
- data/lib/familia/features/relationships/tracking.rb +0 -418
- data/lib/familia/refinements/snake_case.rb +0 -40
- data/lib/familia/validation/command_recorder.rb +0 -336
- data/lib/familia/validation/expectations.rb +0 -519
- data/lib/familia/validation/validation_helpers.rb +0 -443
- data/lib/familia/validation/validator.rb +0 -412
- data/lib/familia/validation.rb +0 -140
- data/try/data_types/set_try.rb +0 -33
- data/try/features/relationships/categorical_permissions_try.rb +0 -515
- data/try/features/safe_dump/module_based_extensions_try.rb +0 -100
- data/try/features/safe_dump/safe_dump_autoloading_try.rb +0 -107
- data/try/validation/atomic_operations_try.rb.disabled +0 -320
- data/try/validation/command_validation_try.rb.disabled +0 -207
- data/try/validation/performance_validation_try.rb.disabled +0 -324
- 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
|
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.
|
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
|
-
#
|
100
|
+
# Request-Level Caching:
|
101
101
|
#
|
102
|
-
# For
|
102
|
+
# For performance optimization, enable key derivation caching per request:
|
103
103
|
#
|
104
|
-
#
|
105
|
-
#
|
106
|
-
#
|
107
|
-
#
|
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
|
-
#
|
113
|
-
#
|
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
|
-
#
|
113
|
+
# Preventing Accidental Leakage:
|
119
114
|
#
|
120
|
-
# Encrypted fields return ConcealedString objects
|
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.
|
129
|
-
#
|
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
|
-
# #
|
134
|
-
#
|
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
|
-
# #
|
147
|
-
#
|
148
|
-
# # => Familia::EncryptionError:
|
139
|
+
# # Missing encryption configuration
|
140
|
+
# Familia.config.encryption_keys = {}
|
141
|
+
# vault.secret_key # => Familia::EncryptionError: No encryption keys configured
|
149
142
|
#
|
150
|
-
# #
|
151
|
-
#
|
152
|
-
# vault.secret_key # => Familia::EncryptionError:
|
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
|
-
# #
|
149
|
+
# # Configure versioned encryption keys (required)
|
157
150
|
# Familia.configure do |config|
|
158
|
-
# config.
|
159
|
-
#
|
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
|
-
# #
|
163
|
-
#
|
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
|
166
|
+
# # Step 1: Add new key version while keeping old keys
|
171
167
|
# Familia.configure do |config|
|
172
|
-
# config.
|
173
|
-
#
|
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
|
177
|
-
# vault.secret_key = "new-secret" # Encrypted with
|
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:
|
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.
|
212
|
-
#
|
213
|
-
#
|
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
|
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
|
262
|
-
base.extend
|
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
|
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: [], **
|
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, **
|
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
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
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
|
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
|
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
|
-
# -
|
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
|
-
# #
|
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
|
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
|
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
|
160
|
-
base.extend
|
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::
|
171
|
+
# Familia::Expiration::ModelClassMethods
|
170
172
|
#
|
171
|
-
module
|
172
|
-
#
|
173
|
+
module ModelClassMethods
|
174
|
+
# UnsortedSet the default expiration time for instances of this class
|
173
175
|
#
|
174
|
-
# @param
|
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
|
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
|
-
#
|
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
|
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,
|
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,
|
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
|
286
|
-
# not exist or the timeout could not be set. Via redis-rb,
|
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
|
-
|
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
|
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
|
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
|
-
|
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
|
13
|
-
base.extend
|
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::
|
132
|
+
# ExternalIdentifier::ModelClassMethods
|
136
133
|
#
|
137
|
-
module
|
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,
|
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
|
-
|
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
|