// Deployment-wide settings: K/V backed by D1, with a short-TTL KV cache so // hot paths don't hit D1 on every request. Used by the update flow to store // the owner's CF API token + related metadata, and reserved for future // deployment-wide config (branding, telemetry retention, etc.). import type { Env } from '../../env'; const KV_PREFIX = 'settings:'; const KV_TTL_SECONDS = 300; // Cache "no value set" as well, so missing keys don't hammer D1 either. KV // can't store null, so we use a sentinel string the caller will never produce. const TOMBSTONE = ''; export async function getSetting(env: Env, key: string): Promise { const cacheKey = KV_PREFIX + key; try { const cached = await env.KV.get(cacheKey); if (cached === TOMBSTONE) return null; if (cached !== null) return cached; } catch { // KV failures should not break request handling — fall through to D1. } let value: string | null = null; try { const row = await env.DB .prepare(`SELECT value FROM deployment_settings WHERE key = ?`) .bind(key) .first<{ value: string }>(); value = row?.value ?? null; } catch { // Table may not exist yet on a brand-new deploy that hasn't bootstrapped. return null; } try { await env.KV.put(cacheKey, value ?? TOMBSTONE, { expirationTtl: KV_TTL_SECONDS }); } catch { /* best-effort */ } return value; } export async function setSetting(env: Env, key: string, value: string | null): Promise { const now = Math.floor(Date.now() / 1000); if (value === null) { await env.DB .prepare(`DELETE FROM deployment_settings WHERE key = ?`) .bind(key) .run(); } else { await env.DB .prepare( `INSERT INTO deployment_settings (key, value, updated_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at` ) .bind(key, value, now) .run(); } // Bust the cache so the next read picks up the new value immediately. try { await env.KV.delete(KV_PREFIX + key); } catch { /* best-effort */ } }