Skip to content

stripe 20.4.1 -> 22.4.0, with the API version pinned first - #244

Open
Jan0707 wants to merge 2 commits into
mainfrom
stripe-api-pin-and-upgrade
Open

stripe 20.4.1 -> 22.4.0, with the API version pinned first#244
Jan0707 wants to merge 2 commits into
mainfrom
stripe-api-pin-and-upgrade

Conversation

@Jan0707

@Jan0707 Jan0707 commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Closes Phase 5. This was the last item, and it was waiting on a payment-path review — which pinning the API version largely removes the need for. See the last section.

The plan item said "check the pinned API version". There wasn't one

server/utils/stripe.ts called new Stripe(config.stripeSecretKey) with no options, so the SDK sent whichever version it bundled:

SDK bundled API version
20.4.1 (was installed) 2026-02-25.clover
22.4.0 (target) 2026-07-29.dahlia

So this was never "a library upgrade that happens to touch types" — it would have silently moved checkout and webhooks onto a different Stripe API major, with nothing in the diff saying so.

Two commits, and the order is the point

  1. Pin the Stripe API version explicitly — sets apiVersion: '2026-02-25.clover'. Verified as a no-op by reading the SDK's own default out of node_modules and comparing: identical strings, so nothing about today's behaviour changes.
  2. stripe 20.4.1 -> 22.4.0 — the bump, with the wire held constant by the pin.

What is left for a reviewer is now one line, cloverdahlia, rather than a version bump with an API migration hidden inside it.

The cast is the price of that split

SDK 22 types apiVersion as the single literal it bundles, so holding clover cannot type-check:

server/utils/stripe.ts(24,63): error TS2322: Type '"2026-02-25.clover"' is not assignable to type '"2026-07-29.dahlia"'.

That was the only type error the entire upgrade produced — which is itself the useful finding: our code is already dahlia-compatible. Resolved with as Stripe.LatestApiVersion, and the cost is written into the file: TypeScript no longer rejects a nonsense version string there.

Named honestly: types now describe dahlia while the wire speaks clover. That cannot break existing calls, but it would let TypeScript accept a dahlia-only field that clover rejects at runtime. It is the argument for doing the dahlia move soon rather than eventually.

Checked against the API surface, not the changelog summary

The app's entire Stripe surface is checkout.sessions.create, webhooks.constructEvent, and the Stripe.Event / Stripe.Checkout.Session types. Each dahlia breaking change against the params buildStripeSessionParams actually sends:

dahlia breaking change our exposure
dynamic_tax_rates removed from line_items[] not used — no occurrence in the repo
twint.setup_future_usage enum widened not used
Issuing / PaymentRecord / Radar / V2 Core none of those resources are called

SDK 22's own breaking changes are clear too: we already use new Stripe(), pass params as a single first argument, and use no callbacks, no per-request host override, and none of the dropped type aliases.

Nine new tests, because the webhook is the one silent failure

tickets/webhook.post.ts is how a paid order becomes a fulfilled order. If verification breaks, the site keeps taking money and stops issuing tickets — and no gate and no visitor would see it. SDK 21 added "throw an error when using the wrong webhook parsing method", so that was the change to worry about.

test/stripeWebhook.test.ts, no network and no real keys (Stripe's own generateTestHeaderString and createFetchHttpClient):

  • a correctly signed payload verifies and parses, including metadata.order_id
  • a payload modified after signing is rejected
  • a signature from a different secret is rejected
  • a signature older than the replay tolerance is rejected
  • a Buffer body works, which is what readRawBody provides
  • the outgoing request carries Stripe-Version: 2026-02-25.clover — proof the pin reaches the wire
  • negative control: an unpinned client sends something different. Without this, the assertion above cannot distinguish "the pin is honoured" from "the SDK sends clover anyway"
  • the checkout request posts amount, currency and metadata[order_id] intact under SDK 22's rewritten argument parsing
  • {CHECKOUT_SESSION_ID} survives unescaped for Stripe to substitute

The signature tests are mutually validating rather than individually trusted: the same payload and secret verify, and each single perturbation fails, so the signing helper cannot be a no-op.

And a runtime probe, because of Pinia

Pinia 4 passed every local gate and failed only on a real request under NODE_ENV=production, because bundling changed how it initialised. SDK 22 reworked its entry points and made Stripe a true ES6 class — same class of risk, and a green build says nothing about it.

Booted .output/server/index.mjs under NODE_ENV=production with dummy credentials and posted to /api/tickets/webhook:

probe result
forged signature t=1,v1=deadbeef 400 Invalid signature
valid signature, same payload 200 {"received":true}

Both were needed — an endpoint that rejects everything passes the first, one that verifies nothing passes the second.

Verification

npm test 100 passing / 11 files (was 91/10) · prettier:check clean · lint 0 errors, 127 warnings · typecheck ratchet steady at 263 · build exit 0 · npm audit 0 vulnerabilities · plus the production-bundle probe. All on Node 24.19.0.

What still wants your colleague

Moving cloverdahlia. It is now one line, it deletes the cast, and it removes the types/wire mismatch. The evidence above says it is a no-op for this app's surface — but "the changelog says our fields are untouched" is not the same as someone who knows the checkout flow confirming it. That is the review this item was waiting for, now reduced to a single decision instead of being bundled with an SDK major. Logged in the backlog with a note that two tests naming clover are designed to fail when it moves.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JkKMceYAzAYLrSyC42FWTf

Jan0707 and others added 2 commits August 5, 2026 16:13
`new Stripe(key)` sends whichever API version the SDK happens to bundle, so
the version this app talks to has been an invisible property of a
transitive default rather than a decision.

That matters for the pending 20.4.1 -> 22.4.0 upgrade: SDK 20.4.1 bundles
2026-02-25.clover and 22.4.0 bundles 2026-07-29.dahlia, so bumping the
library would silently move checkout and webhooks onto a different Stripe
API version — a payment-behaviour change disguised as a dependency bump.

Pinning the current value first makes that separable. Verified as a no-op:
the SDK's own default is read from node_modules and matches the pinned
string exactly, so nothing about today's behaviour changes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JkKMceYAzAYLrSyC42FWTf
The API version is pinned in the previous commit, so this bump changes the
library and provably nothing on the wire. Without that, it would have moved
checkout and webhooks from 2026-02-25.clover to 2026-07-29.dahlia — a
different Stripe API major — with nothing in the diff saying so.

The pin now needs a cast: SDK 22 types apiVersion as the single literal it
bundles, so holding clover cannot type-check. That TS2322 was the *only*
type error the upgrade produced, which is the useful part — our code is
already dahlia-compatible. The cost is recorded in the file: TypeScript no
longer rejects a nonsense version string on that line.

Checked dahlia's breaking changes against the params we actually send, not
the changelog summary. dynamic_tax_rates and the twint enum are both unused;
everything else is in Issuing, PaymentRecord, Radar and V2 Core, which this
app never calls. SDK 22's own breaking changes are clear too: we already use
new Stripe(), pass params as a single first argument, and use no callbacks,
host overrides or dropped type aliases.

Adds test/stripeWebhook.test.ts (9 cases). SDK 21 added "throw an error when
using the wrong webhook parsing method", and the webhook is the one silent
failure in this app: if verification breaks, the site keeps taking money and
stops issuing tickets, with nothing a gate or a visitor would notice. The
tests sign real payloads with Stripe's own helper and capture the outgoing
request with a fake fetch — no network, no keys. One is a negative control
proving the pin does work: an unpinned client sends a different version.

Also probed the built bundle under NODE_ENV=production, because Pinia 4
passed every gate and failed only on a real request. A forged signature
returns 400, a valid one 200.

100 tests pass, lint 0 errors, ratchet steady at 263, audit 0
vulnerabilities, build exit 0, all on Node 24.19.0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JkKMceYAzAYLrSyC42FWTf
Copilot AI lite review requested due to automatic review settings August 5, 2026 14:24
@vercel

vercel Bot commented Aug 5, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
programmierbar-website Ready Ready Preview Aug 5, 2026 2:25pm

Request Review

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR upgrades the Nuxt app’s Stripe SDK from 20.x to 22.4.0 while explicitly pinning the Stripe API version to prevent a silent API-version migration during the dependency bump. It also adds focused Vitest coverage around webhook signature verification and Checkout Session request construction to guard against the primary “silent failure” mode in the payment path.

Changes:

  • Pin Stripe API version via apiVersion when constructing the Stripe client.
  • Upgrade stripe dependency to ^22.4.0 (with corresponding lockfile updates).
  • Add a new Vitest suite to validate webhook signature verification behavior and to assert the pinned Stripe-Version header is sent on checkout requests.

Reviewed changes

Copilot reviewed 4 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
nuxt-app/server/utils/stripe.ts Pins Stripe API version and centralizes Stripe client construction.
nuxt-app/test/stripeWebhook.test.ts Adds tests for webhook signature verification and checkout request/header behavior under Stripe SDK 22.
nuxt-app/package.json Bumps stripe dependency to ^22.4.0.
nuxt-app/package-lock.json Updates lockfile for the Stripe bump and transitive dependency metadata changes.
docs/dependency-upgrade-plan.md Documents the Stripe upgrade approach and follow-up plan for clover → dahlia.
Files not reviewed (1)
  • nuxt-app/package-lock.json: Generated file
Suppressed comments (1)

nuxt-app/test/stripeWebhook.test.ts:178

  • This assertion checks the decoded request body, so what it really guarantees is that {CHECKOUT_SESSION_ID} is preserved after form decoding (i.e. not double-URL-encoded). The current test name/comment reads like it is asserting the raw wire encoding, which is confusing for application/x-www-form-urlencoded.
    it('keeps the {CHECKOUT_SESSION_ID} placeholder unescaped for Stripe to substitute', async () => {
        const { sent } = await capture(PARAMS)

        expect(sent.body).toContain('success_url=https://example.com/success?session_id={CHECKOUT_SESSION_ID}')

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +69 to +71
it('accepts a Buffer body, which is what the raw request provides', () => {
const buffer = Buffer.from(EVENT, 'utf8')
const event = stripe.webhooks.constructEvent(buffer, sign(EVENT), SECRET)
// bundles, so pinning any other version cannot type-check — the SDK is on `2026-07-29.dahlia` while we
// deliberately still talk `2026-02-25.clover`. The cost is that TypeScript no longer rejects a
// nonsense version string here, so treat this line as hand-checked against Stripe's changelog.
export const STRIPE_API_VERSION = '2026-02-25.clover' as Stripe.LatestApiVersion
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants