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 +4 -4
- data/CHANGELOG.md +31 -0
- data/README.md +57 -1
- data/exe/belt-pay +6 -0
- data/lib/belt/generators/pay_generator.rb +7 -10
- data/lib/belt/pay/cli.rb +428 -0
- data/lib/belt/pay/version.rb +1 -1
- data/lib/belt/pay/webhook_handler.rb +11 -0
- data/lib/tasks/pay.rake +28 -0
- metadata +35 -3
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,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
|
-
|
|
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
|
@@ -315,18 +315,15 @@ module Belt
|
|
|
315
315
|
# ...
|
|
316
316
|
end
|
|
317
317
|
|
|
318
|
-
3.
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
data/lib/belt/pay/cli.rb
ADDED
|
@@ -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
|
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
ADDED
|
@@ -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.
|
|
4
|
+
version: 0.0.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Stowzilla
|
|
8
|
-
bindir:
|
|
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
|