fix(billing): add rate limiting, body size check, and logging to checkout endpoint (BUG-079)
All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 11m9s

- Rate limit /checkout to 3 requests per IP per hour via express-rate-limit
- Reject request bodies >1KB (413)
- Log checkout session creation with client IP
- Bump version to 0.3.4
This commit is contained in:
OpenClaw 2026-02-20 07:07:27 +00:00
parent 32a00be0b3
commit 17c1f00e2b
2 changed files with 23 additions and 2 deletions

View file

@ -1,6 +1,6 @@
{ {
"name": "docfast-api", "name": "docfast-api",
"version": "0.3.3", "version": "0.3.4",
"description": "Markdown/HTML to PDF API with built-in invoice templates", "description": "Markdown/HTML to PDF API with built-in invoice templates",
"main": "dist/index.js", "main": "dist/index.js",
"scripts": { "scripts": {

View file

@ -1,4 +1,5 @@
import { Router, Request, Response } from "express"; import { Router, Request, Response } from "express";
import rateLimit from "express-rate-limit";
import Stripe from "stripe"; import Stripe from "stripe";
import { createProKey, downgradeByCustomer, updateEmailByCustomer } from "../services/keys.js"; import { createProKey, downgradeByCustomer, updateEmailByCustomer } from "../services/keys.js";
import logger from "../services/logger.js"; import logger from "../services/logger.js";
@ -45,8 +46,25 @@ async function isDocFastSubscription(subscriptionId: string): Promise<boolean> {
} }
} }
// Rate limit checkout: max 3 requests per IP per hour
const checkoutLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 3,
keyGenerator: (req: Request) => req.ip || req.socket.remoteAddress || "unknown",
standardHeaders: true,
legacyHeaders: false,
message: { error: "Too many checkout requests. Please try again later." },
});
// Create a Stripe Checkout session for Pro subscription // Create a Stripe Checkout session for Pro subscription
router.post("/checkout", async (_req: Request, res: Response) => { router.post("/checkout", checkoutLimiter, async (req: Request, res: Response) => {
// Reject suspiciously large request bodies (>1KB)
const contentLength = parseInt(req.headers["content-length"] || "0", 10);
if (contentLength > 1024) {
res.status(413).json({ error: "Request body too large" });
return;
}
try { try {
const priceId = await getOrCreateProPrice(); const priceId = await getOrCreateProPrice();
@ -58,6 +76,9 @@ router.post("/checkout", async (_req: Request, res: Response) => {
cancel_url: `${process.env.BASE_URL || "https://docfast.dev"}/#pricing`, cancel_url: `${process.env.BASE_URL || "https://docfast.dev"}/#pricing`,
}); });
const clientIp = req.ip || req.socket.remoteAddress || "unknown";
logger.info({ clientIp, sessionId: session.id }, "Checkout session created");
res.json({ url: session.url }); res.json({ url: session.url });
} catch (err: any) { } catch (err: any) {
logger.error({ err }, "Checkout error"); logger.error({ err }, "Checkout error");