#!/usr/bin/env node /** * Generates password-generator defaults constants for all cross-platform clients. * * Input: core/models/src/defaults/PasswordGeneratorDefaults.ts * Outputs: * - core/rust/src/password_generator/defaults.rs (Rust) * - apps/server/Databases/AliasClientDb/Models/PasswordGeneratorDefaults.cs (C#) * * Note: Typescript clients can import the constants directly from the distributed `@/utils/dist/core/models/vault`. */ const fs = require('fs'); const path = require('path'); const REPO_ROOT = path.join(__dirname, '../../..'); const TS_SOURCE = path.join(REPO_ROOT, 'core/models/src/defaults/PasswordGeneratorDefaults.ts'); const TS_SOURCE_REL = 'core/models/src/defaults/PasswordGeneratorDefaults.ts'; const RUST_OUTPUT = path.join(REPO_ROOT, 'core/rust/src/password_generator/defaults.rs'); const CS_OUTPUT = path.join(REPO_ROOT, 'apps/server/Databases/AliasClientDb/Models/PasswordGeneratorDefaults.cs'); /** * Parse the `export const NAME = VALUE;` lines (each with its leading single-line * JSDoc comment) out of the TypeScript source, preserving order. */ function parseConstants(source) { const constants = []; const lines = source.split('\n'); let pendingDoc = null; for (const line of lines) { const trimmed = line.trim(); // Single-line JSDoc comment: /** ... */ if (trimmed.startsWith('/**') && trimmed.endsWith('*/')) { pendingDoc = trimmed.slice(3, -2).trim(); continue; } const constMatch = line.match(/^\s*export const ([A-Z0-9_]+)\s*=\s*(\d+)\s*;/); if (constMatch) { constants.push({ name: constMatch[1], value: Number(constMatch[2]), doc: pendingDoc || `${constMatch[1]}.`, }); pendingDoc = null; continue; } // Any other non-blank line resets a dangling doc comment. if (line.trim() !== '') { pendingDoc = null; } } if (constants.length === 0) { throw new Error(`No 'export const ... = ;' constants found in ${TS_SOURCE_REL}`); } return constants; } /** SCREAMING_SNAKE_CASE -> PascalCase (DEFAULT_PASSWORD_LENGTH -> DefaultPasswordLength). */ function toPascalCase(name) { return name .toLowerCase() .split('_') .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) .join(''); } function generateRust(constants) { const header = `//! Client-facing defaults for the password/passphrase generator. //! //! @generated — do NOT edit this file directly. It is generated from //! ${TS_SOURCE_REL} by core/models/scripts/generate-password-defaults.cjs. //! Edit the TypeScript source and run 'core/models/build.sh' to regenerate. `; const body = constants .map((c) => `/// ${c.doc}\npub const ${c.name}: u32 = ${c.value};`) .join('\n\n'); return `${header}\n${body}\n`; } function generateCSharp(constants) { const header = `// // This file is auto-generated from ${TS_SOURCE_REL} // Do not edit this file directly. Run 'core/models/build.sh' (or // 'node core/models/scripts/generate-password-defaults.cjs') to regenerate. #nullable enable namespace AliasClientDb.Models; /// /// Password generator defaults shared across all AliasVault clients. /// public static class PasswordGeneratorDefaults {`; const body = constants .map((c) => { const csName = toPascalCase(c.name); return ` /// \n /// ${c.doc}\n /// \n public const int ${csName} = ${c.value};`; }) .join('\n\n'); return `${header}\n${body}\n}\n`; } function writeIfChanged(filePath, contents) { const existing = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : null; if (existing === contents) { console.log(` ✓ up to date: ${path.relative(REPO_ROOT, filePath)}`); return; } fs.mkdirSync(path.dirname(filePath), { recursive: true }); fs.writeFileSync(filePath, contents); console.log(` ✎ wrote: ${path.relative(REPO_ROOT, filePath)}`); } function main() { const source = fs.readFileSync(TS_SOURCE, 'utf8'); const constants = parseConstants(source); console.log(`Generating password-generator defaults from ${TS_SOURCE_REL}:`); console.log(` ${constants.map((c) => `${c.name}=${c.value}`).join(', ')}`); writeIfChanged(RUST_OUTPUT, generateRust(constants)); writeIfChanged(CS_OUTPUT, generateCSharp(constants)); } main();