fix: resolve TODO.md security/perf/test-coverage backlog

Fixes the 13-item codebase health scan backlog: wraps meal-plan
generation in a transaction, adds missing userId/GIN indexes, fixes
an IPv6-parsing gap in the webhook SSRF guard (and an identical
duplicated bug in the AI URL-import path, now consolidated onto one
implementation), paginates the collections list, dedupes the AI
recipe Zod schemas, wires up Stripe tier sync, rate-limits AI key
rotation, gets `pnpm typecheck` actually working, and adds test
coverage for the previously-untested admin/webhooks routes.

Two flagged issues (collection removeRecipeId IDOR, tier-limit race)
turned out to already be fixed/non-issues on inspection — noted in
TODO.md rather than silently dropped.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Arnaud
2026-07-02 12:12:42 +02:00
parent 2154512e54
commit d2faf98ac1
38 changed files with 7598 additions and 315 deletions
+1 -34
View File
@@ -1,6 +1,6 @@
import { db } from "@epicure/db";
import { tierDefinitions, userUsage } from "@epicure/db";
import { eq, and, sql } from "@epicure/db";
import { eq, sql } from "@epicure/db";
function currentMonth() {
const now = new Date();
@@ -69,39 +69,6 @@ export async function checkAndIncrementTierLimit(
}
}
/** @deprecated Use checkAndIncrementTierLimit for recipe/aiCall keys to avoid TOCTOU races. */
export async function checkTierLimit(
userId: string,
userTier: "free" | "pro",
key: LimitKey
): Promise<void> {
const [tierDef] = await db
.select()
.from(tierDefinitions)
.where(eq(tierDefinitions.tier, userTier));
if (!tierDef) return;
const month = currentMonth();
const [usage] = await db
.select()
.from(userUsage)
.where(and(eq(userUsage.userId, userId), eq(userUsage.month, month)));
const current = usage ?? {
aiCallsUsed: 0,
recipeCount: 0,
storageUsedMb: 0,
};
if (key === "recipe" && current.recipeCount >= tierDef.maxRecipes) {
throw new TierLimitError("recipe", userTier);
}
if (key === "aiCall" && current.aiCallsUsed >= tierDef.aiCallsPerMonth) {
throw new TierLimitError("aiCall", userTier);
}
}
export async function incrementUsage(
userId: string,
key: LimitKey,