Skip to content
Open
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
5 changes: 4 additions & 1 deletion app/api/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { handleChatStream } from "@/lib/chat/handleChatStream";
import { withMPP } from "@/lib/mpp/withMPP"

/**
* OPTIONS handler for CORS preflight requests.
Expand Down Expand Up @@ -36,6 +37,8 @@ export async function OPTIONS() {
* @param request - The request object
* @returns A streaming response or error
*/
export async function POST(request: NextRequest): Promise<Response> {
async function handler(request: NextRequest): Promise<Response> {
return handleChatStream(request);
}

export const POST = withMPP(handler);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Ensure withMPP returns CORS headers on 402/error responses.

After wrapping POST, middleware-generated payment_required/payment errors become part of this route contract. Those responses currently miss CORS headers, which can block browser clients. Please add CORS headers in withMPP error responses (this impacts all wrapped routes).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/chat/route.ts` at line 44, withMPP middleware is returning
402/payment-required or other error Responses without CORS headers which breaks
browser clients; update withMPP so that any early Response it returns (e.g., the
402 Response or other error Responses created inside withMPP) includes the same
CORS headers used by successful responses (reuse whatever CORS header object
your app uses or construct the standard Access-Control-Allow-* headers) so
wrapped routes like POST = withMPP(handler) always send CORS headers even on
errors; locate the withMPP function and ensure every branch that returns new
Response(...) merges/injects the CORS headers before returning.

5 changes: 4 additions & 1 deletion app/api/content/create/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { NextRequest, NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { createContentHandler } from "@/lib/content/createContentHandler";
import { withMPP } from "@/lib/mpp/withMPP";

/**
* OPTIONS handler for CORS preflight requests.
Expand All @@ -22,10 +23,12 @@ export async function OPTIONS() {
* @param request - Incoming API request.
* @returns Trigger response for the created task run.
*/
export async function POST(request: NextRequest): Promise<NextResponse> {
async function handler(request: NextRequest): Promise<NextResponse> {
return createContentHandler(request);
}

export const POST = withMPP(handler);

export const dynamic = "force-dynamic";
export const fetchCache = "force-no-store";
export const revalidate = 0;
5 changes: 4 additions & 1 deletion app/api/image/generate/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { x402GenerateImage } from "@/lib/x402/recoup/x402GenerateImage";
import { validateGenerateImageQuery } from "@/lib/image/validateGenerateImageQuery";
import { withMPP } from "@/lib/mpp/withMPP"

/**
* OPTIONS handler for CORS preflight requests.
Expand All @@ -22,7 +23,7 @@ export async function OPTIONS() {
* @param request - The request object containing query parameters.
* @returns {Promise<NextResponse>} JSON response matching the Recoup API format.
*/
export async function GET(request: NextRequest) {
async function handler(request: NextRequest) {
try {
const validatedQuery = validateGenerateImageQuery(request);
if (validatedQuery instanceof NextResponse) {
Expand Down Expand Up @@ -54,3 +55,5 @@ export async function GET(request: NextRequest) {
);
}
}

export const GET = withMPP(handler);
5 changes: 4 additions & 1 deletion app/api/x402/image/generate/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { uploadImageAndCreateMoment } from "@/lib/arweave/uploadImageAndCreateMoment";
import { getBuyerAccount } from "@/lib/x402/getBuyerAccount";
import { parseFilesFromQuery } from "@/lib/files/parseFilesFromQuery";
import { withMPP } from "@/lib/mpp/withMPP"

/**
* OPTIONS handler for CORS preflight requests.
Expand All @@ -23,7 +24,7 @@ export async function OPTIONS() {
* @param request - The request object containing query parameters.
* @returns {Promise<NextResponse>} JSON response with generated image URL or error.
*/
export async function GET(request: NextRequest) {
async function handler(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const prompt = searchParams.get("prompt");
Expand Down Expand Up @@ -110,3 +111,5 @@ export async function GET(request: NextRequest) {
);
}
}

export const GET = withMPP(handler);
7 changes: 7 additions & 0 deletions lib/mpp/pricing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export function getPriceForRoute(path: string): number {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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
Check if this issue is valid — if so, understand the root cause and fix it. At lib/mpp/pricing.ts, line 1:

<comment>Exported function name does not match filename basename</comment>

<file context>
@@ -0,0 +1,7 @@
+export function getPriceForRoute(path: string): number {
+  if (path.includes("/ai")) return 0.002
+  if (path.includes("/data")) return 0.001
</file context>

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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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.

pricing.ts exports getPriceForRoute, which violates the repo rule requiring lib/**/*.ts files to be named after their exported function. Please move this to getPriceForRoute.ts (and optionally re-export from an index/barrel if needed).

As per coding guidelines, “File naming rule: The file name MUST match the exported function name.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/mpp/pricing.ts` around lines 1 - 7, The exported function
getPriceForRoute must live in a file named after it: create a new file
getPriceForRoute.ts containing the existing named export function
getPriceForRoute(path: string): number (copy the logic unchanged), remove the
function from pricing.ts, and update any imports to import { getPriceForRoute }
from './getPriceForRoute' (or add a re-export in your barrel/index if you prefer
to keep the old import surface).

5 changes: 5 additions & 0 deletions lib/mpp/providers/index.ts
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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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.

index.ts directly exporting verifyPayment conflicts with the lib/**/*.ts file naming rule. Move the function to verifyPayment.ts and keep index.ts as a pure re-export barrel if needed.

As per coding guidelines, “The file name MUST match the exported function name.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/mpp/providers/index.ts` around lines 3 - 5, Move the exported function
verifyPayment out of index.ts into a new file named verifyPayment.ts and
implement it there to call verifyStripePayment (so verifyPayment(payment:
string) { return verifyStripePayment(payment) }); then update index.ts to be a
pure barrel that re-exports verifyPayment (e.g., export { verifyPayment } from
'./verifyPayment'); ensure the exported symbol names remain verifyPayment and
verifyStripePayment so imports elsewhere continue to work.

14 changes: 14 additions & 0 deletions lib/mpp/providers/stripe.ts
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
}
}
7 changes: 7 additions & 0 deletions lib/mpp/replay.ts
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
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Replay registry needs TTL/eviction to avoid unbounded growth.

used grows forever for process lifetime. Add expiry/pruning (or move to bounded shared storage) so replay tracking doesn’t become a memory leak under sustained traffic.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/mpp/replay.ts` around lines 1 - 6, The replay registry currently uses an
ever-growing Set named used and function isReplay, which will leak memory;
replace it with a bounded/TTL cache (e.g., Map<string, number> storing
timestamps or an LRU cache) and evict old entries: change used from Set to a Map
or an LRU instance, update isReplay to check timestamp/expiry and treat expired
entries as not used (removing them), insert/refresh the id with the current time
when seen, and add a periodic cleanup (setInterval) or configure maxSize/TTL via
an LRU library to ensure entries are pruned and the registry cannot grow
unbounded.

Comment on lines +3 to +6
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

isReplay should not mutate state during the “check” step.

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 hasReplay(id) and markReplay(id) and only mark after successful verification.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/mpp/replay.ts` around lines 3 - 6, The isReplay function currently both
checks and records (mutating the shared used set) — split it into a read-only
hasReplay(id) that returns used.has(id) without modifying state, and a separate
markReplay(id) that performs used.add(id); update any callers (e.g., where
replay checking occurs before payment verification) to call hasReplay(id) to
decide if it’s a replay and only call markReplay(id) after the
payment/verification step succeeds; keep the existing used Set and function
names (isReplay -> hasReplay + markReplay) so locating the change is
straightforward.

}
44 changes: 44 additions & 0 deletions lib/mpp/session.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import crypto from "crypto"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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
Check if this issue is valid — if so, understand the root cause and fix it. At lib/mpp/session.ts, line 12:

<comment>Module exports multiple functions instead of a single primary export matching the filename</comment>

<file context>
@@ -0,0 +1,44 @@
+
+const sessions = new Map<string, Session>()
+
+export function createSession(budget = 1.0): Session {
+  const session = {
+    id: crypto.randomUUID(),
</file context>


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
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

In-memory session state will break across instances/restarts.

Using a process-local Map for payment sessions can return false invalid_session / balance failures in serverless or multi-replica deployments. This state should be moved to shared storage (e.g., Redis) with TTL.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/mpp/session.ts` around lines 10 - 34, The in-memory Map named sessions
causes lost state across instances/restarts; replace it with a shared
Redis-backed store: update createSession to generate the session object (id,
budget, spent, expiresAt) and persist it to Redis (SET key=session:{id}
value=JSON(session) with an appropriate EX TTL), update getSession to GET and
JSON.parse the stored session, treat missing/expired keys as null (no manual
expiry checks), and use DEL to delete sessions; remove or stop using the local
sessions Map and ensure updates to spent/budget use Redis commands (or a small
Lua script/transaction) to avoid race conditions when modifying session state.


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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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 lib/.

This file exports multiple domain functions from one lib/**/*.ts module, which conflicts with the repo’s SRP/file-naming constraints.

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
Verify each finding against the current code and only fix it if needed.

In `@lib/mpp/session.ts` around lines 12 - 44, This file exports multiple session
functions violating SRP/file-naming rules; split each exported function into its
own file named after the function (createSession.ts, getSession.ts,
chargeSession.ts) and update imports/exports accordingly: move the shared
in-memory store and types (sessions Map, Session type, any crypto usage and TTL
constant) into a small shared module (e.g., sessionStore.ts) that exports the
sessions Map and Session type, then have createSession, getSession and
chargeSession import that store and implement their logic unchanged; ensure all
call sites are updated to import the new individual functions from their new
files.

56 changes: 56 additions & 0 deletions lib/mpp/withMPP.ts
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) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Custom agent: Enforce Clear Code Style and Maintainability Practices

Uses Function and any types instead of strict TypeScript typing

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/mpp/withMPP.ts, line 6:

<comment>Uses `Function` and `any` types instead of strict TypeScript typing</comment>

<file context>
@@ -0,0 +1,56 @@
+import { createSession, getSession, chargeSession } from "./session"
+import { isReplay } from "./replay"
+
+export function withMPP(handler: Function) {
+  return async (req: Request, context?: any) => {
+    const path = new URL(req.url).pathname
</file context>

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
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

Critical: replay protection is not safe across instances.

This branch depends on isReplay(...), and the current replay store is process-local (lib/mpp/replay.ts in-memory Set). In multi-replica deployments, the same payment can be replayed against different instances.

Use a shared atomic store (e.g., Redis SETNX + TTL) for replay keys.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/mpp/withMPP.ts` around lines 32 - 34, The replay check in withMPP uses
isReplay which relies on the process-local in-memory Set in lib/mpp/replay.ts
and is unsafe across replicas; replace that local store with a distributed
atomic claim using Redis (or similar) so replay keys are unique cluster-wide.
Update lib/mpp/replay.ts to expose an atomic claim function (e.g.,
claimReplayKey / markReplay) that uses SET key value NX EX <ttl> (or SETNX +
EXPIRE) and returns whether the key was newly set; then change withMPP to call
that claim function instead of isReplay (and treat a failed claim as a replay)
so the Response.json({ error: "replayed_payment" }, { status: 402 }) branch is
enforced across instances. Ensure the TTL is configurable and errors from the
store fall back to failing-safe (treat as replay or log and reject) to avoid
weakening protection.


const valid = await verifyPayment(payment)

if (!valid) {
return Response.json({ error: "invalid_payment" }, { status: 402 })
}
Comment on lines +36 to +40
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle provider failures from verifyPayment explicitly.

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
Verify each finding against the current code and only fix it if needed.

In `@lib/mpp/withMPP.ts` around lines 36 - 40, In withMPP middleware,
verifyPayment can throw and currently bubbles an unhandled error; wrap the call
to verifyPayment in a try/catch inside the withMPP function, log the provider
error, and return a controlled Response.json error (e.g. { error:
"payment_provider_error" } with an appropriate 5xx status such as 502) instead
of letting the exception propagate; keep the existing invalid_payment response
for false returns from verifyPayment.


return handler(req, context)
}

const session = createSession()

return Response.json(
{
error: "payment_required",
price,
session
},
{ status: 402 }
Comment on lines +45 to +53
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

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
Verify each finding against the current code and only fix it if needed.

In `@lib/mpp/withMPP.ts` around lines 45 - 53, The code currently calls
createSession() before payment proof verification and returns that spendable
session in the 402 response, allowing clients to repeatedly obtain usable
sessions for free; move the createSession() call so it only executes after
payment has been validated (e.g., after your payment proof verification
routine), and change the 402 Response.json payload to omit any spendable session
or return a non-spendable placeholder; in short, ensure createSession() is
invoked only on successful payment verification and Response.json (error:
"payment_required") does not include the session.

)
}
}