backend-manager 5.0.197 → 5.0.198

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.
package/CHANGELOG.md CHANGED
@@ -14,6 +14,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
14
14
  - `Fixed` for any bug fixes.
15
15
  - `Security` in case of vulnerabilities.
16
16
 
17
+ # [5.0.198] - 2026-04-10
18
+ ### Security
19
+ - Added Stripe idempotency keys on all Stripe write operations to prevent duplicate charges, refunds, customers, and coupons from webhook retries, concurrent requests, or user double-clicks. Keys are scoped to stable resource identifiers and Stripe caches responses for 24 hours.
20
+ - `bem-dispute-refund-{chargeId}` on auto-refunds from dispute alert Firestore triggers
21
+ - `bem-customer-create-{uid}` on Stripe customer creation
22
+ - `bem-coupon-{couponId}` on coupon creation in the intent route
23
+ - `bem-refund-{resourceId}` on manual subscription refunds
24
+
17
25
  # [5.0.197] - 2026-04-10
18
26
  ### Added
19
27
  - `--retry=N` flag on `npx mgr setup` — re-runs the full setup sequence up to N times, stopping early as soon as all checks pass. Useful for test cases that only succeed after a prior run creates fixtures or indexes propagate.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backend-manager",
3
- "version": "5.0.197",
3
+ "version": "5.0.198",
4
4
  "description": "Quick tools for developing Firebase functions",
5
5
  "main": "src/manager/index.js",
6
6
  "bin": {
@@ -121,9 +121,14 @@ async function processDispute(match, alert, assistant) {
121
121
  // Issue full refund
122
122
  if (match.chargeId) {
123
123
  try {
124
+ // Idempotency key scoped to the charge prevents double-refund when the
125
+ // Firestore trigger fires more than once (retries, re-delivered webhooks,
126
+ // etc.). Stripe caches the response for 24 hours.
124
127
  const refund = await stripe.refunds.create({
125
128
  charge: match.chargeId,
126
129
  amount: amountCents,
130
+ }, {
131
+ idempotencyKey: `bem-dispute-refund-${match.chargeId}`,
127
132
  });
128
133
 
129
134
  result.refundId = refund.id;
@@ -206,6 +206,9 @@ const Stripe = {
206
206
  }
207
207
 
208
208
  // Create new customer
209
+ // Use an idempotency key scoped to the uid so concurrent creates (e.g. user
210
+ // double-clicks checkout) return the same customer instead of duplicates.
211
+ // Stripe caches the response under this key for 24 hours.
209
212
  const params = {
210
213
  metadata: { uid },
211
214
  };
@@ -214,7 +217,9 @@ const Stripe = {
214
217
  params.email = email;
215
218
  }
216
219
 
217
- const customer = await stripe.customers.create(params);
220
+ const customer = await stripe.customers.create(params, {
221
+ idempotencyKey: `bem-customer-create-${uid}`,
222
+ });
218
223
  assistant.log(`Created new Stripe customer: ${customer.id}`);
219
224
  return customer;
220
225
  },
@@ -153,11 +153,16 @@ async function resolveStripeCoupon(stripe, discount, assistant) {
153
153
  }
154
154
 
155
155
  // Create the coupon
156
+ // Idempotency key uses the deterministic couponId so concurrent requests for
157
+ // the same discount code don't race each other into a duplicate-create error.
158
+ // Stripe returns the cached response for 24 hours.
156
159
  await stripe.coupons.create({
157
160
  id: couponId,
158
161
  percent_off: discount.percent,
159
162
  duration: 'once',
160
163
  name: `${discount.code} (${discount.percent}% off first payment)`,
164
+ }, {
165
+ idempotencyKey: `bem-coupon-${couponId}`,
161
166
  });
162
167
 
163
168
  assistant.log(`Stripe coupon created: ${couponId}`);
@@ -79,10 +79,16 @@ module.exports = {
79
79
  ? invoice.payment_intent
80
80
  : invoice.payment_intent.id;
81
81
 
82
+ // Idempotency key scoped to the subscription prevents double-refund from
83
+ // a user double-clicking the refund button. Stripe caches the response for
84
+ // 24 hours — any concurrent or repeated refund request for the same sub
85
+ // within that window returns the original refund instead of issuing a new one.
82
86
  const refund = await stripe.refunds.create({
83
87
  payment_intent: paymentIntentId,
84
88
  amount: refundAmount,
85
89
  reason: 'requested_by_customer',
90
+ }, {
91
+ idempotencyKey: `bem-refund-${resourceId}`,
86
92
  });
87
93
 
88
94
  assistant.log(`Stripe refund created: refundId=${refund.id}, amount=${refundAmount}, full=${isFullRefund}, uid=${uid}`);