From b929e6d72f55d6d9b80c5b1204b4bdb2ed3e23de Mon Sep 17 00:00:00 2001 From: replit Date: Wed, 18 Mar 2026 21:41:14 -0400 Subject: [PATCH] feat(api): X-RateLimit-* headers on /api/demo + createdAt/completedAt on job responses (#19) (#28) --- artifacts/api-server/src/routes/demo.ts | 15 ++++++++++----- artifacts/api-server/src/routes/jobs.ts | 11 +++++++++-- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/artifacts/api-server/src/routes/demo.ts b/artifacts/api-server/src/routes/demo.ts index 57f69cc..c53dcc5 100644 --- a/artifacts/api-server/src/routes/demo.ts +++ b/artifacts/api-server/src/routes/demo.ts @@ -11,21 +11,21 @@ const RATE_LIMIT_WINDOW_MS = 60 * 60 * 1000; const ipHits = new Map(); -function checkRateLimit(ip: string): { allowed: boolean; resetAt: number } { +function checkRateLimit(ip: string): { allowed: boolean; resetAt: number; remaining: number } { const now = Date.now(); const entry = ipHits.get(ip); if (!entry || now >= entry.resetAt) { ipHits.set(ip, { count: 1, resetAt: now + RATE_LIMIT_WINDOW_MS }); - return { allowed: true, resetAt: now + RATE_LIMIT_WINDOW_MS }; + return { allowed: true, resetAt: now + RATE_LIMIT_WINDOW_MS, remaining: RATE_LIMIT_MAX - 1 }; } if (entry.count >= RATE_LIMIT_MAX) { - return { allowed: false, resetAt: entry.resetAt }; + return { allowed: false, resetAt: entry.resetAt, remaining: 0 }; } entry.count += 1; - return { allowed: true, resetAt: entry.resetAt }; + return { allowed: true, resetAt: entry.resetAt, remaining: RATE_LIMIT_MAX - entry.count }; } router.get("/demo", async (req: Request, res: Response) => { @@ -34,7 +34,12 @@ router.get("/demo", async (req: Request, res: Response) => { req.socket.remoteAddress ?? "unknown"; - const { allowed, resetAt } = checkRateLimit(ip); + const { allowed, resetAt, remaining } = checkRateLimit(ip); + + res.setHeader("X-RateLimit-Limit", String(RATE_LIMIT_MAX)); + res.setHeader("X-RateLimit-Remaining", String(remaining)); + res.setHeader("X-RateLimit-Reset", String(Math.floor(resetAt / 1000))); + if (!allowed) { const secsUntilReset = Math.ceil((resetAt - Date.now()) / 1000); logger.warn("demo rate limited", { ip, retry_after_s: secsUntilReset }); diff --git a/artifacts/api-server/src/routes/jobs.ts b/artifacts/api-server/src/routes/jobs.ts index ada8d6e..04d1dc5 100644 --- a/artifacts/api-server/src/routes/jobs.ts +++ b/artifacts/api-server/src/routes/jobs.ts @@ -255,11 +255,12 @@ router.post("/jobs", jobsLimiter, async (req: Request, res: Response) => { const evalFee = pricingService.calculateEvalFeeSats(); const jobId = randomUUID(); const invoiceId = randomUUID(); + const createdAt = new Date(); const lnbitsInvoice = await lnbitsService.createInvoice(evalFee, `Eval fee for job ${jobId}`); await db.transaction(async (tx) => { - await tx.insert(jobs).values({ id: jobId, request, state: "awaiting_eval_payment", evalAmountSats: evalFee }); + await tx.insert(jobs).values({ id: jobId, request, state: "awaiting_eval_payment", evalAmountSats: evalFee, createdAt }); await tx.insert(invoices).values({ id: invoiceId, jobId, @@ -276,6 +277,7 @@ router.post("/jobs", jobsLimiter, async (req: Request, res: Response) => { res.status(201).json({ jobId, + createdAt: createdAt.toISOString(), evalInvoice: { paymentRequest: lnbitsInvoice.paymentRequest, amountSats: evalFee, @@ -303,7 +305,11 @@ router.get("/jobs/:id", async (req: Request, res: Response) => { const advanced = await advanceJob(job); if (advanced) job = advanced; - const base = { jobId: job.id, state: job.state }; + const base = { + jobId: job.id, + state: job.state, + createdAt: job.createdAt.toISOString(), + }; switch (job.state) { case "awaiting_eval_payment": { @@ -350,6 +356,7 @@ router.get("/jobs/:id", async (req: Request, res: Response) => { case "complete": res.json({ ...base, + completedAt: job.updatedAt.toISOString(), result: job.result ?? undefined, ...(job.actualCostUsd != null ? { costLedger: {