/** * Post-build script: inline remaining external assets (favicon) into index.html * and remove leftover files so only a single HTML file remains. */ import { readFileSync, writeFileSync, rmSync, existsSync } from 'fs' import { join, dirname } from 'path' import { fileURLToPath } from 'url' const __dirname = dirname(fileURLToPath(import.meta.url)) const distDir = join(__dirname, '..', 'dist') // Read favicon SVG and encode as data URI const faviconPath = join(distDir, 'favicon.svg') if (existsSync(faviconPath)) { const svgContent = readFileSync(faviconPath, 'utf8') const encoded = Buffer.from(svgContent).toString('base64') const dataUri = `data:image/svg+xml;base64,${encoded}` // Replace the favicon link in index.html const indexPath = join(distDir, 'index.html') let html = readFileSync(indexPath, 'utf8') html = html.replace( /]*href="[^"]*favicon\.svg"[^>]*\/?>/i, `` ) writeFileSync(indexPath, html) // Remove the standalone SVG rmSync(faviconPath) console.log('[inline-assets] Inlined favicon.svg into index.html') } // Remove any other leftover asset files (e.g. icons.svg from Svelte compiler, // and the source pwa-icon.svg which is only needed to regenerate the PNGs). for (const leftover of ['icons.svg', 'pwa-icon.svg']) { const p = join(distDir, leftover) if (existsSync(p)) { rmSync(p) console.log(`[inline-assets] Removed ${leftover}`) } } // Remove assets directory if it exists const assetsDir = join(distDir, 'assets') if (existsSync(assetsDir)) { rmSync(assetsDir, { recursive: true }) console.log('[inline-assets] Removed assets/ directory') } // ---- Production network sandbox CSP ----------------------------- // Replace the dev CSP with the strict, shipped one. The app is single-file and // offline-first, so script/style keep 'unsafe-inline' (they are inline), but // every channel that could exfiltrate vault data is denied at the browser level: // connect-src 'none' (no fetch/XHR/WebSocket anywhere), form-action 'none', // object-src 'none', base-uri 'none'. (Clickjacking via frame-ancestors / // X-Frame-Options is a header-only directive and must be set by the serving // layer, so it is intentionally absent here.) const indexPath = join(distDir, 'index.html') let html = readFileSync(indexPath, 'utf8') const PROD_CSP = [ "default-src 'self';", "script-src 'self' 'unsafe-inline';", "style-src 'self' 'unsafe-inline';", "img-src 'self' data:;", "font-src 'self' data:;", "media-src 'self' data:;", "connect-src 'none';", "form-action 'none';", "object-src 'none';", "base-uri 'none';", "manifest-src 'self';", 'upgrade-insecure-requests', ].join('\n ') html = html.replace( //i, `` ) writeFileSync(indexPath, html) console.log('[inline-assets] Replaced dev CSP with strict production sandbox') // ---- Keep the PWA companion files ------------------------------- // These MUST ship alongside index.html so the app is installable: the web // manifest, the network-sandbox service worker, and the icon set. for (const keep of ['manifest.webmanifest', 'sw.js', 'icons']) { const p = join(distDir, keep) if (existsSync(p)) console.log(`[inline-assets] Preserved PWA ${keep}: ${p}`) } console.log('[inline-assets] Done — dist/ contains index.html + PWA files')