dotenv-secretsmanager 0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: aff4454079cc1d1cbb65a127d128f185343e0f68527fea618e2939282db02a60
4
+ data.tar.gz: 570fb3b58c722c78f5d38ba24657cf299463256e78bd4fffd9832b34e760c330
5
+ SHA512:
6
+ metadata.gz: f59d0999d3ce01cf9d4cc3a0afcda6eacf66b4ee89ee60d0108b2cf895c1deb9ddc99ea0c45b5b08dc02627edf292b97f788326603f40884a5bcd29efd5114e2
7
+ data.tar.gz: af7e008123e0b98461a01091d49d69deb4c9d994eba3f34c0b6e30e55959b1a6b9bfd0e1a00d5b453a399b804a88965fae5e83892d01e6b007cda74604bf60ca
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 key88sf
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,70 @@
1
+ # dotenv-secretsmanager
2
+
3
+ Use AWS Secrets Manager references in your Ruby dotenv files. A `.env` value that
4
+ begins with `aws-sm:` is resolved from AWS Secrets Manager at process boot and
5
+ written into `ENV` in place.
6
+
7
+ ## Installation
8
+
9
+ ```ruby
10
+ # Gemfile — require AFTER dotenv-rails so ENV is populated first
11
+ gem "dotenv-rails"
12
+ gem "dotenv-secretsmanager"
13
+ ```
14
+
15
+ ```sh
16
+ bundle install
17
+ ```
18
+
19
+ ## Reference syntax
20
+
21
+ ```bash
22
+ # whole plaintext secret
23
+ RAILS_MASTER_KEY=aws-sm:myproject/master-key
24
+
25
+ # one key from a JSON secret (all three share a single API call)
26
+ DB_PASSWORD=aws-sm:myproject/prod|db_password
27
+ YELP_SECRET=aws-sm:myproject/prod|yelp_client_secret
28
+ TWILIO_TOKEN=aws-sm:myproject/prod|twilio_auth_token
29
+
30
+ # non-reference values are left untouched
31
+ RAILS_LOG_LEVEL=info
32
+ ```
33
+
34
+ `<secret-id>` may be a friendly name or a full ARN. The optional `|<json-key>`
35
+ selector is split on the last `|`, so ARNs (full of colons, never pipes) parse
36
+ correctly.
37
+
38
+ ## Rails
39
+
40
+ No wiring needed. The railtie resolves references automatically after
41
+ `dotenv-rails` loads and before initializers and `database.yml` run.
42
+
43
+ ## Plain Ruby
44
+
45
+ ```ruby
46
+ require "dotenv/secretsmanager"
47
+ Dotenv::SecretsManager.resolve!(ENV)
48
+ ```
49
+
50
+ ## Configuration
51
+
52
+ ```ruby
53
+ Dotenv::SecretsManager.configure do |c|
54
+ c.on_error = :raise # :raise (default) — aggregate all failures, raise once
55
+ # :warn — log each failure, leave literal in ENV
56
+ c.logger = nil # defaults to Rails.logger if present, else $stderr
57
+ c.client = nil # inject a custom Aws::SecretsManager::Client
58
+ end
59
+ ```
60
+
61
+ Credentials and region come from the standard AWS SDK credential chain. The gem
62
+ makes zero AWS calls and builds no client when no references are present.
63
+
64
+ ## Deployment (AWS Lightsail Container Service)
65
+
66
+ Set only `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, and `AWS_REGION` as
67
+ plaintext deployment env vars (a least-privilege IAM user with
68
+ `secretsmanager:GetSecretValue` scoped to your secrets). Put everything else in
69
+ `.env.production` as `aws-sm:` references. Keep `.env.development` free of
70
+ references so local development needs no AWS access.
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dotenv
4
+ module SecretsManager
5
+ class Configuration
6
+ # :raise (default) | :warn
7
+ attr_accessor :on_error
8
+ # nil => Rails.logger if present, else a $stderr Logger (resolved at use time)
9
+ attr_accessor :logger
10
+ # nil => a default Aws::SecretsManager::Client (built lazily, only if needed)
11
+ attr_accessor :client
12
+
13
+ def initialize
14
+ @on_error = :raise
15
+ @logger = nil
16
+ @client = nil
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dotenv
4
+ module SecretsManager
5
+ # Raised (under on_error: :raise) when one or more references cannot be
6
+ # resolved. The message aggregates every failing env var.
7
+ class ResolutionError < StandardError; end
8
+ end
9
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dotenv/secretsmanager"
4
+
5
+ module Dotenv
6
+ module SecretsManager
7
+ # Auto-resolves aws-sm: references in a Rails app. Runs in the
8
+ # before_configuration phase, which is after dotenv-rails populates ENV
9
+ # (provided this gem is required after dotenv-rails) and before initializers
10
+ # and database.yml consume the values.
11
+ class Railtie < ::Rails::Railtie
12
+ config.before_configuration do
13
+ Dotenv::SecretsManager.resolve!(ENV)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dotenv
4
+ module SecretsManager
5
+ # Parses a single .env value into its Secrets Manager components.
6
+ #
7
+ # aws-sm:<secret-id> => whole secret string
8
+ # aws-sm:<secret-id>|<json-key> => one key from a JSON secret
9
+ #
10
+ # The remainder after the scheme is split on the LAST pipe, so neither a
11
+ # friendly name nor an ARN (which contain no pipe) is ever mis-split.
12
+ class Reference
13
+ SCHEME = "aws-sm:"
14
+
15
+ attr_reader :raw, :secret_id, :json_key
16
+
17
+ def self.reference?(value)
18
+ value.is_a?(String) && value.start_with?(SCHEME)
19
+ end
20
+
21
+ def self.parse(value)
22
+ new(value)
23
+ end
24
+
25
+ def initialize(raw)
26
+ @raw = raw
27
+ remainder = raw[SCHEME.length..] || ""
28
+
29
+ if remainder.include?("|")
30
+ before, _, after = remainder.rpartition("|")
31
+ @secret_id = before
32
+ @json_key = after
33
+ else
34
+ @secret_id = remainder
35
+ @json_key = nil
36
+ end
37
+ end
38
+
39
+ def malformed?
40
+ return true if @secret_id.nil? || @secret_id.empty?
41
+ return true if !@json_key.nil? && @json_key.empty?
42
+
43
+ false
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Dotenv
6
+ module SecretsManager
7
+ class Resolver
8
+ Failure = Struct.new(:env_key, :reference, :reason)
9
+
10
+ # Internal control-flow signal for a single unresolvable reference.
11
+ # Never escapes resolve!; the public type is ResolutionError.
12
+ class ResolutionFailure < StandardError; end
13
+
14
+ def initialize(env:, config:)
15
+ @env = env
16
+ @config = config
17
+ @secret_cache = {}
18
+ end
19
+
20
+ def resolve!
21
+ references = collect_references
22
+ return @env if references.empty?
23
+
24
+ failures = []
25
+ references.each do |env_key, reference|
26
+ @env[env_key] = resolve_one(reference)
27
+ rescue ResolutionFailure => e
28
+ failures << Failure.new(env_key, reference, e.message)
29
+ end
30
+
31
+ handle_failures(failures) unless failures.empty?
32
+ @env
33
+ end
34
+
35
+ private
36
+
37
+ def collect_references
38
+ @env.keys.each_with_object([]) do |key, acc|
39
+ value = @env[key]
40
+ acc << [key, Reference.parse(value)] if Reference.reference?(value)
41
+ end
42
+ end
43
+
44
+ def resolve_one(reference)
45
+ raise ResolutionFailure, "malformed reference" if reference.malformed?
46
+
47
+ secret = fetch(reference.secret_id)
48
+ reference.json_key ? extract_json_key(secret, reference) : secret
49
+ end
50
+
51
+ # One GetSecretValue per distinct secret-id for the whole pass. A failed
52
+ # fetch is cached as the failure so repeated references neither refetch nor
53
+ # silently succeed.
54
+ def fetch(secret_id)
55
+ @secret_cache[secret_id] = fetch_uncached(secret_id) unless @secret_cache.key?(secret_id)
56
+
57
+ result = @secret_cache[secret_id]
58
+ raise result if result.is_a?(ResolutionFailure)
59
+
60
+ result
61
+ end
62
+
63
+ def fetch_uncached(secret_id)
64
+ response = client.get_secret_value(secret_id: secret_id)
65
+ string = response.secret_string
66
+ return ResolutionFailure.new("secret '#{secret_id}' has no string value") if string.nil?
67
+
68
+ string
69
+ rescue Aws::Errors::ServiceError => e
70
+ ResolutionFailure.new("AWS error for '#{secret_id}': #{e.message}")
71
+ end
72
+
73
+ def extract_json_key(secret, reference)
74
+ data =
75
+ begin
76
+ JSON.parse(secret)
77
+ rescue JSON::ParserError
78
+ raise ResolutionFailure, "secret '#{reference.secret_id}' is not valid JSON"
79
+ end
80
+
81
+ unless data.is_a?(Hash)
82
+ raise ResolutionFailure, "secret '#{reference.secret_id}' is not a JSON object"
83
+ end
84
+ unless data.key?(reference.json_key)
85
+ raise ResolutionFailure,
86
+ "key '#{reference.json_key}' not found in secret '#{reference.secret_id}'"
87
+ end
88
+
89
+ data.fetch(reference.json_key).to_s
90
+ end
91
+
92
+ def handle_failures(failures)
93
+ if @config.on_error == :warn
94
+ failures.each { |f| logger.warn("[dotenv-secretsmanager] #{failure_line(f)}") }
95
+ else
96
+ raise ResolutionError, build_error_message(failures)
97
+ end
98
+ end
99
+
100
+ def failure_line(failure)
101
+ "#{failure.env_key} (#{failure.reference.raw}): #{failure.reason}"
102
+ end
103
+
104
+ def build_error_message(failures)
105
+ lines = failures.map { |f| " - #{failure_line(f)}" }
106
+ "Failed to resolve #{failures.size} Secrets Manager reference(s):\n#{lines.join("\n")}"
107
+ end
108
+
109
+ def client
110
+ @client ||= @config.client || Aws::SecretsManager::Client.new
111
+ end
112
+
113
+ def logger
114
+ @logger ||= @config.logger || default_logger
115
+ end
116
+
117
+ def default_logger
118
+ if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
119
+ Rails.logger
120
+ else
121
+ require "logger"
122
+ Logger.new($stderr)
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dotenv
4
+ module SecretsManager
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dotenv/secretsmanager/version"
4
+ require "dotenv/secretsmanager/errors"
5
+ require "dotenv/secretsmanager/configuration"
6
+ require "dotenv/secretsmanager/reference"
7
+ require "aws-sdk-secretsmanager"
8
+ require "dotenv/secretsmanager/resolver"
9
+
10
+ module Dotenv
11
+ module SecretsManager
12
+ class << self
13
+ def configuration
14
+ @configuration ||= Configuration.new
15
+ end
16
+
17
+ def configure
18
+ yield(configuration) if block_given?
19
+ configuration
20
+ end
21
+
22
+ def reset_configuration!
23
+ @configuration = Configuration.new
24
+ end
25
+
26
+ def resolve!(env = ENV)
27
+ Resolver.new(env: env, config: configuration).resolve!
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ require "dotenv/secretsmanager/railtie" if defined?(Rails::Railtie)
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dotenv/secretsmanager"
metadata ADDED
@@ -0,0 +1,92 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dotenv-secretsmanager
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - key88sf
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: aws-sdk-secretsmanager
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rake
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '13'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '13'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rspec
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '3.13'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '3.13'
54
+ description: 'Treats .env values beginning with aws-sm: as references to AWS Secrets
55
+ Manager secrets, resolving them into ENV at process boot. Framework-agnostic core
56
+ with an optional Rails railtie.'
57
+ executables: []
58
+ extensions: []
59
+ extra_rdoc_files: []
60
+ files:
61
+ - LICENSE
62
+ - README.md
63
+ - lib/dotenv-secretsmanager.rb
64
+ - lib/dotenv/secretsmanager.rb
65
+ - lib/dotenv/secretsmanager/configuration.rb
66
+ - lib/dotenv/secretsmanager/errors.rb
67
+ - lib/dotenv/secretsmanager/railtie.rb
68
+ - lib/dotenv/secretsmanager/reference.rb
69
+ - lib/dotenv/secretsmanager/resolver.rb
70
+ - lib/dotenv/secretsmanager/version.rb
71
+ homepage: https://github.com/key88sf/dotenv-secretsmanager
72
+ licenses:
73
+ - MIT
74
+ metadata: {}
75
+ rdoc_options: []
76
+ require_paths:
77
+ - lib
78
+ required_ruby_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '3.0'
83
+ required_rubygems_version: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ requirements: []
89
+ rubygems_version: 4.0.11
90
+ specification_version: 4
91
+ summary: Resolve AWS Secrets Manager references inside .env files at boot.
92
+ test_files: []