belt-pay 0.0.3 → 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 +4 -4
- data/CHANGELOG.md +15 -0
- data/README.md +13 -5
- data/lib/belt/generators/pay_generator.rb +7 -12
- data/lib/belt/pay/cli.rb +133 -3
- data/lib/belt/pay/version.rb +1 -1
- data/lib/belt/pay/webhook_handler.rb +11 -0
- data/lib/tasks/pay.rake +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 225d94bd8d7f1b25849f237c4bb8432ce5b485b3a7ca5b88aee833d928481c4d
|
|
4
|
+
data.tar.gz: 7cabb1843e20857030506f0a3efe42a26aa375563f65c8ffb14334d84dc39b07
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ccf4f5972eabe4ffa0aa4c4d4be3207ac640970cea1ddaae857043bdf7d81a6df507932cc8c7704020326f578c46ee73ef0ea8e978b1443f761ca34f59deb960
|
|
7
|
+
data.tar.gz: 91f585c4b462664b2f193d8575fd5b551e969d613456ada465c4d3fa83387b4577b910fa64dd6e49f51b903082788ad8e48532a84174a76003368b5471ab40d1
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
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
|
+
|
|
3
18
|
## 0.0.3 — 2026-10-02
|
|
4
19
|
|
|
5
20
|
- Add Belt-native `pay:setup`, `pay:show`, and `pay:edit` tasks through Belt's
|
data/README.md
CHANGED
|
@@ -176,10 +176,17 @@ guided happy path uses `BELT_ENV` to select the environment:
|
|
|
176
176
|
BELT_ENV=dev belt pay:setup
|
|
177
177
|
```
|
|
178
178
|
|
|
179
|
-
It prompts without echo for the Stripe secret key
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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.
|
|
183
190
|
|
|
184
191
|
You can also pass the environment with standard Rake task-argument syntax:
|
|
185
192
|
|
|
@@ -354,7 +361,8 @@ module "pay" {
|
|
|
354
361
|
|
|
355
362
|
## Stripe Dashboard Setup
|
|
356
363
|
|
|
357
|
-
|
|
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:
|
|
358
366
|
|
|
359
367
|
1. Go to Stripe Dashboard → Developers → Webhooks
|
|
360
368
|
2. Add endpoint: `https://<your-api-domain>/pay/webhooks`
|
|
@@ -315,20 +315,15 @@ module Belt
|
|
|
315
315
|
# ...
|
|
316
316
|
end
|
|
317
317
|
|
|
318
|
-
3.
|
|
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.
|
|
323
|
-
|
|
324
|
-
4. Deploy:
|
|
318
|
+
3. Deploy:
|
|
325
319
|
belt apply <env>
|
|
326
320
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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.
|
|
332
327
|
|
|
333
328
|
Quick Usage:
|
|
334
329
|
# Create a checkout session
|
data/lib/belt/pay/cli.rb
CHANGED
|
@@ -2,10 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
require 'io/console'
|
|
4
4
|
require 'json'
|
|
5
|
+
require 'open3'
|
|
5
6
|
require 'optparse'
|
|
6
7
|
require 'shellwords'
|
|
7
8
|
require 'tempfile'
|
|
8
9
|
require 'aws-sdk-secretsmanager'
|
|
10
|
+
require 'stripe'
|
|
11
|
+
require 'belt/pay/webhook_handler'
|
|
9
12
|
|
|
10
13
|
module Belt
|
|
11
14
|
module Pay
|
|
@@ -13,6 +16,7 @@ module Belt
|
|
|
13
16
|
class Error < StandardError; end
|
|
14
17
|
|
|
15
18
|
COMMANDS = %w[secrets:setup secrets:show secrets:edit].freeze
|
|
19
|
+
WEBHOOK_PATH = '/pay/webhooks'
|
|
16
20
|
|
|
17
21
|
def self.start(args, input: $stdin, out: $stdout, err: $stderr)
|
|
18
22
|
new(args, input: input, out: out, err: err).run
|
|
@@ -24,6 +28,9 @@ module Belt
|
|
|
24
28
|
rescue Aws::Errors::ServiceError => e
|
|
25
29
|
err.puts "AWS error: #{e.message}"
|
|
26
30
|
1
|
|
31
|
+
rescue ::Stripe::StripeError => e
|
|
32
|
+
err.puts "Stripe error: #{e.message}"
|
|
33
|
+
1
|
|
27
34
|
end
|
|
28
35
|
|
|
29
36
|
def initialize(args, input:, out:, err:)
|
|
@@ -78,6 +85,12 @@ module Belt
|
|
|
78
85
|
opts.on('--region REGION', 'Override the AWS region') do |value|
|
|
79
86
|
@options[:region] = value
|
|
80
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
|
|
81
94
|
opts.on('-h', '--help', 'Show this help') do
|
|
82
95
|
@out.puts help
|
|
83
96
|
exit 0
|
|
@@ -108,11 +121,122 @@ module Belt
|
|
|
108
121
|
updated = original.dup
|
|
109
122
|
|
|
110
123
|
secret_key = hidden_prompt('Stripe secret key [blank keeps existing]')
|
|
111
|
-
webhook_secret = hidden_prompt('Stripe webhook signing secret [blank keeps existing]')
|
|
112
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'])
|
|
113
127
|
updated['stripe_webhook_secret'] = webhook_secret unless blank?(webhook_secret)
|
|
114
128
|
|
|
115
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')
|
|
116
240
|
end
|
|
117
241
|
|
|
118
242
|
def show
|
|
@@ -171,7 +295,7 @@ module Belt
|
|
|
171
295
|
@input.gets
|
|
172
296
|
end
|
|
173
297
|
@out.puts
|
|
174
|
-
raise Error, 'input ended before
|
|
298
|
+
raise Error, 'input ended before the Stripe secrets were read' if value.nil?
|
|
175
299
|
|
|
176
300
|
value.chomp
|
|
177
301
|
end
|
|
@@ -264,7 +388,9 @@ module Belt
|
|
|
264
388
|
belt-pay secrets:edit ENV [options]
|
|
265
389
|
|
|
266
390
|
Commands:
|
|
267
|
-
secrets:setup Prompt without echo for Stripe
|
|
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)
|
|
268
394
|
secrets:show Print the decrypted JSON secret
|
|
269
395
|
secrets:edit Edit the secret with $VISUAL or $EDITOR
|
|
270
396
|
|
|
@@ -273,12 +399,16 @@ module Belt
|
|
|
273
399
|
--secret-name NAME Override <app>-<environment>-stripe
|
|
274
400
|
--profile PROFILE Override the environment's AWS profile
|
|
275
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)
|
|
276
404
|
-h, --help Show this help
|
|
277
405
|
|
|
278
406
|
Environment:
|
|
279
407
|
BELT_ENV Environment when ENV is omitted
|
|
280
408
|
AWS_PROFILE AWS profile (overrides infrastructure/ENV/belt.rb)
|
|
281
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
|
|
282
412
|
VISUAL, EDITOR Editor command for secrets:edit
|
|
283
413
|
|
|
284
414
|
Examples:
|
data/lib/belt/pay/version.rb
CHANGED
|
@@ -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
|
data/lib/tasks/pay.rake
CHANGED
|
@@ -11,7 +11,7 @@ run_pay_secret_command = lambda do |command, environment|
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
namespace :pay do
|
|
14
|
-
desc '
|
|
14
|
+
desc 'Store the Stripe key and create the webhook endpoint (BELT_ENV or pay:setup[env])'
|
|
15
15
|
task :setup, [:environment] do |_task, args|
|
|
16
16
|
run_pay_secret_command.call(:setup, args[:environment])
|
|
17
17
|
end
|