datewise/app/lib/proxy.server.ts
metatroncubeswdev e56182e0e3 chore: initial Datewise app (rebranded from SlotSure prototype)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 20:06:49 -04:00

48 lines
2.2 KiB
TypeScript

import { ZodError } from "zod";
import { authenticate } from "../shopify.server";
/** Simple per-instance token bucket (use Redis/edge rate limiting when running several instances). */
const buckets = new Map<string, { tokens: number; at: number }>();
export function rateLimit(key: string, perMinute: number): boolean {
const now = Date.now();
const b = buckets.get(key) ?? { tokens: perMinute, at: now };
b.tokens = Math.min(perMinute, b.tokens + ((now - b.at) / 60000) * perMinute);
b.at = now;
if (b.tokens < 1) {
buckets.set(key, b);
return false;
}
b.tokens -= 1;
buckets.set(key, b);
if (buckets.size > 50000) buckets.clear();
return true;
}
/**
* Wrap an app-proxy handler: verifies Shopify's HMAC signature, rate limits,
* parses JSON and turns validation errors into 400s.
*/
export async function proxyHandler(
request: Request,
limitPerMinute: number,
fn: (ctx: { shop: string; admin?: Awaited<ReturnType<typeof authenticate.public.appProxy>>["admin"]; body: unknown; url: URL }) => Promise<unknown>,
): Promise<Response> {
const ctx = await authenticate.public.appProxy(request);
const url = new URL(request.url);
const shop = ctx.session?.shop ?? url.searchParams.get("shop") ?? "";
if (!ctx.session) return Response.json({ ok: false, message: "App not installed" }, { status: 404 });
const ip = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "anon";
if (!rateLimit(`${shop}|${ip}|${url.pathname}`, limitPerMinute))
return Response.json({ ok: false, message: "Too many requests. Please wait a moment." }, { status: 429 });
try {
const body = request.method === "GET" ? Object.fromEntries(url.searchParams) : await request.json().catch(() => ({}));
const out = await fn({ shop, admin: ctx.admin, body, url });
return Response.json(out, { headers: { "Cache-Control": "no-store" } });
} catch (e) {
if (e instanceof ZodError) return Response.json({ ok: false, message: "Invalid request", issues: e.issues.slice(0, 5) }, { status: 400 });
if (e instanceof Response) return e;
console.error("proxy error", e);
return Response.json({ ok: false, message: "Something went wrong. Please try again." }, { status: 500 });
}
}