Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ach-conform

A static check for one specific Stripe footgun: webhook handlers that treat payment_intent.succeeded as final for an ACH (us_bank_account) charge.

Card payments settle in seconds and succeeded really is the end of the story. ACH debits don't — Stripe can flip a succeeded PaymentIntent to failed up to several business days later when the bank returns it (R01 insufficient funds, R02/R03 account closed or unauthorized). If your webhook handler fulfils the order or marks the invoice paid on succeeded and never listens for the later payment_intent.payment_failed / charge.failed, you ship a silent false-success bug: paid-looking orders that were never actually paid.

ach-conform scans your webhook handler source for that pattern (and for a second one: ACH intents that never account for requires_action, the micro-deposit verification state card payments never hit) and fails your CI when it finds them.

Install

npm install --save-dev ach-conform

Or run it without installing:

npx ach-conform src/webhooks/

Usage

npx ach-conform [path...]

Defaults to scanning the current directory. Exits 1 if it finds a succeeded-without-failed handler, 0 otherwise (the requires_action check is a warning and doesn't fail the build on its own).

$ npx ach-conform src/
[error] src/webhooks/stripe.js:14 (ach-succeeded-not-final)
  Handles payment_intent.succeeded for an ACH (us_bank_account) charge but never handles
  payment_intent.payment_failed or charge.failed. ACH debits can flip from succeeded to failed
  days later (R01 insufficient funds, R02/R03 closed or unauthorized). Treating succeeded as
  terminal ships a silent false-success bug -- fulfil/mark-paid logic needs a corresponding
  failure-path reversal.

ach-conform: 1 finding(s) in src/.

CI

# .github/workflows/ach-conform.yml
name: ach-conform
on: [push, pull_request]
jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npx ach-conform src/

What it checks

  • ach-succeeded-not-final (error) — a file references us_bank_account and handles payment_intent.succeeded, but never references payment_intent.payment_failed or charge.failed.
  • ach-missing-requires-action (warn) — a file handles ACH succeeded/processing states but never references requires_action or micro-deposits, so verification-pending customers may have no path forward.

This is deliberately a narrow, textual check — a couple of regexes over your source, not a full AST analysis of control flow. It will miss handlers split across multiple files or built with heavy indirection, and it can false-flag a file that handles the failure case somewhere it doesn't expect to look. Treat a clean run as "no smoking gun found," not a correctness proof.

Testing the real thing

A static check only catches the anti-pattern in your code, not whether your Stripe integration actually behaves as expected end to end. examples/test-clock-replay.js uses Stripe Test Clocks to advance a test-mode ACH PaymentIntent through the multi-day settlement window and confirm your webhook handler receives both events and reverses fulfilment correctly:

STRIPE_SECRET_KEY=sk_test_... node examples/test-clock-replay.js

examples/bad-handler.js and examples/good-handler.js are minimal handlers ach-conform flags and clears, respectively — useful as a reference for what the fix looks like.

Provenance

Built autonomously by an AI agent (fernforge).

License

MIT

About

Static check for the Stripe ACH succeeded-isnt-final webhook trap (us_bank_account payment_intent.succeeded without payment_failed handling).

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages