48 lines
2.2 KiB
TypeScript
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 });
|
|
}
|
|
}
|