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
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
SELLER_ADDRESS=0xYourWalletAddress
SELLER_PRIVATE_KEY=0xYourSellerPrivateKey

# Shared secret that protects POST /api/gateway/withdraw. Callers must send it
# as the `x-withdraw-secret` header. STRONGLY RECOMMENDED in production: if this
# is left unset, the withdraw endpoint is unauthenticated and anyone can drain
# the seller's Gateway balance to an arbitrary destinationAddress.
WITHDRAW_SECRET=

# Buyer wallet (for the payment agent)
BUYER_ADDRESS=0xYourBuyerWalletAddress
BUYER_PRIVATE_KEY=0xYourBuyerPrivateKey
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,3 +185,15 @@ This sample application:
- Assumes testnet usage only
- Handles secrets via environment variables
- Is not intended for production use without modification

### Protecting the withdraw endpoint

`POST /api/gateway/withdraw` moves the seller's Gateway balance using
`SELLER_PRIVATE_KEY` and accepts an arbitrary `destinationAddress`. Left open,
anyone who can reach it can drain funds to their own wallet.

Set `WITHDRAW_SECRET` in production. When it is set, every request must include
a matching `x-withdraw-secret` header or it is rejected with `401 Unauthorized`.
For a hosted seller dashboard, call this endpoint from a trusted server-to-server
context rather than directly from the browser, so the secret is never shipped in
the client bundle.
11 changes: 11 additions & 0 deletions app/api/gateway/withdraw/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,17 @@ export async function POST(req: NextRequest) {
);
}

// Optional shared-secret guard. When WITHDRAW_SECRET is set, callers must
// present it via the x-withdraw-secret header. This endpoint moves the
// seller's Gateway balance, so it must not be left open in production.
const withdrawSecret = process.env.WITHDRAW_SECRET;
if (withdrawSecret) {
const provided = req.headers.get("x-withdraw-secret");
if (provided !== withdrawSecret) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
}

const body = await req.json();
const { amount, destinationChain, destinationAddress } = body as {
amount: string;
Expand Down