From 2a006be5c299410638af3622dc27cab1729f421a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 15 Oct 2026 06:00:26 +0000 Subject: [PATCH 1/2] Fix parse request rate limit cost never being applied The rate limiter runs as app-level middleware before the /api router, where req.route is still undefined, so `req.route?.path === "/request/:match_id"` never matched and POST /api/request/:match_id cost 1 instead of the documented 10. Match on req.method and req.path instead. --- svc/web.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/svc/web.ts b/svc/web.ts index b793437f7..1a39576bb 100644 --- a/svc/web.ts +++ b/svc/web.ts @@ -461,7 +461,9 @@ app.use(async (req, res, next) => { !unlimitedPaths.includes(req.originalUrl.split("?")[0]) ) { let rateCost = 1; - if (req.method === "POST" && req.route?.path === "/request/:match_id") { + // NOTE: req.route is not set yet in app-level middleware (routing happens later in the /api router), + // so match on the method and path instead + if (req.method === "POST" && /^\/api\/request\/[^/]+\/?$/.test(req.path)) { rateCost = 10; } const command = redis.multi(); From 5bf7ecc67071372a3e0f283c5a7996c10c670c24 Mon Sep 17 00:00:00 2001 From: Howard Chung Date: Thu, 15 Oct 2026 07:20:49 +0000 Subject: [PATCH 2/2] billing fixes --- svc/api/keyManagement.ts | 8 +++++--- svc/apiadmin.ts | 14 ++++++++++---- svc/syncSubs.ts | 8 +++++++- 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/svc/api/keyManagement.ts b/svc/api/keyManagement.ts index 5c07d6564..b940080c6 100644 --- a/svc/api/keyManagement.ts +++ b/svc/api/keyManagement.ts @@ -97,16 +97,18 @@ keys api_key, ip, concat(date_part('year', timestamp), '-', date_part('month', timestamp)) as month, + date_trunc('month', timestamp) as month_start, MAX(usage_count) as usage_count FROM api_key_usage WHERE timestamp >= ? AND timestamp <= ? AND account_id = ? - GROUP BY account_id, api_key, ip, month + GROUP BY account_id, api_key, ip, month, month_start ) as T1 - GROUP BY account_id, month - ORDER BY month DESC + GROUP BY account_id, month, month_start + -- "month" is an unpadded string (e.g. 2026-9), which sorts lexicographically (2026-9 > 2026-10), so order by the real date + ORDER BY month_start DESC `, [ moment.utc().subtract(5, "month").startOf("month"), diff --git a/svc/apiadmin.ts b/svc/apiadmin.ts index b9efc44fd..75a5e0fe5 100644 --- a/svc/apiadmin.ts +++ b/svc/apiadmin.ts @@ -32,10 +32,16 @@ await runInLoop( ); continue; } + // Use UTC: usage rows are bucketed by moment.utc().startOf("month") in web.ts, + // so a non-UTC server timezone would otherwise select the wrong month near boundaries const startTime = moment .unix(sub.current_period_end - 1) + .utc() .startOf("month"); - const endTime = moment.unix(sub.current_period_end - 1).endOf("month"); + const endTime = moment + .unix(sub.current_period_end - 1) + .utc() + .endOf("month"); const res = await db.raw( ` SELECT @@ -45,10 +51,10 @@ await runInLoop( api_key_usage.api_key, api_key_usage.ip, MAX(api_key_usage.usage_count) as usage - FROM api_key_usage, api_keys + FROM api_key_usage + JOIN api_keys ON api_key_usage.api_key = api_keys.api_key WHERE - api_key_usage.account_id = api_keys.account_id - AND timestamp >= ? + timestamp >= ? AND timestamp <= ? AND subscription_id = ? GROUP BY api_key_usage.api_key, api_key_usage.ip diff --git a/svc/syncSubs.ts b/svc/syncSubs.ts index 5a153eb5b..4d22cbd64 100644 --- a/svc/syncSubs.ts +++ b/svc/syncSubs.ts @@ -22,7 +22,13 @@ await runInLoop(async function doSyncSubs() { // Reconcile, insert any missing subscribers const accountId = sub.metadata['account_id']; if (accountId) { - await trx.raw("INSERT INTO subscriber(account_id, customer_id, status) VALUES (?, ?, 'active') ON CONFLICT DO NOTHING", [Number(accountId), sub.customer]); + // If the account already has a row (e.g. from an earlier subscription under a different Stripe customer), + // point it at the customer with the active subscription, otherwise the UPDATE below matches nothing + // and a paying user stays inactive + await trx.raw( + "INSERT INTO subscriber(account_id, customer_id, status) VALUES (?, ?, 'active') ON CONFLICT (account_id) DO UPDATE SET customer_id = EXCLUDED.customer_id, status = 'active'", + [Number(accountId), sub.customer], + ); } // Mark subscribers as active await trx.raw("UPDATE subscriber SET status = 'active' WHERE customer_id = ?", [