secvault 2.7.1 → 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: 3f3f850252ea738c99ee2fd7bb7c2e9a2688082abc78245f0ac1ae4f262c9f57
4
- data.tar.gz: 95c9d1b54bdf4fdeaf490e7bbf4bd1bb13411d50ce9a11291f89848049258636
3
+ metadata.gz: d57e110e287dd498f1751e5d823283c7f673c5f7511fca743772bec1ba25c5fc
4
+ data.tar.gz: b4c48354b471bc32634eb40dc15a99869e86825aae010f9f338ca4ab91850d66
5
5
  SHA512:
6
- metadata.gz: a2399e023247f056496230dc96da152b5c9fcdd1db98b5f7359a5406f9ce45dc190ce0c93cd5dc4736abb16e7396043d78482b0879785faa60353fe0a1d773dd
7
- data.tar.gz: e3deef79404b1c382c2e6167e6cb2b2981f511b6e9058fdf4fc484d8b9fce0f041551121631165f60f2220ee1d3be0ef444fb12296dac803a58afb4800b18ce0
6
+ metadata.gz: 6c1f2e1f452cfca7bbcc34117a32b05d252589d156b59175158577548f9158628bf1d0f01ace21714b3f8cf7fd8de195769ecf0d60b6d11915d4ca10163070e3
7
+ data.tar.gz: 1b5ffc0246423e154b2c0e12799e6d11370c8fa242eed3bec9f902e98df9119e70d0ae1da27f02c206a62a47b839c8e0788f35afd0d5a5cca7fe360739c66ced
data/README.md CHANGED
@@ -1,101 +1,109 @@
1
1
  # Secvault
2
2
 
3
- Restores Rails `secrets.yml` functionality for environment-specific secrets management using YAML files with ERB templating.
3
+ Simple YAML secrets management for Rails. Uses standard YAML anchors for sharing configuration.
4
4
 
5
- ## Rails Version Support
6
-
7
- - **Rails 7.2+**: Automatic setup (drop-in replacement for removed functionality)
8
- - **Rails 7.1**: Manual setup required
9
- - **Rails 8.0+**: Full compatibility
5
+ [![Gem Version](https://img.shields.io/gem/v/secvault.svg)](https://rubygems.org/gems/secvault)
10
6
 
11
7
  ## Installation
12
8
 
13
9
  ```ruby
14
- # Gemfile
15
10
  gem 'secvault'
16
11
  ```
17
12
 
18
- ## Quick Start
13
+ ## Usage
19
14
 
20
- Create `config/secrets.yml`:
15
+ **1. Add to initializer:**
16
+ ```ruby
17
+ # config/initializers/secvault.rb
18
+ Secvault.start!
19
+ ```
21
20
 
21
+ **2. Create secrets file:**
22
22
  ```yaml
23
+ # config/secrets.yml
24
+ defaults: &defaults
25
+ app_name: "MyApp"
26
+
23
27
  development:
24
- api_key: "dev_key_123"
25
- database_url: "postgresql://localhost/myapp_dev"
28
+ <<: *defaults
29
+ secret_key_base: "dev_secret"
30
+ api_key: "dev_key"
26
31
 
27
32
  production:
33
+ <<: *defaults
34
+ secret_key_base: <%= ENV['SECRET_KEY_BASE'] %>
28
35
  api_key: <%= ENV['API_KEY'] %>
29
- database_url: <%= ENV['DATABASE_URL'] %>
30
36
  ```
31
37
 
32
- Access secrets in your app:
33
-
38
+ **3. Use in your app:**
34
39
  ```ruby
35
- Rails.application.secrets.api_key
36
- Rails.application.secrets.database_url
40
+ Secvault.secrets.api_key
41
+ Secvault.secrets.app_name
37
42
  ```
38
43
 
39
- ## Multi-File Configuration
40
-
41
- Load and merge multiple secrets files:
44
+ ## Options
42
45
 
43
46
  ```ruby
44
- # config/initializers/secvault.rb
45
- Secvault.setup_multi_file!([
46
- 'config/secrets.yml',
47
- 'config/secrets.oauth.yml',
48
- 'config/secrets.local.yml'
49
- ])
47
+ Secvault.start!(
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
53
+ )
50
54
  ```
51
55
 
52
- Files are merged in order with deep merge support for nested hashes.
53
-
54
- ## Manual API
55
-
56
+ **Multiple files:**
56
57
  ```ruby
57
- # Parse specific files
58
- secrets = Rails::Secrets.parse(['config/secrets.yml'], env: Rails.env)
59
-
60
- # Load default config/secrets.yml
61
- secrets = Rails::Secrets.load(env: 'production')
62
-
63
- # Check if active
64
- Secvault.active? # => true/false
58
+ # Later files override earlier ones
59
+ Secvault.start!(files: ['secrets.yml', 'local.yml'])
65
60
  ```
66
61
 
67
- ## Rails 7.1 Integration
68
-
69
- For Rails 7.1 with existing secrets functionality:
62
+ **Rails integration:**
63
+ ```ruby
64
+ Secvault.start!(integrate_with_rails: true)
65
+ Rails.application.secrets.api_key # Now available
66
+ ```
70
67
 
68
+ **Secret key base:**
71
69
  ```ruby
72
- # config/initializers/secvault.rb
73
- Secvault.setup_backward_compatibility_with_older_rails!
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
74
73
  ```
75
74
 
76
- ## ERB Templating
77
75
 
78
- Supports full ERB templating for environment variables:
76
+ ## Advanced
79
77
 
78
+ **ERB templating:**
80
79
  ```yaml
81
80
  production:
82
81
  api_key: <%= ENV['API_KEY'] %>
83
82
  pool_size: <%= ENV.fetch('DB_POOL', '5').to_i %>
84
- features:
85
- enabled: <%= ENV.fetch('FEATURES_ON', 'false') == 'true' %>
86
- hosts: <%= ENV.fetch('ALLOWED_HOSTS', 'localhost').split(',') %>
87
83
  ```
88
84
 
89
- ## Development Tools
85
+ **YAML anchors for sharing:**
86
+ ```yaml
87
+ defaults: &defaults
88
+ app_name: "MyApp"
89
+ timeout: 30
90
90
 
91
- Reload secrets in development:
91
+ development:
92
+ <<: *defaults
93
+ debug: true
92
94
 
95
+ production:
96
+ <<: *defaults
97
+ timeout: 10 # Override specific values
98
+ ```
99
+
100
+ **Development helpers:**
93
101
  ```ruby
94
- # Available after setup_multi_file!
95
- reload_secrets!
96
- Rails.application.reload_secrets!
102
+ reload_secrets! # Reload files
103
+ Secvault.active? # Check status
97
104
  ```
98
105
 
106
+
99
107
  ## License
100
108
 
101
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 = "2.7.1"
4
+ VERSION = "3.1.0"
5
5
  end
data/lib/secvault.rb CHANGED
@@ -15,7 +15,7 @@ loader.setup
15
15
  #
16
16
  # Secvault restores the classic Rails secrets.yml functionality using simple,
17
17
  # plain YAML files for environment-specific secrets management. Works consistently
18
- # across all Rails versions with automatic deprecation warning suppression.
18
+ # across all Rails versions.
19
19
  #
20
20
  # ## Rails Version Support:
21
21
  # - Rails 7.1+: Full compatibility with automatic setup
@@ -26,18 +26,20 @@ loader.setup
26
26
  # Add this to an initializer:
27
27
  #
28
28
  # # config/initializers/secvault.rb
29
- # Secvault.setup!
29
+ # Secvault.start!
30
30
  #
31
31
  # ## Usage:
32
32
  # Rails.application.secrets.api_key
33
33
  # Rails.application.secrets.oauth_settings[:google_client_id]
34
+ # Secvault.secrets.your_key # Direct access
34
35
  # Rails::Secrets.load(env: 'development') # Load default config/secrets.yml
35
36
  # Rails::Secrets.parse(['custom.yml'], env: Rails.env) # Parse custom files
36
37
  #
37
38
  # ## Getting Started:
38
39
  # 1. Create config/secrets.yml with your secrets
39
- # 2. Use Rails.application.secrets.your_secret in your app
40
- # 3. For production, use environment variables with ERB syntax
40
+ # 2. Call Secvault.start! in an initializer
41
+ # 3. Use Rails.application.secrets.your_secret in your app
42
+ # 4. For production, use environment variables with ERB syntax
41
43
  #
42
44
  # @see https://github.com/unnitallman/secvault
43
45
  module Secvault
@@ -63,122 +65,135 @@ module Secvault
63
65
  defined?(Rails) && Rails::Secrets == Secvault::RailsSecrets
64
66
  end
65
67
 
66
- def install!
67
- return if defined?(Rails::Railtie).nil?
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)
68
72
 
69
- require "secvault/railtie"
70
- require "secvault/rails_secrets"
71
- end
73
+ # Default files if not provided
74
+ files ||= begin
75
+ default_files = ["config/secrets.yml"]
72
76
 
73
- # Set up Secvault for all Rails versions
74
- # This provides a universal way to integrate Secvault into Rails apps
75
- # with consistent behavior across all Rails versions.
76
- #
77
- # Usage in an initializer:
78
- # Secvault.setup!
79
- # Secvault.setup!(suppress_warnings: false)
80
- #
81
- # This will:
82
- # 1. Set up Rails::Secrets with Secvault implementation
83
- # 2. Replace Rails.application.secrets with Secvault-powered functionality
84
- # 3. Load secrets from config/secrets.yml automatically
85
- # 4. Suppress Rails deprecation warnings about secrets (default: true)
86
- # 5. Set Rails.application.config.secret_key_base from secrets (default: true)
87
- def setup!(suppress_warnings: true, set_secret_key_base: true)
88
- # Override native Rails::Secrets
89
- Rails.send(:remove_const, :Secrets) if defined?(Rails::Secrets)
90
- Rails.const_set(:Secrets, Secvault::RailsSecrets)
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
91
81
 
92
- # Set up Rails.application.secrets replacement
93
- Rails.application.config.after_initialize do
94
- # Suppress Rails deprecation warnings about secrets if requested
95
- suppress_secrets_deprecation_warning! if suppress_warnings
82
+ default_files
83
+ end
96
84
 
97
- secrets_path = Rails.root.join("config/secrets.yml")
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
98
115
 
99
- if secrets_path.exist?
100
- # Load secrets using Secvault
101
- loaded_secrets = Rails::Secrets.parse([secrets_path], env: Rails.env)
116
+ # Set up Rails.application to point to this temporary object
117
+ Rails.define_singleton_method(:application) { temp_app }
118
+ end
102
119
 
103
- # Create ActiveSupport::OrderedOptions object for compatibility
104
- secrets_object = ActiveSupport::OrderedOptions.new
105
- secrets_object.merge!(loaded_secrets)
120
+ true
121
+ rescue => e
122
+ warn "[Secvault] Early application secrets setup failed: #{e.message}"
123
+ false
124
+ end
106
125
 
107
- # Replace Rails.application.secrets
108
- Rails.application.define_singleton_method(:secrets) do
109
- secrets_object
110
- end
126
+ # Alias for backward compatibility
127
+ alias_method :setup_early_secrets!, :setup_early_application_secrets!
111
128
 
112
- # Set secret_key_base in Rails config to avoid accessing it from secrets
113
- if set_secret_key_base && loaded_secrets.key?("secret_key_base")
114
- Rails.application.config.secret_key_base = loaded_secrets["secret_key_base"]
115
- unless Rails.env.production?
116
- Rails.logger&.info "[Secvault] Set Rails.application.config.secret_key_base from secrets.yml"
117
- end
118
- end
129
+ def install!
130
+ return if defined?(Rails::Railtie).nil?
119
131
 
120
- # Log integration success (except in production)
121
- unless Rails.env.production?
122
- Rails.logger&.info "[Secvault] Integration complete. Loaded #{loaded_secrets.keys.size} secret keys."
123
- end
124
- else
125
- Rails.logger&.warn "[Secvault] No secrets.yml file found at #{secrets_path}"
126
- end
127
- end
132
+ require "secvault/railtie"
133
+ require "secvault/rails_secrets"
128
134
  end
129
135
 
130
- # Set up multi-file secrets loading with a clean API
131
- # Just pass an array of file paths and Secvault handles the rest
136
+ # Start Secvault with simplified, unified API
137
+ # This is the main entry point for all Secvault functionality
132
138
  #
133
- # Usage in an initializer:
134
- # Secvault.setup_multi_file!([
135
- # 'config/secrets.yml',
136
- # 'config/secrets.oauth.yml',
137
- # 'config/secrets.local.yml'
138
- # ])
139
+ # Usage examples:
140
+ # Secvault.start! # Simple: config/secrets.yml + Rails integration
141
+ # Secvault.start!(files: ['custom.yml']) # Custom single file
142
+ # Secvault.start!(files: ['base.yml', 'local.yml']) # Multiple files
143
+ # Secvault.start!(integrate_with_rails: false) # Load only, no Rails integration
144
+ # Secvault.start!(hot_reload: true) # Enable hot reload in development
145
+ #
146
+ # Access secrets:
147
+ # Rails.application.secrets.your_key # When integrate_rails: true (default)
148
+ # Secvault.secrets.your_key # Direct access (always available)
139
149
  #
140
150
  # Options:
141
- # - files: Array of file paths (String or Pathname)
142
- # - reload_method: Add a reload helper method (default: true in development)
143
- # - logger: Enable/disable logging (default: true except in production)
144
- # - suppress_warnings: Suppress Rails deprecation warnings about secrets (default: true)
151
+ # - files: Array of file paths (String or Pathname). Defaults to ['config/secrets.yml']
152
+ # - integrate_with_rails: Integrate with Rails.application.secrets (default: false)
145
153
  # - set_secret_key_base: Set Rails.application.config.secret_key_base from secrets (default: true)
146
- def setup_multi_file!(files, reload_method: Rails.env.development?, logger: !Rails.env.production?,
147
- suppress_warnings: true, set_secret_key_base: true)
148
- # Ensure Secvault integration is active
149
- setup!(suppress_warnings: suppress_warnings, set_secret_key_base: set_secret_key_base) unless active?
150
-
151
- # Convert strings to Pathname objects and resolve relative to Rails.root
152
- file_paths = Array(files).map do |file|
154
+ # - hot_reload: Add reload_secrets! methods for development (default: true in development)
155
+ # - logger: Enable logging (default: true except production)
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))
159
+ # Default to config/secrets.yml if no files specified
160
+ files_to_load = files.empty? ? ["config/secrets.yml"] : Array(files)
161
+
162
+ # Convert to Pathname objects and resolve relative to Rails.root
163
+ file_paths = files_to_load.map do |file|
153
164
  file.is_a?(Pathname) ? file : Rails.root.join(file)
154
165
  end
155
166
 
156
- # Set up the multi-file loading
157
- Rails.application.config.after_initialize do
158
- load_multi_file_secrets!(file_paths, logger: logger, suppress_warnings: suppress_warnings,
159
- set_secret_key_base: set_secret_key_base)
167
+ # Load secrets into Secvault.secrets
168
+ load_secrets!(file_paths, logger: logger)
169
+
170
+ # Integrate with Rails if requested
171
+ if integrate_with_rails
172
+ setup_rails_integration!(file_paths, set_secret_key_base: set_secret_key_base, logger: logger)
160
173
  end
161
174
 
162
- # Add reload helper in development
163
- return unless reload_method
175
+ # Add hot reload functionality if requested
176
+ if hot_reload
177
+ add_hot_reload!(file_paths)
178
+ end
164
179
 
165
- add_reload_helper!(file_paths)
180
+ true
181
+ rescue => e
182
+ Rails.logger&.error "[Secvault] Failed to start: #{e.message}" if defined?(Rails) && logger
183
+ false
166
184
  end
167
185
 
168
- # Load secrets into Secvault.secrets only (no Rails integration)
169
- def load_secrets_only!(files, logger: !Rails.env.production?)
170
- # Convert strings to Pathname objects and resolve relative to Rails.root
171
- file_paths = Array(files).map do |file|
172
- file.is_a?(Pathname) ? file : Rails.root.join(file)
173
- end
186
+ private
174
187
 
188
+ # Load secrets into Secvault.secrets (internal storage)
189
+ def load_secrets!(file_paths, logger: ((defined?(Rails) && Rails.env.respond_to?(:production?)) ? !Rails.env.production? : true))
175
190
  existing_files = file_paths.select(&:exist?)
176
191
 
177
192
  if existing_files.any?
178
- # Load and merge all secrets files using Secvault's parser directly
193
+ # Load and merge all secrets files
179
194
  merged_secrets = Secvault::Secrets.parse(existing_files, env: Rails.env)
180
195
 
181
- # Store in Secvault.secrets (ActiveSupport::OrderedOptions for compatibility)
196
+ # Store in internal storage with ActiveSupport::OrderedOptions for compatibility
182
197
  @@loaded_secrets = ActiveSupport::OrderedOptions.new
183
198
  @@loaded_secrets.merge!(merged_secrets)
184
199
 
@@ -197,51 +212,51 @@ module Secvault
197
212
  end
198
213
  end
199
214
 
200
- # Load secrets from multiple files and merge them (with Rails integration)
201
- def load_multi_file_secrets!(file_paths, logger: !Rails.env.production?, suppress_warnings: true,
202
- set_secret_key_base: true)
203
- existing_files = file_paths.select(&:exist?)
204
-
205
- if existing_files.any?
206
- # Suppress Rails deprecation warnings about secrets if requested
207
- suppress_secrets_deprecation_warning! if suppress_warnings
208
-
209
- # Load and merge all secrets files
210
- merged_secrets = Rails::Secrets.parse(existing_files, env: Rails.env)
211
-
212
- # Create ActiveSupport::OrderedOptions object for Rails compatibility
213
- secrets_object = ActiveSupport::OrderedOptions.new
214
- secrets_object.merge!(merged_secrets)
215
+ # Set up Rails integration
216
+ def setup_rails_integration!(file_paths, set_secret_key_base: true, logger: ((defined?(Rails) && Rails.env.respond_to?(:production?)) ? !Rails.env.production? : true))
217
+ # Override native Rails::Secrets with Secvault implementation
218
+ Rails.send(:remove_const, :Secrets) if defined?(Rails::Secrets)
219
+ Rails.const_set(:Secrets, Secvault::RailsSecrets)
215
220
 
216
- # Replace Rails.application.secrets
217
- Rails.application.define_singleton_method(:secrets) { secrets_object }
221
+ # Set up Rails.application.secrets replacement in after_initialize
222
+ Rails.application.config.after_initialize do
223
+ if @@loaded_secrets && !@@loaded_secrets.empty?
224
+ # Replace Rails.application.secrets with our loaded secrets
225
+ Rails.application.define_singleton_method(:secrets) do
226
+ @@loaded_secrets
227
+ end
218
228
 
219
- # Set secret_key_base in Rails config to avoid accessing it from secrets
220
- if set_secret_key_base && merged_secrets.key?("secret_key_base")
221
- Rails.application.config.secret_key_base = merged_secrets["secret_key_base"]
222
- Rails.logger&.info "[Secvault Multi-File] Set Rails.application.config.secret_key_base from secrets" if logger
223
- end
229
+ # Set secret_key_base in Rails config to avoid accessing it from secrets
230
+ if set_secret_key_base && @@loaded_secrets.key?(:secret_key_base)
231
+ Rails.application.config.secret_key_base = @@loaded_secrets[:secret_key_base]
232
+ Rails.logger&.info "[Secvault] Set Rails.application.config.secret_key_base from secrets" if logger
233
+ end
224
234
 
225
- # Log successful loading
226
- if logger
227
- file_names = existing_files.map(&:basename)
228
- Rails.logger&.info "[Secvault Multi-File] Loaded #{existing_files.size} files: #{file_names.join(", ")}"
229
- Rails.logger&.info "[Secvault Multi-File] Merged #{merged_secrets.keys.size} secret keys for #{Rails.env}"
235
+ # Log integration success (except in production)
236
+ if logger
237
+ Rails.logger&.info "[Secvault] Rails integration complete. #{@@loaded_secrets.keys.size} secret keys available."
238
+ end
239
+ elsif logger
240
+ Rails.logger&.warn "[Secvault] No secrets loaded for Rails integration"
230
241
  end
231
-
232
- merged_secrets
233
- else
234
- Rails.logger&.warn "[Secvault Multi-File] No secrets files found" if logger
235
- {}
236
242
  end
237
243
  end
238
244
 
239
- # Add reload helper method for development
240
- def add_reload_helper!(file_paths)
245
+ # Add hot reload functionality for development
246
+ def add_hot_reload!(file_paths)
241
247
  # Define reload method on Rails.application
242
248
  Rails.application.define_singleton_method(:reload_secrets!) do
243
- Secvault.load_multi_file_secrets!(file_paths, logger: true)
244
- puts "🔄 Reloaded secrets from #{file_paths.size} files"
249
+ # Reload secrets
250
+ Secvault.send(:load_secrets!, file_paths, logger: true)
251
+
252
+ # Re-apply Rails integration if needed
253
+ if Secvault.rails_integrated? && @@loaded_secrets
254
+ Rails.application.define_singleton_method(:secrets) do
255
+ @@loaded_secrets
256
+ end
257
+ end
258
+
259
+ puts "🔄 Hot reloaded secrets from #{file_paths.size} files"
245
260
  true
246
261
  end
247
262
 
@@ -249,63 +264,63 @@ module Secvault
249
264
  Object.define_method(:reload_secrets!) do
250
265
  Rails.application.reload_secrets!
251
266
  end
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?
252
269
  end
253
270
 
254
- # Start Secvault and load secrets (without Rails integration)
255
- #
256
- # Usage:
257
- # Secvault.start! # Uses config/secrets.yml only
258
- # Secvault.start!(files: []) # Same as above
259
- # Secvault.start!(files: ['path/to/secrets.yml']) # Custom single file
260
- # Secvault.start!(files: ['gem.yml', 'app.yml']) # Multiple files
261
- #
262
- # Access loaded secrets via: Secvault.secrets.your_key
263
- # To integrate with Rails.application.secrets, call: Secvault.integrate_with_rails!
264
- #
265
- # Options:
266
- # - files: Array of file paths (String or Pathname). Defaults to ['config/secrets.yml']
267
- # - logger: Enable logging (default: true except production)
268
- def start!(files: [], logger: !Rails.env.production?)
269
- # Default to host app's config/secrets.yml if no files specified
270
- files_to_load = files.empty? ? ["config/secrets.yml"] : files
271
+ public
271
272
 
272
- # Load secrets into Secvault.secrets (completely independent of Rails)
273
- load_secrets_only!(files_to_load, logger: logger)
273
+ end
274
274
 
275
- true
276
- rescue => e
277
- Rails.logger&.error "[Secvault] Failed to start: #{e.message}" if defined?(Rails)
278
- false
279
- end
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
280
291
 
281
- # Integrate loaded secrets with Rails.application.secrets
282
- def integrate_with_rails!
283
- return false unless @@loaded_secrets
292
+ files_to_load = [commons_secrets_file, default_secrets_file].compact.select(&:exist?)
284
293
 
285
- begin
286
- # Set up Rails::Secrets to use Secvault's parser (only when integrating)
287
- unless rails_integrated?
288
- Rails.send(:remove_const, :Secrets) if defined?(Rails::Secrets)
289
- Rails.const_set(:Secrets, Secvault::RailsSecrets)
290
- end
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)
291
297
 
292
- # Replace Rails.application.secrets with Secvault's loaded secrets
293
- Rails.application.define_singleton_method(:secrets) do
294
- Secvault.secrets
295
- end
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
296
317
 
297
- Rails.logger&.info "[Secvault] Integrated with Rails.application.secrets" unless Rails.env.production?
298
- true
299
- rescue => e
300
- Rails.logger&.error "[Secvault] Failed to integrate with Rails: #{e.message}" if defined?(Rails)
301
- false
318
+ Rails.define_singleton_method(:application) { temp_app } unless Rails.respond_to?(:application)
319
+ end
320
+ end
302
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?
303
325
  end
304
-
305
- # Backward compatibility aliases
306
- alias_method :setup_backward_compatibility_with_older_rails!, :setup! # Legacy name
307
- alias_method :setup_rails_71_integration!, :setup! # Legacy name
308
- alias_method :setup_multi_files!, :setup_multi_file! # Alternative name
309
326
  end
310
-
311
- Secvault.install! if defined?(Rails)
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: 2.7.1
4
+ version: 3.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Unnikrishnan KP