diff --git a/src/chat.js b/src/chat.js index 7e77cda..511366d 100644 --- a/src/chat.js +++ b/src/chat.js @@ -22,20 +22,6 @@ function isHex(s) { return typeof s === 'string' && /^#[0-9a-f]{3,8}$/i.test(s.trim()); } -function hexToRgb(hex) { - const m = String(hex).trim().toLowerCase().replace(/^#/, ''); - const full = m.length === 3 ? m.split('').map((c) => c + c).join('') : m.slice(0, 6); - return { - r: parseInt(full.slice(0, 2), 16) || 0, - g: parseInt(full.slice(2, 4), 16) || 0, - b: parseInt(full.slice(4, 6), 16) || 0, - }; -} - -function rgbToHex({ r, g, b }) { - return '#' + [r, g, b].map((v) => Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, '0')).join(''); -} - function opSharpenRadii(design, factor = 0.5) { const radii = design.borders?.radii || []; const next = radii.map((r) => ({ ...r, value: Math.max(0, Math.round((r.value || 0) * factor)) })); diff --git a/src/studio.js b/src/studio.js index 7562469..405c4ba 100644 --- a/src/studio.js +++ b/src/studio.js @@ -302,9 +302,9 @@ export async function runStudio(opts) { res.end('not found'); return; } - // Race-safe read — single try/catch instead of exists→stat→read chain. + // Race-free read — let readFileSync surface ENOENT / EISDIR / EACCES + // in one syscall instead of a stat→read pair (which would TOCTOU). try { - if (!statSync(filePath).isFile()) throw new Error('not a file'); const body = readFileSync(filePath); const ext = extname(filePath).toLowerCase(); res.writeHead(200, { 'content-type': MIME[ext] || 'application/octet-stream' }); diff --git a/src/sync.js b/src/sync.js index 1f87148..aeb4c6e 100644 --- a/src/sync.js +++ b/src/sync.js @@ -5,17 +5,28 @@ import { formatTokens } from './formatters/tokens.js'; import { formatTailwind } from './formatters/tailwind.js'; import { formatCssVars } from './formatters/css-vars.js'; import { saveSnapshot, getHistory } from './history.js'; -import { writeFileSync, statSync } from 'fs'; +import { openSync, closeSync, ftruncateSync, writeSync } from 'fs'; import { join } from 'path'; -// Race-safe "update only if file exists" — statSync inside try/catch -// closes the toctou window vs. existsSync→writeFileSync. +// Race-free "update only if file exists" — open with 'r+' atomically +// requires an existing file (throws ENOENT otherwise) and gives us a +// write-capable descriptor in one syscall, eliminating the toctou window +// that statSync→writeFileSync would have. Truncate then write through the +// same fd so no other process can sneak between check and write. function updateIfExists(path, content) { + let fd; try { - if (!statSync(path).isFile()) return false; - writeFileSync(path, content, 'utf-8'); + fd = openSync(path, 'r+'); + ftruncateSync(fd, 0); + writeSync(fd, content, 0, 'utf-8'); return true; - } catch { return false; } + } catch { + return false; + } finally { + if (fd !== undefined) { + try { closeSync(fd); } catch { /* best-effort close */ } + } + } } export async function syncDesign(url, options = {}) { diff --git a/website/app/components/HeroExtractor.js b/website/app/components/HeroExtractor.js index 01f289b..286684c 100644 --- a/website/app/components/HeroExtractor.js +++ b/website/app/components/HeroExtractor.js @@ -32,7 +32,6 @@ export default function HeroExtractor() { const [errorMsg, setErrorMsg] = useState(null); const [rateLimitMsg, setRateLimitMsg] = useState(null); const [downloadBusy, setDownloadBusy] = useState(false); - const [copied, setCopied] = useState(false); const inputRef = useRef(null); // Accept ?url= query param (Chrome extension handoff, deep links). Only @@ -63,7 +62,6 @@ export default function HeroExtractor() { setFiles(null); setErrorMsg(null); setRateLimitMsg(null); - setCopied(false); }, []); const handleEvent = useCallback((event) => { @@ -203,19 +201,6 @@ export default function HeroExtractor() { } }, [files, downloadBusy]); - const handleCopyMarkdown = useCallback(async () => { - if (!files) return; - const mdKey = Object.keys(files).find((k) => k.endsWith('-design-language.md')); - if (!mdKey) return; - try { - await navigator.clipboard.writeText(files[mdKey]); - setCopied(true); - setTimeout(() => setCopied(false), 1800); - } catch { - // no-op - } - }, [files]); - const stageLabel = stage ? STAGE_LABEL[stage] || stage : null; const streaming = status === 'streaming'; const disabled = streaming;