Something went wrong. Try again.
Browser extension: detect and subscribe to standard.site publications on ATProto
Something went wrong. Try again.
8.9 kB · 198 lines
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199// Verifies the built frontpage before it is deployed: the page exists, the// PostHog snippet survived the build with the intended config (EU ingestion// host, cookieless persistence, exactly one init), the link-unfurl tags point// at a card image that actually shipped, and the screenshot gallery is the// current popup captures rather than an older copy of them. Plain node, no// dependencies beyond the repo's own render-icons and shots helpers.//// Usage: node scripts/verify-dist.mjs [distDir]
import { createHash } from "node:crypto";import { existsSync, readFileSync } from "node:fs";import { join, resolve } from "node:path";import { fileURLToPath } from "node:url";
import { pngSize } from "../../scripts/render-icons.mjs";import { SHOTS, capturePath } from "../../scripts/shots.mjs";
const SITE = "https://substandard.blog";
const webRoot = resolve(fileURLToPath(import.meta.url), "../..");const dist = resolve(process.argv[2] ?? join(webRoot, "dist"));const index = join(dist, "index.html");
if (!existsSync(index)) { console.error( `verify-dist: ${index} does not exist — run \`npm run build\` first`, ); process.exit(1);}
const html = readFileSync(index, "utf8");const problems = [];
const inits = html.match(/posthog\.init\(/g)?.length ?? 0;if (inits !== 1) { problems.push(`expected exactly one posthog.init call, found ${inits}`);}if (!html.includes('"phc_yBvzGPADSPgFV7hBaHJWQxHa4ZniXUhdd6nkqaHgk2Mi"')) { problems.push("PostHog project API key missing from index.html");}if (!html.includes("https://eu.i.posthog.com")) { problems.push("PostHog EU ingestion host missing — events would go nowhere");}if (!/persistence:\s*"memory"/.test(html)) { problems.push('persistence: "memory" missing — tracking must stay cookieless');}
// The CSP served by CloudFront allows the inline PostHog snippet by hash, so// any change to that snippet — a PostHog config edit, a different Astro// inlining — silently blocks it in production unless infra/main.tf is updated// to match. Read the expected hash out of the terraform rather than keeping a// second copy here, so there is exactly one value to keep right.const mainTf = readFileSync(resolve(webRoot, "../infra/main.tf"), "utf8");const expectedHash = mainTf.match(/posthog_script_hash\s*=\s*"([^"]+)"/)?.[1];const inlineScript = html.match(/<script>([\s\S]*?)<\/script>/)?.[1];
if (!expectedHash) { problems.push("infra/main.tf has no posthog_script_hash to check the CSP against");} else if (inlineScript === undefined) { problems.push("no inline script in index.html — the CSP hash has nothing to cover");} else { const actual = `sha256-${createHash("sha256").update(inlineScript, "utf8").digest("base64")}`; if (actual !== expectedHash) { problems.push( `inline script hash is ${actual}, but the CSP in infra/main.tf allows ${expectedHash} — ` + "update posthog_script_hash there or analytics will be blocked in production", ); }}
// Link unfurls: crawlers resolve nothing relative and follow no scripts, so// the tags must be absolute and the card must be a real file in this deploy.const meta = (attr, name) => html.match(new RegExp(`<meta ${attr}="${name}" content="([^"]*)"`))?.[1];
const image = meta("property", "og:image");if (image !== `${SITE}/og.png`) { problems.push(`og:image is ${image ?? "missing"}, want ${SITE}/og.png`);} else if (!existsSync(join(dist, "og.png"))) { problems.push("og.png missing from dist — unfurls would show a dead image");} else { const dim = pngSize(readFileSync(join(dist, "og.png"))); if (dim.width !== 1200 || dim.height !== 630) { problems.push(`og.png is ${dim.width}x${dim.height}, want 1200x630`); }}for (const name of ["og:title", "og:description", "og:url"]) { if (!meta("property", name)) problems.push(`${name} missing or empty`);}if (meta("name", "twitter:card") !== "summary_large_image") { problems.push("twitter:card missing or not summary_large_image");}if (!html.includes(`<link rel="canonical" href="${SITE}/"`)) { problems.push("canonical link missing or not absolute");}
// The homepage gallery ships the popup captures themselves, copied out of// docs/img by scripts/sync-shots.mjs. Nothing in the Astro build re-runs that// copy, so a capture regenerated without a re-sync would deploy a screenshot// of a popup the extension no longer has. Compare the bytes about to be// uploaded against the captures in the repo, and the manifest against the// list both the site and the store listing are built from.const manifestPath = resolve(webRoot, "src/shots.json");if (!existsSync(manifestPath)) { problems.push("web/src/shots.json missing — run `npm run shots`");} else { const shots = JSON.parse(readFileSync(manifestPath, "utf8")); const named = shots.map((shot) => shot.name).join(","); const wanted = SHOTS.map((shot) => shot.name).join(","); if (named !== wanted) { problems.push( `shots.json lists [${named}] but scripts/shots.mjs lists [${wanted}] — run \`npm run shots\``, ); } for (const shot of shots) { const shipped = join(dist, shot.src.replace(/^\//, "")); const capture = resolve(webRoot, "..", capturePath(shot)); if (!existsSync(shipped)) { problems.push(`${shot.src} missing from dist — the homepage would show a dead image`); continue; } const bytes = readFileSync(shipped); if (!existsSync(capture)) { problems.push(`${capturePath(shot)} does not exist, but ${shot.src} claims to be a copy of it`); } else if (!bytes.equals(readFileSync(capture))) { problems.push( `${shot.src} differs from ${capturePath(shot)} — a stale screenshot; run \`npm run shots\``, ); } const dim = pngSize(bytes); if (dim.width !== shot.width || dim.height !== shot.height) { problems.push( `${shot.src} is ${dim.width}x${dim.height}, but shots.json reserves ${shot.width}x${shot.height}`, ); } if (!html.includes(shot.src)) { problems.push(`${shot.src} shipped but the homepage does not reference it`); } }}
// The Chrome Web Store listing points at the privacy policy; deploying a// site without it would break the listing's required URL.if (!existsSync(join(dist, "privacy/index.html"))) { problems.push("privacy/index.html missing — the store listing links it");}
// The architecture diagram is inlined SVG under the same CSP as everything// else: `script-src` has no 'unsafe-inline' and `style-src` is bare 'self',// which covers inline SVG in the document. A regenerated diagram that brought// Mermaid's own <style> or a style attribute back would render as black boxes// in production and pass every local check, since nothing serves the CSP in// dev. Check the shipped bytes rather than trusting the render script.const architecture = join(dist, "architecture/index.html");if (!existsSync(architecture)) { problems.push("architecture/index.html missing — the homepage links it");} else { const page = readFileSync(architecture, "utf8"); if (!page.includes("<svg")) { problems.push("architecture/index.html has no inline SVG — the diagram did not render"); } if (/<style[\s>]/i.test(page)) { problems.push("architecture/index.html has a <style> element, which style-src 'self' would drop"); } // `font-style="normal"` is all over Mermaid's text, hence the boundary. if (/(^|[\s"'])style\s*=/i.test(page)) { problems.push("architecture/index.html has a style attribute, which style-src 'self' would drop"); }}
// Unknown paths are served this document, with status 404, by the// custom_error_response blocks in infra/main.tf. It is the only thing between// a stale link and S3's raw AccessDenied XML, since the bucket is private and// answers a missing key with 403. If the build stops emitting the object the// terraform names — a renamed page, a changed build.format — CloudFront has// nothing to serve and the error goes back to being unreadable. Read the path// out of the terraform, as with the CSP hash above, so there is one value to// keep right rather than a second copy here.const errorPage = mainTf.match(/response_page_path\s*=\s*"([^"]+)"/)?.[1];if (!errorPage) { problems.push( "infra/main.tf has no custom_error_response response_page_path — unknown paths would serve S3's AccessDenied XML", );} else if (!existsSync(join(dist, errorPage.replace(/^\//, "")))) { problems.push( `${errorPage} missing from dist — CloudFront serves it for every unknown path`, );}
if (problems.length > 0) { console.error(`verify-dist: FAILED (${problems.length} problem(s)):`); for (const p of problems) console.error(` - ${p}`); process.exit(1);}console.log( `verify-dist: OK — PostHog cookieless, unfurl tags intact, and ${SHOTS.length} ` + `screenshot(s) current in ${index}`,);