Skip to content

fix(checkout): auth-gate /app/checkout + fail-closed razorpay_configured (BUG-P111/P112/P013/P088/P121)#146

Merged
mastermanas805 merged 1 commit into
mainfrom
fix/checkout-auth-gate-and-cache-reset
May 29, 2026
Merged

fix(checkout): auth-gate /app/checkout + fail-closed razorpay_configured (BUG-P111/P112/P013/P088/P121)#146
mastermanas805 merged 1 commit into
mainfrom
fix/checkout-auth-gate-and-cache-reset

Conversation

@mastermanas805
Copy link
Copy Markdown
Member

Summary

QA found that visiting /app/checkout/?plan=hobby while unauthenticated
landed at a LIVE-mode Razorpay subscription page (sub_Sv96Mt2n8nnDYL).
A stale localStorage JWT was passing the App-level AuthGate, so the SPA
short-circuited into a real Razorpay create-subscription call.
BUG-P111 (P0 critical).

Plus three sibling bugs from the same QA session:

  • BUG-P013 (P1): /login dropped the plan context, losing the funnel after signin.
  • BUG-P088 (P1): razorpay_configured defaulted to TRUE when the API omitted the flag — clicking upgrade hit a 502 from Razorpay because recurring is not enabled on the prod account (per CLAUDE.md memory project_razorpay_recurring_not_enabled.md).
  • BUG-P121/P122 (P1): a future caching regression of sub_* shortlinks would survive logout without a dedicated purge.

Fixes

  1. CheckoutPage gains a second-layer auth gate that re-checks getToken() on mount before any createCheckout call. On miss → window.location.assign('/login?next=<encoded path>'). Preserves plan + frequency so post-signin returns the user to the same checkout.
  2. CheckoutPage handles the new 503 billing_misconfigured envelope (from the companion api PR's server-side guard) the same way as billing_not_configured — friendly fallback panel, no live Razorpay URL.
  3. CheckoutPage registers a logout hook that purges every localStorage key under CHECKOUT_CACHE_KEY_PREFIX. The page does NOT cache sub_* today; the prefix + purge guarantee is the defensive belt against any future caching regression.
  4. LoginPage honors ?next=<encoded path> from the query string, falling back to loc.state.from then /app. Only relative same-origin paths are honoured — /login?next=https://evil.com and protocol-relative //evil.com both fall back to /app to defeat open-redirect phishing.
  5. mapBillingState flips razorpay_configured ?? true → ?? false. When the API doesn't say, hide the upgrade button. Honest copy beats a button that 502s.

Surface checklist (rule 22)

Surface Status
api/plans.yaml not affected
common/plans/plans.go defaultYAML not affected
api/internal/handlers/openapi.go updated in companion api PR (fix/billing-traffic-env-and-misconfig-detection)
instanode-web/PricingPage.tsx not affected (CTA wiring unchanged)
content/llms.txt not affected

Tests added

All 1069 vitest tests pass (76 files):

  • TestCheckoutPage_UnauthRedirectsToLogin
  • TestCheckoutPage_PreservesNextParam
  • TestCheckoutPage_PreservesFrequencyMonthlyWhenOnlyPlan
  • TestCheckoutPage_FlipsToUnauthOnMidFlight401
  • TestCheckoutPage_ClearsCachedSubOnLogout (clearCheckoutCache suite)
  • TestCheckoutPage_RendersFallbackOn503BillingMisconfigured (BUG-P112 server-side guard handshake)
  • TestLoginPage_HonorsNextRoundTripsToCheckout (BUG-P013)
  • TestLoginPage_RejectsAbsoluteEvilCom (open-redirect)
  • TestLoginPage_RejectsProtocolRelativeNext (open-redirect)
  • TestFetchBilling_DefaultsRazorpayConfiguredFalse (BUG-P088 fail-closed)

Live verification (rule 14)

Queued for after merge & GH Pages deploy. Per brief:

  • curl https://instanode.dev/app/checkout/?plan=hobby | grep -c "rzp_live" should be 0.
  • Unauth navigation lands at /login?next=/app/checkout?... (Chrome MCP screenshot to follow).
  • curl /api/v1/billing/checkout response includes traffic_env (verified via companion api PR).

Risk

The auth-gate change adds a redundant pre-flight check that the App-level AuthGate already covers for the no-token case — but the brief is specifically about the stale-JWT case where the App gate passes and the server 401s. Mid-flight 401 path tagged unauthenticated. Logged-in upgrade flow tests still pass (the redirect happens BEFORE the auth gate in the effect, and the existing getToken() returns 'test-token' by default in the test mock).

🤖 Generated with Claude Code

…red (BUG-P111/P112/P013/P088/P121)

QA found that visiting /app/checkout/?plan=hobby while unauthenticated
landed at a LIVE-mode Razorpay subscription page (sub_Sv96Mt2n8nnDYL).
A stale localStorage JWT was passing the App-level AuthGate (which only
checks getToken(), not validity), so the SPA short-circuited into a
real Razorpay create-subscription call. BUG-P111 (P0 critical).

Plus three sibling bugs from the same QA session:
- BUG-P013 (P1): /login redirect dropped the plan context, losing the
  funnel after signin.
- BUG-P088 (P1): razorpay_configured defaulted to TRUE when the API
  omitted the flag — clicking upgrade hit a 502 from Razorpay because
  recurring is not enabled on the prod account (per CLAUDE.md memory
  project_razorpay_recurring_not_enabled.md).
- BUG-P121/P122 (P1): a future caching regression of sub_* shortlinks
  would survive logout without a dedicated purge.

Fixes:
1. CheckoutPage gains a SECOND-LAYER auth gate that re-checks getToken()
   on mount BEFORE any createCheckout call. On miss it fails CLOSED with
   window.location.assign('/login?next=<encoded path>'), preserving plan
   + frequency so post-signin returns the user to the same checkout.
2. CheckoutPage also handles the new 503 billing_misconfigured envelope
   from the server-side guard (fix/billing-traffic-env-and-misconfig-
   detection on the api repo) the same way as billing_not_configured —
   honest fallback panel instead of a raw error banner.
3. CheckoutPage registers a logout hook that purges every localStorage
   key under CHECKOUT_CACHE_KEY_PREFIX. The page does NOT cache sub_*
   today; the prefix + purge guarantee is the defensive belt against
   any future regression that adds such caching.
4. LoginPage now honors ?next=<encoded path> from the query string,
   falling back to loc.state.from (existing AuthGate path) then /app.
   Only relative same-origin paths are honoured — /login?next=https:
   //evil.com and protocol-relative //evil.com both fall back to /app
   to defeat open-redirect phishing.
5. mapBillingState flips razorpay_configured `?? true` → `?? false`.
   When the API doesn't say, hide the upgrade button. Honest copy beats
   a button that 502s.

### Surface checklist (rule 22)
- api/plans.yaml                         not affected
- common/plans/plans.go defaultYAML      not affected
- api/internal/handlers/openapi.go       updated in companion PR (api)
- instanode-web/.../PricingPage.tsx      not affected (CTA wiring unchanged)
- content/llms.txt                       not affected
- dashboard upgradeCopy.ts                not present in this repo

Tests added (all 1069 vitest pass):
- TestCheckoutPage_UnauthRedirectsToLogin
- TestCheckoutPage_PreservesNextParam
- TestCheckoutPage_PreservesFrequencyMonthlyWhenOnlyPlan
- TestCheckoutPage_FlipsToUnauthOnMidFlight401
- TestCheckoutPage_ClearsCachedSubOnLogout (clearCheckoutCache suite)
- TestCheckoutPage_RendersFallbackOn503BillingMisconfigured (BUG-P112)
- LoginPage_HonorsNextRoundTripsToCheckout (BUG-P013)
- LoginPage_RejectsAbsoluteEvilCom (open-redirect)
- LoginPage_RejectsProtocolRelativeNext (open-redirect)
- TestFetchBilling_DefaultsRazorpayConfiguredFalse (BUG-P088 fail-closed)

Live verification queued in companion api PR — see body for curl + Chrome MCP evidence.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

size-limit report 📦

Path Size
dist/assets/index-BUkatQ42.js 0 B (-100% 🔽)
dist/assets/index-BsJUZYRr.css 6.13 KB (0%)
dist/assets/index-Btm4w76P.js 163.5 KB (+100% 🔺)

@mastermanas805 mastermanas805 merged commit ce78974 into main May 29, 2026
16 checks passed
@mastermanas805 mastermanas805 deleted the fix/checkout-auth-gate-and-cache-reset branch May 29, 2026 11:09
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.

1 participant