// Dross improvements — versioned CSS-layer changes with instant rollback. import { pool } from '../pool.js'; const q = (text, params) => pool.query(text, params); // Same exfil guards as elsewhere: an approved improvement still can't phone home // or pull remote CSS. Pure visual tweaks only. const BANNED = /url\s*\(|@import|@charset|expression\s*\(|behavior\s*:|javascript:/i; const MAX_CSS = 20_000; export function validateCss(css) { if (typeof css !== 'string' || !css.trim()) return 'css required'; if (css.length > MAX_CSS) return `css too large (max ${MAX_CSS} chars)`; if (BANNED.test(css)) return 'css may not use url()/@import/expression — visual tweaks only'; return null; } export async function create({ summary, css }) { const { rows } = await q( `INSERT INTO dross_improvements (summary, css) VALUES ($1, $2) RETURNING *`, [String(summary).slice(0, 200), css]); return rows[0]; } export async function list() { const { rows } = await q( `SELECT id, summary, status, created_at, decided_at, length(css) AS css_len FROM dross_improvements ORDER BY created_at DESC LIMIT 100`); return rows; } export async function get(id) { const { rows } = await q(`SELECT * FROM dross_improvements WHERE id = $1`, [id]); return rows[0] ?? null; } // pending→active (approve) · active→rolled_back · rolled_back→active (restore) · pending→rejected const TRANSITIONS = { approve: { from: ['pending'], to: 'active' }, rollback: { from: ['active'], to: 'rolled_back' }, restore: { from: ['rolled_back'], to: 'active' }, reject: { from: ['pending'], to: 'rejected' }, }; export async function transition(id, verb, actor) { const t = TRANSITIONS[verb]; if (!t) return null; const { rows } = await q( `UPDATE dross_improvements SET status = $1, decided_at = now(), decided_by = $2 WHERE id = $3 AND status = ANY($4) RETURNING *`, [t.to, actor ?? 'owner', id, t.from]); return rows[0] ?? null; } export async function activeCss() { const { rows } = await q( `SELECT summary, css FROM dross_improvements WHERE status = 'active' ORDER BY created_at`); return rows.map((r) => `/* dross: ${r.summary.replace(/\*\//g, '')} */\n${r.css}`).join('\n\n'); }