feat(@messenger): add per-minute rate limit with burst cap

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-05-25 13:43:17 -07:00
parent 098b51ce83
commit 6667fb0905

View file

@ -14,7 +14,8 @@ interface SendMessageParams {
delay_seconds?: number;
}
const MAX_HOURLY = 15;
const MAX_PER_MINUTE = 2;
const MAX_HOURLY = 120;
const MAX_DAILY = 150;
// Schema: macsync.send_queue (uuid id, device_id uuid, to_handle, body, status,
@ -52,14 +53,21 @@ export async function sendMessage(params: SendMessageParams): Promise<string> {
}
// Rate limits — count rows queued/sent in the windows. Cancelled doesn't
// burn quota.
// burn quota. Three tiers: per-minute (burst cap), hourly, daily.
const minuteAgo = new Date(Date.now() - 60_000).toISOString();
const hourAgo = new Date(Date.now() - 3600_000).toISOString();
const dayStart = new Date();
dayStart.setHours(0, 0, 0, 0);
let minuteCount: number;
let hourlyCount: number;
let dailyCount: number;
try {
const minuteResult = await query(
`SELECT COUNT(*)::int AS count FROM macsync.send_queue
WHERE created_at > $1 AND status <> 'cancelled'`,
[minuteAgo],
);
const hourlyResult = await query(
`SELECT COUNT(*)::int AS count FROM macsync.send_queue
WHERE created_at > $1 AND status <> 'cancelled'`,
@ -70,6 +78,7 @@ export async function sendMessage(params: SendMessageParams): Promise<string> {
WHERE created_at > $1 AND status <> 'cancelled'`,
[dayStart.toISOString()],
);
minuteCount = (minuteResult.rows[0]?.count as number) ?? 0;
hourlyCount = (hourlyResult.rows[0]?.count as number) ?? 0;
dailyCount = (dailyResult.rows[0]?.count as number) ?? 0;
} catch (err) {
@ -77,9 +86,14 @@ export async function sendMessage(params: SendMessageParams): Promise<string> {
throw new Error(`send_message: rate-limit query failed (${msg})`);
}
const minuteRemaining = Math.max(0, MAX_PER_MINUTE - minuteCount);
const hourlyRemaining = Math.max(0, MAX_HOURLY - hourlyCount);
const dailyRemaining = Math.max(0, MAX_DAILY - dailyCount);
if (minuteRemaining === 0) {
return `Rate limit exceeded: ${MAX_PER_MINUTE} messages per minute (burst cap). Try again in ~30s.`;
}
if (hourlyRemaining === 0) {
return `Rate limit exceeded: ${MAX_HOURLY} messages per hour. Try again later.`;
}
@ -97,7 +111,7 @@ export async function sendMessage(params: SendMessageParams): Promise<string> {
`To: ${displayRecipient}`,
`Body: ${body}`,
'',
`Rate limits: ${hourlyRemaining}/${MAX_HOURLY} hourly, ${dailyRemaining}/${MAX_DAILY} daily`,
`Rate limits: ${minuteRemaining}/${MAX_PER_MINUTE} per-min, ${hourlyRemaining}/${MAX_HOURLY} hourly, ${dailyRemaining}/${MAX_DAILY} daily`,
'',
'To send this message, call send_message again with confirm: true',
].join('\n');