Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 17 additions & 12 deletions apps/supercode-cli/client/app/studio/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,14 +71,6 @@ type CreditBalance = {
resetAt: string | null
}

const CHECKOUT_BASE =
process.env.NEXT_PUBLIC_DODO_CHECKOUT_BASE ?? "https://checkout.dodopayments.com/buy"

function getCheckoutUrl(plan: Plan): string | null {
if (!plan.dodoProductId) return null
return `${CHECKOUT_BASE}/${plan.dodoProductId}?quantity=1`
}

const TIER_COLORS: Record<string, string> = {
spark: "text-emerald-400 border-emerald-500/30",
"spark-premium": "text-cyan-400 border-cyan-500/30",
Expand Down Expand Up @@ -293,12 +285,25 @@ function StudioPage() {

const handleConfirmPayNow = useCallback(async () => {
if (!confirmingPlan || !userId) return
const url = getCheckoutUrl(confirmingPlan)
if (url) {
window.open(url, "_blank", "noopener,noreferrer")
} else {
if (!confirmingPlan.dodoProductId) {
setConfirmingPlan(null)
toast.error("Checkout URL not available for this plan")
return
}
try {
const res = await fetch("/api/billing/checkout", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ userId, planId: confirmingPlan.id }),
Comment on lines +294 to +297

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline apps/supercode-cli/server/src/api/billing/checkout.ts --items all

rg -n -C 10 \
  'router\.(post|use)|req\.body|userId|planId|session|auth|unauthorized|forbidden' \
  apps/supercode-cli/server/src/api/billing/checkout.ts

Repository: yashdev9274/supercli

Length of output: 3782


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== client checkout request context =="
sed -n '270,310p' apps/supercode-cli/client/app/studio/page.tsx

echo
echo "== server api/billing checkout complete =="
cat -n apps/supercode-cli/server/src/api/billing/checkout.ts

echo
echo "== auth/session/ownership patterns in server routes =="
rg -n -C 4 "req\\.user|\\.session|session|auth|authorize|forbidden|unauthorized|middleware|Router\\.use|beforeEach|withAuth|getServerSession|getSession" apps/supercode-cli/server/src/api apps/supercode-cli/server/src | head -n 240

Repository: yashdev9274/supercli

Length of output: 25788


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== server app auth usage =="
sed -n '1,220p' apps/supercode-cli/server/src/index.ts

echo
echo "== auth definition outline and relevant auth usages =="
sed -n '1,260p' apps/supercode-cli/server/src/lib/auth.ts

echo
echo "== server-side auth/session helpers =="
rg -n -C 3 "\.sign\s*\(|\.verify\s*\(|getSession|getServerSession|middleware|router\.use|express-session|cookie-session|better-auth" apps/supercode-cli/server/src

Repository: yashdev9274/supercli

Length of output: 13442


Reject checkout requests where userId does not match the authenticated user.

/api/billing/checkout accepts userId directly from the request body and creates the Dodo checkout using that value. Add middleware that resolves the request user, then compare it with req.body.userId before querying User or creating the checkout session.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/supercode-cli/client/app/studio/page.tsx` around lines 294 - 297,
Protect the checkout flow around the POST request to /api/billing/checkout by
resolving the authenticated request user before any User query or
checkout-session creation, then reject the request when its authenticated
identity does not match req.body.userId. Ensure downstream billing logic uses
only the validated authenticated user identity.

})
const data = await res.json()
if (!res.ok || !data.checkout_url) {
throw new Error(data.error ?? "Failed to create checkout session")
}
window.location.href = data.checkout_url as string
} catch (err) {
setConfirmingPlan(null)
toast.error(err instanceof Error ? err.message : "Checkout failed")
}
}, [confirmingPlan, userId])

Expand Down
2 changes: 1 addition & 1 deletion apps/supercode-cli/server/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "supercode-cli",
"version": "0.1.93",
"version": "0.1.94",
"description": "AI-powered coding agent CLI",
"main": "dist/main.js",
"bin": {
Expand Down
19 changes: 12 additions & 7 deletions apps/supercode-cli/server/src/api/billing/checkout.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,9 @@
import { Router } from "express"
import prisma from "../../lib/prisma"
import { DodoPayments } from "dodopayments"
import { getDodo, getDodoEnvironment } from "../../lib/dodo"

const router = Router()

function getDodo(): DodoPayments | null {
const key = process.env.DODO_PAYMENTS_API_KEY
if (!key) return null
return new DodoPayments({ bearerToken: key })
}

function studioUrl(path: string): string {
const clientUrl = process.env.CLIENT_URL || "http://localhost:3000"
return `${clientUrl.replace(/\/$/, "")}${path}`
Expand Down Expand Up @@ -87,6 +81,17 @@ router.post("/", async (req, res) => {
})
} catch (error) {
console.error("[billing/checkout] Checkout creation failed:", error)
const message = error instanceof Error ? error.message : String(error)
const mode = getDodoEnvironment()
const looksMissing =
/not found|404|invalid product|product_id/i.test(message)
if (looksMissing) {
res.status(400).json({
error:
`Dodo product not found in ${mode}. Re-seed plans with DODO_MODE=${mode === "test_mode" ? "test" : "live"} so dodoProductId matches this mode.`,
})
return
}
res.status(500).json({ error: "Failed to create checkout session" })
}
})
Expand Down
8 changes: 1 addition & 7 deletions apps/supercode-cli/server/src/api/billing/refund.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,9 @@
import { Router } from "express"
import prisma from "../../lib/prisma"
import { DodoPayments } from "dodopayments"
import { getDodo } from "../../lib/dodo"

const router = Router()

function getDodo(): DodoPayments | null {
const key = process.env.DODO_PAYMENTS_API_KEY
if (!key) return null
return new DodoPayments({ bearerToken: key })
}

router.post("/", async (req, res) => {
try {
const userId = req.body.userId as string | undefined
Expand Down
8 changes: 1 addition & 7 deletions apps/supercode-cli/server/src/api/billing/status.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,9 @@
import { Router } from "express"
import prisma from "../../lib/prisma"
import { DodoPayments } from "dodopayments"
import { getDodo } from "../../lib/dodo"

const router = Router()

function getDodo(): DodoPayments | null {
const key = process.env.DODO_PAYMENTS_API_KEY
if (!key) return null
return new DodoPayments({ bearerToken: key })
}

function studioUrl(path: string): string {
const clientUrl = process.env.CLIENT_URL || "http://localhost:3000"
return `${clientUrl.replace(/\/$/, "")}${path}`
Expand Down
11 changes: 2 additions & 9 deletions apps/supercode-cli/server/src/api/billing/webhook.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,11 @@
import { Router } from "express"
import prisma from "../../lib/prisma"
import { DodoPayments } from "dodopayments"
import { getDodo } from "../../lib/dodo"
import { invalidateModelCache } from "../../lib/model-access"
import type { Prisma } from "../../generated"

const router = Router()

function getDodo(): DodoPayments | null {
const key = process.env.DODO_PAYMENTS_API_KEY
const webhookKey = process.env.DODO_PAYMENTS_WEBHOOK_KEY
if (!key) return null
return new DodoPayments({ bearerToken: key, webhookKey })
}

// ── Event shapes (mirrors dodopayments SDK types) ──

interface DodoSubscriptionEvent {
Expand Down Expand Up @@ -361,7 +354,7 @@ router.post("/", async (req, res) => {
try {
const body = typeof req.body === "string" ? req.body : JSON.stringify(req.body)

const dodo = getDodo()
const dodo = getDodo({ webhookKey: true })
if (!dodo) {
console.error("[webhook] Dodo keys not configured — cannot verify webhook")
res.status(503).json({ received: false })
Expand Down
21 changes: 21 additions & 0 deletions apps/supercode-cli/server/src/lib/dodo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { DodoPayments } from "dodopayments"

export type DodoEnvironment = "test_mode" | "live_mode"

export function getDodoEnvironment(): DodoEnvironment {
return process.env.DODO_MODE === "test" ? "test_mode" : "live_mode"
}
Comment on lines +5 to +7

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reject unknown DODO_MODE values.

An unset or invalid value, such as "testing", selects "live_mode". This can make a non-production deployment call live Dodo endpoints. Accept only "test" and "live", then fail closed for other values.

Proposed fix
 export function getDodoEnvironment(): DodoEnvironment {
-  return process.env.DODO_MODE === "test" ? "test_mode" : "live_mode"
+  if (process.env.DODO_MODE === "test") return "test_mode"
+  if (process.env.DODO_MODE === "live") return "live_mode"
+
+  throw new Error('DODO_MODE must be "test" or "live"')
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function getDodoEnvironment(): DodoEnvironment {
return process.env.DODO_MODE === "test" ? "test_mode" : "live_mode"
}
export function getDodoEnvironment(): DodoEnvironment {
if (process.env.DODO_MODE === "test") return "test_mode"
if (process.env.DODO_MODE === "live") return "live_mode"
throw new Error('DODO_MODE must be "test" or "live"')
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/supercode-cli/server/src/lib/dodo.ts` around lines 5 - 7, Update
getDodoEnvironment to accept only the explicit DODO_MODE values "test" and
"live"; map them to "test_mode" and "live_mode" respectively, and fail closed
for unset or any other value instead of defaulting to live_mode.


/** Shared Dodo SDK client. Honors DODO_MODE so test keys hit the test API. */
export function getDodo(options?: { webhookKey?: boolean }): DodoPayments | null {
const key = process.env.DODO_PAYMENTS_API_KEY
if (!key) return null

return new DodoPayments({
bearerToken: key,
environment: getDodoEnvironment(),
...(options?.webhookKey
? { webhookKey: process.env.DODO_PAYMENTS_WEBHOOK_KEY }
: {}),
})
}
Loading