You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(billing): detect live-key-in-dev + surface traffic_env on /checkout (BUG-P112)
QA team reported that visiting /app/checkout/?plan=hobby while
unauthenticated landed at a LIVE-mode Razorpay subscription page
(sub_Sv96Mt2n8nnDYL). BUG-P111 was the SPA-side root cause (now fixed
in instanode-web fix/checkout-auth-gate-and-cache-reset); BUG-P112 is
the server-side belt-and-braces: an operator pointing a non-prod
deployment at a LIVE Razorpay key would let any caller reaching this
handler mint a REAL subscription on the prod Razorpay account.
This change adds two coupled guards to CreateCheckoutAPI:
1. trafficEnv() + detectBillingMisconfiguration() classify the
configured RAZORPAY_KEY_ID via its documented prefix convention
(rzp_live_* → production, rzp_test_* → test). When ENVIRONMENT !=
"production" but the key is live, the handler short-circuits with
503 billing_misconfigured + a clear operator agent_action — BEFORE
the create-subscription call. Razorpay's dashboard never sees the
phantom subscription; the operator gets a single actionable error.
The guard is intentionally placed BEFORE the existing
billing_not_configured branch: a live-in-dev pairing is dangerous
even if RAZORPAY_PLAN_ID_TEAM happens to be unset on that
environment.
2. Successful 200 responses (both fresh-create and F7 reuse paths)
now include traffic_env: "production" | "test". Agents and the SPA
can branch on the field without ever seeing the raw key value.
SECURITY CONTRACT: the actual RAZORPAY_KEY_ID is NEVER echoed in
ANY response body — only the derived two-state field. Multiple
tests pin this constraint.
OpenAPI doc updated to match (rule 22).
### Surface checklist (rule 22)
- api/plans.yaml not affected
- common/plans/plans.go defaultYAML not affected
- api/internal/handlers/openapi.go UPDATED (new 200 field + 503 description)
- instanode-web/.../CheckoutPage.tsx UPDATED in companion PR (web)
- content/llms.txt not affected (response shape change only)
- dashboard upgradeCopy.ts not affected
Tests added (all checkout tests pass — `go test ./internal/handlers/ -short -run TestCheckout`):
- TestBillingCheckout_DetectsLiveKeyInDevEnv (full env × key matrix)
- TestBillingCheckout_TrafficEnvDerivation_OnlyProductionOrTest
- TestBillingCheckout_ResponseIncludesTrafficEnv (DB-required — happy path)
- TestBillingCheckout_ResponseTrafficEnvIsTestForTestKey (DB-required)
- TestBillingCheckout_TrafficEnvSurfacesOnReusePath (DB-required — F7 reuse)
The 'NEVER leak the RAZORPAY_KEY_ID' security contract is asserted via
json.Marshal scanning of every response body in every relevant test.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
"Razorpay LIVE key configured on a non-production deployment (ENVIRONMENT="+dep+"). Refusing to mint a real subscription. Operator: rotate to a test key (rzp_test_*) or set ENVIRONMENT=production. See https://instanode.dev/docs/operator/billing-modes."
186
+
}
187
+
return"", ""
188
+
}
189
+
101
190
// BillingHandler handles billing and Razorpay webhook endpoints.
"Operator: a LIVE Razorpay key is configured on a non-production deployment. Either rotate to a test key (rzp_test_*) or set ENVIRONMENT=production. Real subscriptions cannot be minted against this deployment until that is fixed.",
0 commit comments