138 lines
4.4 KiB
TypeScript
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();
|
|
}
|