Update features and dependencies

This commit is contained in:
Arnaud
2026-07-01 11:10:37 +02:00
parent 9d9dfb46c6
commit 8b57a3fd87
107 changed files with 14654 additions and 458 deletions
+54
View File
@@ -16,6 +16,60 @@ export class TierLimitError extends Error {
}
}
/**
* Atomically checks the tier limit and increments the usage counter in a
* single SQL statement, eliminating the TOCTOU race that existed when
* checkTierLimit and incrementUsage were called separately.
*
* Throws TierLimitError if the limit has already been reached.
* Use this instead of the separate checkTierLimit + incrementUsage pair
* for "recipe" and "aiCall" keys.
*/
export async function checkAndIncrementTierLimit(
userId: string,
userTier: "free" | "pro",
key: "recipe" | "aiCall"
): Promise<void> {
const [tierDef] = await db
.select()
.from(tierDefinitions)
.where(eq(tierDefinitions.tier, userTier));
if (!tierDef) return;
const month = currentMonth();
const id = `${userId}-${month}`;
if (key === "aiCall") {
const limit = tierDef.aiCallsPerMonth;
const result = await db.execute(sql`
INSERT INTO user_usage (id, user_id, month, ai_calls_used, recipe_count, storage_used_mb)
VALUES (${id}, ${userId}, ${month}, 1, 0, 0)
ON CONFLICT (user_id, month) DO UPDATE
SET ai_calls_used = user_usage.ai_calls_used + 1
WHERE user_usage.ai_calls_used < ${limit}
RETURNING ai_calls_used
`);
if (result.length === 0) {
throw new TierLimitError("aiCall", userTier);
}
} else {
const limit = tierDef.maxRecipes;
const result = await db.execute(sql`
INSERT INTO user_usage (id, user_id, month, ai_calls_used, recipe_count, storage_used_mb)
VALUES (${id}, ${userId}, ${month}, 0, 1, 0)
ON CONFLICT (user_id, month) DO UPDATE
SET recipe_count = user_usage.recipe_count + 1
WHERE user_usage.recipe_count < ${limit}
RETURNING recipe_count
`);
if (result.length === 0) {
throw new TierLimitError("recipe", userTier);
}
}
}
/** @deprecated Use checkAndIncrementTierLimit for recipe/aiCall keys to avoid TOCTOU races. */
export async function checkTierLimit(
userId: string,
userTier: "free" | "pro",