secvault 2.4.0 → 2.6.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 18d7c81d380aa63a8092f4bd91a438ee3e02282695b6fcbe6841ffbb81a30a9b
4
- data.tar.gz: 2dbe018b171e0840e9a6047027fcf8e60ef73ec60867e4f6f4f97240df74f126
3
+ metadata.gz: 804b4325743910209d494c3c81b4f5f0dc8cf89dd802b4b424544923e42cee34
4
+ data.tar.gz: 6c456cdd91bf12b27ab70922deb6736c42c8afdeb3e91c7173428c791441ff2c
5
5
  SHA512:
6
- metadata.gz: c864336bbb71f184bf09e9f3bc0c191cbd7f7d750ee5a9c541cc3711b0ddc0cc608103e574dffd940b64dc814b4d5b5f433940ef0adf6235e3d17caf9f5784ce
7
- data.tar.gz: 6c62bb91d5a9a0ca1ac0a984f94c92d11c9815d8832e371b7b362d2d6142e1a5f6ea5b058ce8d7762708c271c4cfe848b753d8ea2c6c1dbf47ace950c6df16fd
6
+ metadata.gz: 82efdd937655a3ba4eaefe8d732df4e2def7fd12a91302b182d1c300f1a17c10031339a53efe14af1f7c23bdfc40bdc44d43f578bc487a4e425551b75169f1c0
7
+ data.tar.gz: 0bf0eab87edf50ef01b0d5f0989d8403cd2b96795a393504de87875541ef458fbe3342ba2d8d0e8316b252259bbfee00abff8d1cb265bf0d73cdd951e979c27a
data/README.md CHANGED
@@ -7,16 +7,6 @@ Restores the classic Rails `secrets.yml` functionality that was removed in Rails
7
7
  - **Rails 7.2+**: Automatic setup
8
8
  - **Rails 8.0+**: Full compatibility
9
9
 
10
- ## ✨ Key Features
11
-
12
- - 🔗 **YAML Anchor/Alias Support**: Use `default: &default` for shared configuration
13
- - 🌍 **ERB Interpolation**: Environment variables with type conversion (`ENV['VAR'].to_i`, boolean logic)
14
- - 📁 **Multi-File Loading**: Merge multiple YAML files (e.g., base + OAuth + local overrides)
15
- - 🔄 **Environment Switching**: Load different environments dynamically
16
- - 🛠️ **Development Tools**: Hot-reload secrets without server restart
17
- - 🔍 **Utility Methods**: `Secvault.active?` to check integration status
18
- - 🏗️ **Flexible Organization**: Feature-based, environment-based, or namespace-based file structures
19
-
20
10
  ## Installation
21
11
 
22
12
  ```ruby
@@ -86,38 +76,35 @@ production:
86
76
 
87
77
  ## Multi-File Configuration
88
78
 
89
- Organize secrets across multiple files for better maintainability:
79
+ Organize secrets across multiple files with a **super clean API**:
90
80
 
91
81
  ```ruby
92
82
  # config/initializers/secvault.rb
93
83
  require "secvault"
94
- Secvault.setup_backward_compatibility_with_older_rails!
95
84
 
96
- Rails.application.config.after_initialize do
97
- # Load multiple files in order (later files override earlier ones)
98
- secrets_files = [
99
- Rails.root.join('config', 'secrets.yml'), # Base secrets
100
- Rails.root.join('config', 'secrets.oauth.yml'), # OAuth & APIs
101
- Rails.root.join('config', 'secrets.local.yml') # Local overrides
102
- ]
103
-
104
- existing_files = secrets_files.select(&:exist?)
105
-
106
- if existing_files.any?
107
- merged_secrets = Rails::Secrets.parse(existing_files, env: Rails.env)
108
- secrets_object = ActiveSupport::OrderedOptions.new
109
- secrets_object.merge!(merged_secrets)
110
- Rails.application.define_singleton_method(:secrets) { secrets_object }
111
- end
112
- end
85
+ # That's it! Just pass your files array
86
+ Secvault.setup_multi_file!([
87
+ 'config/secrets.yml', # Base secrets
88
+ 'config/secrets.oauth.yml', # OAuth & APIs
89
+ 'config/secrets.local.yml' # Local overrides
90
+ ])
113
91
  ```
114
92
 
115
- **File organization example:**
116
- ```
117
- config/
118
- ├── secrets.yml # Base application secrets
119
- ├── secrets.oauth.yml # OAuth providers & external APIs
120
- ├── secrets.local.yml # Local development overrides (gitignored)
93
+ **What this does:**
94
+ - ✅ Loads and merges all files in order (later files override earlier ones)
95
+ - ✅ Handles missing files gracefully
96
+ - Creates Rails.application.secrets with merged configuration
97
+
98
+ **Advanced options:**
99
+ ```ruby
100
+ # Disable reload helper or logging
101
+ Secvault.setup_multi_file!(files, reload_method: false, logger: false)
102
+
103
+ # Use Pathname objects if needed
104
+ Secvault.setup_multi_file!([
105
+ Rails.root.join('config', 'secrets.yml'),
106
+ Rails.root.join('config', 'secrets.oauth.yml')
107
+ ])
121
108
  ```
122
109
 
123
110
  ## Advanced Usage
@@ -152,7 +139,7 @@ dev_secrets = Rails::Secrets.load(env: 'development')
152
139
 
153
140
  ## ERB Features & Type Conversion
154
141
 
155
- Secvault supports powerful ERB templating with automatic type conversion:
142
+ Secvault supports ERB templating with automatic type conversion:
156
143
 
157
144
  ```yaml
158
145
  production:
@@ -175,10 +162,14 @@ production:
175
162
 
176
163
  ## Development Tools
177
164
 
178
- **Hot-reload secrets (development only):**
165
+ **Hot-reload secrets (automatically available in development):**
179
166
  ```ruby
180
- # In Rails console or code
181
- reload_secrets! # Reloads all secrets files without server restart
167
+ # In Rails console - automatically added by setup_multi_file!
168
+ reload_secrets! # Reloads all configured files without server restart
169
+ # 🔄 Reloaded secrets from 3 files
170
+
171
+ # Also available as:
172
+ Rails.application.reload_secrets!
182
173
  ```
183
174
 
184
175
  **Check integration status:**
@@ -213,60 +204,12 @@ else
213
204
  end
214
205
  ```
215
206
 
216
- ## Usage Examples
217
-
218
- **Basic usage:**
219
- ```ruby
220
- # Access secrets
221
- Rails.application.secrets.api_key
222
- Rails.application.secrets.database.host
223
- Rails.application.secrets.oauth.google.client_id
224
-
225
- # With YAML defaults, you get deep merging:
226
- Rails.application.secrets.database.adapter # "postgresql" (from default)
227
- Rails.application.secrets.database.host # "localhost" (from environment)
228
- ```
229
-
230
- **Multi-file merging:**
231
- ```ruby
232
- # Files loaded in order: base → oauth → local
233
- # Later files override earlier ones for the same keys
234
- # Hash values are deep merged, scalars are replaced
235
-
236
- Rails.application.secrets.api_key # Could be from base or local file
237
- Rails.application.secrets.oauth.google # From oauth file
238
- Rails.application.secrets.features.debug # From local file override
239
- ```
240
-
241
207
  ## Security Best Practices
242
208
 
243
- ### ⚠️ Production Security
244
209
  - **Never commit production secrets** to version control
245
210
  - **Use environment variables** in production with ERB: `<%= ENV['SECRET'] %>`
246
211
  - **Use ENV.fetch()** with fallbacks: `<%= ENV.fetch('SECRET', 'default') %>`
247
212
 
248
- ### 📝 File Management
249
- - **Add sensitive files** to `.gitignore`:
250
- ```gitignore
251
- config/secrets.yml # If contains sensitive data
252
- config/secrets.local.yml # Local development overrides
253
- config/secrets.production.yml # If used
254
- ```
255
-
256
- ### 🔑 Recommended Structure
257
- ```yaml
258
- # ✅ GOOD: Base file with safe defaults
259
- development:
260
- api_key: "safe_dev_key_for_team"
261
-
262
- production:
263
- api_key: <%= ENV['API_KEY'] %> # ✅ From environment
264
-
265
- # ❌ BAD: Secrets hardcoded in base file
266
- production:
267
- api_key: "super_secret_production_key" # ❌ Never do this
268
- ```
269
-
270
213
  ## License
271
214
 
272
215
  MIT License - see [LICENSE](https://opensource.org/licenses/MIT)
data/USAGE_EXAMPLES.md ADDED
@@ -0,0 +1,65 @@
1
+ # Secvault API
2
+
3
+ Secvault provides separate control over secrets loading and Rails integration.
4
+
5
+ ## Core Methods
6
+
7
+ ### `Secvault.start!(files: [])`
8
+
9
+ Loads secrets from YAML files. Returns `true`/`false`.
10
+
11
+ - **Default**: Uses `config/secrets.yml` if `files` is empty
12
+ - **Access**: Secrets available via `Secvault.secrets`
13
+ - **Non-invasive**: Does not modify `Rails.application.secrets`
14
+
15
+ ### `Secvault.integrate_with_rails!`
16
+
17
+ Replaces `Rails.application.secrets` with Secvault's loaded secrets. Returns `true`/`false`.
18
+
19
+ ### `Secvault.secrets`
20
+
21
+ Access to loaded secrets as `ActiveSupport::OrderedOptions`.
22
+
23
+ ### Status Methods
24
+
25
+ - `Secvault.active?` - Returns `true` if secrets have been loaded
26
+ - `Secvault.rails_integrated?` - Returns `true` if Rails integration is active
27
+
28
+ ## Usage
29
+
30
+ ### Standalone
31
+
32
+ ```ruby
33
+ # Load secrets independently
34
+ Secvault.start!
35
+ api_key = Secvault.secrets.api_key
36
+ ```
37
+
38
+ ### With Rails Integration
39
+
40
+ ```ruby
41
+ # Load and integrate with Rails
42
+ Secvault.start!
43
+ Secvault.integrate_with_rails!
44
+ api_key = Rails.application.secrets.api_key
45
+ ```
46
+
47
+ ### Multiple Files
48
+
49
+ ```ruby
50
+ # Files are deep-merged in order
51
+ Secvault.start!(files: [
52
+ 'config/shared_secrets.yml',
53
+ 'config/secrets.yml'
54
+ ])
55
+ ```
56
+
57
+ ### Error Handling
58
+
59
+ ```ruby
60
+ if Secvault.start!(files: ['config/secrets.yml'])
61
+ if Secvault.integrate_with_rails!
62
+ # Both operations successful
63
+ end
64
+ end
65
+ ```
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_support/core_ext/hash/keys"
4
+ require "active_support/core_ext/hash/deep_merge"
4
5
  require "active_support/ordered_options"
5
6
  require "pathname"
6
7
  require "erb"
@@ -89,9 +90,9 @@ module Secvault
89
90
 
90
91
  secrets ||= {}
91
92
 
92
- # Merge shared secrets first, then environment-specific
93
- all_secrets.merge!(secrets["shared"].deep_symbolize_keys) if secrets["shared"]
94
- all_secrets.merge!(secrets[env].deep_symbolize_keys) if secrets[env]
93
+ # Merge shared secrets first, then environment-specific (using deep merge)
94
+ all_secrets.deep_merge!(secrets["shared"].deep_symbolize_keys) if secrets["shared"]
95
+ all_secrets.deep_merge!(secrets[env].deep_symbolize_keys) if secrets[env]
95
96
  end
96
97
  end
97
98
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Secvault
4
- VERSION = "2.4.0"
4
+ VERSION = "2.6.0"
5
5
  end
data/lib/secvault.rb CHANGED
@@ -57,9 +57,22 @@ module Secvault
57
57
  class Error < StandardError; end
58
58
 
59
59
  extend self
60
+
61
+ # Internal storage for loaded secrets
62
+ @@loaded_secrets = nil
60
63
 
61
- # Check if Secvault is currently active in the Rails application
64
+ # Access to loaded secrets without Rails integration
65
+ def secrets
66
+ @@loaded_secrets || ActiveSupport::OrderedOptions.new
67
+ end
68
+
69
+ # Check if Secvault is currently active (started)
62
70
  def active?
71
+ @@loaded_secrets != nil
72
+ end
73
+
74
+ # Check if Secvault is integrated with Rails.application.secrets
75
+ def rails_integrated?
63
76
  defined?(Rails) && Rails::Secrets == Secvault::RailsSecrets
64
77
  end
65
78
 
@@ -115,8 +128,174 @@ module Secvault
115
128
  end
116
129
  end
117
130
 
118
- # Backward compatibility alias
131
+ # Set up multi-file secrets loading with a clean API
132
+ # Just pass an array of file paths and Secvault handles the rest
133
+ #
134
+ # Usage in an initializer:
135
+ # Secvault.setup_multi_file!([
136
+ # 'config/secrets.yml',
137
+ # 'config/secrets.oauth.yml',
138
+ # 'config/secrets.local.yml'
139
+ # ])
140
+ #
141
+ # Options:
142
+ # - files: Array of file paths (String or Pathname)
143
+ # - reload_method: Add a reload helper method (default: true in development)
144
+ # - logger: Enable/disable logging (default: true except in production)
145
+ def setup_multi_file!(files, reload_method: Rails.env.development?, logger: !Rails.env.production?)
146
+ # Ensure Secvault integration is active
147
+ setup_backward_compatibility_with_older_rails! unless active?
148
+
149
+ # Convert strings to Pathname objects and resolve relative to Rails.root
150
+ file_paths = Array(files).map do |file|
151
+ file.is_a?(Pathname) ? file : Rails.root.join(file)
152
+ end
153
+
154
+ # Set up the multi-file loading
155
+ Rails.application.config.after_initialize do
156
+ load_multi_file_secrets!(file_paths, logger: logger)
157
+ end
158
+
159
+ # Add reload helper in development
160
+ if reload_method
161
+ add_reload_helper!(file_paths)
162
+ end
163
+ end
164
+
165
+ # Load secrets into Secvault.secrets only (no Rails integration)
166
+ def load_secrets_only!(files, logger: !Rails.env.production?)
167
+ # Convert strings to Pathname objects and resolve relative to Rails.root
168
+ file_paths = Array(files).map do |file|
169
+ file.is_a?(Pathname) ? file : Rails.root.join(file)
170
+ end
171
+
172
+ existing_files = file_paths.select(&:exist?)
173
+
174
+ if existing_files.any?
175
+ # Load and merge all secrets files using Secvault's parser directly
176
+ merged_secrets = Secvault::Secrets.parse(existing_files, env: Rails.env)
177
+
178
+ # Store in Secvault.secrets (ActiveSupport::OrderedOptions for compatibility)
179
+ @@loaded_secrets = ActiveSupport::OrderedOptions.new
180
+ @@loaded_secrets.merge!(merged_secrets)
181
+
182
+ # Log successful loading
183
+ if logger
184
+ file_names = existing_files.map(&:basename)
185
+ Rails.logger&.info "[Secvault] Loaded #{existing_files.size} files: #{file_names.join(', ')}"
186
+ Rails.logger&.info "[Secvault] Parsed #{merged_secrets.keys.size} secret keys for #{Rails.env}"
187
+ end
188
+
189
+ true
190
+ else
191
+ Rails.logger&.warn "[Secvault] No secrets files found" if logger
192
+ @@loaded_secrets = ActiveSupport::OrderedOptions.new
193
+ false
194
+ end
195
+ end
196
+
197
+ # Load secrets from multiple files and merge them (with Rails integration)
198
+ def load_multi_file_secrets!(file_paths, logger: !Rails.env.production?)
199
+ existing_files = file_paths.select(&:exist?)
200
+
201
+ if existing_files.any?
202
+ # Load and merge all secrets files
203
+ merged_secrets = Rails::Secrets.parse(existing_files, env: Rails.env)
204
+
205
+ # Create ActiveSupport::OrderedOptions object for Rails compatibility
206
+ secrets_object = ActiveSupport::OrderedOptions.new
207
+ secrets_object.merge!(merged_secrets)
208
+
209
+ # Replace Rails.application.secrets
210
+ Rails.application.define_singleton_method(:secrets) { secrets_object }
211
+
212
+ # Log successful loading
213
+ if logger
214
+ file_names = existing_files.map(&:basename)
215
+ Rails.logger&.info "[Secvault Multi-File] Loaded #{existing_files.size} files: #{file_names.join(', ')}"
216
+ Rails.logger&.info "[Secvault Multi-File] Merged #{merged_secrets.keys.size} secret keys for #{Rails.env}"
217
+ end
218
+
219
+ merged_secrets
220
+ else
221
+ Rails.logger&.warn "[Secvault Multi-File] No secrets files found" if logger
222
+ {}
223
+ end
224
+ end
225
+
226
+ # Add reload helper method for development
227
+ def add_reload_helper!(file_paths)
228
+ # Define reload method on Rails.application
229
+ Rails.application.define_singleton_method(:reload_secrets!) do
230
+ Secvault.load_multi_file_secrets!(file_paths, logger: true)
231
+ puts "🔄 Reloaded secrets from #{file_paths.size} files"
232
+ true
233
+ end
234
+
235
+ # Also make it available as a top-level method
236
+ Object.define_method(:reload_secrets!) do
237
+ Rails.application.reload_secrets!
238
+ end
239
+ end
240
+
241
+ # Start Secvault and load secrets (without Rails integration)
242
+ #
243
+ # Usage:
244
+ # Secvault.start! # Uses config/secrets.yml only
245
+ # Secvault.start!(files: []) # Same as above
246
+ # Secvault.start!(files: ['path/to/secrets.yml']) # Custom single file
247
+ # Secvault.start!(files: ['gem.yml', 'app.yml']) # Multiple files
248
+ #
249
+ # Access loaded secrets via: Secvault.secrets.your_key
250
+ # To integrate with Rails.application.secrets, call: Secvault.integrate_with_rails!
251
+ #
252
+ # Options:
253
+ # - files: Array of file paths (String or Pathname). Defaults to ['config/secrets.yml']
254
+ # - logger: Enable logging (default: true except production)
255
+ def start!(files: [], logger: !Rails.env.production?)
256
+ begin
257
+ # Default to host app's config/secrets.yml if no files specified
258
+ files_to_load = files.empty? ? ['config/secrets.yml'] : files
259
+
260
+ # Load secrets into Secvault.secrets (completely independent of Rails)
261
+ load_secrets_only!(files_to_load, logger: logger)
262
+
263
+ true
264
+ rescue => e
265
+ Rails.logger&.error "[Secvault] Failed to start: #{e.message}" if defined?(Rails)
266
+ false
267
+ end
268
+ end
269
+
270
+ # Integrate loaded secrets with Rails.application.secrets
271
+ def integrate_with_rails!
272
+ return false unless @@loaded_secrets
273
+
274
+ begin
275
+ # Set up Rails::Secrets to use Secvault's parser (only when integrating)
276
+ unless rails_integrated?
277
+ if defined?(Rails::Secrets)
278
+ Rails.send(:remove_const, :Secrets)
279
+ end
280
+ Rails.const_set(:Secrets, Secvault::RailsSecrets)
281
+ end
282
+
283
+ # Replace Rails.application.secrets with Secvault's loaded secrets
284
+ Rails.application.define_singleton_method(:secrets) do
285
+ Secvault.secrets
286
+ end
287
+
288
+ Rails.logger&.info "[Secvault] Integrated with Rails.application.secrets" unless Rails.env.production?
289
+ true
290
+ rescue => e
291
+ Rails.logger&.error "[Secvault] Failed to integrate with Rails: #{e.message}" if defined?(Rails)
292
+ false
293
+ end
294
+ end
295
+
296
+ # Backward compatibility aliases
119
297
  alias_method :setup_rails_71_integration!, :setup_backward_compatibility_with_older_rails!
298
+ alias_method :setup_multi_files!, :setup_multi_file! # Alternative name
120
299
  end
121
300
 
122
301
  Secvault.install! if defined?(Rails)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: secvault
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.4.0
4
+ version: 2.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Unnikrishnan KP
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-09-22 00:00:00.000000000 Z
11
+ date: 2025-09-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -54,6 +54,7 @@ files:
54
54
  - LICENSE.txt
55
55
  - README.md
56
56
  - Rakefile
57
+ - USAGE_EXAMPLES.md
57
58
  - lib/secvault.rb
58
59
  - lib/secvault/generators/secrets_generator.rb
59
60
  - lib/secvault/rails_secrets.rb