Files
Jyotisha/frontend/src/lib/request-rate-limit.ts
T
Jesse_Chen c38f11dbd3
Independent Staging Quality Gate / validate (push) Has been cancelled
Independent Staging Quality Gate / publish (push) Has been cancelled
fix(api): bind daily and synastry charts to stored profiles
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>
2026-08-19 19:39:01 +08:00

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,
});
}