diff --git a/changelog.d/fixes/9241-registered-keys-window-reset.md b/changelog.d/fixes/9241-registered-keys-window-reset.md new file mode 100644 index 0000000000..deeb1e4baa --- /dev/null +++ b/changelog.d/fixes/9241-registered-keys-window-reset.md @@ -0,0 +1 @@ +- **fix(db):** `validateRegisteredKey` no longer rejects the first request of a fresh budget window when the previous window's usage already met the daily/hourly budget — the reset `UPDATE` zeroed the counters in the DB but the budget check still read the stale pre-reset snapshot, so the reset is now mirrored into the row before checking ([#9241](https://github.com/diegosouzapw/OmniRoute/pull/9241)) diff --git a/src/lib/db/registeredKeys.ts b/src/lib/db/registeredKeys.ts index e56fd8851b..2b0d0ddaf4 100644 --- a/src/lib/db/registeredKeys.ts +++ b/src/lib/db/registeredKeys.ts @@ -384,6 +384,8 @@ export function validateRegisteredKey(rawKey: string): RegisteredKey | null { const today = nowDay(); const hour = nowHour(); if (row.last_reset_day !== today || row.last_reset_hour !== hour) { + const dailyReset = row.last_reset_day !== today; + const hourlyReset = row.last_reset_hour !== hour; db.prepare( ` UPDATE registered_keys @@ -393,6 +395,10 @@ export function validateRegisteredKey(rawKey: string): RegisteredKey | null { WHERE id = ? ` ).run(today, hour, today, hour, row.id); + if (dailyReset) row.daily_used = 0; + if (hourlyReset) row.hourly_used = 0; + row.last_reset_day = today; + row.last_reset_hour = hour; } // Budget check diff --git a/tests/unit/db-registeredKeys-crud.test.ts b/tests/unit/db-registeredKeys-crud.test.ts index a9a3894062..06718ff56d 100644 --- a/tests/unit/db-registeredKeys-crud.test.ts +++ b/tests/unit/db-registeredKeys-crud.test.ts @@ -213,6 +213,38 @@ test("validateRegisteredKey respects budget limits", async () => { assert.equal(rk.validateRegisteredKey(created.rawKey), null); }); +test("validateRegisteredKey resets budget counters on a fresh window", async () => { + await resetStorage(); + const issued = rk.issueRegisteredKey({ + name: "Window Reset", + dailyBudget: 3, + }); + assert.ok("rawKey" in issued); + if (!("rawKey" in issued)) return; + + const db = core.getDbInstance(); + const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000) + .toISOString() + .slice(0, 10); + const previousHour = new Date(Date.now() - 60 * 60 * 1000) + .toISOString() + .slice(0, 13); + + rk.incrementRegisteredKeyUsage(issued.id); + rk.incrementRegisteredKeyUsage(issued.id); + rk.incrementRegisteredKeyUsage(issued.id); + db.prepare( + `UPDATE registered_keys SET daily_used = ?, hourly_used = ?, last_reset_day = ?, last_reset_hour = ? WHERE id = ?`, + ).run(3, 3, yesterday, previousHour, issued.id); + + // First validation of the new window must be accepted (not rejected against the + // stale pre-reset counters) and must return the freshly-reset counters. + const validated = rk.validateRegisteredKey(issued.rawKey); + assert.ok(validated !== null); + assert.equal(validated.dailyUsed, 0); + assert.equal(validated.hourlyUsed, 0); +}); + // ──────────────── checkQuota ──────────────── test("checkQuota returns allowed true when no limits set", async () => {