belt-pay 0.0.2 → 0.0.3

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: 614581460579c56502eed359de29c1f36f746c3a8697b6ba3aa073266740649b
4
- data.tar.gz: 00d84db5eb7ce1c47f23f666b8eab1511ace5745e2ad18e48e8c9753b2751503
3
+ metadata.gz: 6949c31f8f5adaec1e1a6c6d08e36bac5fb3cdac59fd2054a46be18869fa196c
4
+ data.tar.gz: e07b2808dc053c34235ea4ec6ef183858c43401279a5f3d4b1c92ce7b46d2af1
5
5
  SHA512:
6
- metadata.gz: 51d01a9a411c28d3e080352bb265553b8de72d24e0166f953248910926b55bd139aa9a7864c7b7ba504a9a8470a12ef013ce1b5153f441c653ef97e847f1e65f
7
- data.tar.gz: 4b8e05e523ca9abc3400a3455dfa7fb3b1adc0bd409eef90ed8856878e8938719663d45001ecbb3434e42fa17da07160be33f257acb753c900f9704ac05a7779
6
+ metadata.gz: 5ba551d781ba509f1aa8b93f47c84b8d22e6af8727659f7643ec73d6876b58811c7383fd8780c086a1c38dd8eefc3a546efc11297ab9ea2856be0c4a6fcb58ba
7
+ data.tar.gz: 0e0b3dddaa5d235ba5d10362cc7d9d737ebfbc5f69649620b0adc5d42eb43d92a884560947a47cc53604f2080fd5179106eaab3015d527c6c274b9cd6f3f83b9
data/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.0.3 — 2026-10-02
4
+
5
+ - Add Belt-native `pay:setup`, `pay:show`, and `pay:edit` tasks through Belt's
6
+ gem Rake-task extension mechanism. Select an environment with `BELT_ENV` or standard
7
+ task-argument syntax such as `belt 'pay:setup[dev]'`.
8
+ - Make `belt pay:setup` the happy path: it prompts without echo for Stripe's API and
9
+ webhook signing secrets, preserves either existing value when left blank, and writes
10
+ directly through the AWS SDK.
11
+ - Add `belt pay:show` and `belt pay:edit` for inspecting and securely editing each
12
+ environment's Stripe JSON secret in AWS Secrets Manager.
13
+ - Detect the Belt app name, AWS profile, and region from the selected environment,
14
+ with environment-variable and explicit overrides for custom setups.
15
+ - Retain the packaged `belt-pay secrets:*` executable as a backward-compatible alias,
16
+ pin the AWS Secrets Manager SDK, and include the pinned Rake runtime required for
17
+ task dispatch in fresh Belt apps.
18
+
3
19
  ## 0.0.2 — 2026-09-05
4
20
 
5
21
  - **Plan DSL (convention over configuration).** Declare subscription plans, their
data/README.md CHANGED
@@ -167,6 +167,54 @@ The Stripe secret should contain:
167
167
  }
168
168
  ```
169
169
 
170
+ ### Setting Up and Managing Secrets
171
+
172
+ Belt Pay hooks into Belt through its standard gem-provided Rake task mechanism. The
173
+ guided happy path uses `BELT_ENV` to select the environment:
174
+
175
+ ```bash
176
+ BELT_ENV=dev belt pay:setup
177
+ ```
178
+
179
+ It prompts without echo for the Stripe secret key and webhook signing secret, then
180
+ writes them directly through the AWS SDK. Leave either prompt blank to preserve its
181
+ existing value, so you can run setup once for the API key and again after Stripe gives
182
+ you the webhook signing secret.
183
+
184
+ You can also pass the environment with standard Rake task-argument syntax:
185
+
186
+ ```bash
187
+ belt 'pay:setup[dev]'
188
+ ```
189
+
190
+ Rails-like inspection and arbitrary JSON editing remain available for advanced use:
191
+
192
+ ```bash
193
+ BELT_ENV=dev belt pay:show
194
+ EDITOR="code --wait" BELT_ENV=dev belt pay:edit
195
+ # Or: belt 'pay:show[dev]' / belt 'pay:edit[dev]'
196
+ ```
197
+
198
+ `pay:show` prints the complete decrypted JSON to stdout. `pay:edit` opens it with
199
+ `$VISUAL` or `$EDITOR`, validates that the result is a JSON object, and writes a new
200
+ Secrets Manager version only when the content changed. Its temporary file is mode
201
+ `0600` and is removed when the editor closes. All write paths refuse to overwrite a
202
+ secret that changed while the command was open.
203
+
204
+ The tasks detect the application name from Terraform, the AWS profile from
205
+ `infrastructure/<env>/belt.rb`, and the region from Terraform or AWS environment
206
+ variables. Custom setups can override those values with `APP_NAME`,
207
+ `BELT_PAY_SECRET_NAME`, `AWS_PROFILE`, and `AWS_REGION`:
208
+
209
+ ```bash
210
+ APP_NAME=my_app BELT_ENV=prod AWS_PROFILE=my-prod-profile belt pay:setup
211
+ BELT_PAY_SECRET_NAME=custom-stripe-secret BELT_ENV=prod belt pay:edit
212
+ ```
213
+
214
+ Because `pay:show` intentionally reveals credentials, avoid redirecting its output to
215
+ logs or sharing it in chat. The packaged `belt-pay secrets:*` executable remains as a
216
+ backward-compatible alias, but the Belt-native tasks are the preferred interface.
217
+
170
218
  ## Common Patterns
171
219
 
172
220
  ### Annual Subscription (Feature Gating)
data/exe/belt-pay ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'belt/pay/cli'
5
+
6
+ exit Belt::Pay::CLI.start(ARGV)
@@ -315,9 +315,11 @@ module Belt
315
315
  # ...
316
316
  end
317
317
 
318
- 3. Create your Stripe API keys in Secrets Manager:
319
- Secret name: <app_name>-<env>-stripe
320
- Keys: stripe_secret_key, stripe_webhook_secret
318
+ 3. Add your Stripe API keys to Secrets Manager:
319
+ BELT_ENV=<env> belt pay:setup
320
+
321
+ Values are prompted without echo. Leave the webhook secret blank
322
+ until Stripe gives it to you, then run setup again.
321
323
 
322
324
  4. Deploy:
323
325
  belt apply <env>
@@ -0,0 +1,298 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'io/console'
4
+ require 'json'
5
+ require 'optparse'
6
+ require 'shellwords'
7
+ require 'tempfile'
8
+ require 'aws-sdk-secretsmanager'
9
+
10
+ module Belt
11
+ module Pay
12
+ class CLI
13
+ class Error < StandardError; end
14
+
15
+ COMMANDS = %w[secrets:setup secrets:show secrets:edit].freeze
16
+
17
+ def self.start(args, input: $stdin, out: $stdout, err: $stderr)
18
+ new(args, input: input, out: out, err: err).run
19
+ 0
20
+ rescue Error, OptionParser::ParseError => e
21
+ err.puts "Error: #{e.message}"
22
+ err.puts "Run `belt tasks --grep pay` for available Belt Pay commands."
23
+ 1
24
+ rescue Aws::Errors::ServiceError => e
25
+ err.puts "AWS error: #{e.message}"
26
+ 1
27
+ end
28
+
29
+ def initialize(args, input:, out:, err:)
30
+ @args = args.dup
31
+ @input = input
32
+ @out = out
33
+ @err = err
34
+ @options = {}
35
+ end
36
+
37
+ def run
38
+ command = @args.shift
39
+ if command.nil? || %w[help --help -h].include?(command)
40
+ @out.puts help
41
+ return
42
+ end
43
+
44
+ raise Error, "unknown command #{command.inspect}" unless COMMANDS.include?(command)
45
+
46
+ parse_options!
47
+ resolve_context!
48
+
49
+ case command
50
+ when 'secrets:setup' then setup
51
+ when 'secrets:show' then show
52
+ when 'secrets:edit' then edit
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ def parse_options!
59
+ parser.parse!(@args)
60
+ @environment = @args.shift || ENV['BELT_ENV']
61
+ raise Error, 'environment is required (for example, dev or prod)' if blank?(@environment)
62
+ raise Error, "unexpected argument #{@args.first.inspect}" unless @args.empty?
63
+ end
64
+
65
+ def parser
66
+ OptionParser.new do |opts|
67
+ opts.banner = 'Usage: belt-pay secrets:setup|secrets:show|secrets:edit ENV [options]'
68
+
69
+ opts.on('--app-name NAME', 'Override the detected Belt application name') do |value|
70
+ @options[:app_name] = value
71
+ end
72
+ opts.on('--secret-name NAME', 'Override the Secrets Manager secret name') do |value|
73
+ @options[:secret_name] = value
74
+ end
75
+ opts.on('--profile PROFILE', 'Override the AWS profile') do |value|
76
+ @options[:profile] = value
77
+ end
78
+ opts.on('--region REGION', 'Override the AWS region') do |value|
79
+ @options[:region] = value
80
+ end
81
+ opts.on('-h', '--help', 'Show this help') do
82
+ @out.puts help
83
+ exit 0
84
+ end
85
+ end
86
+ end
87
+
88
+ def resolve_context!
89
+ @root = find_project_root
90
+ env_dir = File.join(@root, 'infrastructure', @environment)
91
+ raise Error, "environment not found: #{env_dir}" unless Dir.exist?(env_dir)
92
+
93
+ @app_name = @options[:app_name] || ENV['APP_NAME'] || detect_app_name(env_dir)
94
+ raise Error, 'could not determine the application name; pass --app-name or set APP_NAME' if blank?(@app_name)
95
+
96
+ @secret_name = @options[:secret_name] || ENV['BELT_PAY_SECRET_NAME'] ||
97
+ "#{@app_name}-#{@environment}-stripe"
98
+ @profile = @options[:profile] || ENV['AWS_PROFILE'] || detect_profile(env_dir)
99
+ @region = @options[:region] || ENV['AWS_REGION'] || ENV['AWS_DEFAULT_REGION'] || detect_region(env_dir)
100
+
101
+ ENV['AWS_PROFILE'] = @profile unless blank?(@profile)
102
+ @client = Aws::SecretsManager::Client.new(region: @region)
103
+ end
104
+
105
+ def setup
106
+ response = fetch_secret
107
+ original = parse_secret(response.secret_string)
108
+ updated = original.dup
109
+
110
+ secret_key = hidden_prompt('Stripe secret key [blank keeps existing]')
111
+ webhook_secret = hidden_prompt('Stripe webhook signing secret [blank keeps existing]')
112
+ updated['stripe_secret_key'] = secret_key unless blank?(secret_key)
113
+ updated['stripe_webhook_secret'] = webhook_secret unless blank?(webhook_secret)
114
+
115
+ update_secret(response, original, updated)
116
+ end
117
+
118
+ def show
119
+ response = fetch_secret
120
+ @out.puts JSON.pretty_generate(parse_secret(response.secret_string))
121
+ end
122
+
123
+ def edit
124
+ response = fetch_secret
125
+ original = parse_secret(response.secret_string)
126
+ edited = edit_json(original)
127
+
128
+ update_secret(response, original, edited)
129
+ end
130
+
131
+ def update_secret(response, original, updated)
132
+ if updated == original
133
+ @out.puts "Secret unchanged: #{@secret_name}"
134
+ return
135
+ end
136
+
137
+ latest = fetch_secret
138
+ if latest.version_id != response.version_id
139
+ raise Error, "#{@secret_name} changed while it was open; reopen it and apply your edits again"
140
+ end
141
+
142
+ @client.put_secret_value(
143
+ secret_id: @secret_name,
144
+ secret_string: JSON.generate(updated)
145
+ )
146
+ @out.puts "Updated secret: #{@secret_name}"
147
+ end
148
+
149
+ def fetch_secret
150
+ response = @client.get_secret_value(secret_id: @secret_name)
151
+ raise Error, "#{@secret_name} is binary; belt-pay only supports JSON string secrets" if response.secret_string.nil?
152
+
153
+ response
154
+ end
155
+
156
+ def parse_secret(value)
157
+ parsed = JSON.parse(value)
158
+ raise Error, "#{@secret_name} must contain a JSON object" unless parsed.is_a?(Hash)
159
+
160
+ parsed
161
+ rescue JSON::ParserError => e
162
+ raise Error, "#{@secret_name} does not contain valid JSON: #{e.message}"
163
+ end
164
+
165
+ def hidden_prompt(label)
166
+ @out.print "#{label}: "
167
+ @out.flush
168
+ value = if @input.respond_to?(:tty?) && @input.tty? && @input.respond_to?(:noecho)
169
+ @input.noecho { @input.gets }
170
+ else
171
+ @input.gets
172
+ end
173
+ @out.puts
174
+ raise Error, 'input ended before both Stripe secrets were read' if value.nil?
175
+
176
+ value.chomp
177
+ end
178
+
179
+ def edit_json(secret)
180
+ editor = ENV['VISUAL'] || ENV['EDITOR']
181
+ raise Error, 'set $VISUAL or $EDITOR before running secrets:edit' if blank?(editor)
182
+
183
+ Tempfile.create(["belt-pay-#{@environment}-", '.json']) do |file|
184
+ File.chmod(0o600, file.path)
185
+ file.write("#{JSON.pretty_generate(secret)}\n")
186
+ file.flush
187
+
188
+ success = system(*Shellwords.split(editor), file.path)
189
+ raise Error, 'editor exited unsuccessfully; secret was not updated' unless success
190
+
191
+ file.rewind
192
+ parse_secret(file.read)
193
+ end
194
+ end
195
+
196
+ def find_project_root
197
+ dir = File.expand_path(Dir.pwd)
198
+ loop do
199
+ routes = [
200
+ File.join(dir, 'config', 'routes.rb'),
201
+ File.join(dir, 'config', 'routes.tf.rb'),
202
+ File.join(dir, 'infrastructure', 'routes.tf.rb')
203
+ ]
204
+ return dir if routes.any? { |path| File.file?(path) }
205
+
206
+ parent = File.dirname(dir)
207
+ break if parent == dir
208
+
209
+ dir = parent
210
+ end
211
+
212
+ raise Error, 'could not find a Belt application; run this command inside one'
213
+ end
214
+
215
+ def detect_app_name(env_dir)
216
+ from_assignment(File.join(env_dir, 'terraform.tfvars'), 'app_name') ||
217
+ from_variable_default(File.join(env_dir, 'variables.tf'), 'app_name') ||
218
+ Dir.glob(File.join(@root, 'infrastructure', '*', 'terraform.tfvars')).sort.filter_map do |path|
219
+ from_assignment(path, 'app_name')
220
+ end.first ||
221
+ Dir.glob(File.join(@root, 'infrastructure', '*', 'variables.tf')).sort.filter_map do |path|
222
+ from_variable_default(path, 'app_name')
223
+ end.first ||
224
+ File.basename(@root)
225
+ end
226
+
227
+ def detect_profile(env_dir)
228
+ path = File.join(env_dir, 'belt.rb')
229
+ return unless File.file?(path)
230
+
231
+ File.read(path)[/config\.aws_profile\s*=\s*['\"]([^'\"]+)['\"]/, 1]
232
+ end
233
+
234
+ def detect_region(env_dir)
235
+ from_assignment(File.join(env_dir, 'terraform.tfvars'), 'aws_region') ||
236
+ from_variable_default(File.join(env_dir, 'variables.tf'), 'aws_region') ||
237
+ 'us-east-1'
238
+ end
239
+
240
+ def from_assignment(path, key)
241
+ return unless File.file?(path)
242
+
243
+ File.read(path)[/^\s*#{Regexp.escape(key)}\s*=\s*['\"]([^'\"]+)['\"]/, 1]
244
+ end
245
+
246
+ def from_variable_default(path, key)
247
+ return unless File.file?(path)
248
+
249
+ block = File.read(path)[/variable\s+['\"]#{Regexp.escape(key)}['\"]\s*\{(.*?)\}/m, 1]
250
+ block&.match(/^\s*default\s*=\s*['\"]([^'\"]+)['\"]/)&.captures&.first
251
+ end
252
+
253
+ def blank?(value)
254
+ value.nil? || value.strip.empty?
255
+ end
256
+
257
+ def help
258
+ <<~HELP
259
+ Manage a Belt application's Stripe secret in AWS Secrets Manager.
260
+
261
+ Usage:
262
+ belt-pay secrets:setup ENV [options]
263
+ belt-pay secrets:show ENV [options]
264
+ belt-pay secrets:edit ENV [options]
265
+
266
+ Commands:
267
+ secrets:setup Prompt without echo for Stripe's secret values
268
+ secrets:show Print the decrypted JSON secret
269
+ secrets:edit Edit the secret with $VISUAL or $EDITOR
270
+
271
+ Options:
272
+ --app-name NAME Override the detected application name
273
+ --secret-name NAME Override <app>-<environment>-stripe
274
+ --profile PROFILE Override the environment's AWS profile
275
+ --region REGION Override the detected AWS region
276
+ -h, --help Show this help
277
+
278
+ Environment:
279
+ BELT_ENV Environment when ENV is omitted
280
+ AWS_PROFILE AWS profile (overrides infrastructure/ENV/belt.rb)
281
+ AWS_REGION AWS region (default: us-east-1)
282
+ VISUAL, EDITOR Editor command for secrets:edit
283
+
284
+ Examples:
285
+ belt-pay secrets:setup dev
286
+ belt-pay secrets:show dev
287
+ belt-pay secrets:edit prod --profile my-prod-profile
288
+ BELT_ENV=staging belt-pay secrets:setup
289
+
290
+ Security:
291
+ secrets:setup reads values without echo and sends them directly through the
292
+ AWS SDK. secrets:show writes the complete secret to stdout. secrets:edit uses
293
+ a mode-0600 temporary file, validates JSON, and removes the file afterward.
294
+ HELP
295
+ end
296
+ end
297
+ end
298
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Belt
4
4
  module Pay
5
- VERSION = '0.0.2'
5
+ VERSION = '0.0.3'
6
6
  end
7
7
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'belt/pay/cli'
4
+
5
+ run_pay_secret_command = lambda do |command, environment|
6
+ args = ["secrets:#{command}"]
7
+ args << environment unless environment.nil? || environment.to_s.strip.empty?
8
+
9
+ status = Belt::Pay::CLI.start(args)
10
+ exit status unless status.zero?
11
+ end
12
+
13
+ namespace :pay do
14
+ desc 'Prompt without echo for Stripe secrets (BELT_ENV or pay:setup[env])'
15
+ task :setup, [:environment] do |_task, args|
16
+ run_pay_secret_command.call(:setup, args[:environment])
17
+ end
18
+
19
+ desc 'Print the decrypted Stripe secret (BELT_ENV or pay:show[env])'
20
+ task :show, [:environment] do |_task, args|
21
+ run_pay_secret_command.call(:show, args[:environment])
22
+ end
23
+
24
+ desc 'Edit the Stripe secret as JSON (BELT_ENV or pay:edit[env])'
25
+ task :edit, [:environment] do |_task, args|
26
+ run_pay_secret_command.call(:edit, args[:environment])
27
+ end
28
+ end
metadata CHANGED
@@ -1,14 +1,28 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: belt-pay
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stowzilla
8
- bindir: bin
8
+ bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
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.136.0
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - '='
24
+ - !ruby/object:Gem::Version
25
+ version: 1.136.0
12
26
  - !ruby/object:Gem::Dependency
13
27
  name: belt
14
28
  requirement: !ruby/object:Gem::Requirement
@@ -23,6 +37,20 @@ dependencies:
23
37
  - - ">="
24
38
  - !ruby/object:Gem::Version
25
39
  version: '0.2'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rake
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - '='
45
+ - !ruby/object:Gem::Version
46
+ version: 13.4.2
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - '='
52
+ - !ruby/object:Gem::Version
53
+ version: 13.4.2
26
54
  - !ruby/object:Gem::Dependency
27
55
  name: stripe
28
56
  requirement: !ruby/object:Gem::Requirement
@@ -43,18 +71,21 @@ description: Belt plugin providing payment collection, subscriptions, and billin
43
71
  email:
44
72
  - andy@stowzilla.com
45
73
  - adam@stowzilla.com
46
- executables: []
74
+ executables:
75
+ - belt-pay
47
76
  extensions: []
48
77
  extra_rdoc_files: []
49
78
  files:
50
79
  - CHANGELOG.md
51
80
  - LICENSE
52
81
  - README.md
82
+ - exe/belt-pay
53
83
  - lib/belt-pay.rb
54
84
  - lib/belt/generators/pay_generator.rb
55
85
  - lib/belt/pay.rb
56
86
  - lib/belt/pay/billable.rb
57
87
  - lib/belt/pay/checkout.rb
88
+ - lib/belt/pay/cli.rb
58
89
  - lib/belt/pay/configuration.rb
59
90
  - lib/belt/pay/controllers/webhooks_controller.rb
60
91
  - lib/belt/pay/customer_provisioner.rb
@@ -73,6 +104,7 @@ files:
73
104
  - lib/belt/pay/transaction.rb
74
105
  - lib/belt/pay/version.rb
75
106
  - lib/belt/pay/webhook_handler.rb
107
+ - lib/tasks/pay.rake
76
108
  homepage: https://github.com/stowzilla/belt-pay
77
109
  licenses:
78
110
  - MIT