secvault 3.0.0 → 3.1.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: 7e9d55bf6f12233064ea7ced4e9b40c84f8f3a6ce38d6b5b79d5f0e8f4a7214a
4
- data.tar.gz: 7ade516b0c550c0b3c98f04fc39ff1079d5c46436da6a7fd6153bdc1d9eac4fb
3
+ metadata.gz: d57e110e287dd498f1751e5d823283c7f673c5f7511fca743772bec1ba25c5fc
4
+ data.tar.gz: b4c48354b471bc32634eb40dc15a99869e86825aae010f9f338ca4ab91850d66
5
5
  SHA512:
6
- metadata.gz: b2939f3acd3e9525d000b7195fd9967a4e8f80c04800e4cbac9da774625bc1cae7b26e1f0320d4d57d53abbf1bdb53a3899977deb9625880fd84458c7c302277
7
- data.tar.gz: 84de3b9f8ecd84e820668d3ac2bd7fedeb7946e2707f88e00cd7219f258d597417b025a72b889dc93bf6e408c2686b2c444b90e1d2eaee856f1040d9f5b8d63a
6
+ metadata.gz: 6c1f2e1f452cfca7bbcc34117a32b05d252589d156b59175158577548f9158628bf1d0f01ace21714b3f8cf7fd8de195769ecf0d60b6d11915d4ca10163070e3
7
+ data.tar.gz: 1b5ffc0246423e154b2c0e12799e6d11370c8fa242eed3bec9f902e98df9119e70d0ae1da27f02c206a62a47b839c8e0788f35afd0d5a5cca7fe360739c66ced
data/README.md CHANGED
@@ -1,456 +1,109 @@
1
1
  # Secvault
2
2
 
3
- Secvault restores the classic Rails `secrets.yml` functionality using simple, plain YAML files for environment-specific secrets management. Compatible with all modern Rails versions (7.1+, 7.2+, 8.0+).
3
+ Simple YAML secrets management for Rails. Uses standard YAML anchors for sharing configuration.
4
4
 
5
5
  [![Gem Version](https://img.shields.io/gem/v/secvault.svg)](https://rubygems.org/gems/secvault)
6
6
 
7
- ## Why Secvault?
8
-
9
- - **Drop-in replacement** for Rails 7.2+'s removed `secrets.yml` functionality
10
- - **Universal compatibility** across Rails 7.1+, 7.2+, and 8.0+
11
- - **ERB templating** support for environment variables
12
- - **Multi-file support** with deep merging capabilities
13
- - **Shared sections** for common configuration across environments
14
- - **Simple YAML** - no complex credential management required
15
-
16
7
  ## Installation
17
8
 
18
9
  ```ruby
19
- # Gemfile
20
10
  gem 'secvault'
21
11
  ```
22
12
 
23
- ```bash
24
- bundle install
25
- ```
26
-
27
- ## Quick Start
28
-
29
- ### 1. Simple Setup
30
-
31
- Create `config/initializers/secvault.rb`:
13
+ ## Usage
32
14
 
15
+ **1. Add to initializer:**
33
16
  ```ruby
34
- # The simplest setup - works across all Rails versions
17
+ # config/initializers/secvault.rb
35
18
  Secvault.start!
36
19
  ```
37
20
 
38
- Create `config/secrets.yml`:
39
-
21
+ **2. Create secrets file:**
40
22
  ```yaml
41
- shared:
42
- app_name: "My Rails App"
43
- timeout: 30
23
+ # config/secrets.yml
24
+ defaults: &defaults
25
+ app_name: "MyApp"
44
26
 
45
27
  development:
46
- secret_key_base: "dev_secret_key_here"
47
- api_key: "dev_key_123"
48
- database_url: "postgresql://localhost/myapp_dev"
49
- debug: true
50
-
51
- test:
52
- secret_key_base: "test_secret_key_here"
53
- api_key: "test_key_123"
28
+ <<: *defaults
29
+ secret_key_base: "dev_secret"
30
+ api_key: "dev_key"
54
31
 
55
32
  production:
33
+ <<: *defaults
56
34
  secret_key_base: <%= ENV['SECRET_KEY_BASE'] %>
57
35
  api_key: <%= ENV['API_KEY'] %>
58
- database_url: <%= ENV['DATABASE_URL'] %>
59
- debug: false
60
- ```
61
-
62
- ### 2. Access Your Secrets
63
-
64
- ```ruby
65
- # In your Rails application
66
- Rails.application.secrets.api_key
67
- Rails.application.secrets.app_name
68
- Rails.application.secrets.database_url
69
-
70
- # Nested secrets work too
71
- Rails.application.secrets.database.host
72
- Rails.application.secrets.features.analytics
73
36
  ```
74
37
 
75
- ## Setup Methods
76
-
77
- ### Unified start! Method
78
-
79
- Secvault now uses a single, simplified `start!` method for all use cases:
80
-
38
+ **3. Use in your app:**
81
39
  ```ruby
82
- # Simplest setup - loads config/secrets.yml with Rails integration
83
- Secvault.start!
84
-
85
- # Custom single file
86
- Secvault.start!(files: ['custom.yml'])
87
-
88
- # Multiple files with hot reload
89
- Secvault.start!(
90
- files: ['config/secrets.yml', 'config/secrets.local.yml'],
91
- hot_reload: true
92
- )
93
-
94
- # All available options
95
- Secvault.start!(
96
- files: ['config/secrets.yml'], # Default: ['config/secrets.yml']
97
- integrate_with_rails: true, # Default: true
98
- set_secret_key_base: true, # Default: true
99
- hot_reload: true, # Default: true in development
100
- logger: true # Default: true except production
101
- )
40
+ Secvault.secrets.api_key
41
+ Secvault.secrets.app_name
102
42
  ```
103
43
 
104
- ### Advanced Usage
44
+ ## Options
105
45
 
106
46
  ```ruby
107
- # Load without Rails integration (standalone mode)
108
- Secvault.start!(integrate_with_rails: false)
109
- # Access via: Secvault.secrets.your_key
110
-
111
- # Multi-file with hot reload for development
112
47
  Secvault.start!(
113
- files: [
114
- 'config/secrets.yml',
115
- 'config/secrets.oauth.yml',
116
- 'config/secrets.local.yml' # Git-ignored local overrides
117
- ],
118
- hot_reload: true,
119
- logger: true
48
+ files: ['config/secrets.yml'], # Files to load (later files override earlier ones)
49
+ integrate_with_rails: false, # Add Rails.application.secrets
50
+ set_secret_key_base: true, # Auto-set Rails.application.config.secret_key_base from secrets
51
+ hot_reload: true, # Auto-reload in development
52
+ logger: true # Log loading activity
120
53
  )
121
54
  ```
122
55
 
123
-
124
- ## Advanced Features
125
-
126
- ### ERB Templating
127
-
128
- Secvault supports full ERB templating for dynamic configuration:
129
-
130
- ```yaml
131
- production:
132
- secret_key_base: <%= ENV['SECRET_KEY_BASE'] %>
133
- api_key: <%= ENV['API_KEY'] %>
134
- pool_size: <%= ENV.fetch('DB_POOL', '5').to_i %>
135
-
136
- # Complex expressions
137
- features:
138
- enabled: <%= ENV.fetch('FEATURES_ON', 'false') == 'true' %>
139
- analytics: <%= Rails.env.production? && ENV['ANALYTICS'] != 'false' %>
140
-
141
- # Arrays and complex data structures
142
- allowed_hosts: <%= ENV.fetch('ALLOWED_HOSTS', 'localhost').split(',') %>
143
-
144
- # Conditional values
145
- redis_url: <%=
146
- if ENV['REDIS_URL']
147
- ENV['REDIS_URL']
148
- else
149
- "redis://localhost:6379/#{Rails.env}"
150
- end
151
- %>
152
- ```
153
-
154
- ### Shared Sections
155
-
156
- Define common secrets that apply to all environments:
157
-
158
- ```yaml
159
- shared:
160
- app_name: "MyApp"
161
- version: "2.1.0"
162
- timeout: 30
163
- features:
164
- analytics: true
165
-
166
- development:
167
- secret_key_base: "dev_secret"
168
- features:
169
- debug: true # Merges with shared.features
170
-
171
- production:
172
- secret_key_base: <%= ENV['SECRET_KEY_BASE'] %>
173
- features:
174
- analytics: false # Overrides shared.features.analytics
175
- ```
176
-
177
- ### Multi-File Configuration
178
-
179
- Organize your secrets across multiple files for better maintainability:
180
-
56
+ **Multiple files:**
181
57
  ```ruby
182
- Secvault.setup_multi_file!([
183
- 'config/secrets.yml', # Base secrets
184
- 'config/secrets.oauth.yml', # OAuth provider settings
185
- 'config/secrets.database.yml', # Database configurations
186
- 'config/secrets.local.yml' # Local overrides (git-ignored)
187
- ])
58
+ # Later files override earlier ones
59
+ Secvault.start!(files: ['secrets.yml', 'local.yml'])
188
60
  ```
189
61
 
190
- **File merging behavior:**
191
- - Files are processed in order
192
- - Later files override earlier ones
193
- - Deep merging for nested hashes
194
- - Shared sections are merged first, then environment-specific
195
-
196
- ### Hot Reload (Development)
197
-
198
- Secvault provides hot reload functionality for development:
199
-
62
+ **Rails integration:**
200
63
  ```ruby
201
- # Enable hot reload when starting Secvault
202
- Secvault.start!(hot_reload: true) # Default: true in development
203
-
204
- # Then reload secrets without restarting Rails
205
- reload_secrets!
206
-
207
- # Or via Rails.application
208
- Rails.application.reload_secrets!
209
- ```
210
-
211
- Hot reload is automatically enabled in development mode and provides instant feedback when you change your secrets files.
212
-
213
- ## Manual API
214
-
215
- For advanced use cases, you can use the lower-level API:
216
-
217
- ```ruby
218
- # Parse specific files
219
- secrets = Rails::Secrets.parse(['config/secrets.yml'], env: 'production')
220
-
221
- # Load from default location
222
- secrets = Rails::Secrets.load(env: 'development')
223
-
224
- # Check if Secvault is active
225
- Secvault.active? # => true/false
226
-
227
- # Check if integrated with Rails
228
- Secvault.rails_integrated? # => true/false
229
-
230
- # Access loaded secrets directly
231
- Secvault.secrets.api_key # Available after Secvault.start!
64
+ Secvault.start!(integrate_with_rails: true)
65
+ Rails.application.secrets.api_key # Now available
232
66
  ```
233
67
 
234
- ## Rails Version Compatibility
235
-
236
- | Rails Version | Support Level | Notes |
237
- |---------------|---------------|-------|
238
- | **Rails 7.1+** | ✅ Full compatibility | Manual setup required |
239
- | **Rails 7.2+** | ✅ Drop-in replacement | Automatic setup works |
240
- | **Rails 8.0+** | ✅ Full compatibility | Future-proof |
241
-
242
- ### Rails 7.2+ Notes
243
- Rails 7.2 removed the built-in `secrets.yml` functionality. Secvault provides a complete replacement with the same API.
244
-
245
- ### Rails 7.1 Notes
246
- Rails 7.1 still has `secrets.yml` support but shows deprecation warnings. Secvault provides a consistent experience across Rails versions.
247
-
248
- ## Migration Guide
249
-
250
- ### From Previous Secvault Versions
251
-
252
- **BREAKING CHANGE**: The API has been simplified. Update your initializers:
253
-
68
+ **Secret key base:**
254
69
  ```ruby
255
- # Old API (no longer supported):
256
- Secvault.setup!
257
- Secvault.setup_multi_file!(['file1.yml', 'file2.yml'])
258
- Secvault.integrate_with_rails!
259
-
260
- # New unified API:
261
- Secvault.start! # Simple case
262
- Secvault.start!(files: ['file1.yml', 'file2.yml']) # Multi-file case
263
- Secvault.start!(integrate_with_rails: false) # Standalone mode
70
+ # If your secrets.yml has secret_key_base, it's automatically set
71
+ # This replaces the need for Rails.application.config.secret_key_base
72
+ Secvault.start!(set_secret_key_base: true) # Default behavior
264
73
  ```
265
74
 
266
- ### From Rails < 7.2 Built-in Secrets
267
-
268
- 1. **Add Secvault to your Gemfile**:
269
- ```ruby
270
- gem 'secvault'
271
- ```
272
-
273
- 2. **Create initializer**:
274
- ```ruby
275
- # config/initializers/secvault.rb
276
- Secvault.start!
277
- ```
278
-
279
- 3. **Your existing `config/secrets.yml` works as-is** - no changes needed!
280
-
281
- ### From Rails Credentials
282
-
283
- 1. **Extract your credentials to YAML**:
284
- ```bash
285
- # Export existing credentials
286
- rails credentials:show > config/secrets.yml
287
- ```
288
-
289
- 2. **Format as environment-specific YAML**:
290
- ```yaml
291
- development:
292
- secret_key_base: "your_dev_secret"
293
- # ... other secrets
294
-
295
- production:
296
- secret_key_base: <%= ENV['SECRET_KEY_BASE'] %>
297
- # ... other secrets
298
- ```
299
-
300
- 3. **Set up Secvault**:
301
- ```ruby
302
- # config/initializers/secvault.rb
303
- Secvault.start!
304
- ```
305
75
 
306
- ## Configuration Examples
307
-
308
- ### Basic Application
76
+ ## Advanced
309
77
 
78
+ **ERB templating:**
310
79
  ```yaml
311
- # config/secrets.yml
312
- shared:
313
- app_name: "MyApp"
314
-
315
- development:
316
- secret_key_base: "long_random_string_for_dev"
317
- database_url: "postgresql://localhost/myapp_dev"
318
-
319
- test:
320
- secret_key_base: "long_random_string_for_test"
321
- database_url: "postgresql://localhost/myapp_test"
322
-
323
80
  production:
324
- secret_key_base: <%= ENV['SECRET_KEY_BASE'] %>
325
- database_url: <%= ENV['DATABASE_URL'] %>
326
- ```
327
-
328
- ### Multi-Service Application
329
-
330
- ```ruby
331
- # config/initializers/secvault.rb
332
- Secvault.start!(
333
- files: [
334
- 'config/secrets.yml',
335
- 'config/secrets.oauth.yml',
336
- 'config/secrets.external_apis.yml',
337
- 'config/secrets.local.yml' # Git-ignored
338
- ],
339
- hot_reload: true
340
- )
81
+ api_key: <%= ENV['API_KEY'] %>
82
+ pool_size: <%= ENV.fetch('DB_POOL', '5').to_i %>
341
83
  ```
342
84
 
85
+ **YAML anchors for sharing:**
343
86
  ```yaml
344
- # config/secrets.yml (base)
345
- shared:
87
+ defaults: &defaults
346
88
  app_name: "MyApp"
347
89
  timeout: 30
348
90
 
349
91
  development:
350
- secret_key_base: "dev_secret"
92
+ <<: *defaults
351
93
  debug: true
352
94
 
353
95
  production:
354
- secret_key_base: <%= ENV['SECRET_KEY_BASE'] %>
355
- debug: false
356
- ```
357
-
358
- ```yaml
359
- # config/secrets.oauth.yml
360
- shared:
361
- oauth:
362
- google:
363
- scope: "email profile"
364
-
365
- development:
366
- oauth:
367
- google:
368
- client_id: "dev_google_client_id"
369
- client_secret: "dev_google_client_secret"
370
-
371
- production:
372
- oauth:
373
- google:
374
- client_id: <%= ENV['GOOGLE_CLIENT_ID'] %>
375
- client_secret: <%= ENV['GOOGLE_CLIENT_SECRET'] %>
376
- ```
377
-
378
- ## Troubleshooting
379
-
380
- ### Common Issues
381
-
382
- **1. "No secrets.yml file found"**
383
- ```bash
384
- # Create the file
385
- mkdir -p config
386
- touch config/secrets.yml
387
- ```
388
-
389
- **2. "undefined method `secrets' for Rails.application"**
390
- ```ruby
391
- # Make sure Secvault is set up in an initializer
392
- # config/initializers/secvault.rb
393
- Secvault.start!
394
- ```
395
-
396
- **3. "Secrets not loading in tests"**
397
- ```ruby
398
- # In your test helper or rails_helper.rb
399
- Secvault.start! if defined?(Secvault)
400
- ```
401
-
402
- **4. "Environment variables not working"**
403
- ```yaml
404
- # Make sure you're using ERB syntax
405
- production:
406
- api_key: <%= ENV['API_KEY'] %> # ✅ Correct
407
- api_key: $API_KEY # ❌ Wrong
96
+ <<: *defaults
97
+ timeout: 10 # Override specific values
408
98
  ```
409
99
 
410
- ### Debug Mode
411
-
100
+ **Development helpers:**
412
101
  ```ruby
413
- # Enable detailed logging (development/test only)
414
- Secvault.start!(files: ['config/secrets.yml'], logger: true)
415
-
416
- # Check if Secvault is working
417
- Secvault.active? # Should return true
418
- Secvault.rails_integrated? # Should return true
419
- Rails.application.secrets # Should show your secrets
102
+ reload_secrets! # Reload files
103
+ Secvault.active? # Check status
420
104
  ```
421
105
 
422
- ## API Reference
423
-
424
- ### Setup Methods
425
-
426
- - `Secvault.start!(files: [], integrate_with_rails: true, set_secret_key_base: true, hot_reload: auto, logger: auto)` - Main and only setup method
427
-
428
- ### Status Methods
429
-
430
- - `Secvault.active?` - Check if secrets are loaded
431
- - `Secvault.rails_integrated?` - Check if Rails integration is active
432
- - `Secvault.secrets` - Access loaded secrets directly
433
-
434
- ### Rails API Compatibility
435
-
436
- - `Rails::Secrets.parse(files, env:)` - Parse specific files
437
- - `Rails::Secrets.load(env:)` - Load from default config/secrets.yml
438
- - `Rails.application.secrets` - Access secrets (same as classic Rails)
439
-
440
- ### Legacy Aliases
441
-
442
- - `Secvault.setup_backward_compatibility_with_older_rails!` (alias for `setup!`)
443
- - `Secvault.setup_rails_71_integration!` (alias for `setup!`)
444
- - `Secvault.setup_multi_files!` (alias for `setup_multi_file!`)
445
-
446
- ## Contributing
447
-
448
- 1. Fork it
449
- 2. Create your feature branch (`git checkout -b my-new-feature`)
450
- 3. Commit your changes (`git commit -am 'Add some feature'`)
451
- 4. Push to the branch (`git push origin my-new-feature`)
452
- 5. Create new Pull Request
453
106
 
454
107
  ## License
455
108
 
456
- MIT License. See [LICENSE](LICENSE) for details.
109
+ MIT
@@ -1,54 +1,57 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Secvault
4
- # Rails::Secrets compatibility module
4
+ # Rails::Secrets compatibility class
5
5
  # Provides the classic Rails::Secrets interface for backwards compatibility
6
- # This replicates the Rails < 7.2 Rails::Secrets module functionality
7
- module RailsSecrets
8
- extend self
6
+ # This replicates the Rails < 7.2 Rails::Secrets class functionality
7
+ class RailsSecrets
8
+ class << self
9
+ attr_accessor :root
9
10
 
10
- # Parse secrets from one or more YAML files
11
- #
12
- # Supports:
13
- # - ERB templating for environment variables
14
- # - Shared sections that apply to all environments
15
- # - Environment-specific sections
16
- # - Multiple files (merged in order)
17
- # - Deep symbolized keys
18
- #
19
- # Examples:
20
- # # Single file
21
- # Rails::Secrets.parse(['config/secrets.yml'], env: 'development')
22
- #
23
- # # Multiple files (merged in order)
24
- # Rails::Secrets.parse([
25
- # 'config/secrets.yml',
26
- # 'config/secrets.local.yml'
27
- # ], env: 'development')
28
- #
29
- # # Load default config/secrets.yml
30
- # Rails::Secrets.load # uses current Rails.env
31
- # Rails::Secrets.load(env: 'production')
32
- def parse(paths, env:)
33
- Secvault::Secrets.parse(paths, env: env.to_s)
34
- end
11
+ # Parse secrets from one or more YAML files
12
+ #
13
+ # Supports:
14
+ # - ERB templating for environment variables
15
+ # - Environment-specific sections (YAML anchors handle sharing)
16
+ # - Multiple files (merged in order)
17
+ # - Deep symbolized keys
18
+ #
19
+ # Examples:
20
+ # # Single file
21
+ # Rails::Secrets.parse(['config/secrets.yml'], env: 'development')
22
+ #
23
+ # # Multiple files (merged in order)
24
+ # Rails::Secrets.parse([
25
+ # 'config/secrets.yml',
26
+ # 'config/secrets.local.yml'
27
+ # ], env: 'development')
28
+ #
29
+ # # Load default config/secrets.yml
30
+ # Rails::Secrets.load # uses current Rails.env
31
+ # Rails::Secrets.load(env: 'production')
32
+ def parse(paths, env:)
33
+ Secvault::Secrets.parse(paths, env: env.to_s)
34
+ end
35
35
 
36
- # Load secrets from the default config/secrets.yml file
37
- def load(env: Rails.env)
38
- secrets_path = Rails.root.join("config/secrets.yml")
39
- parse([secrets_path], env: env)
40
- end
36
+ # Load secrets from the default config/secrets.yml file
37
+ def load(env: Rails.env)
38
+ secrets_path = Rails.root.join("config/secrets.yml")
39
+ parse([secrets_path], env: env)
40
+ end
41
41
 
42
- # Backward compatibility aliases (deprecated)
43
- alias_method :parse_default, :load
44
- alias_method :read, :load
42
+ # Backward compatibility aliases (deprecated)
43
+ alias_method :parse_default, :load
44
+ alias_method :read, :load
45
+ end
45
46
  end
46
47
  end
47
48
 
48
- # Monkey patch to restore Rails::Secrets interface for backwards compatibility
49
+ # Replace Rails::Secrets interface for backwards compatibility
49
50
  # Works consistently across all Rails versions with warning suppression
50
51
  if defined?(Rails)
51
52
  module Rails
53
+ # Remove existing constant to avoid warnings
54
+ remove_const(:Secrets) if const_defined?(:Secrets, false)
52
55
  Secrets = Secvault::RailsSecrets
53
56
  end
54
57
  end
@@ -2,34 +2,154 @@
2
2
 
3
3
  require "rails/railtie"
4
4
 
5
+ # Extremely early hook to set up Rails.application.secrets before Application class is defined
6
+ if defined?(Rails)
7
+ # Set up a robust Rails.application with secrets support
8
+ unless Rails.respond_to?(:application) && Rails.application.respond_to?(:secrets)
9
+ # Create a minimal application-like object
10
+ temp_app = Object.new
11
+
12
+ # Add secrets method with default empty secrets that include needed encryption keys
13
+ temp_app.define_singleton_method(:secrets) do
14
+ @secrets ||= begin
15
+ secrets = ActiveSupport::OrderedOptions.new
16
+
17
+ # Add empty encryption section to prevent NoMethodError
18
+ secrets.encryption = {
19
+ primary_key: nil,
20
+ deterministic_key: nil,
21
+ key_derivation_salt: nil
22
+ }
23
+
24
+ secrets
25
+ end
26
+ end
27
+
28
+ # Set up Rails.application if it doesn't exist
29
+ Rails.define_singleton_method(:application) { temp_app } unless Rails.respond_to?(:application)
30
+ end
31
+ end
32
+
5
33
  module Secvault
6
34
  class Railtie < Rails::Railtie
7
35
  railtie_name :secvault
8
36
 
37
+ # Hook to set up early secrets access before application configuration
38
+ config.before_configuration do |app|
39
+ Secvault::EarlyLoader.setup_early_secrets(app)
40
+ end
41
+
9
42
  initializer "secvault.initialize", before: :load_environment_hook do |app|
10
43
  Secvault::Secrets.setup(app)
11
44
  end
45
+ end
12
46
 
13
- # Ensure initialization happens early in all environments
14
- config.before_configuration do |app|
15
- secrets_path = app.root.join("config/secrets.yml")
47
+ # Early loader class to handle secrets before application configuration
48
+ class EarlyLoader
49
+ class << self
50
+ def setup_early_secrets(app)
51
+ puts "[Secvault Debug] setup_early_secrets called" unless Rails.env.production?
52
+
53
+ if Rails.application.respond_to?(:secrets) && !Rails.application.secrets.empty?
54
+ puts "[Secvault Debug] Secrets already exist, skipping early load" unless Rails.env.production?
55
+ return
56
+ end
57
+
58
+ # Look for Secvault configuration in the app
59
+ secrets_config = find_secvault_config(app)
60
+ puts "[Secvault Debug] Found config: #{secrets_config&.keys}" unless Rails.env.production?
61
+ return unless secrets_config
16
62
 
17
- if secrets_path.exist? && !Rails.application.respond_to?(:secrets)
18
- # Early initialization for test environment compatibility
19
- current_env = ENV["RAILS_ENV"] || "development"
20
- secrets = Secvault::Secrets.read_secrets(secrets_path, current_env)
63
+ begin
64
+ # Load secrets using the configuration found
65
+ all_secrets = Secvault::Secrets.parse(secrets_config[:files], env: Rails.env)
66
+ puts "[Secvault Debug] Loaded secrets keys: #{all_secrets.keys}" unless Rails.env.production?
21
67
 
22
- if secrets
68
+ # Set up Rails.application.secrets immediately
23
69
  Rails.application.define_singleton_method(:secrets) do
24
70
  @secrets ||= begin
25
71
  current_secrets = ActiveSupport::OrderedOptions.new
26
- env_secrets = Secvault::Secrets.read_secrets(secrets_path, Rails.env)
27
- current_secrets.merge!(env_secrets) if env_secrets
72
+ current_secrets.merge!(all_secrets)
73
+ puts "[Secvault Debug] Returning secrets with encryption: #{current_secrets.encryption}" unless Rails.env.production?
28
74
  current_secrets
29
75
  end
30
76
  end
77
+
78
+ # Test the secrets immediately
79
+ test_encryption = Rails.application.secrets.encryption
80
+ puts "[Secvault Debug] Test access - encryption: #{test_encryption.class} - #{test_encryption}" unless Rails.env.production?
81
+
82
+ Rails.logger&.info "[Secvault] Early secrets loaded from #{secrets_config[:files].size} files" unless Rails.env.production?
83
+ rescue => e
84
+ Rails.logger&.warn "[Secvault] Failed to load early secrets: #{e.message}"
31
85
  end
32
86
  end
87
+
88
+ private
89
+
90
+ def find_secvault_config(app)
91
+ # Look for Secvault configuration in various locations
92
+ config_locations = [
93
+ app.root.join("config/initializers/secvault.rb"),
94
+ app.root.join("config/secvault.rb")
95
+ ]
96
+
97
+ config_locations.each do |config_file|
98
+ next unless config_file.exist?
99
+
100
+ config = parse_secvault_config(config_file)
101
+ return config if config
102
+ end
103
+
104
+ # Fallback to default configuration
105
+ default_files = [app.root.join("config/secrets.yml")]
106
+
107
+ # Check if neeto-commons-backend is available for default config
108
+ if defined?(NeetoCommonsBackend) && NeetoCommonsBackend.respond_to?(:shared_secrets_file)
109
+ default_files.unshift(NeetoCommonsBackend.shared_secrets_file)
110
+ end
111
+
112
+ # Only return default if at least one file exists
113
+ existing_files = default_files.select(&:exist?)
114
+ return {files: existing_files} if existing_files.any?
115
+
116
+ nil
117
+ end
118
+
119
+ def parse_secvault_config(config_file)
120
+ # Read the configuration file and extract Secvault.start! parameters
121
+ content = config_file.read
122
+
123
+ # Look for Secvault.start! calls
124
+ if /Secvault\.start!\s*\(/m.match?(content)
125
+ # Try to extract the files parameter using a simple regex
126
+ files_match = content.match(/files:\s*\[(.*?)\]/m)
127
+ if files_match
128
+ # Parse the files array (basic string parsing)
129
+ files_content = files_match[1]
130
+ files = []
131
+
132
+ # Handle various file specification patterns
133
+ files_content.scan(/["'](.*?)["']|([A-Za-z_][\w.]*\([^)]*\))/) do |quoted, method_call|
134
+ if quoted
135
+ files << Rails.root.join(quoted.strip)
136
+ elsif method_call
137
+ # Handle method calls like NeetoCommonsBackend.shared_secrets_file
138
+ if method_call.include?("NeetoCommonsBackend.shared_secrets_file") && defined?(NeetoCommonsBackend)
139
+ files << NeetoCommonsBackend.shared_secrets_file
140
+ end
141
+ end
142
+ end
143
+
144
+ return {files: files.compact} if files.any?
145
+ end
146
+ end
147
+
148
+ nil
149
+ rescue => e
150
+ Rails.logger&.warn "[Secvault] Failed to parse config file #{config_file}: #{e.message}"
151
+ nil
152
+ end
33
153
  end
34
154
  end
35
155
  end
@@ -56,7 +56,7 @@ module Secvault
56
56
  end
57
57
 
58
58
  # Classic Rails::Secrets.parse implementation
59
- # Parses plain YAML secrets files and merges shared + environment-specific sections
59
+ # Parses plain YAML secrets files for specific environment
60
60
  def parse(paths, env:)
61
61
  paths.each_with_object({}) do |path, all_secrets|
62
62
  # Handle string paths by converting to Pathname
@@ -72,8 +72,7 @@ module Secvault
72
72
 
73
73
  secrets ||= {}
74
74
 
75
- # Merge shared secrets first, then environment-specific (using deep merge)
76
- all_secrets.deep_merge!(secrets["shared"].deep_symbolize_keys) if secrets["shared"]
75
+ # Only load environment-specific section (YAML anchors handle sharing)
77
76
  all_secrets.deep_merge!(secrets[env].deep_symbolize_keys) if secrets[env]
78
77
  end
79
78
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Secvault
4
- VERSION = "3.0.0"
4
+ VERSION = "3.1.0"
5
5
  end
data/lib/secvault.rb CHANGED
@@ -65,6 +65,67 @@ module Secvault
65
65
  defined?(Rails) && Rails::Secrets == Secvault::RailsSecrets
66
66
  end
67
67
 
68
+ # Early setup method for use in config/application.rb before other configuration
69
+ # This ensures Rails.application has secrets available during application class definition
70
+ def setup_early_application_secrets!(files: nil, application_class: nil)
71
+ return false unless defined?(Rails)
72
+
73
+ # Default files if not provided
74
+ files ||= begin
75
+ default_files = ["config/secrets.yml"]
76
+
77
+ # Add neeto-commons-backend file if available
78
+ if defined?(NeetoCommonsBackend) && NeetoCommonsBackend.respond_to?(:shared_secrets_file)
79
+ default_files.unshift(NeetoCommonsBackend.shared_secrets_file)
80
+ end
81
+
82
+ default_files
83
+ end
84
+
85
+ # Create a temporary Rails.application if it doesn't exist
86
+ unless Rails.respond_to?(:application) && Rails.application
87
+ # Create a temporary application-like object with secrets
88
+ temp_app = Object.new
89
+
90
+ # Add lazy secrets loading
91
+ temp_app.define_singleton_method(:secrets) do
92
+ @secrets ||= begin
93
+ # Convert to full paths and filter existing files
94
+ file_paths = files.map do |file|
95
+ file.is_a?(Pathname) ? file : Rails.root.join(file)
96
+ end.select(&:exist?)
97
+
98
+ if file_paths.any?
99
+ # Load secrets using Secvault
100
+ all_secrets = Secvault::Secrets.parse(file_paths, env: Rails.env)
101
+ current_secrets = ActiveSupport::OrderedOptions.new
102
+ current_secrets.merge!(all_secrets)
103
+ current_secrets
104
+ else
105
+ # Return empty secrets if no files found but include encryption structure
106
+ secrets = ActiveSupport::OrderedOptions.new
107
+ secrets.encryption = ActiveSupport::OrderedOptions.new
108
+ secrets.encryption.primary_key = nil
109
+ secrets.encryption.deterministic_key = nil
110
+ secrets.encryption.key_derivation_salt = nil
111
+ secrets
112
+ end
113
+ end
114
+ end
115
+
116
+ # Set up Rails.application to point to this temporary object
117
+ Rails.define_singleton_method(:application) { temp_app }
118
+ end
119
+
120
+ true
121
+ rescue => e
122
+ warn "[Secvault] Early application secrets setup failed: #{e.message}"
123
+ false
124
+ end
125
+
126
+ # Alias for backward compatibility
127
+ alias_method :setup_early_secrets!, :setup_early_application_secrets!
128
+
68
129
  def install!
69
130
  return if defined?(Rails::Railtie).nil?
70
131
 
@@ -88,35 +149,34 @@ module Secvault
88
149
  #
89
150
  # Options:
90
151
  # - files: Array of file paths (String or Pathname). Defaults to ['config/secrets.yml']
91
- # - integrate_with_rails: Integrate with Rails.application.secrets (default: true)
152
+ # - integrate_with_rails: Integrate with Rails.application.secrets (default: false)
92
153
  # - set_secret_key_base: Set Rails.application.config.secret_key_base from secrets (default: true)
93
154
  # - hot_reload: Add reload_secrets! methods for development (default: true in development)
94
155
  # - logger: Enable logging (default: true except production)
95
- def start!(files: [], integrate_with_rails: true, set_secret_key_base: true,
96
- hot_reload: (defined?(Rails) && Rails.env.respond_to?(:development?) ? Rails.env.development? : false),
97
- logger: (defined?(Rails) && Rails.env.respond_to?(:production?) ? !Rails.env.production? : true))
98
-
156
+ def start!(files: [], integrate_with_rails: false, set_secret_key_base: true,
157
+ hot_reload: ((defined?(Rails) && Rails.env.respond_to?(:development?)) ? Rails.env.development? : false),
158
+ logger: ((defined?(Rails) && Rails.env.respond_to?(:production?)) ? !Rails.env.production? : true))
99
159
  # Default to config/secrets.yml if no files specified
100
160
  files_to_load = files.empty? ? ["config/secrets.yml"] : Array(files)
101
-
161
+
102
162
  # Convert to Pathname objects and resolve relative to Rails.root
103
163
  file_paths = files_to_load.map do |file|
104
164
  file.is_a?(Pathname) ? file : Rails.root.join(file)
105
165
  end
106
-
166
+
107
167
  # Load secrets into Secvault.secrets
108
168
  load_secrets!(file_paths, logger: logger)
109
-
169
+
110
170
  # Integrate with Rails if requested
111
171
  if integrate_with_rails
112
172
  setup_rails_integration!(file_paths, set_secret_key_base: set_secret_key_base, logger: logger)
113
173
  end
114
-
174
+
115
175
  # Add hot reload functionality if requested
116
176
  if hot_reload
117
177
  add_hot_reload!(file_paths)
118
178
  end
119
-
179
+
120
180
  true
121
181
  rescue => e
122
182
  Rails.logger&.error "[Secvault] Failed to start: #{e.message}" if defined?(Rails) && logger
@@ -126,24 +186,24 @@ module Secvault
126
186
  private
127
187
 
128
188
  # Load secrets into Secvault.secrets (internal storage)
129
- def load_secrets!(file_paths, logger: (defined?(Rails) && Rails.env.respond_to?(:production?) ? !Rails.env.production? : true))
189
+ def load_secrets!(file_paths, logger: ((defined?(Rails) && Rails.env.respond_to?(:production?)) ? !Rails.env.production? : true))
130
190
  existing_files = file_paths.select(&:exist?)
131
-
191
+
132
192
  if existing_files.any?
133
193
  # Load and merge all secrets files
134
194
  merged_secrets = Secvault::Secrets.parse(existing_files, env: Rails.env)
135
-
195
+
136
196
  # Store in internal storage with ActiveSupport::OrderedOptions for compatibility
137
197
  @@loaded_secrets = ActiveSupport::OrderedOptions.new
138
198
  @@loaded_secrets.merge!(merged_secrets)
139
-
199
+
140
200
  # Log successful loading
141
201
  if logger
142
202
  file_names = existing_files.map(&:basename)
143
203
  Rails.logger&.info "[Secvault] Loaded #{existing_files.size} files: #{file_names.join(", ")}"
144
204
  Rails.logger&.info "[Secvault] Parsed #{merged_secrets.keys.size} secret keys for #{Rails.env}"
145
205
  end
146
-
206
+
147
207
  true
148
208
  else
149
209
  Rails.logger&.warn "[Secvault] No secrets files found" if logger
@@ -151,13 +211,13 @@ module Secvault
151
211
  false
152
212
  end
153
213
  end
154
-
214
+
155
215
  # Set up Rails integration
156
- def setup_rails_integration!(file_paths, set_secret_key_base: true, logger: (defined?(Rails) && Rails.env.respond_to?(:production?) ? !Rails.env.production? : true))
216
+ def setup_rails_integration!(file_paths, set_secret_key_base: true, logger: ((defined?(Rails) && Rails.env.respond_to?(:production?)) ? !Rails.env.production? : true))
157
217
  # Override native Rails::Secrets with Secvault implementation
158
218
  Rails.send(:remove_const, :Secrets) if defined?(Rails::Secrets)
159
219
  Rails.const_set(:Secrets, Secvault::RailsSecrets)
160
-
220
+
161
221
  # Set up Rails.application.secrets replacement in after_initialize
162
222
  Rails.application.config.after_initialize do
163
223
  if @@loaded_secrets && !@@loaded_secrets.empty?
@@ -165,51 +225,102 @@ module Secvault
165
225
  Rails.application.define_singleton_method(:secrets) do
166
226
  @@loaded_secrets
167
227
  end
168
-
228
+
169
229
  # Set secret_key_base in Rails config to avoid accessing it from secrets
170
230
  if set_secret_key_base && @@loaded_secrets.key?(:secret_key_base)
171
231
  Rails.application.config.secret_key_base = @@loaded_secrets[:secret_key_base]
172
232
  Rails.logger&.info "[Secvault] Set Rails.application.config.secret_key_base from secrets" if logger
173
233
  end
174
-
234
+
175
235
  # Log integration success (except in production)
176
236
  if logger
177
237
  Rails.logger&.info "[Secvault] Rails integration complete. #{@@loaded_secrets.keys.size} secret keys available."
178
238
  end
179
- else
180
- Rails.logger&.warn "[Secvault] No secrets loaded for Rails integration" if logger
239
+ elsif logger
240
+ Rails.logger&.warn "[Secvault] No secrets loaded for Rails integration"
181
241
  end
182
242
  end
183
243
  end
184
-
244
+
185
245
  # Add hot reload functionality for development
186
246
  def add_hot_reload!(file_paths)
187
247
  # Define reload method on Rails.application
188
248
  Rails.application.define_singleton_method(:reload_secrets!) do
189
249
  # Reload secrets
190
250
  Secvault.send(:load_secrets!, file_paths, logger: true)
191
-
251
+
192
252
  # Re-apply Rails integration if needed
193
253
  if Secvault.rails_integrated? && @@loaded_secrets
194
254
  Rails.application.define_singleton_method(:secrets) do
195
255
  @@loaded_secrets
196
256
  end
197
257
  end
198
-
258
+
199
259
  puts "🔄 Hot reloaded secrets from #{file_paths.size} files"
200
260
  true
201
261
  end
202
-
262
+
203
263
  # Also make it available as a top-level method
204
264
  Object.define_method(:reload_secrets!) do
205
265
  Rails.application.reload_secrets!
206
266
  end
207
-
208
- Rails.logger&.info "[Secvault] Hot reload enabled. Use reload_secrets! to refresh secrets." unless (defined?(Rails) && Rails.env.respond_to?(:production?) && Rails.env.production?)
267
+
268
+ Rails.logger&.info "[Secvault] Hot reload enabled. Use reload_secrets! to refresh secrets." unless defined?(Rails) && Rails.env.respond_to?(:production?) && Rails.env.production?
209
269
  end
210
-
270
+
211
271
  public
212
272
 
213
273
  end
214
274
 
215
- Secvault.install! if defined?(Rails)
275
+ # Auto-install and setup when Rails is available
276
+ if defined?(Rails)
277
+ Secvault.install!
278
+
279
+ # Immediate setup for early access during application loading
280
+ begin
281
+ # Try to detect and load secrets immediately if Rails.root is available
282
+ if Rails.respond_to?(:root) && Rails.root
283
+ # Look for default secrets or configuration
284
+ default_secrets_file = Rails.root.join("config/secrets.yml")
285
+ commons_secrets_file = nil
286
+
287
+ # Check for neeto-commons-backend integration
288
+ if defined?(NeetoCommonsBackend) && NeetoCommonsBackend.respond_to?(:shared_secrets_file)
289
+ commons_secrets_file = NeetoCommonsBackend.shared_secrets_file
290
+ end
291
+
292
+ files_to_load = [commons_secrets_file, default_secrets_file].compact.select(&:exist?)
293
+
294
+ if files_to_load.any? && Rails.respond_to?(:env)
295
+ # Load secrets immediately
296
+ all_secrets = Secvault::Secrets.parse(files_to_load, env: Rails.env)
297
+
298
+ # Set up Rails.application.secrets if Rails.application exists
299
+ if Rails.respond_to?(:application) && Rails.application
300
+ Rails.application.define_singleton_method(:secrets) do
301
+ @secrets ||= begin
302
+ current_secrets = ActiveSupport::OrderedOptions.new
303
+ current_secrets.merge!(all_secrets)
304
+ current_secrets
305
+ end
306
+ end
307
+ else
308
+ # Create a minimal Rails.application for early access
309
+ temp_app = Object.new
310
+ temp_app.define_singleton_method(:secrets) do
311
+ @secrets ||= begin
312
+ current_secrets = ActiveSupport::OrderedOptions.new
313
+ current_secrets.merge!(all_secrets)
314
+ current_secrets
315
+ end
316
+ end
317
+
318
+ Rails.define_singleton_method(:application) { temp_app } unless Rails.respond_to?(:application)
319
+ end
320
+ end
321
+ end
322
+ rescue => e
323
+ # Silent fail - normal initialization will handle it
324
+ warn "[Secvault] Early auto-load failed: #{e.message}" unless Rails.env&.production?
325
+ end
326
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: secvault
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.0
4
+ version: 3.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Unnikrishnan KP