import { randomBytes } from "crypto"; import logger from "./logger.js"; import { queryWithRetry } from "./db.js"; // In-memory cache for fast lookups, synced with PostgreSQL let keysCache = []; /** Look up a key row in the DB by a given column. Returns null if not found. */ export async function findKeyInCacheOrDb(column, value) { const result = await queryWithRetry(`SELECT key, tier, email, created_at, stripe_customer_id FROM api_keys WHERE ${column} = $1 LIMIT 1`, [value]); if (result.rows.length === 0) return null; const r = result.rows[0]; return { key: r.key, tier: r.tier, email: r.email, createdAt: r.created_at instanceof Date ? r.created_at.toISOString() : r.created_at, stripeCustomerId: r.stripe_customer_id || undefined, }; } export async function loadKeys() { try { const result = await queryWithRetry("SELECT key, tier, email, created_at, stripe_customer_id FROM api_keys"); keysCache = result.rows.map((r) => ({ key: r.key, tier: r.tier, email: r.email, createdAt: r.created_at instanceof Date ? r.created_at.toISOString() : r.created_at, stripeCustomerId: r.stripe_customer_id || undefined, })); } catch (err) { logger.error({ err }, "Failed to load keys from PostgreSQL"); keysCache = []; } // Also load seed keys from env const envKeys = process.env.API_KEYS?.split(",").map((k) => k.trim()).filter(Boolean) || []; for (const k of envKeys) { if (!keysCache.find((e) => e.key === k)) { const entry = { key: k, tier: "pro", email: "seed@docfast.dev", createdAt: new Date().toISOString() }; keysCache.push(entry); // Upsert into DB await queryWithRetry(`INSERT INTO api_keys (key, tier, email, created_at) VALUES ($1, $2, $3, $4) ON CONFLICT (key) DO NOTHING`, [k, "pro", "seed@docfast.dev", new Date().toISOString()]).catch(() => { }); } } } export function isValidKey(key) { return keysCache.some((k) => k.key === key); } export function getKeyInfo(key) { return keysCache.find((k) => k.key === key); } export function isProKey(key) { const info = getKeyInfo(key); return info?.tier === "pro"; } function generateKey(prefix) { return `${prefix}_${randomBytes(24).toString("hex")}`; } export async function createFreeKey(email) { if (email) { const existing = keysCache.find((k) => k.email === email && k.tier === "free"); if (existing) return existing; } const entry = { key: generateKey("df_free"), tier: "free", email: email || "", createdAt: new Date().toISOString(), }; await queryWithRetry("INSERT INTO api_keys (key, tier, email, created_at) VALUES ($1, $2, $3, $4)", [entry.key, entry.tier, entry.email, entry.createdAt]); keysCache.push(entry); return entry; } export async function createProKey(email, stripeCustomerId) { // Check in-memory cache first (fast path) const existing = keysCache.find((k) => k.stripeCustomerId === stripeCustomerId); if (existing) { existing.tier = "pro"; await queryWithRetry("UPDATE api_keys SET tier = 'pro' WHERE key = $1", [existing.key]); return existing; } // UPSERT: handles duplicate webhooks across pods via DB unique index const newKey = generateKey("df_pro"); const now = new Date().toISOString(); const result = await queryWithRetry(`INSERT INTO api_keys (key, tier, email, created_at, stripe_customer_id) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (stripe_customer_id) WHERE stripe_customer_id IS NOT NULL DO UPDATE SET tier = 'pro' RETURNING key, tier, email, created_at, stripe_customer_id`, [newKey, "pro", email, now, stripeCustomerId]); const row = result.rows[0]; const entry = { key: row.key, tier: row.tier, email: row.email, createdAt: row.created_at instanceof Date ? row.created_at.toISOString() : row.created_at, stripeCustomerId: row.stripe_customer_id || undefined, }; // Refresh in-memory cache const cacheIdx = keysCache.findIndex((k) => k.stripeCustomerId === stripeCustomerId); if (cacheIdx >= 0) { keysCache[cacheIdx] = entry; } else { keysCache.push(entry); } return entry; } export async function downgradeByCustomer(stripeCustomerId) { const entry = keysCache.find((k) => k.stripeCustomerId === stripeCustomerId); if (entry) { entry.tier = "free"; await queryWithRetry("UPDATE api_keys SET tier = 'free' WHERE stripe_customer_id = $1", [stripeCustomerId]); return true; } // DB fallback: key may exist on another pod's cache or after a restart logger.info({ stripeCustomerId }, "downgradeByCustomer: cache miss, falling back to DB"); const dbKey = await findKeyInCacheOrDb("stripe_customer_id", stripeCustomerId); if (!dbKey) { logger.warn({ stripeCustomerId }, "downgradeByCustomer: customer not found in cache or DB"); return false; } await queryWithRetry("UPDATE api_keys SET tier = 'free' WHERE stripe_customer_id = $1", [stripeCustomerId]); dbKey.tier = "free"; keysCache.push(dbKey); logger.info({ stripeCustomerId, key: dbKey.key }, "downgradeByCustomer: downgraded via DB fallback"); return true; } export async function findKeyByCustomerId(stripeCustomerId) { return findKeyInCacheOrDb("stripe_customer_id", stripeCustomerId); } export function getAllKeys() { return [...keysCache]; } export async function updateKeyEmail(apiKey, newEmail) { const entry = keysCache.find((k) => k.key === apiKey); if (entry) { entry.email = newEmail; await queryWithRetry("UPDATE api_keys SET email = $1 WHERE key = $2", [newEmail, apiKey]); return true; } // DB fallback: key may exist on another pod's cache or after a restart logger.info({ apiKey: apiKey.slice(0, 10) + "..." }, "updateKeyEmail: cache miss, falling back to DB"); const dbKey = await findKeyInCacheOrDb("key", apiKey); if (!dbKey) { logger.warn({ apiKey: apiKey.slice(0, 10) + "..." }, "updateKeyEmail: key not found in cache or DB"); return false; } await queryWithRetry("UPDATE api_keys SET email = $1 WHERE key = $2", [newEmail, apiKey]); dbKey.email = newEmail; keysCache.push(dbKey); logger.info({ apiKey: apiKey.slice(0, 10) + "..." }, "updateKeyEmail: updated via DB fallback"); return true; } export async function updateEmailByCustomer(stripeCustomerId, newEmail) { const entry = keysCache.find(k => k.stripeCustomerId === stripeCustomerId); if (entry) { entry.email = newEmail; await queryWithRetry("UPDATE api_keys SET email = $1 WHERE stripe_customer_id = $2", [newEmail, stripeCustomerId]); return true; } // DB fallback: key may exist on another pod's cache or after a restart logger.info({ stripeCustomerId }, "updateEmailByCustomer: cache miss, falling back to DB"); const dbKey = await findKeyInCacheOrDb("stripe_customer_id", stripeCustomerId); if (!dbKey) { logger.warn({ stripeCustomerId }, "updateEmailByCustomer: customer not found in cache or DB"); return false; } await queryWithRetry("UPDATE api_keys SET email = $1 WHERE stripe_customer_id = $2", [newEmail, stripeCustomerId]); dbKey.email = newEmail; keysCache.push(dbKey); logger.info({ stripeCustomerId, key: dbKey.key }, "updateEmailByCustomer: updated via DB fallback"); return true; }