familia 2.0.0.pre4 → 2.0.0.pre6

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 (178) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/.rubocop_todo.yml +17 -17
  4. data/CLAUDE.md +11 -8
  5. data/Gemfile +5 -1
  6. data/Gemfile.lock +19 -3
  7. data/README.md +36 -157
  8. data/docs/overview.md +359 -0
  9. data/docs/wiki/API-Reference.md +347 -0
  10. data/docs/wiki/Connection-Pooling-Guide.md +437 -0
  11. data/docs/wiki/Encrypted-Fields-Overview.md +101 -0
  12. data/docs/wiki/Expiration-Feature-Guide.md +596 -0
  13. data/docs/wiki/Feature-System-Guide.md +600 -0
  14. data/docs/wiki/Features-System-Developer-Guide.md +892 -0
  15. data/docs/wiki/Field-System-Guide.md +784 -0
  16. data/docs/wiki/Home.md +106 -0
  17. data/docs/wiki/Implementation-Guide.md +276 -0
  18. data/docs/wiki/Quantization-Feature-Guide.md +721 -0
  19. data/docs/wiki/RelatableObjects-Guide.md +563 -0
  20. data/docs/wiki/Security-Model.md +183 -0
  21. data/docs/wiki/Transient-Fields-Guide.md +280 -0
  22. data/lib/familia/base.rb +18 -27
  23. data/lib/familia/connection.rb +6 -5
  24. data/lib/familia/{datatype → data_type}/commands.rb +2 -5
  25. data/lib/familia/{datatype → data_type}/serialization.rb +8 -10
  26. data/lib/familia/data_type/types/counter.rb +38 -0
  27. data/lib/familia/{datatype → data_type}/types/hashkey.rb +20 -2
  28. data/lib/familia/{datatype → data_type}/types/list.rb +17 -18
  29. data/lib/familia/data_type/types/lock.rb +43 -0
  30. data/lib/familia/{datatype → data_type}/types/sorted_set.rb +17 -17
  31. data/lib/familia/{datatype → data_type}/types/string.rb +11 -3
  32. data/lib/familia/{datatype → data_type}/types/unsorted_set.rb +17 -18
  33. data/lib/familia/{datatype.rb → data_type.rb} +12 -14
  34. data/lib/familia/encryption/encrypted_data.rb +137 -0
  35. data/lib/familia/encryption/manager.rb +119 -0
  36. data/lib/familia/encryption/provider.rb +49 -0
  37. data/lib/familia/encryption/providers/aes_gcm_provider.rb +123 -0
  38. data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +184 -0
  39. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +138 -0
  40. data/lib/familia/encryption/registry.rb +50 -0
  41. data/lib/familia/encryption.rb +178 -0
  42. data/lib/familia/encryption_request_cache.rb +68 -0
  43. data/lib/familia/errors.rb +17 -3
  44. data/lib/familia/features/encrypted_fields/concealed_string.rb +295 -0
  45. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +221 -0
  46. data/lib/familia/features/encrypted_fields.rb +28 -0
  47. data/lib/familia/features/expiration.rb +107 -77
  48. data/lib/familia/features/quantization.rb +5 -9
  49. data/lib/familia/features/relatable_objects.rb +2 -4
  50. data/lib/familia/features/safe_dump.rb +14 -17
  51. data/lib/familia/features/transient_fields/redacted_string.rb +159 -0
  52. data/lib/familia/features/transient_fields/single_use_redacted_string.rb +62 -0
  53. data/lib/familia/features/transient_fields/transient_field_type.rb +139 -0
  54. data/lib/familia/features/transient_fields.rb +47 -0
  55. data/lib/familia/features.rb +40 -24
  56. data/lib/familia/field_type.rb +273 -0
  57. data/lib/familia/horreum/{connection.rb → core/connection.rb} +6 -15
  58. data/lib/familia/horreum/{commands.rb → core/database_commands.rb} +20 -21
  59. data/lib/familia/horreum/core/serialization.rb +535 -0
  60. data/lib/familia/horreum/{utils.rb → core/utils.rb} +9 -12
  61. data/lib/familia/horreum/core.rb +21 -0
  62. data/lib/familia/horreum/{settings.rb → shared/settings.rb} +10 -4
  63. data/lib/familia/horreum/subclass/definition.rb +469 -0
  64. data/lib/familia/horreum/{class_methods.rb → subclass/management.rb} +27 -250
  65. data/lib/familia/horreum/{related_fields_management.rb → subclass/related_fields_management.rb} +15 -10
  66. data/lib/familia/horreum.rb +30 -22
  67. data/lib/familia/logging.rb +14 -14
  68. data/lib/familia/settings.rb +39 -3
  69. data/lib/familia/utils.rb +45 -0
  70. data/lib/familia/version.rb +1 -1
  71. data/lib/familia.rb +3 -2
  72. data/try/core/base_enhancements_try.rb +115 -0
  73. data/try/core/connection_try.rb +0 -1
  74. data/try/core/create_method_try.rb +240 -0
  75. data/try/core/database_consistency_try.rb +299 -0
  76. data/try/core/errors_try.rb +25 -5
  77. data/try/core/familia_extended_try.rb +3 -4
  78. data/try/core/familia_try.rb +1 -2
  79. data/try/core/persistence_operations_try.rb +297 -0
  80. data/try/core/pools_try.rb +2 -2
  81. data/try/core/secure_identifier_try.rb +0 -1
  82. data/try/core/settings_try.rb +0 -1
  83. data/try/core/utils_try.rb +0 -1
  84. data/try/{datatypes → data_types}/boolean_try.rb +1 -2
  85. data/try/data_types/counter_try.rb +93 -0
  86. data/try/{datatypes → data_types}/datatype_base_try.rb +2 -3
  87. data/try/{datatypes → data_types}/hash_try.rb +1 -2
  88. data/try/{datatypes → data_types}/list_try.rb +1 -2
  89. data/try/data_types/lock_try.rb +133 -0
  90. data/try/{datatypes → data_types}/set_try.rb +1 -2
  91. data/try/{datatypes → data_types}/sorted_set_try.rb +1 -2
  92. data/try/{datatypes → data_types}/string_try.rb +1 -2
  93. data/try/debugging/README.md +32 -0
  94. data/try/debugging/cache_behavior_tracer.rb +91 -0
  95. data/try/debugging/debug_aad_process.rb +82 -0
  96. data/try/debugging/debug_concealed_internal.rb +59 -0
  97. data/try/debugging/debug_concealed_reveal.rb +61 -0
  98. data/try/debugging/debug_context_aad.rb +68 -0
  99. data/try/debugging/debug_context_simple.rb +80 -0
  100. data/try/debugging/debug_cross_context.rb +62 -0
  101. data/try/debugging/debug_database_load.rb +64 -0
  102. data/try/debugging/debug_encrypted_json_check.rb +53 -0
  103. data/try/debugging/debug_encrypted_json_step_by_step.rb +62 -0
  104. data/try/debugging/debug_exists_lifecycle.rb +54 -0
  105. data/try/debugging/debug_field_decrypt.rb +74 -0
  106. data/try/debugging/debug_fresh_cross_context.rb +73 -0
  107. data/try/debugging/debug_load_path.rb +66 -0
  108. data/try/debugging/debug_method_definition.rb +46 -0
  109. data/try/debugging/debug_method_resolution.rb +41 -0
  110. data/try/debugging/debug_minimal.rb +24 -0
  111. data/try/debugging/debug_provider.rb +68 -0
  112. data/try/debugging/debug_secure_behavior.rb +73 -0
  113. data/try/debugging/debug_string_class.rb +46 -0
  114. data/try/debugging/debug_test.rb +46 -0
  115. data/try/debugging/debug_test_design.rb +80 -0
  116. data/try/debugging/encryption_method_tracer.rb +138 -0
  117. data/try/debugging/provider_diagnostics.rb +110 -0
  118. data/try/edge_cases/hash_symbolization_try.rb +0 -1
  119. data/try/edge_cases/json_serialization_try.rb +0 -1
  120. data/try/edge_cases/reserved_keywords_try.rb +42 -11
  121. data/try/encryption/config_persistence_try.rb +192 -0
  122. data/try/encryption/encryption_core_try.rb +328 -0
  123. data/try/encryption/instance_variable_scope_try.rb +31 -0
  124. data/try/encryption/module_loading_try.rb +28 -0
  125. data/try/encryption/providers/aes_gcm_provider_try.rb +178 -0
  126. data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +169 -0
  127. data/try/encryption/roundtrip_validation_try.rb +28 -0
  128. data/try/encryption/secure_memory_handling_try.rb +125 -0
  129. data/try/features/encrypted_fields_core_try.rb +125 -0
  130. data/try/features/encrypted_fields_integration_try.rb +216 -0
  131. data/try/features/encrypted_fields_no_cache_security_try.rb +219 -0
  132. data/try/features/encrypted_fields_security_try.rb +377 -0
  133. data/try/features/encryption_fields/aad_protection_try.rb +138 -0
  134. data/try/features/encryption_fields/concealed_string_core_try.rb +250 -0
  135. data/try/features/encryption_fields/context_isolation_try.rb +141 -0
  136. data/try/features/encryption_fields/error_conditions_try.rb +116 -0
  137. data/try/features/encryption_fields/fresh_key_derivation_try.rb +128 -0
  138. data/try/features/encryption_fields/fresh_key_try.rb +168 -0
  139. data/try/features/encryption_fields/key_rotation_try.rb +123 -0
  140. data/try/features/encryption_fields/memory_security_try.rb +37 -0
  141. data/try/features/encryption_fields/missing_current_key_version_try.rb +23 -0
  142. data/try/features/encryption_fields/nonce_uniqueness_try.rb +56 -0
  143. data/try/features/encryption_fields/secure_by_default_behavior_try.rb +310 -0
  144. data/try/features/encryption_fields/thread_safety_try.rb +199 -0
  145. data/try/features/encryption_fields/universal_serialization_safety_try.rb +174 -0
  146. data/try/features/expiration_try.rb +0 -1
  147. data/try/features/feature_dependencies_try.rb +159 -0
  148. data/try/features/quantization_try.rb +0 -1
  149. data/try/features/real_feature_integration_try.rb +148 -0
  150. data/try/features/relatable_objects_try.rb +0 -1
  151. data/try/features/safe_dump_advanced_try.rb +0 -1
  152. data/try/features/safe_dump_try.rb +0 -1
  153. data/try/features/transient_fields/redacted_string_try.rb +248 -0
  154. data/try/features/transient_fields/refresh_reset_try.rb +164 -0
  155. data/try/features/transient_fields/simple_refresh_test.rb +50 -0
  156. data/try/features/transient_fields/single_use_redacted_string_try.rb +310 -0
  157. data/try/features/transient_fields_core_try.rb +181 -0
  158. data/try/features/transient_fields_integration_try.rb +260 -0
  159. data/try/helpers/test_helpers.rb +67 -0
  160. data/try/horreum/base_try.rb +157 -3
  161. data/try/horreum/enhanced_conflict_handling_try.rb +176 -0
  162. data/try/horreum/field_categories_try.rb +118 -0
  163. data/try/horreum/field_definition_try.rb +96 -0
  164. data/try/horreum/initialization_try.rb +1 -2
  165. data/try/horreum/relations_try.rb +1 -2
  166. data/try/horreum/serialization_persistent_fields_try.rb +165 -0
  167. data/try/horreum/serialization_try.rb +41 -7
  168. data/try/memory/memory_basic_test.rb +73 -0
  169. data/try/memory/memory_detailed_test.rb +121 -0
  170. data/try/memory/memory_docker_ruby_dump.sh +80 -0
  171. data/try/memory/memory_search_for_string.rb +83 -0
  172. data/try/memory/test_actual_redactedstring_protection.rb +38 -0
  173. data/try/models/customer_safe_dump_try.rb +1 -2
  174. data/try/models/customer_try.rb +1 -2
  175. data/try/models/datatype_base_try.rb +1 -2
  176. data/try/models/familia_object_try.rb +0 -1
  177. metadata +131 -23
  178. data/lib/familia/horreum/serialization.rb +0 -445
@@ -0,0 +1,183 @@
1
+ # Security Model
2
+
3
+ ## Cryptographic Design
4
+
5
+ ### Provider-Based Architecture
6
+
7
+ Familia uses a modular provider system that automatically selects the best available encryption algorithm:
8
+
9
+ ### Encryption Algorithms
10
+
11
+ **XChaCha20-Poly1305 Provider (Priority: 100)**
12
+ - Requires: `rbnacl` gem (libsodium bindings)
13
+ - Key Size: 256 bits (32 bytes)
14
+ - Nonce Size: 192 bits (24 bytes) - extended nonce space
15
+ - Authentication Tag: 128 bits (16 bytes)
16
+ - Key Derivation: BLAKE2b with personalization string
17
+
18
+ **AES-256-GCM Provider (Priority: 50)**
19
+ - Requires: OpenSSL (always available)
20
+ - Key Size: 256 bits (32 bytes)
21
+ - Nonce Size: 96 bits (12 bytes) - standard GCM nonce
22
+ - Authentication Tag: 128 bits (16 bytes)
23
+ - Key Derivation: HKDF-SHA256
24
+
25
+ ### Key Derivation
26
+
27
+ Each field gets a unique key derived from the master key:
28
+
29
+ ```
30
+ Field Key = KDF(Master Key, Context)
31
+
32
+ Where Context = "ClassName:field_name:record_identifier"
33
+ ```
34
+
35
+ **Provider-Specific KDF:**
36
+ - **XChaCha20-Poly1305**: BLAKE2b with customizable personalization string
37
+ - **AES-256-GCM**: HKDF-SHA256 with salt and info parameters
38
+
39
+ The personalization string provides cryptographic domain separation:
40
+ ```ruby
41
+ Familia.configure do |config|
42
+ config.encryption_personalization = 'MyApp-2024' # Default: 'Familia'
43
+ end
44
+ ```
45
+
46
+ ### Ciphertext Format
47
+
48
+ The encrypted data is stored as JSON with algorithm-specific fields:
49
+
50
+ **XChaCha20-Poly1305:**
51
+ ```json
52
+ {
53
+ "algorithm": "xchacha20poly1305",
54
+ "nonce": "base64_24_byte_nonce",
55
+ "ciphertext": "base64_encrypted_data",
56
+ "auth_tag": "base64_16_byte_tag",
57
+ "key_version": "v1"
58
+ }
59
+ ```
60
+
61
+ **AES-256-GCM:**
62
+ ```json
63
+ {
64
+ "algorithm": "aes-256-gcm",
65
+ "nonce": "base64_12_byte_iv",
66
+ "ciphertext": "base64_encrypted_data",
67
+ "auth_tag": "base64_16_byte_tag",
68
+ "key_version": "v1"
69
+ }
70
+ ```
71
+
72
+ ## Threat Model
73
+
74
+ ### Protected Against
75
+
76
+ #### Database Compromise
77
+ - All sensitive fields encrypted with strong keys
78
+ - Attackers see only ciphertext
79
+
80
+ #### Field Value Swapping
81
+ - Field-specific key derivation prevents cross-field decryption
82
+ - Swapped values fail to decrypt
83
+
84
+ #### Replay Attacks
85
+ - Each encryption uses unique random nonce
86
+ - Old values remain valid but are distinct encryptions
87
+
88
+ #### Tampering
89
+ - Authenticated encryption (Poly1305/GCM)
90
+ - Modified ciphertext fails authentication
91
+
92
+ ### Not Protected Against
93
+
94
+ #### Application Memory Compromise
95
+ - Plaintext values exist in Ruby memory
96
+ - Mitigation: Use libsodium for memory wiping, minimize plaintext lifetime
97
+
98
+ #### Master Key Compromise
99
+ - All encrypted data compromised if keys obtained
100
+ - Mitigation: Secure key storage, regular rotation, hardware security modules
101
+
102
+ #### Side-Channel Attacks
103
+ - Key recovery through timing/power analysis
104
+ - Mitigation: Libsodium provides constant-time operations
105
+
106
+ ## Additional Security Features
107
+
108
+ ### Passphrase Protection
109
+
110
+ For ultra-sensitive fields, add user passphrases:
111
+
112
+ ```ruby
113
+ encrypted_field :love_letter
114
+
115
+ # Passphrase required for decryption
116
+ vault.love_letter(passphrase_value: user_passphrase)
117
+ ```
118
+
119
+ **How it works:**
120
+ 1. Passphrase hashed with SHA-256
121
+ 2. Hash included in Additional Authenticated Data (AAD)
122
+ 3. Wrong passphrase = authentication failure
123
+ 4. Passphrase never stored, only verified
124
+
125
+ ### Memory Safety
126
+
127
+ **⚠️ Critical Ruby Memory Limitations:**
128
+
129
+ Ruby provides **NO** memory safety guarantees for cryptographic secrets. This affects ALL providers:
130
+
131
+ - **No secure memory wiping**: Ruby cannot guarantee memory zeroing
132
+ - **GC copying**: Garbage collector may copy secrets before cleanup
133
+ - **String operations**: Every `.dup`, `+`, or interpolation creates uncontrolled copies
134
+ - **Memory dumps**: Secrets may persist in swap files or core dumps
135
+ - **Finalizer uncertainty**: `ObjectSpace.define_finalizer` timing is unpredictable
136
+
137
+ **Provider-Specific Mitigations:**
138
+
139
+ Both providers attempt best-effort memory clearing:
140
+ - Call `.clear` on sensitive strings after use
141
+ - Set variables to `nil` when done
142
+ - Use finalizers for cleanup (no guarantees)
143
+
144
+ **Recommendation**: For production systems with high-security requirements, consider:
145
+ - Hardware Security Modules (HSMs)
146
+ - External key management services
147
+ - Languages with manual memory management (C, Rust)
148
+ - Cryptographic appliances with secure enclaves
149
+
150
+ ### RedactedString
151
+
152
+ Prevents accidental logging of sensitive data:
153
+
154
+ ```ruby
155
+ class RedactedString < String
156
+ def to_s
157
+ '[REDACTED]'
158
+ end
159
+
160
+ def inspect
161
+ '[REDACTED]'
162
+ end
163
+ end
164
+
165
+ # In logs:
166
+ logger.info "Love letter: #{user.love_letter}" # => "Love letter: [REDACTED]"
167
+ ```
168
+
169
+ ## Security Checklist
170
+
171
+ ### Development
172
+
173
+ - [ ] Never log plaintext sensitive fields
174
+ - [ ] Use RedactedString for extra protection
175
+ - [ ] Use libsodium for production when possible
176
+ - [ ] Validate encryption at startup
177
+ - [ ] Test encryption round-trips
178
+
179
+ ### Operations
180
+
181
+ - [ ] Regular key rotation schedule
182
+ - [ ] Monitor decryption failures
183
+ - [ ] Log field access patterns for auditing purposes
@@ -0,0 +1,280 @@
1
+ # Transient Fields Guide
2
+
3
+ ## Overview
4
+
5
+ Transient fields provide secure handling of sensitive runtime data that should never be persisted to Redis/Valkey. Unlike encrypted fields, transient fields exist only in memory and are automatically wrapped in `RedactedString` for security.
6
+
7
+ ## When to Use Transient Fields
8
+
9
+ Use transient fields for:
10
+ - API keys and tokens that change frequently
11
+ - Temporary passwords or passphrases
12
+ - Session-specific secrets
13
+ - Any sensitive data that should never touch persistent storage
14
+ - Debug or development secrets that need secure handling
15
+
16
+ ## Basic Usage
17
+
18
+ ### Define Transient Fields
19
+
20
+ ```ruby
21
+ class ApiClient < Familia::Horreum
22
+ feature :transient_fields
23
+
24
+ field :endpoint # Regular persistent field
25
+ transient_field :token # Transient field (not persisted)
26
+ transient_field :secret, as: :api_secret # Custom accessor name
27
+ end
28
+ ```
29
+
30
+ ### Working with Transient Fields
31
+
32
+ ```ruby
33
+ client = ApiClient.new(
34
+ endpoint: 'https://api.example.com',
35
+ token: ENV['API_TOKEN'],
36
+ secret: ENV['API_SECRET']
37
+ )
38
+
39
+ # Regular field persists
40
+ client.save
41
+ client.endpoint # => "https://api.example.com"
42
+
43
+ # Transient fields are RedactedString instances
44
+ puts client.token # => "[REDACTED]"
45
+
46
+ # Access the actual value safely
47
+ client.token.expose do |token|
48
+ response = HTTP.post(client.endpoint,
49
+ headers: { 'Authorization' => "Bearer #{token}" }
50
+ )
51
+ # Token value is only available within this block
52
+ end
53
+
54
+ # Explicit cleanup when done
55
+ client.token.clear!
56
+ ```
57
+
58
+ ## RedactedString Security
59
+
60
+ ### Automatic Wrapping
61
+
62
+ All transient field values are automatically wrapped in `RedactedString`:
63
+
64
+ ```ruby
65
+ client = ApiClient.new(token: 'secret123')
66
+ client.token.class # => RedactedString
67
+ ```
68
+
69
+ ### Safe Access Pattern
70
+
71
+ ```ruby
72
+ # ✅ Recommended: Use .expose block
73
+ client.token.expose do |token|
74
+ # Use token directly without creating copies
75
+ HTTP.auth("Bearer #{token}") # Safe
76
+ end
77
+
78
+ # ✅ Direct access (use carefully)
79
+ raw_token = client.token.value
80
+ # Remember to clear original source if needed
81
+
82
+ # ❌ Avoid: These create uncontrolled copies
83
+ token_copy = client.token.value.dup # Creates copy in memory
84
+ interpolated = "Bearer #{client.token}" # Creates copy via to_s
85
+ ```
86
+
87
+ ### Memory Management
88
+
89
+ ```ruby
90
+ # Clear individual fields
91
+ client.token.clear!
92
+
93
+ # Check if cleared
94
+ client.token.cleared? # => true
95
+
96
+ # Accessing cleared values raises error
97
+ client.token.value # => SecurityError: Value already cleared
98
+ ```
99
+
100
+ ## Advanced Features
101
+
102
+ ### Custom Accessor Names
103
+
104
+ ```ruby
105
+ class Service < Familia::Horreum
106
+ transient_field :api_key, as: :secret_key
107
+ end
108
+
109
+ service = Service.new(api_key: 'secret123')
110
+ service.secret_key.expose { |key| use_api_key(key) }
111
+ ```
112
+
113
+ ### Integration with Encrypted Fields
114
+
115
+ ```ruby
116
+ class SecureService < Familia::Horreum
117
+ feature :transient_fields
118
+
119
+ encrypted_field :long_term_secret # Persisted, encrypted
120
+ transient_field :session_token # Runtime only, not persisted
121
+ field :public_endpoint # Normal field
122
+ end
123
+
124
+ service = SecureService.new(
125
+ long_term_secret: 'stored encrypted in Redis',
126
+ session_token: 'temporary runtime secret',
127
+ public_endpoint: 'https://api.example.com'
128
+ )
129
+
130
+ service.save
131
+ # Only long_term_secret and public_endpoint are saved to Redis
132
+ # session_token exists only in memory
133
+ ```
134
+
135
+ ## RedactedString API Reference
136
+
137
+ ### Core Methods
138
+
139
+ ```ruby
140
+ # Create (usually automatic via transient_field)
141
+ secret = RedactedString.new('sensitive_value')
142
+
143
+ # Safe access
144
+ secret.expose { |value| use_value(value) }
145
+
146
+ # Direct access (use with caution)
147
+ value = secret.value
148
+
149
+ # Cleanup
150
+ secret.clear!
151
+
152
+ # Status
153
+ secret.cleared? # => true/false
154
+ ```
155
+
156
+ ### Security Methods
157
+
158
+ ```ruby
159
+ # Logging/debugging protection
160
+ puts secret.to_s # => "[REDACTED]"
161
+ puts secret.inspect # => "[REDACTED]"
162
+
163
+ # Equality (object identity only)
164
+ secret1 == secret2 # => false (unless same object)
165
+
166
+ # Hash (constant for all instances)
167
+ secret.hash # => Same for all RedactedString instances
168
+ ```
169
+
170
+ ## Security Considerations
171
+
172
+ ### Ruby Memory Limitations
173
+
174
+ **⚠️ Important**: Ruby provides no memory safety guarantees:
175
+
176
+ - **No secure wiping**: `.clear!` is best-effort only
177
+ - **GC copying**: Garbage collector may duplicate secrets
178
+ - **String operations**: Every manipulation creates copies
179
+ - **Memory persistence**: Secrets may remain in memory indefinitely
180
+
181
+ ### Best Practices
182
+
183
+ ```ruby
184
+ # ✅ Wrap immediately after input
185
+ password = RedactedString.new(params[:password])
186
+ params[:password] = nil # Clear original reference
187
+
188
+ # ✅ Use .expose for short operations
189
+ token.expose { |t| api_call(t) }
190
+
191
+ # ✅ Clear explicitly when done
192
+ token.clear!
193
+
194
+ # ✅ Avoid string operations that create copies
195
+ token.expose { |t| "Bearer #{t}" } # Creates copy
196
+ # Better: Pass token directly to methods that need it
197
+
198
+ # ❌ Don't pass RedactedString to logging
199
+ logger.info "Token: #{token}" # Still logs "[REDACTED]" but safer to avoid
200
+
201
+ # ❌ Don't store in instance variables outside field system
202
+ @raw_token = token.value # Creates uncontrolled copy
203
+ ```
204
+
205
+ ### Production Recommendations
206
+
207
+ For highly sensitive applications, consider:
208
+ - External secrets management (HashiCorp Vault, AWS Secrets Manager)
209
+ - Hardware Security Modules (HSMs)
210
+ - Languages with secure memory handling
211
+ - Encrypted swap and memory protection at OS level
212
+
213
+ ## Integration Examples
214
+
215
+ ### Rails Controller
216
+
217
+ ```ruby
218
+ class ApiController < ApplicationController
219
+ def authenticate
220
+ service = ApiService.new(
221
+ endpoint: params[:endpoint],
222
+ token: params[:token] # Auto-wrapped in RedactedString
223
+ )
224
+
225
+ result = service.token.expose do |token|
226
+ # Token only accessible within this block
227
+ ExternalAPI.authenticate(token)
228
+ end
229
+
230
+ # Clear token when request is done
231
+ service.token.clear!
232
+
233
+ render json: { status: result }
234
+ end
235
+ end
236
+ ```
237
+
238
+ ### Background Job
239
+
240
+ ```ruby
241
+ class ApiSyncJob
242
+ def perform(user_id, token)
243
+ user = User.find(user_id)
244
+
245
+ # Wrap external token securely
246
+ secure_token = RedactedString.new(token)
247
+ token.clear if token.respond_to?(:clear) # Clear original
248
+
249
+ client = ApiClient.new(token: secure_token)
250
+
251
+ begin
252
+ sync_data(client)
253
+ ensure
254
+ client.token.clear! # Always cleanup
255
+ end
256
+ end
257
+
258
+ private
259
+
260
+ def sync_data(client)
261
+ client.token.expose do |token|
262
+ # Use token for API calls
263
+ fetch_and_process_data(token)
264
+ end
265
+ end
266
+ end
267
+ ```
268
+
269
+ ## Comparison with Encrypted Fields
270
+
271
+ | Feature | Encrypted Fields | Transient Fields |
272
+ |---------|------------------|------------------|
273
+ | **Persistence** | Saved to Redis/Valkey | Memory only |
274
+ | **Encryption** | AES/XChaCha20 | None (not stored) |
275
+ | **Use Case** | Long-term secrets | Runtime secrets |
276
+ | **Access** | Automatic decrypt | RedactedString wrapper |
277
+ | **Performance** | Crypto overhead | No crypto operations |
278
+ | **Lifecycle** | Survives restarts | Cleared on restart |
279
+
280
+ Choose encrypted fields for data that must persist across sessions. Choose transient fields for sensitive runtime data that should never be stored.
data/lib/familia/base.rb CHANGED
@@ -14,7 +14,8 @@ module Familia
14
14
  # @see Familia::DataType
15
15
  #
16
16
  module Base
17
- @features = nil
17
+ @features_available = nil
18
+ @feature_definitions = nil
18
19
  @dump_method = :to_json
19
20
  @load_method = :from_json
20
21
 
@@ -29,43 +30,33 @@ module Familia
29
30
  end
30
31
 
31
32
  class << self
32
- attr_reader :features
33
+ attr_reader :features_available, :feature_definitions
33
34
  attr_accessor :dump_method, :load_method
34
35
 
35
- def add_feature(klass, methname)
36
- @features ||= {}
37
- Familia.ld "[#{self}] Adding feature #{klass} as #{methname.inspect}"
36
+ def add_feature(klass, feature_name, depends_on: [])
37
+ @features_available ||= {}
38
+ Familia.trace :ADD_FEATURE, klass, feature_name, caller(1..1) if Familia.debug?
38
39
 
39
- features[methname] = klass
40
- end
41
- end
40
+ # Create field definition object
41
+ feature_def = FeatureDefinition.new(
42
+ name: feature_name,
43
+ depends_on: depends_on,
44
+ )
42
45
 
43
- # Base implementation of update_expiration that maintains API compatibility
44
- # with the :expiration feature's implementation.
45
- #
46
- # This is a no-op implementation that gets overridden by features like
47
- # :expiration. It accepts an optional default_expiration parameter to maintain interface
48
- # compatibility with the overriding implementations.
49
- #
50
- # @param default_expiration [Integer, nil] Time To Live in seconds (ignored in base implementation)
51
- # @return [nil] Always returns nil
52
- #
53
- # @note This is a no-op implementation. Classes that need expiration
54
- # functionality should include the :expiration feature.
55
- #
56
- def update_expiration(default_expiration: nil)
57
- Familia.ld "[update_expiration] Feature not enabled for #{self.class}. Key: #{dbkey} (caller: #{caller(1..1)})"
58
- nil
46
+ # Track field definitions after defining field methods
47
+ @feature_definitions ||= {}
48
+ @feature_definitions[feature_name] = feature_def
49
+
50
+ features_available[feature_name] = klass
51
+ end
59
52
  end
60
53
 
61
54
  def generate_id
62
- @identifier ||= Familia.generate_id
63
- @identifier
55
+ @identifier ||= Familia.generate_id # rubocop:disable Naming/MemoizedInstanceVariableName
64
56
  end
65
57
 
66
58
  def uuid
67
59
  @uuid ||= SecureRandom.uuid
68
- @uuid
69
60
  end
70
61
  end
71
62
  end
@@ -106,18 +106,19 @@ module Familia
106
106
  # Always pass normalized URI with database to provider
107
107
  # Provider MUST return connection already on the correct database
108
108
  parsed_uri = normalize_uri(uri)
109
- connection = connection_provider.call(parsed_uri.to_s)
109
+ client = connection_provider.call(parsed_uri.to_s)
110
110
 
111
111
  # In debug mode, verify the provider honored the contract
112
- if Familia.debug? && connection.respond_to?(:client)
113
- current_db = connection.client.db
112
+ if Familia.debug? && client.respond_to?(:client)
113
+ current_db = client.connection[:db]
114
114
  expected_db = parsed_uri.db || 0
115
+ Familia.ld "Connection provider returned client on DB #{current_db}, expected #{expected_db}"
115
116
  if current_db != expected_db
116
- Familia.warn "Connection provider returned connection on DB #{current_db}, expected #{expected_db}"
117
+ Familia.warn "Connection provider returned client on DB #{current_db}, expected #{expected_db}"
117
118
  end
118
119
  end
119
120
 
120
- return connection
121
+ return client
121
122
  end
122
123
 
123
124
  # Third priority: Fallback behavior or error
@@ -1,11 +1,9 @@
1
- # lib/familia/datatype/commands.rb
1
+ # lib/familia/data_type/commands.rb
2
2
 
3
3
  class Familia::DataType
4
-
5
4
  # Must be included in all DataType classes to provide Redis
6
5
  # commands. The class must have a dbkey method.
7
6
  module Commands
8
-
9
7
  def move(logical_database)
10
8
  dbclient.move dbkey, logical_database
11
9
  end
@@ -52,8 +50,7 @@ class Familia::DataType
52
50
  end
53
51
 
54
52
  def echo(meth, trace)
55
- dbclient.echo "[#{self.class}\##{meth}] #{trace} (#{@opts[:class]}\#)"
53
+ dbclient.echo "[#{self.class}##{meth}] #{trace} (#{@opts[:class]}#)"
56
54
  end
57
-
58
55
  end
59
56
  end
@@ -1,9 +1,7 @@
1
- # lib/familia/datatype/serialization.rb
1
+ # lib/familia/data_type/serialization.rb
2
2
 
3
3
  class Familia::DataType
4
-
5
4
  module Serialization
6
-
7
5
  # Serializes a value for storage in Redis.
8
6
  #
9
7
  # @param val [Object] The value to be serialized.
@@ -33,7 +31,7 @@ class Familia::DataType
33
31
 
34
32
  if opts[:class]
35
33
  prepared = Familia.distinguisher(opts[:class], strict_values: strict_values)
36
- Familia.ld " from opts[class] <#{opts[:class]}>: #{prepared||'<nil>'}"
34
+ Familia.ld " from opts[class] <#{opts[:class]}>: #{prepared || '<nil>'}"
37
35
  end
38
36
 
39
37
  if prepared.nil?
@@ -42,9 +40,12 @@ class Familia::DataType
42
40
  Familia.ld " from <#{val.class}> => <#{prepared.class}>"
43
41
  end
44
42
 
45
- Familia.trace :TOREDIS, dbclient, "#{val}<#{val.class}|#{opts[:class]}> => #{prepared}<#{prepared.class}>", caller(1..1) if Familia.debug?
43
+ if Familia.debug?
44
+ Familia.trace :TOREDIS, dbclient, "#{val}<#{val.class}|#{opts[:class]}> => #{prepared}<#{prepared.class}>",
45
+ caller(1..1)
46
+ end
46
47
 
47
- Familia.warn "[#{self.class}\#serialize_value] nil returned for #{opts[:class]}\##{name}" if prepared.nil?
48
+ Familia.warn "[#{self.class}#serialize_value] nil returned for #{opts[:class]}##{name}" if prepared.nil?
48
49
  prepared
49
50
  end
50
51
 
@@ -88,9 +89,7 @@ class Familia::DataType
88
89
  next if obj.nil?
89
90
 
90
91
  val = @opts[:class].send load_method, obj
91
- if val.nil?
92
- Familia.ld "[#{self.class}\#deserialize_values] nil returned for #{@opts[:class]}\##{name}"
93
- end
92
+ Familia.ld "[#{self.class}#deserialize_values] nil returned for #{@opts[:class]}##{name}" if val.nil?
94
93
 
95
94
  val
96
95
  rescue StandardError => e
@@ -125,5 +124,4 @@ class Familia::DataType
125
124
  ret&.first # return the object or nil
126
125
  end
127
126
  end
128
-
129
127
  end
@@ -0,0 +1,38 @@
1
+ # lib/familia/data_type/types/counter.rb
2
+
3
+ module Familia
4
+ class Counter < String
5
+ def initialize(*args)
6
+ super
7
+ @opts[:default] ||= 0
8
+ end
9
+
10
+ # Enhanced counter semantics
11
+ def reset(val = 0)
12
+ set(val).to_s.eql?('OK')
13
+ end
14
+
15
+ def increment_if_less_than(threshold, amount = 1)
16
+ current = to_i
17
+ return false if current >= threshold
18
+
19
+ incrementby(amount)
20
+ true
21
+ end
22
+
23
+ def atomic_increment_and_get(amount = 1)
24
+ incrementby(amount)
25
+ end
26
+
27
+ # Override to ensure integer serialization
28
+ def value=(val)
29
+ super(val.to_i)
30
+ end
31
+
32
+ def value
33
+ super.to_i
34
+ end
35
+ end
36
+ end
37
+
38
+ Familia::DataType.register Familia::Counter, :counter