-
Notifications
You must be signed in to change notification settings - Fork 9
Add MPP to API submodule #495
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| export function getPriceForRoute(path: string): number { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Custom agent: Module should export a single primary function whose name matches the filename Exported function name does not match filename basename Prompt for AI agents |
||
| if (path.includes("/ai")) return 0.002 | ||
| if (path.includes("/data")) return 0.001 | ||
| if (path.includes("/trade")) return 0.01 | ||
|
|
||
| return 0.0005 | ||
| } | ||
|
Comment on lines
+1
to
+7
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win Move this export to a filename that matches the function name.
As per coding guidelines, “File naming rule: The file name MUST match the exported function name.” 🤖 Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| import { verifyStripePayment } from "./stripe" | ||
|
|
||
| export async function verifyPayment(payment: string) { | ||
| return verifyStripePayment(payment) | ||
| } | ||
|
Comment on lines
+3
to
+5
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win Use a function-matching filename for this export.
As per coding guidelines, “The file name MUST match the exported function name.” 🤖 Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| import Stripe from "stripe" | ||
|
|
||
| const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { | ||
| apiVersion: "2024-06-20" | ||
| }) | ||
|
|
||
| export async function verifyStripePayment(paymentId: string) { | ||
| try { | ||
| const payment = await stripe.paymentIntents.retrieve(paymentId) | ||
| return payment.status === "succeeded" | ||
| } catch { | ||
| return false | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| const used = new Set<string>() | ||
|
|
||
| export function isReplay(id: string) { | ||
| if (used.has(id)) return true | ||
| used.add(id) | ||
| return false | ||
|
Comment on lines
+1
to
+6
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Replay registry needs TTL/eviction to avoid unbounded growth.
🤖 Prompt for AI Agents
Comment on lines
+3
to
+6
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This API both checks and records, which is fragile with the current middleware flow (replay check happens before payment verification). A transient verification failure can permanently poison a payment ID as replay. Split into 🤖 Prompt for AI Agents |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| import crypto from "crypto" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Custom agent: Module should export a single primary function whose name matches the filename Module exports multiple functions instead of a single primary export matching the filename Prompt for AI agents |
||
|
|
||
| type Session = { | ||
| id: string | ||
| budget: number | ||
| spent: number | ||
| expiresAt: number | ||
| } | ||
|
|
||
| const sessions = new Map<string, Session>() | ||
|
|
||
| export function createSession(budget = 1.0): Session { | ||
| const session = { | ||
| id: crypto.randomUUID(), | ||
| budget, | ||
| spent: 0, | ||
| expiresAt: Date.now() + 10 * 60 * 1000 | ||
| } | ||
|
|
||
| sessions.set(session.id, session) | ||
| return session | ||
| } | ||
|
|
||
| export function getSession(id: string): Session | null { | ||
| const s = sessions.get(id) | ||
| if (!s) return null | ||
|
|
||
| if (Date.now() > s.expiresAt) { | ||
| sessions.delete(id) | ||
| return null | ||
| } | ||
|
|
||
| return s | ||
| } | ||
|
Comment on lines
+10
to
+34
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In-memory session state will break across instances/restarts. Using a process-local 🤖 Prompt for AI Agents |
||
|
|
||
| export function chargeSession(id: string, amount: number): boolean { | ||
| const s = getSession(id) | ||
| if (!s) return false | ||
|
|
||
| if (s.spent + amount > s.budget) return false | ||
|
|
||
| s.spent += amount | ||
| return true | ||
| } | ||
|
Comment on lines
+12
to
+44
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift Split session operations into function-named files under This file exports multiple domain functions from one As per coding guidelines, “Apply Single Responsibility Principle (SRP): one exported function per file; each file should do one thing well” and “The file name MUST match the exported function name.” 🤖 Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| import { getPriceForRoute } from "./pricing" | ||
| import { verifyPayment } from "./providers" | ||
| import { createSession, getSession, chargeSession } from "./session" | ||
| import { isReplay } from "./replay" | ||
|
|
||
| export function withMPP(handler: Function) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Custom agent: Enforce Clear Code Style and Maintainability Practices Uses Prompt for AI agents |
||
| return async (req: Request, context?: any) => { | ||
| const path = new URL(req.url).pathname | ||
| const price = getPriceForRoute(path) | ||
|
|
||
| const payment = req.headers.get("x-mpp-payment") | ||
| const sessionId = req.headers.get("x-mpp-session") | ||
|
|
||
| if (sessionId) { | ||
| const session = getSession(sessionId) | ||
|
|
||
| if (!session) { | ||
| return Response.json({ error: "invalid_session" }, { status: 402 }) | ||
| } | ||
|
|
||
| if (!chargeSession(sessionId, price)) { | ||
| return Response.json( | ||
| { error: "insufficient_balance", required: price }, | ||
| { status: 402 } | ||
| ) | ||
| } | ||
|
|
||
| return handler(req, context) | ||
| } | ||
|
|
||
| if (payment) { | ||
| if (isReplay(payment)) { | ||
| return Response.json({ error: "replayed_payment" }, { status: 402 }) | ||
| } | ||
|
Comment on lines
+32
to
+34
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Critical: replay protection is not safe across instances. This branch depends on Use a shared atomic store (e.g., Redis 🤖 Prompt for AI Agents |
||
|
|
||
| const valid = await verifyPayment(payment) | ||
|
|
||
| if (!valid) { | ||
| return Response.json({ error: "invalid_payment" }, { status: 402 }) | ||
| } | ||
|
Comment on lines
+36
to
+40
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Handle provider failures from If the provider call rejects/throws, the middleware will bubble an unhandled error. Return a controlled payment-provider error response instead. Proposed fix- const valid = await verifyPayment(payment)
+ let valid = false
+ try {
+ valid = await verifyPayment(payment)
+ } catch {
+ return Response.json(
+ { error: "payment_verification_unavailable" },
+ { status: 502 }
+ )
+ }As per coding guidelines, "Handle errors gracefully". 🤖 Prompt for AI Agents |
||
|
|
||
| return handler(req, context) | ||
| } | ||
|
|
||
| const session = createSession() | ||
|
|
||
| return Response.json( | ||
| { | ||
| error: "payment_required", | ||
| price, | ||
| session | ||
| }, | ||
| { status: 402 } | ||
|
Comment on lines
+45
to
+53
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Critical: unfunded session issuance allows unlimited free usage. Line 45 creates a fresh spendable session before any payment proof is verified. A client can repeatedly request new sessions and bypass paid access entirely. 🤖 Prompt for AI Agents |
||
| ) | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ensure
withMPPreturns CORS headers on 402/error responses.After wrapping
POST, middleware-generatedpayment_required/payment errors become part of this route contract. Those responses currently miss CORS headers, which can block browser clients. Please add CORS headers inwithMPPerror responses (this impacts all wrapped routes).🤖 Prompt for AI Agents