// Spærre før build: intet blogindlæg går live uden at være godkendt. // // Hvert indlæg i src/content/blog skal bære et `godkendt:`-stempel i frontmatter. // Stemplet er en HMAC-SHA256 over filens sti + indhold (uden selve stempel-linjen), // lavet med en nøgle som kun godkendelsestjenesten og denne workflow kender. Et // indlæg der er lagt direkte i repoet, eller ændret efter godkendelse, har derfor // intet eller et forkert stempel, og så stopper buildet før noget bliver deployet. // // Udkast hører ikke hjemme her: repoet er offentligt, så `draft: true` ville stadig // udgive teksten via git. Udkast ligger i et privat repo indtil de er godkendt. // // PUBLISH_KEY= node scripts/verify-approvals.mjs import crypto from 'node:crypto'; import fs from 'node:fs'; import path from 'node:path'; 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) => { 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); } } return `${relPath}\n${lines.join('\n')}`; }; 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 files = fs.readdirSync(DIR, { recursive: true }) .filter((f) => f.endsWith('.md') || f.endsWith('.mdx')) .map((f) => path.posix.join(DIR, f.split(path.sep).join('/'))); let bad = 0; 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; } const want = 'v1:' + crypto.createHmac('sha256', Buffer.from(key, 'hex')).update(canonical(rel, text)).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}`); } 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.`);