163 lines
7.0 KiB
JavaScript
163 lines
7.0 KiB
JavaScript
#!/usr/bin/env node
|
|
import { createHash } from "node:crypto";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
|
|
export const BASELINE_MIGRATION_NAME = "00000000000000_hivegrid_baseline";
|
|
|
|
function sha256(value) {
|
|
return createHash("sha256").update(value).digest("hex");
|
|
}
|
|
|
|
function occurrences(value, pattern) {
|
|
return value.match(pattern)?.length ?? 0;
|
|
}
|
|
|
|
export function verifyPrismaBaseline(rootDirectory = process.cwd()) {
|
|
const passes = [];
|
|
const failures = [];
|
|
const migrationsDirectory = path.join(rootDirectory, "prisma/migrations");
|
|
const baselinePath = path.join(migrationsDirectory, BASELINE_MIGRATION_NAME, "migration.sql");
|
|
const lockPath = path.join(migrationsDirectory, "migration_lock.toml");
|
|
const manifestPath = path.join(rootDirectory, "prisma/baseline-legacy-manifest.json");
|
|
const legacyArchivePath = path.join(rootDirectory, "prisma/baseline-legacy-migrations.tar.gz");
|
|
|
|
if (
|
|
!fs.existsSync(migrationsDirectory) ||
|
|
!fs.existsSync(baselinePath) ||
|
|
!fs.existsSync(lockPath) ||
|
|
!fs.existsSync(manifestPath) ||
|
|
!fs.existsSync(legacyArchivePath)
|
|
) {
|
|
return {
|
|
passes,
|
|
failures: ["Prisma baseline requires its migration, migration_lock.toml, legacy manifest, and audit archive"],
|
|
};
|
|
}
|
|
|
|
const migrationDirectories = fs
|
|
.readdirSync(migrationsDirectory, { withFileTypes: true })
|
|
.filter((entry) => entry.isDirectory())
|
|
.map((entry) => entry.name)
|
|
.sort();
|
|
if (migrationDirectories[0] === BASELINE_MIGRATION_NAME) {
|
|
passes.push("active Prisma history starts with the consolidated baseline");
|
|
} else {
|
|
failures.push(`active Prisma history must start with ${BASELINE_MIGRATION_NAME}`);
|
|
}
|
|
|
|
let manifest;
|
|
try {
|
|
manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
} catch {
|
|
failures.push("Prisma baseline legacy manifest must be valid JSON");
|
|
return { passes, failures };
|
|
}
|
|
const baselineSql = fs.readFileSync(baselinePath, "utf8");
|
|
const baselineChecksum = sha256(baselineSql);
|
|
const legacyArchiveChecksum = sha256(fs.readFileSync(legacyArchivePath));
|
|
if (
|
|
manifest.version === 1 &&
|
|
manifest.baselineMigration === BASELINE_MIGRATION_NAME &&
|
|
manifest.baselineChecksum === baselineChecksum &&
|
|
manifest.legacyArchiveChecksum === legacyArchiveChecksum &&
|
|
/^[0-9a-f]{40}$/.test(manifest.legacySourceCommit ?? "")
|
|
) {
|
|
passes.push("baseline, audit archive, and source revision match the immutable legacy manifest");
|
|
} else {
|
|
failures.push("baseline checksum, audit archive, migration name, or source revision differs from the legacy manifest");
|
|
}
|
|
|
|
const legacyMigrations = Array.isArray(manifest.migrations) ? manifest.migrations : [];
|
|
const names = legacyMigrations.map((migration) => migration?.name);
|
|
const uniqueNames = new Set(names);
|
|
const checksumsValid = legacyMigrations.every((migration) => typeof migration?.name === "string" && /^[0-9a-f]{64}$/.test(migration?.checksum ?? ""));
|
|
if (
|
|
legacyMigrations.length === manifest.legacyMigrationCount &&
|
|
legacyMigrations.length === 195 &&
|
|
uniqueNames.size === legacyMigrations.length &&
|
|
checksumsValid &&
|
|
names.every((name, index) => index === 0 || names[index - 1].localeCompare(name) < 0) &&
|
|
manifest.legacyHead === names.at(-1)
|
|
) {
|
|
passes.push("legacy adoption manifest pins all 195 ordered migration checksums");
|
|
} else {
|
|
failures.push("legacy adoption manifest must pin exactly 195 unique ordered migrations through its declared head");
|
|
}
|
|
|
|
const legacyChecksumVariants = Array.isArray(manifest.legacyChecksumVariants) ? manifest.legacyChecksumVariants : [];
|
|
const migrationChecksums = new Map(legacyMigrations.map((migration) => [migration?.name, migration?.checksum]));
|
|
const variantKeys = new Set();
|
|
const variantsValid = legacyChecksumVariants.every((variant) => {
|
|
const key = `${variant?.name}:${variant?.checksum}`;
|
|
const valid =
|
|
migrationChecksums.has(variant?.name) &&
|
|
/^[0-9a-f]{64}$/.test(variant?.checksum ?? "") &&
|
|
/^[0-9a-f]{40}$/.test(variant?.sourceCommit ?? "") &&
|
|
migrationChecksums.get(variant.name) !== variant.checksum &&
|
|
!variantKeys.has(key);
|
|
variantKeys.add(key);
|
|
return valid;
|
|
});
|
|
if (legacyChecksumVariants.length === 10 && variantsValid && variantKeys.size === legacyChecksumVariants.length) {
|
|
passes.push("legacy adoption manifest pins all 10 reviewed historical checksum variants with source revisions");
|
|
} else {
|
|
failures.push("legacy adoption manifest must pin exactly 10 unique reviewed checksum variants with source revisions");
|
|
}
|
|
|
|
const forbiddenMetaCommands = /^\\(?:restrict|unrestrict)\b/m.test(baselineSql);
|
|
const referencesMigrationLedger = /_prisma_migrations/.test(baselineSql);
|
|
if (!forbiddenMetaCommands && !referencesMigrationLedger) {
|
|
passes.push("baseline is non-interactive and does not manufacture Prisma ledger rows");
|
|
} else {
|
|
failures.push("baseline must not contain pg_dump session meta-commands or _prisma_migrations statements");
|
|
}
|
|
|
|
const surface = {
|
|
tables: occurrences(baselineSql, /^CREATE TABLE /gm),
|
|
policies: occurrences(baselineSql, /^CREATE POLICY /gm),
|
|
functions: occurrences(baselineSql, /^CREATE FUNCTION /gm),
|
|
rlsEnabled: occurrences(baselineSql, / ENABLE ROW LEVEL SECURITY;$/gm),
|
|
rlsForced: occurrences(baselineSql, / FORCE ROW LEVEL SECURITY;$/gm),
|
|
extensions: occurrences(baselineSql, /^CREATE EXTENSION /gm),
|
|
grantsAndRevokes: occurrences(baselineSql, /^(?:GRANT|REVOKE) /gm),
|
|
};
|
|
if (
|
|
surface.tables === 293 &&
|
|
surface.policies === 844 &&
|
|
surface.functions === 92 &&
|
|
surface.rlsEnabled === 293 &&
|
|
surface.rlsForced === 293 &&
|
|
surface.extensions === 2 &&
|
|
surface.grantsAndRevokes === 1010 &&
|
|
occurrences(baselineSql, /^INSERT INTO public\."Onboarding" /gm) === 1 &&
|
|
occurrences(baselineSql, /^INSERT INTO public\."OnboardingStep" /gm) === 1 &&
|
|
occurrences(baselineSql, /'shk-first-steps-(?:profile|contacts|projects|billing)-v1'/g) === 4 &&
|
|
occurrences(baselineSql, /^INSERT INTO public\."FeatureFlag" /gm) === 1 &&
|
|
occurrences(baselineSql, /^INSERT INTO public\."FeatureFlagAudit" /gm) === 1
|
|
) {
|
|
passes.push("baseline preserves the reviewed schema, RLS, function, extension, and grant surface");
|
|
} else {
|
|
failures.push(`baseline database surface changed unexpectedly: ${JSON.stringify(surface)}`);
|
|
}
|
|
|
|
if (/^provider = "postgresql"$/m.test(fs.readFileSync(lockPath, "utf8"))) {
|
|
passes.push("Prisma migration provider remains PostgreSQL");
|
|
} else {
|
|
failures.push("migration_lock.toml must retain the PostgreSQL provider");
|
|
}
|
|
|
|
return { passes, failures };
|
|
}
|
|
|
|
export function formatPrismaBaselineResult(result) {
|
|
return [...result.passes.map((message) => `PASS ${message}`), ...result.failures.map((message) => `FAIL ${message}`)].join("\n");
|
|
}
|
|
|
|
const isCli = process.argv[1] && path.join(import.meta.dirname, "verify-prisma-baseline.mjs") === path.resolve(process.argv[1]);
|
|
if (isCli) {
|
|
const result = verifyPrismaBaseline();
|
|
process.stdout.write(`${formatPrismaBaselineResult(result)}\n`);
|
|
process.exitCode = result.failures.length === 0 ? 0 : 1;
|
|
}
|