Require an approval stamp on every blog post before building
Build & Deploy / deploy (push) Successful in 3m9s
Build & Deploy / deploy (push) Successful in 3m9s
Posts are written in a private drafts repo and only reach this repo through an approval step that stamps them with an HMAC over path and content. The deploy workflow now runs scripts/verify-approvals.mjs before the build and refuses to build if a post is unstamped, changed after approval, or still marked as a draft (this repo is public, so a draft here is already published). PW_PREVIEW=1 builds the site under /udkast for the private review page: noindex, a banner, no service worker, sitemap or comments. The nine existing posts are stamped as approved. Claude-Session: https://claude.ai/code/session_01JmhJG6JnhXfzNiKv8f2vHu
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
// 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=<hex> 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.`);
|
||||
Reference in New Issue
Block a user