belt-pay 0.0.2 → 0.0.4

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: 225d94bd8d7f1b25849f237c4bb8432ce5b485b3a7ca5b88aee833d928481c4d
4
+ data.tar.gz: 7cabb1843e20857030506f0a3efe42a26aa375563f65c8ffb14334d84dc39b07
5
5
  SHA512:
6
- metadata.gz: 51d01a9a411c28d3e080352bb265553b8de72d24e0166f953248910926b55bd139aa9a7864c7b7ba504a9a8470a12ef013ce1b5153f441c653ef97e847f1e65f
7
- data.tar.gz: 4b8e05e523ca9abc3400a3455dfa7fb3b1adc0bd409eef90ed8856878e8938719663d45001ecbb3434e42fa17da07160be33f257acb753c900f9704ac05a7779
6
+ metadata.gz: ccf4f5972eabe4ffa0aa4c4d4be3207ac640970cea1ddaae857043bdf7d81a6df507932cc8c7704020326f578c46ee73ef0ea8e978b1443f761ca34f59deb960
7
+ data.tar.gz: 91f585c4b462664b2f193d8575fd5b551e969d613456ada465c4d3fa83387b4577b910fa64dd6e49f51b903082788ad8e48532a84174a76003368b5471ab40d1
data/CHANGELOG.md CHANGED
@@ -1,5 +1,36 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.0.4 — 2026-10-20
4
+
5
+ - **`belt pay:setup` creates the Stripe webhook endpoint for you.** After the secret-key
6
+ prompt it reads `api_url` from `terraform output`, offers to create
7
+ `<api_url>/pay/webhooks` in Stripe (subscribed to every event Belt Pay handles), and
8
+ stores the returned `whsec_` signing secret directly. No Dashboard clicking.
9
+ - Shows whether the key is test or live mode before creating anything.
10
+ - If Stripe already has an endpoint for that URL, offers to replace it (the old one
11
+ is deleted only after the new secret is saved) or links to it in the Dashboard.
12
+ - Declining, or missing terraform output, prints step-by-step Dashboard instructions
13
+ with the exact URL and events, then prompts for the secret as before.
14
+ - `--webhook-url` / `BELT_PAY_WEBHOOK_URL` and `--webhook-events` /
15
+ `BELT_PAY_WEBHOOK_EVENTS` override the defaults.
16
+ - `Belt::Pay::WebhookHandler::EVENTS` lists the Stripe events the handler acts on.
17
+
18
+ ## 0.0.3 — 2026-10-02
19
+
20
+ - Add Belt-native `pay:setup`, `pay:show`, and `pay:edit` tasks through Belt's
21
+ gem Rake-task extension mechanism. Select an environment with `BELT_ENV` or standard
22
+ task-argument syntax such as `belt 'pay:setup[dev]'`.
23
+ - Make `belt pay:setup` the happy path: it prompts without echo for Stripe's API and
24
+ webhook signing secrets, preserves either existing value when left blank, and writes
25
+ directly through the AWS SDK.
26
+ - Add `belt pay:show` and `belt pay:edit` for inspecting and securely editing each
27
+ environment's Stripe JSON secret in AWS Secrets Manager.
28
+ - Detect the Belt app name, AWS profile, and region from the selected environment,
29
+ with environment-variable and explicit overrides for custom setups.
30
+ - Retain the packaged `belt-pay secrets:*` executable as a backward-compatible alias,
31
+ pin the AWS Secrets Manager SDK, and include the pinned Rake runtime required for
32
+ task dispatch in fresh Belt apps.
33
+
3
34
  ## 0.0.2 — 2026-09-05
4
35
 
5
36
  - **Plan DSL (convention over configuration).** Declare subscription plans, their
data/README.md CHANGED
@@ -167,6 +167,61 @@ 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, then offers to create the webhook
180
+ endpoint in Stripe for you: it reads `api_url` from `terraform output`, creates
181
+ `<api_url>/pay/webhooks` subscribed to every [handled event](#webhook-events), and stores
182
+ the `whsec_` signing secret Stripe returns. Everything goes directly through the Stripe
183
+ API and AWS SDK. Run it after `belt apply` so the URL is known.
184
+
185
+ Decline (or run it before the first apply) and it prints step-by-step Dashboard
186
+ instructions with the exact URL and events, then prompts for the signing secret. Leave
187
+ either prompt blank to preserve its existing value. Use `--webhook-url` /
188
+ `BELT_PAY_WEBHOOK_URL` or `--webhook-events` / `BELT_PAY_WEBHOOK_EVENTS` to override the
189
+ defaults. Test (`sk_test_`) and live (`sk_live_`) keys get separate endpoints and secrets.
190
+
191
+ You can also pass the environment with standard Rake task-argument syntax:
192
+
193
+ ```bash
194
+ belt 'pay:setup[dev]'
195
+ ```
196
+
197
+ Rails-like inspection and arbitrary JSON editing remain available for advanced use:
198
+
199
+ ```bash
200
+ BELT_ENV=dev belt pay:show
201
+ EDITOR="code --wait" BELT_ENV=dev belt pay:edit
202
+ # Or: belt 'pay:show[dev]' / belt 'pay:edit[dev]'
203
+ ```
204
+
205
+ `pay:show` prints the complete decrypted JSON to stdout. `pay:edit` opens it with
206
+ `$VISUAL` or `$EDITOR`, validates that the result is a JSON object, and writes a new
207
+ Secrets Manager version only when the content changed. Its temporary file is mode
208
+ `0600` and is removed when the editor closes. All write paths refuse to overwrite a
209
+ secret that changed while the command was open.
210
+
211
+ The tasks detect the application name from Terraform, the AWS profile from
212
+ `infrastructure/<env>/belt.rb`, and the region from Terraform or AWS environment
213
+ variables. Custom setups can override those values with `APP_NAME`,
214
+ `BELT_PAY_SECRET_NAME`, `AWS_PROFILE`, and `AWS_REGION`:
215
+
216
+ ```bash
217
+ APP_NAME=my_app BELT_ENV=prod AWS_PROFILE=my-prod-profile belt pay:setup
218
+ BELT_PAY_SECRET_NAME=custom-stripe-secret BELT_ENV=prod belt pay:edit
219
+ ```
220
+
221
+ Because `pay:show` intentionally reveals credentials, avoid redirecting its output to
222
+ logs or sharing it in chat. The packaged `belt-pay secrets:*` executable remains as a
223
+ backward-compatible alias, but the Belt-native tasks are the preferred interface.
224
+
170
225
  ## Common Patterns
171
226
 
172
227
  ### Annual Subscription (Feature Gating)
@@ -306,7 +361,8 @@ module "pay" {
306
361
 
307
362
  ## Stripe Dashboard Setup
308
363
 
309
- After deploying, configure your Stripe webhook endpoint:
364
+ `BELT_ENV=<env> belt pay:setup` creates the webhook endpoint and stores its signing
365
+ secret for you. To do it by hand instead:
310
366
 
311
367
  1. Go to Stripe Dashboard → Developers → Webhooks
312
368
  2. Add endpoint: `https://<your-api-domain>/pay/webhooks`
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,18 +315,15 @@ 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
321
-
322
- 4. Deploy:
318
+ 3. Deploy:
323
319
  belt apply <env>
324
320
 
325
- 5. Configure Stripe webhook endpoint:
326
- URL: https://<your-api-domain>/pay/webhooks
327
- Events: checkout.session.completed, checkout.session.expired,
328
- invoice.paid, invoice.payment_failed,
329
- customer.subscription.deleted, customer.subscription.updated
321
+ 4. Store your Stripe key and create the webhook endpoint:
322
+ BELT_ENV=<env> belt pay:setup
323
+
324
+ Prompts without echo for the Stripe secret key, then creates
325
+ <api_url>/pay/webhooks in Stripe and stores its signing secret.
326
+ Decline to get step-by-step Dashboard instructions instead.
330
327
 
331
328
  Quick Usage:
332
329
  # Create a checkout session
@@ -0,0 +1,428 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'io/console'
4
+ require 'json'
5
+ require 'open3'
6
+ require 'optparse'
7
+ require 'shellwords'
8
+ require 'tempfile'
9
+ require 'aws-sdk-secretsmanager'
10
+ require 'stripe'
11
+ require 'belt/pay/webhook_handler'
12
+
13
+ module Belt
14
+ module Pay
15
+ class CLI
16
+ class Error < StandardError; end
17
+
18
+ COMMANDS = %w[secrets:setup secrets:show secrets:edit].freeze
19
+ WEBHOOK_PATH = '/pay/webhooks'
20
+
21
+ def self.start(args, input: $stdin, out: $stdout, err: $stderr)
22
+ new(args, input: input, out: out, err: err).run
23
+ 0
24
+ rescue Error, OptionParser::ParseError => e
25
+ err.puts "Error: #{e.message}"
26
+ err.puts "Run `belt tasks --grep pay` for available Belt Pay commands."
27
+ 1
28
+ rescue Aws::Errors::ServiceError => e
29
+ err.puts "AWS error: #{e.message}"
30
+ 1
31
+ rescue ::Stripe::StripeError => e
32
+ err.puts "Stripe error: #{e.message}"
33
+ 1
34
+ end
35
+
36
+ def initialize(args, input:, out:, err:)
37
+ @args = args.dup
38
+ @input = input
39
+ @out = out
40
+ @err = err
41
+ @options = {}
42
+ end
43
+
44
+ def run
45
+ command = @args.shift
46
+ if command.nil? || %w[help --help -h].include?(command)
47
+ @out.puts help
48
+ return
49
+ end
50
+
51
+ raise Error, "unknown command #{command.inspect}" unless COMMANDS.include?(command)
52
+
53
+ parse_options!
54
+ resolve_context!
55
+
56
+ case command
57
+ when 'secrets:setup' then setup
58
+ when 'secrets:show' then show
59
+ when 'secrets:edit' then edit
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ def parse_options!
66
+ parser.parse!(@args)
67
+ @environment = @args.shift || ENV['BELT_ENV']
68
+ raise Error, 'environment is required (for example, dev or prod)' if blank?(@environment)
69
+ raise Error, "unexpected argument #{@args.first.inspect}" unless @args.empty?
70
+ end
71
+
72
+ def parser
73
+ OptionParser.new do |opts|
74
+ opts.banner = 'Usage: belt-pay secrets:setup|secrets:show|secrets:edit ENV [options]'
75
+
76
+ opts.on('--app-name NAME', 'Override the detected Belt application name') do |value|
77
+ @options[:app_name] = value
78
+ end
79
+ opts.on('--secret-name NAME', 'Override the Secrets Manager secret name') do |value|
80
+ @options[:secret_name] = value
81
+ end
82
+ opts.on('--profile PROFILE', 'Override the AWS profile') do |value|
83
+ @options[:profile] = value
84
+ end
85
+ opts.on('--region REGION', 'Override the AWS region') do |value|
86
+ @options[:region] = value
87
+ end
88
+ opts.on('--webhook-url URL', 'Override the Stripe webhook endpoint URL') do |value|
89
+ @options[:webhook_url] = value
90
+ end
91
+ opts.on('--webhook-events LIST', 'Comma-separated Stripe events for a new endpoint') do |value|
92
+ @options[:webhook_events] = value
93
+ end
94
+ opts.on('-h', '--help', 'Show this help') do
95
+ @out.puts help
96
+ exit 0
97
+ end
98
+ end
99
+ end
100
+
101
+ def resolve_context!
102
+ @root = find_project_root
103
+ env_dir = File.join(@root, 'infrastructure', @environment)
104
+ raise Error, "environment not found: #{env_dir}" unless Dir.exist?(env_dir)
105
+
106
+ @app_name = @options[:app_name] || ENV['APP_NAME'] || detect_app_name(env_dir)
107
+ raise Error, 'could not determine the application name; pass --app-name or set APP_NAME' if blank?(@app_name)
108
+
109
+ @secret_name = @options[:secret_name] || ENV['BELT_PAY_SECRET_NAME'] ||
110
+ "#{@app_name}-#{@environment}-stripe"
111
+ @profile = @options[:profile] || ENV['AWS_PROFILE'] || detect_profile(env_dir)
112
+ @region = @options[:region] || ENV['AWS_REGION'] || ENV['AWS_DEFAULT_REGION'] || detect_region(env_dir)
113
+
114
+ ENV['AWS_PROFILE'] = @profile unless blank?(@profile)
115
+ @client = Aws::SecretsManager::Client.new(region: @region)
116
+ end
117
+
118
+ def setup
119
+ response = fetch_secret
120
+ original = parse_secret(response.secret_string)
121
+ updated = original.dup
122
+
123
+ secret_key = hidden_prompt('Stripe secret key [blank keeps existing]')
124
+ updated['stripe_secret_key'] = secret_key unless blank?(secret_key)
125
+
126
+ webhook_secret, replaced = webhook_secret_for(updated['stripe_secret_key'], original['stripe_webhook_secret'])
127
+ updated['stripe_webhook_secret'] = webhook_secret unless blank?(webhook_secret)
128
+
129
+ update_secret(response, original, updated)
130
+ delete_replaced_endpoints(updated['stripe_secret_key'], replaced)
131
+ end
132
+
133
+ # Returns [signing_secret, endpoints_to_delete]. Offers to create the Stripe
134
+ # webhook endpoint (Stripe only returns a signing secret at creation time);
135
+ # otherwise prints Dashboard instructions and prompts for the secret.
136
+ def webhook_secret_for(api_key, existing_secret)
137
+ if blank?(api_key)
138
+ @out.puts 'No Stripe secret key stored yet, so the webhook endpoint cannot be created for you.'
139
+ return [manual_webhook_secret(nil), []]
140
+ end
141
+
142
+ mode = stripe_mode(api_key)
143
+ url = webhook_url
144
+ @out.puts "Stripe account mode: #{mode}"
145
+ @out.puts "Webhook endpoint URL: #{url || '(unknown; run `belt apply` first or pass --webhook-url)'}"
146
+
147
+ create = !url.nil? && confirm("Create the #{mode}-mode webhook endpoint in Stripe now?", default: blank?(existing_secret))
148
+ return [manual_webhook_secret(url, mode: mode), []] unless create
149
+
150
+ existing = stripe_endpoints(api_key).select { |endpoint| endpoint.url == url }
151
+ if existing.any?
152
+ ids = existing.map(&:id).join(', ')
153
+ @out.puts "Stripe already has an endpoint for this URL (#{ids}). Stripe never re-sends a signing secret,"
154
+ @out.puts 'so setup can only replace it with a new endpoint (the old secret stops working).'
155
+ unless confirm('Replace it?', default: false)
156
+ @out.puts "Reveal its signing secret at #{dashboard_url(mode, existing.first.id)}"
157
+ return [hidden_prompt('Stripe webhook signing secret [blank keeps existing]'), []]
158
+ end
159
+ end
160
+
161
+ endpoint = ::Stripe::WebhookEndpoint.create(
162
+ { url: url, enabled_events: webhook_events, description: "Belt Pay (#{@app_name} #{@environment})" },
163
+ { api_key: api_key }
164
+ )
165
+ @out.puts "Created #{mode}-mode webhook endpoint #{endpoint.id} for #{webhook_events.size} events."
166
+ [endpoint.secret, existing.map(&:id)]
167
+ end
168
+
169
+ def manual_webhook_secret(url, mode: nil)
170
+ @out.puts <<~STEPS
171
+
172
+ To set up the webhook in the Stripe Dashboard#{" (#{mode} mode)" if mode}:
173
+ 1. Developers -> Webhooks -> Add destination (#{mode == 'live' ? 'https://dashboard.stripe.com/webhooks' : 'https://dashboard.stripe.com/test/webhooks'})
174
+ 2. Events from: Your account. API version: the default.
175
+ 3. Events: #{webhook_events.join(', ')}
176
+ 4. Destination type: Webhook endpoint
177
+ 5. URL: #{url || "your API base URL (`belt output #{@environment}` api_url) + #{WEBHOOK_PATH}"}
178
+ 6. Open the new endpoint -> Signing secret -> Reveal, and paste it below.
179
+ Each endpoint (and test vs. live mode) has its own whsec_ secret. Do not use the
180
+ one `stripe listen` prints; it only works for local forwarding.
181
+
182
+ STEPS
183
+ hidden_prompt('Stripe webhook signing secret [blank keeps existing]')
184
+ end
185
+
186
+ def delete_replaced_endpoints(api_key, ids)
187
+ ids.each do |id|
188
+ ::Stripe::WebhookEndpoint.delete(id, {}, { api_key: api_key })
189
+ @out.puts "Deleted replaced webhook endpoint #{id}"
190
+ end
191
+ end
192
+
193
+ def stripe_endpoints(api_key)
194
+ ::Stripe::WebhookEndpoint.list({ limit: 100 }, { api_key: api_key }).auto_paging_each.to_a
195
+ end
196
+
197
+ def webhook_url
198
+ return @webhook_url if defined?(@webhook_url)
199
+
200
+ explicit = @options[:webhook_url] || ENV['BELT_PAY_WEBHOOK_URL']
201
+ @webhook_url = if blank?(explicit)
202
+ api_url = terraform_output('api_url')
203
+ "#{api_url.chomp('/')}#{WEBHOOK_PATH}" unless blank?(api_url)
204
+ else
205
+ explicit.strip
206
+ end
207
+ end
208
+
209
+ def webhook_events
210
+ list = @options[:webhook_events] || ENV['BELT_PAY_WEBHOOK_EVENTS']
211
+ return Belt::Pay::WebhookHandler::EVENTS if blank?(list)
212
+
213
+ list.split(',').map(&:strip).reject(&:empty?)
214
+ end
215
+
216
+ def terraform_output(name)
217
+ env_dir = File.join(@root, 'infrastructure', @environment)
218
+ stdout, _stderr, status = Open3.capture3('terraform', "-chdir=#{env_dir}", 'output', '-raw', name)
219
+ stdout.strip if status.success?
220
+ rescue SystemCallError
221
+ nil
222
+ end
223
+
224
+ def stripe_mode(api_key)
225
+ api_key.match?(/\A[sr]k_live_/) ? 'live' : 'test'
226
+ end
227
+
228
+ def dashboard_url(mode, endpoint_id)
229
+ "https://dashboard.stripe.com/#{'test/' unless mode == 'live'}webhooks/#{endpoint_id}"
230
+ end
231
+
232
+ def confirm(question, default:)
233
+ @out.print "#{question} #{default ? '[Y/n]' : '[y/N]'} "
234
+ @out.flush
235
+ answer = @input.gets
236
+ @out.puts if answer.nil?
237
+ return default if answer.nil? || answer.strip.empty?
238
+
239
+ answer.strip.downcase.start_with?('y')
240
+ end
241
+
242
+ def show
243
+ response = fetch_secret
244
+ @out.puts JSON.pretty_generate(parse_secret(response.secret_string))
245
+ end
246
+
247
+ def edit
248
+ response = fetch_secret
249
+ original = parse_secret(response.secret_string)
250
+ edited = edit_json(original)
251
+
252
+ update_secret(response, original, edited)
253
+ end
254
+
255
+ def update_secret(response, original, updated)
256
+ if updated == original
257
+ @out.puts "Secret unchanged: #{@secret_name}"
258
+ return
259
+ end
260
+
261
+ latest = fetch_secret
262
+ if latest.version_id != response.version_id
263
+ raise Error, "#{@secret_name} changed while it was open; reopen it and apply your edits again"
264
+ end
265
+
266
+ @client.put_secret_value(
267
+ secret_id: @secret_name,
268
+ secret_string: JSON.generate(updated)
269
+ )
270
+ @out.puts "Updated secret: #{@secret_name}"
271
+ end
272
+
273
+ def fetch_secret
274
+ response = @client.get_secret_value(secret_id: @secret_name)
275
+ raise Error, "#{@secret_name} is binary; belt-pay only supports JSON string secrets" if response.secret_string.nil?
276
+
277
+ response
278
+ end
279
+
280
+ def parse_secret(value)
281
+ parsed = JSON.parse(value)
282
+ raise Error, "#{@secret_name} must contain a JSON object" unless parsed.is_a?(Hash)
283
+
284
+ parsed
285
+ rescue JSON::ParserError => e
286
+ raise Error, "#{@secret_name} does not contain valid JSON: #{e.message}"
287
+ end
288
+
289
+ def hidden_prompt(label)
290
+ @out.print "#{label}: "
291
+ @out.flush
292
+ value = if @input.respond_to?(:tty?) && @input.tty? && @input.respond_to?(:noecho)
293
+ @input.noecho { @input.gets }
294
+ else
295
+ @input.gets
296
+ end
297
+ @out.puts
298
+ raise Error, 'input ended before the Stripe secrets were read' if value.nil?
299
+
300
+ value.chomp
301
+ end
302
+
303
+ def edit_json(secret)
304
+ editor = ENV['VISUAL'] || ENV['EDITOR']
305
+ raise Error, 'set $VISUAL or $EDITOR before running secrets:edit' if blank?(editor)
306
+
307
+ Tempfile.create(["belt-pay-#{@environment}-", '.json']) do |file|
308
+ File.chmod(0o600, file.path)
309
+ file.write("#{JSON.pretty_generate(secret)}\n")
310
+ file.flush
311
+
312
+ success = system(*Shellwords.split(editor), file.path)
313
+ raise Error, 'editor exited unsuccessfully; secret was not updated' unless success
314
+
315
+ file.rewind
316
+ parse_secret(file.read)
317
+ end
318
+ end
319
+
320
+ def find_project_root
321
+ dir = File.expand_path(Dir.pwd)
322
+ loop do
323
+ routes = [
324
+ File.join(dir, 'config', 'routes.rb'),
325
+ File.join(dir, 'config', 'routes.tf.rb'),
326
+ File.join(dir, 'infrastructure', 'routes.tf.rb')
327
+ ]
328
+ return dir if routes.any? { |path| File.file?(path) }
329
+
330
+ parent = File.dirname(dir)
331
+ break if parent == dir
332
+
333
+ dir = parent
334
+ end
335
+
336
+ raise Error, 'could not find a Belt application; run this command inside one'
337
+ end
338
+
339
+ def detect_app_name(env_dir)
340
+ from_assignment(File.join(env_dir, 'terraform.tfvars'), 'app_name') ||
341
+ from_variable_default(File.join(env_dir, 'variables.tf'), 'app_name') ||
342
+ Dir.glob(File.join(@root, 'infrastructure', '*', 'terraform.tfvars')).sort.filter_map do |path|
343
+ from_assignment(path, 'app_name')
344
+ end.first ||
345
+ Dir.glob(File.join(@root, 'infrastructure', '*', 'variables.tf')).sort.filter_map do |path|
346
+ from_variable_default(path, 'app_name')
347
+ end.first ||
348
+ File.basename(@root)
349
+ end
350
+
351
+ def detect_profile(env_dir)
352
+ path = File.join(env_dir, 'belt.rb')
353
+ return unless File.file?(path)
354
+
355
+ File.read(path)[/config\.aws_profile\s*=\s*['\"]([^'\"]+)['\"]/, 1]
356
+ end
357
+
358
+ def detect_region(env_dir)
359
+ from_assignment(File.join(env_dir, 'terraform.tfvars'), 'aws_region') ||
360
+ from_variable_default(File.join(env_dir, 'variables.tf'), 'aws_region') ||
361
+ 'us-east-1'
362
+ end
363
+
364
+ def from_assignment(path, key)
365
+ return unless File.file?(path)
366
+
367
+ File.read(path)[/^\s*#{Regexp.escape(key)}\s*=\s*['\"]([^'\"]+)['\"]/, 1]
368
+ end
369
+
370
+ def from_variable_default(path, key)
371
+ return unless File.file?(path)
372
+
373
+ block = File.read(path)[/variable\s+['\"]#{Regexp.escape(key)}['\"]\s*\{(.*?)\}/m, 1]
374
+ block&.match(/^\s*default\s*=\s*['\"]([^'\"]+)['\"]/)&.captures&.first
375
+ end
376
+
377
+ def blank?(value)
378
+ value.nil? || value.strip.empty?
379
+ end
380
+
381
+ def help
382
+ <<~HELP
383
+ Manage a Belt application's Stripe secret in AWS Secrets Manager.
384
+
385
+ Usage:
386
+ belt-pay secrets:setup ENV [options]
387
+ belt-pay secrets:show ENV [options]
388
+ belt-pay secrets:edit ENV [options]
389
+
390
+ Commands:
391
+ secrets:setup Prompt without echo for the Stripe secret key, then create
392
+ the webhook endpoint in Stripe and store its signing
393
+ secret (or walk through creating it in the Dashboard)
394
+ secrets:show Print the decrypted JSON secret
395
+ secrets:edit Edit the secret with $VISUAL or $EDITOR
396
+
397
+ Options:
398
+ --app-name NAME Override the detected application name
399
+ --secret-name NAME Override <app>-<environment>-stripe
400
+ --profile PROFILE Override the environment's AWS profile
401
+ --region REGION Override the detected AWS region
402
+ --webhook-url URL Override <terraform api_url>#{WEBHOOK_PATH}
403
+ --webhook-events L Comma-separated events (default: every event Belt Pay handles)
404
+ -h, --help Show this help
405
+
406
+ Environment:
407
+ BELT_ENV Environment when ENV is omitted
408
+ AWS_PROFILE AWS profile (overrides infrastructure/ENV/belt.rb)
409
+ AWS_REGION AWS region (default: us-east-1)
410
+ BELT_PAY_WEBHOOK_URL Same as --webhook-url
411
+ BELT_PAY_WEBHOOK_EVENTS Same as --webhook-events
412
+ VISUAL, EDITOR Editor command for secrets:edit
413
+
414
+ Examples:
415
+ belt-pay secrets:setup dev
416
+ belt-pay secrets:show dev
417
+ belt-pay secrets:edit prod --profile my-prod-profile
418
+ BELT_ENV=staging belt-pay secrets:setup
419
+
420
+ Security:
421
+ secrets:setup reads values without echo and sends them directly through the
422
+ AWS SDK. secrets:show writes the complete secret to stdout. secrets:edit uses
423
+ a mode-0600 temporary file, validates JSON, and removes the file afterward.
424
+ HELP
425
+ end
426
+ end
427
+ end
428
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Belt
4
4
  module Pay
5
- VERSION = '0.0.2'
5
+ VERSION = '0.0.4'
6
6
  end
7
7
  end
@@ -5,6 +5,17 @@ module Belt
5
5
  # Handles incoming webhooks from the payment provider.
6
6
  # Verifies signatures, dispatches events, and records transactions.
7
7
  module WebhookHandler
8
+ # Stripe events #dispatch acts on. `belt pay:setup` subscribes new webhook
9
+ # endpoints to exactly this list.
10
+ EVENTS = %w[
11
+ checkout.session.completed
12
+ checkout.session.expired
13
+ invoice.paid
14
+ invoice.payment_failed
15
+ customer.subscription.deleted
16
+ customer.subscription.updated
17
+ ].freeze
18
+
8
19
  class << self
9
20
  # Process a raw webhook request.
10
21
  # @param payload [String] Raw request body
@@ -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 'Store the Stripe key and create the webhook endpoint (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.4
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