// Stopper buildet hvis et blogindlæg mangler et gyldigt `godkendt:`-stempel // (HMAC-SHA256 over sti, indhold og billeder). // // PUBLISH_KEY= node scripts/verify-approvals.mjs import crypto from 'node:crypto'; import fs from 'node:fs'; const DIR = 'src/content/blog'; const key = process.env.PUBLISH_KEY; if (!key) { console.error('PUBLISH_KEY mangler; kan ikke verificere godkendelser.'); process.exit(1); } // Skal matche godkendelsestjenesten byte for byte. const canonical = (relPath, text, assets = []) => { const lines = text.replace(/\r\n/g, '\n').split('\n'); if (lines[0] === '---') { const end = lines.indexOf('---', 1); if (end > 0) { for (let i = end - 1; i > 0; i--) if (/^godkendt:/.test(lines[i])) lines.splice(i, 1); } } const tail = assets.map(([p, h]) => `\n@asset ${p} ${h}`).join(''); return `${relPath}\n${lines.join('\n')}${tail}`; }; const sha256 = (buf) => crypto.createHash('sha256').update(buf).digest('hex'); // Billeder til .md ligger fladt i mappen / ved siden af. const assetsFor = (rel) => { const dir = rel.replace(/\.mdx?$/, ''); if (!fs.existsSync(dir)) return []; return fs.readdirSync(dir, { withFileTypes: true }) .map((e) => { if (!e.isFile()) throw new Error(`${dir}/${e.name}: undermapper understøttes ikke`); const p = `${dir}/${e.name}`; return [p, sha256(fs.readFileSync(p))]; }) .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)); }; const stampOf = (text) => { const lines = text.replace(/\r\n/g, '\n').split('\n'); const end = lines[0] === '---' ? lines.indexOf('---', 1) : -1; if (end < 0) return { stamp: null, draft: false }; const fm = lines.slice(1, end); const g = fm.find((l) => /^godkendt:/.test(l)); const draft = fm.some((l) => /^draft:\s*true\s*$/.test(l)); return { stamp: g ? g.replace(/^godkendt:\s*/, '').replace(/^['"]|['"]$/g, '') : null, draft }; }; const entries = fs.readdirSync(DIR, { withFileTypes: true }); const files = entries.filter((e) => e.isFile() && /\.mdx?$/.test(e.name)).map((e) => `${DIR}/${e.name}`); let bad = 0; // En billedmappe uden et indlæg ved siden af er aldrig blevet godkendt. for (const e of entries.filter((e) => e.isDirectory())) { if (!files.some((f) => f.replace(/\.mdx?$/, '') === `${DIR}/${e.name}`)) { console.error(`✗ ${DIR}/${e.name}/: filer uden et godkendt indlæg`); bad++; } } for (const rel of files) { const text = fs.readFileSync(rel, 'utf8'); const { stamp, draft } = stampOf(text); if (draft) { console.error(`✗ ${rel}: udkast (draft: true) må ikke ligge i det offentlige repo`); bad++; continue; } if (!stamp) { console.error(`✗ ${rel}: mangler godkendelse`); bad++; continue; } let assets; try { assets = assetsFor(rel); } catch (err) { console.error(`✗ ${err.message}`); bad++; continue; } const want = 'v1:' + crypto.createHmac('sha256', Buffer.from(key, 'hex')).update(canonical(rel, text, assets)).digest('hex'); const ok = stamp.length === want.length && crypto.timingSafeEqual(Buffer.from(stamp), Buffer.from(want)); if (!ok) { console.error(`✗ ${rel}: godkendelsen passer ikke til indholdet (ændret efter godkendelse?)`); bad++; continue; } console.log(`✓ ${rel}${assets.length ? ` (+${assets.length} billeder)` : ''}`); } if (bad) { console.error(`\n${bad} indlæg er ikke godkendt. Intet bliver udgivet.`); process.exit(1); } console.log(`\nAlle ${files.length} indlæg er godkendt.`);