Skip to content

Commit 64ac8fe

Browse files
Pankaj3112himself65
andcommitted
fix(api-key): correct refill interval time calculation (#4871)
Co-authored-by: Alex Yang <himself65@outlook.com>
1 parent 85f3b35 commit 64ac8fe

File tree

6 files changed

+186
-6
lines changed

6 files changed

+186
-6
lines changed

packages/better-auth/src/plugins/api-key/api-key.test.ts

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2120,4 +2120,184 @@ describe("api-key", async () => {
21202120
expect(apiKey).not.toBeNull();
21212121
expect(apiKey.permissions).toEqual(permissions);
21222122
});
2123+
2124+
it("should refill API key credits after refill interval (milliseconds)", async () => {
2125+
vi.useRealTimers();
2126+
2127+
const refillInterval = 3600000; // 1 hour in milliseconds
2128+
const refillAmount = 5;
2129+
const initialRemaining = 2;
2130+
2131+
const apiKey = await auth.api.createApiKey({
2132+
body: {
2133+
userId: user.id,
2134+
remaining: initialRemaining,
2135+
refillInterval: refillInterval,
2136+
refillAmount: refillAmount,
2137+
},
2138+
});
2139+
2140+
// First verification - should reduce remaining to 1
2141+
let result = await auth.api.verifyApiKey({
2142+
body: {
2143+
key: apiKey.key,
2144+
},
2145+
});
2146+
expect(result.valid).toBe(true);
2147+
expect(result.key?.remaining).toBe(initialRemaining - 1);
2148+
2149+
// Second verification - should reduce remaining to 0
2150+
result = await auth.api.verifyApiKey({
2151+
body: {
2152+
key: apiKey.key,
2153+
},
2154+
});
2155+
expect(result.valid).toBe(true);
2156+
expect(result.key?.remaining).toBe(0);
2157+
2158+
// Third verification - should fail due to no remaining credits
2159+
result = await auth.api.verifyApiKey({
2160+
body: {
2161+
key: apiKey.key,
2162+
},
2163+
});
2164+
expect(result.valid).toBe(false);
2165+
expect(result.error?.code).toBe("USAGE_EXCEEDED");
2166+
2167+
// Advance time by more than refill interval
2168+
vi.useFakeTimers();
2169+
await vi.advanceTimersByTimeAsync(refillInterval + 1000);
2170+
2171+
// Fourth verification - should succeed with refilled credits
2172+
result = await auth.api.verifyApiKey({
2173+
body: {
2174+
key: apiKey.key,
2175+
},
2176+
});
2177+
expect(result.valid).toBe(true);
2178+
expect(result.key?.remaining).toBe(refillAmount - 1);
2179+
2180+
vi.useRealTimers();
2181+
});
2182+
2183+
it("should not refill API key credits before refill interval expires", async () => {
2184+
vi.useRealTimers();
2185+
2186+
const refillInterval = 86400000; // 24 hours in milliseconds
2187+
const refillAmount = 10;
2188+
const initialRemaining = 1;
2189+
2190+
const apiKey = await auth.api.createApiKey({
2191+
body: {
2192+
userId: user.id,
2193+
remaining: initialRemaining,
2194+
refillInterval: refillInterval,
2195+
refillAmount: refillAmount,
2196+
},
2197+
});
2198+
2199+
// First verification - should reduce remaining to 0
2200+
let result = await auth.api.verifyApiKey({
2201+
body: {
2202+
key: apiKey.key,
2203+
},
2204+
});
2205+
expect(result.valid).toBe(true);
2206+
expect(result.key?.remaining).toBe(0);
2207+
2208+
// Advance time by less than refill interval
2209+
vi.useFakeTimers();
2210+
await vi.advanceTimersByTimeAsync(refillInterval / 2); // Only advance half the interval
2211+
2212+
// Second verification - should still fail (no refill yet)
2213+
result = await auth.api.verifyApiKey({
2214+
body: {
2215+
key: apiKey.key,
2216+
},
2217+
});
2218+
expect(result.valid).toBe(false);
2219+
expect(result.error?.code).toBe("USAGE_EXCEEDED");
2220+
2221+
// Advance time to complete the refill interval
2222+
await vi.advanceTimersByTimeAsync(refillInterval / 2 + 1000);
2223+
2224+
// Third verification - should succeed with refilled credits
2225+
result = await auth.api.verifyApiKey({
2226+
body: {
2227+
key: apiKey.key,
2228+
},
2229+
});
2230+
expect(result.valid).toBe(true);
2231+
expect(result.key?.remaining).toBe(refillAmount - 1);
2232+
2233+
vi.useRealTimers();
2234+
});
2235+
2236+
it("should handle multiple refill cycles correctly", async () => {
2237+
vi.useRealTimers();
2238+
2239+
const refillInterval = 3600000; // 1 hour in milliseconds
2240+
const refillAmount = 3;
2241+
2242+
const apiKey = await auth.api.createApiKey({
2243+
body: {
2244+
userId: user.id,
2245+
remaining: 1,
2246+
refillInterval: refillInterval,
2247+
refillAmount: refillAmount,
2248+
},
2249+
});
2250+
2251+
// Use the initial credit
2252+
let result = await auth.api.verifyApiKey({
2253+
body: {
2254+
key: apiKey.key,
2255+
},
2256+
});
2257+
expect(result.valid).toBe(true);
2258+
expect(result.key?.remaining).toBe(0);
2259+
2260+
vi.useFakeTimers();
2261+
2262+
// First refill cycle
2263+
await vi.advanceTimersByTimeAsync(refillInterval + 1000);
2264+
result = await auth.api.verifyApiKey({
2265+
body: {
2266+
key: apiKey.key,
2267+
},
2268+
});
2269+
expect(result.valid).toBe(true);
2270+
expect(result.key?.remaining).toBe(refillAmount - 1);
2271+
2272+
// Use all refilled credits
2273+
for (let i = 0; i < refillAmount - 1; i++) {
2274+
result = await auth.api.verifyApiKey({
2275+
body: {
2276+
key: apiKey.key,
2277+
},
2278+
});
2279+
expect(result.valid).toBe(true);
2280+
}
2281+
2282+
// Should be out of credits now
2283+
result = await auth.api.verifyApiKey({
2284+
body: {
2285+
key: apiKey.key,
2286+
},
2287+
});
2288+
expect(result.valid).toBe(false);
2289+
expect(result.error?.code).toBe("USAGE_EXCEEDED");
2290+
2291+
// Second refill cycle
2292+
await vi.advanceTimersByTimeAsync(refillInterval + 1000);
2293+
result = await auth.api.verifyApiKey({
2294+
body: {
2295+
key: apiKey.key,
2296+
},
2297+
});
2298+
expect(result.valid).toBe(true);
2299+
expect(result.key?.remaining).toBe(refillAmount - 1);
2300+
2301+
vi.useRealTimers();
2302+
});
21232303
});

packages/better-auth/src/plugins/api-key/routes/get-api-key.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ export function getApiKey({
6969
type: "number",
7070
nullable: true,
7171
description:
72-
"The interval in which the `remaining` count is refilled by day. Example: 1 // every day",
72+
"The interval in milliseconds between refills of the `remaining` count. Example: 3600000 // refill every hour (3600000ms = 1h)",
7373
},
7474
refillAmount: {
7575
type: "number",

packages/better-auth/src/plugins/api-key/routes/list-api-keys.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ export function listApiKeys({
6464
type: "number",
6565
nullable: true,
6666
description:
67-
"The interval in which the `remaining` count is refilled by day. Example: 1 // every day",
67+
"The interval in milliseconds between refills of the `remaining` count. Example: 3600000 // refill every hour (3600000ms = 1h)",
6868
},
6969
refillAmount: {
7070
type: "number",

packages/better-auth/src/plugins/api-key/routes/update-api-key.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ export function updateApiKey({
143143
type: "number",
144144
nullable: true,
145145
description:
146-
"The interval in which the `remaining` count is refilled by day. Example: 1 // every day",
146+
"The interval in milliseconds between refills of the `remaining` count. Example: 3600000 // refill every hour (3600000ms = 1h)",
147147
},
148148
refillAmount: {
149149
type: "number",

packages/better-auth/src/plugins/api-key/routes/verify-api-key.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ export async function validateApiKey({
126126
if (refillInterval && refillAmount) {
127127
// if they provide refill info, then we should refill once the interval is reached.
128128

129-
const timeSinceLastRequest = (now - lastTime) / (1000 * 60 * 60 * 24); // in days
129+
const timeSinceLastRequest = now - lastTime;
130130
if (timeSinceLastRequest > refillInterval) {
131131
remaining = refillAmount;
132132
lastRefillAt = new Date();

packages/better-auth/src/plugins/api-key/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -221,9 +221,9 @@ export type ApiKey = {
221221
*/
222222
userId: string;
223223
/**
224-
* The interval in which the `remaining` count is refilled by day
224+
* The interval in milliseconds between refills of the `remaining` count
225225
*
226-
* @example 1 // every day
226+
* @example 3600000 // refill every hour (3600000ms = 1h)
227227
*/
228228
refillInterval: number | null;
229229
/**

0 commit comments

Comments
 (0)