c38f11dbd3
Stop accepting client-supplied birth data on those paths, and cap session writes plus location lookups so a logged-in caller cannot farm compute. Co-authored-by: Cursor <cursoragent@cursor.com>
62 lines
1.9 KiB
TypeScript
62 lines
1.9 KiB
TypeScript
export type RequestRateLimitStore = Map<string, number[]>;
|
|
|
|
export type RequestRateLimitDecision =
|
|
| { readonly ok: true }
|
|
| { readonly ok: false; readonly retryAfterSeconds: number };
|
|
|
|
export type RequestRateLimitInput = Readonly<{
|
|
key: string;
|
|
limit: number;
|
|
windowMs: number;
|
|
now?: number;
|
|
store?: RequestRateLimitStore;
|
|
}>;
|
|
|
|
const state = globalThis as typeof globalThis & {
|
|
jyotishaRequestRateLimits?: RequestRateLimitStore;
|
|
};
|
|
|
|
export const requestRateLimits = {
|
|
locationSearch: { limit: 20, windowMs: 60_000 },
|
|
locationTimezone: { limit: 30, windowMs: 60_000 },
|
|
dailyStarlanguage: { limit: 10, windowMs: 60_000 },
|
|
synastry: { limit: 8, windowMs: 60 * 60_000 },
|
|
sessionWrite: { limit: 40, windowMs: 60_000 },
|
|
} as const;
|
|
|
|
export type RequestRateLimitBucket = keyof typeof requestRateLimits;
|
|
|
|
function defaultStore(): RequestRateLimitStore {
|
|
state.jyotishaRequestRateLimits ??= new Map();
|
|
return state.jyotishaRequestRateLimits;
|
|
}
|
|
|
|
export function consumeRequestRateLimit(input: RequestRateLimitInput): RequestRateLimitDecision {
|
|
const now = input.now ?? Date.now();
|
|
const store = input.store ?? defaultStore();
|
|
const windowStart = now - input.windowMs;
|
|
const recent = (store.get(input.key) ?? []).filter((timestamp) => timestamp > windowStart);
|
|
if (recent.length >= input.limit) {
|
|
const retryAfterSeconds = Math.max(1, Math.ceil((recent[0]! + input.windowMs - now) / 1000));
|
|
store.set(input.key, recent);
|
|
return { ok: false, retryAfterSeconds };
|
|
}
|
|
recent.push(now);
|
|
store.set(input.key, recent);
|
|
return { ok: true };
|
|
}
|
|
|
|
export function consumeUserRequestRateLimit(
|
|
bucket: RequestRateLimitBucket,
|
|
userId: string,
|
|
now?: number,
|
|
): RequestRateLimitDecision {
|
|
const spec = requestRateLimits[bucket];
|
|
return consumeRequestRateLimit({
|
|
key: `${bucket}:${userId}`,
|
|
limit: spec.limit,
|
|
windowMs: spec.windowMs,
|
|
now,
|
|
});
|
|
}
|