Files
2026-08-10 02:00:15 +08:00

52 lines
2.6 KiB
JavaScript

import { access, readFile, readdir } from "node:fs/promises";
import { fileURLToPath } from "node:url";
import { join } from "node:path";
const root = new URL("../", import.meta.url);
const contentRoot = new URL("../content/", import.meta.url);
const required = ["title", "summary", "date", "updated", "topic", "kind", "status", "visibility", "canonicalUrl"];
const forbidden = [
[/\/(?:Users|home|mnt)\/[^\s)\]>'"]+/, "private absolute path"],
[/\b(?:10\.(?:\d{1,3}\.){2}\d{1,3}|192\.168\.(?:\d{1,3}\.)\d{1,3}|100\.(?:6[4-9]|[7-9]\d|1[01]\d|12[0-7])\.(?:\d{1,3}\.)\d{1,3})\b/, "private IPv4"],
[/(?:git\.n\.xiaomi\.com|coro\.doc|\.codex\/sessions|<codex_internal_context)/, "internal material"],
[/-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/, "private key"],
[/"?sourcePaths"?\s*:/, "Knowledge Hub private metadata"]
];
async function files(dir) {
const entries = await readdir(dir, { withFileTypes: true });
const base = fileURLToPath(dir);
return entries.flatMap((entry) => entry.isFile() && entry.name.endsWith(".md") ? [join(base, entry.name)] : []);
}
function parse(text, path) {
const match = text.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
if (!match) throw new Error(`${path}: invalid frontmatter delimiters`);
return { data: JSON.parse(match[1]), body: match[2].trim() };
}
const paths = [...await files(new URL("posts/", contentRoot)), ...await files(new URL("sites/", contentRoot))];
const errors = [];
const urls = new Set();
for (const path of paths) {
const text = await readFile(path, "utf8");
let parsed;
try { parsed = parse(text, path); } catch (error) { errors.push(error.message); continue; }
for (const key of required) if (!parsed.data[key]) errors.push(`${path}: missing ${key}`);
if (parsed.data.visibility !== "public") errors.push(`${path}: visibility must be public`);
if (!/^https:\/\//.test(parsed.data.canonicalUrl ?? "")) errors.push(`${path}: canonicalUrl must use HTTPS`);
if (urls.has(parsed.data.canonicalUrl)) errors.push(`${path}: duplicate canonicalUrl`);
urls.add(parsed.data.canonicalUrl);
if (parsed.data.kind === "article" && !parsed.body) errors.push(`${path}: empty article body`);
if (parsed.data.preview?.startsWith("/")) {
try { await access(new URL(`../public${parsed.data.preview}`, import.meta.url)); }
catch { errors.push(`${path}: missing preview asset ${parsed.data.preview}`); }
}
for (const [pattern, label] of forbidden) if (pattern.test(text)) errors.push(`${path}: forbidden ${label}`);
}
if (errors.length) {
console.error(errors.join("\n"));
process.exit(1);
}
console.log(`valid: ${paths.length} public content entries at ${root.pathname}`);