@kilnjs/payments-stripe
v0.1.3
Published
Stripe for KilnJS: webhook signatures verified and purchases normalised, over the HTTP API and no SDK
Readme
@kilnjs/payments-stripe
Stripe for KilnJS, over its HTTP API and nothing else. No SDK: what this needs is an HMAC, two GETs and a vocabulary.
yarn add @kilnjs/payments @kilnjs/payments-stripe// app.config.ts
import { payments } from "@kilnjs/payments";
import "@kilnjs/payments-stripe";Importing it registers the provider, after which Payments.stripe is
resolvable and typed, and notifications arrive at /payments/stripe already
verified.
| option | environment | |
| --- | --- | --- |
| secretKey | STRIPE_SECRET_KEY | required. Reaches everything the account can do |
| webhookSecret | STRIPE_WEBHOOK_SECRET | required. The signing secret for this endpoint, which is a different secret |
| tolerance | STRIPE_WEBHOOK_TOLERANCE | 300 seconds. How far out of step a notification may be |
| api | STRIPE_API_URL | the API, overridable for a local mock |
| timeout | STRIPE_TIMEOUT_MS | 10000. How long Stripe has to answer a lookup |
What verification checks
Three things, and leaving out any one of them is a hole somebody has shipped:
- the signature, compared in constant time, against any of the signatures in the header, because a rotation means two secrets are live at once;
- the body it signed, byte for byte, which is why these routes keep the raw body;
- the timestamp, within
tolerance, which is what stops a notification captured once from being replayed forever.
A refusal answers 400, says which shape of problem it was, and never echoes the signature it was given or the secret it expected.
What it maps
| Stripe | kind |
| --- | --- |
| checkout.session.completed, customer.subscription.created | purchased |
| invoice.paid, invoice.payment_succeeded | renewed |
| invoice.payment_failed | billing_failed |
| customer.subscription.deleted | cancelled |
| customer.subscription.paused / resumed | paused / recovered |
| charge.refunded, charge.dispute.created | refunded |
| anything else | other, with the event whole in raw |
Which of your users this is cannot be inferred: Stripe knows a customer id,
not a user. So event.actor.reference is whatever the application asked Stripe
to carry for it - client_reference_id on a session, or metadata.userId - and
putting something recognisable there is the application's half of the bargain.
A subscription told to stop at the end of its period is reported as active and
renewing: false, which is exactly the state a customer has to be told about.
Verifying
yarn verify # ts:check, lint, build, exports:check, testStripe is never reached: a signature is arithmetic, and the lookups are checked by what they send and what they refuse to say in a failure.
