Files
suaveplan/scripts/gen-crypto-selects.ts
2026-05-27 23:47:53 +01:00

138 lines
4.4 KiB
TypeScript

/**
* Crypto primitive select codegen.
*
* Reads `crypto.config.ts` and, for every registered primitive, writes
* `packages/universal/crypto-<category>/src/primitives/<id>/<id>.select.ts`
* as a one-line re-export of `./impl/<id>.<flavour>.js`.
*
* The generated file carries a "GENERATED DO NOT EDIT" banner comment.
* Any hand-edit is detected by `check-crypto-selects.ts` and fails CI.
*/
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { cryptoConfig, type PrimitiveFlavour } from "../crypto.config.js";
// Inlined from @suaveplan/crypto-utils/primitive-id so this script can run
// without needing the crypto-utils package to be built/linked first.
// Must stay in sync with universal/crypto/crypto-utils/src/primitive-id/primitive-id.ts.
const HASH_PRIMITIVE_IDS = [
"sha256",
"sha384",
"sha512",
"hmac",
"hkdf",
"pbkdf2",
"argon2id",
] as const;
const CURVE_PRIMITIVE_IDS = ["secp256k1", "ed25519", "x25519", "p256"] as const;
const CIPHER_PRIMITIVE_IDS = [
"aes-256-gcm",
"chacha20-poly1305",
"xchacha20-poly1305",
] as const;
type HashId = (typeof HASH_PRIMITIVE_IDS)[number];
type CurveId = (typeof CURVE_PRIMITIVE_IDS)[number];
type CipherId = (typeof CIPHER_PRIMITIVE_IDS)[number];
export type PrimitiveId = HashId | CurveId | CipherId;
function primitiveCategory(id: PrimitiveId): "hash" | "curve" | "cipher" {
if ((HASH_PRIMITIVE_IDS as readonly string[]).includes(id)) return "hash";
if ((CURVE_PRIMITIVE_IDS as readonly string[]).includes(id)) return "curve";
return "cipher";
}
const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
export const BANNER =
"/* GENERATED — DO NOT EDIT. Regenerate with `bun scripts/gen-crypto-selects.ts`. */";
export function selectFileContents(
id: PrimitiveId,
flavour: PrimitiveFlavour
): string {
return `${BANNER}\nexport * from "./impl/${id}.${flavour}.js";\n`;
}
function categoryPackageName(id: PrimitiveId): string {
const cat = primitiveCategory(id);
if (cat === "hash") return "crypto-hash";
if (cat === "curve") return "crypto-curve";
return "crypto-cipher";
}
export function selectPath(id: PrimitiveId): string {
const pkg = categoryPackageName(id);
return resolve(
REPO_ROOT,
`universal/auth/${pkg}/src/primitives/${id}/${id}.select.ts`
);
}
export interface SelectPlan {
readonly id: PrimitiveId;
readonly flavour: PrimitiveFlavour;
readonly path: string;
readonly contents: string;
}
export function plan(): readonly SelectPlan[] {
const out: SelectPlan[] = [];
const allIds = [
...HASH_PRIMITIVE_IDS,
...CURVE_PRIMITIVE_IDS,
...CIPHER_PRIMITIVE_IDS,
] as readonly PrimitiveId[];
for (const id of allIds) {
const flavour = cryptoConfig[id];
out.push({
id,
flavour,
path: selectPath(id),
contents: selectFileContents(id, flavour),
});
}
return out;
}
export function writePlan(plans: readonly SelectPlan[]): readonly {
readonly path: string;
readonly action: "written" | "unchanged";
}[] {
const results: { path: string; action: "written" | "unchanged" }[] = [];
for (const p of plans) {
mkdirSync(dirname(p.path), { recursive: true });
let current: string | undefined;
if (existsSync(p.path)) {
current = readFileSync(p.path, "utf8");
}
if (current === p.contents) {
results.push({ path: p.path, action: "unchanged" });
continue;
}
if (current !== undefined && !current.startsWith(BANNER)) {
throw new Error(
`Refusing to overwrite hand-edited select file (banner missing): ${p.path}`
);
}
writeFileSync(p.path, p.contents, "utf8");
results.push({ path: p.path, action: "written" });
}
return results;
}
async function main(): Promise<void> {
const results = writePlan(plan());
const written = results.filter((r) => r.action === "written").length;
const unchanged = results.filter((r) => r.action === "unchanged").length;
console.log(
`crypto-selects: ${written} written, ${unchanged} unchanged (${results.length} total)`
);
}
if (import.meta.main) {
await main();
}