Skip to content

🔒 Billing & auth stack hardening: webhook retries, canceled guard, fail-closed metering, grant and verify races #4151

Description

@PierreBrisorgueil

Small, independent hardening fixes in the billing and auth stack modules. They were found by formal models and property tests of the billing and signup flows. Each one is a rare edge case whose correct behaviour costs a few lines. No new persisted field, collection or flag.

Fixes (one commit each; red test first, then green):

  1. Checkout webhook: when stripe.subscriptions.retrieve fails in the checkout.session.completed handler (billing.webhook.service.js, around the retrieve catch), throw instead of return. Stripe then redelivers the event, instead of it being recorded as processed while the subscription row stays unlinked on the free plan. Update the unit test that pins the old behaviour.
  2. Invoice webhooks: in the invoice.payment_failed and invoice.payment_succeeded handlers, right after findByStripeSubscriptionId, add if (existing.status === 'canceled') return;. A late invoice event must not bring a canceled subscription back to active or past_due.
  3. Metering: incrementMeter (billing.usage.service.js) must bill against the free plan when the subscription status is fail-closed (unpaid / paused / incomplete, the same list the quota gate uses in billing.quota.service.js). Today the gate treats these as free while the meter still consumes the paid quota. Widen the findPlan projection to include status and reuse the gate's list; don't duplicate it.
  4. Extras debit: wrap BillingExtraService.debit in the existing retryWithBackoff (modules/billing/lib/billing.retry.js). The debit is idempotent by key, so a retry cannot double-charge, and a transient DB error no longer makes the overflow free.
  5. Grant idempotency across orgs: in creditGrant (billing.extraBalance.repository.js), before the write, return { applied: false, reason: 'duplicate_grant' } when exists({ 'ledger.refId': idempotencyKey }) holds in any org. Today the guard is per org, so a grant whose target org changed between retries is credited twice.
  6. Email verification race: in verifyEmail (auth controller), consume the token with one atomic findOneAndUpdate on the unexpired token that sets it to null and marks the email verified. A null result returns the existing 400. Two concurrent verifications must not provision two workspaces.
  7. Runbook: add one line to modules/billing/RUNBOOKS.md: "customer paid but no credit → POST /api/admin/billing/webhook/replay {eventId}" (an event lost mid-handler is replayed by hand).

Tests: one unit or integration test per fix, reproducing the edge case (red before the fix, green after).

Scope: validated 2026-09-28

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions