126 lines
3.9 KiB
TypeScript
126 lines
3.9 KiB
TypeScript
import { execFileSync, spawnSync } from 'child_process';
|
|
import { stdin as input, stdout as output } from 'process';
|
|
import * as readline from 'readline/promises';
|
|
|
|
import {
|
|
CONTROLPLANE_HOST_LABEL,
|
|
CONTROLPLANE_INTERNAL_DOMAIN,
|
|
PLATFORM_PUBLIC_BASE,
|
|
} from '../src/config.js';
|
|
import {
|
|
deriveControlplaneOperatorEmail,
|
|
} from '../src/controlplane-auth-bootstrap.js';
|
|
import { logger } from '../src/logger.js';
|
|
import {
|
|
resetOperatorAuthCredentials,
|
|
} from '../src/operator-auth-reset.js';
|
|
import { commandExists } from './platform.js';
|
|
import { emitStatus } from './status.js';
|
|
|
|
interface OperatorAuthArgs {
|
|
email?: string;
|
|
name?: string;
|
|
}
|
|
|
|
function isValidEmail(value: string): boolean {
|
|
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/u.test(value);
|
|
}
|
|
|
|
export function parseArgs(args: string[]): OperatorAuthArgs {
|
|
const result: OperatorAuthArgs = {};
|
|
for (let i = 0; i < args.length; i++) {
|
|
if (args[i] === '--email' && args[i + 1]) result.email = args[++i];
|
|
if (args[i] === '--name' && args[i + 1]) result.name = args[++i];
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function runBsddialogPassword(title: string, text: string): string {
|
|
const result = spawnSync(
|
|
'bsddialog',
|
|
['--title', title, '--passwordbox', text, '0', '0', ''],
|
|
{
|
|
encoding: 'utf-8',
|
|
stdio: ['inherit', 'inherit', 'pipe'],
|
|
},
|
|
);
|
|
|
|
const status = result.status ?? 1;
|
|
if (result.error) throw result.error;
|
|
if (status === 1 || status === 255) process.exit(130);
|
|
if (status !== 0) throw new Error(`bsddialog exited with status ${status}`);
|
|
return (result.stderr ?? '').trim();
|
|
}
|
|
|
|
async function promptHidden(label: string): Promise<string> {
|
|
if (process.stdin.isTTY && commandExists('bsddialog')) {
|
|
return runBsddialogPassword('Operator Password', label).trim();
|
|
}
|
|
|
|
if (!process.stdin.isTTY) {
|
|
throw new Error('operator-auth requires a TTY for secure password entry');
|
|
}
|
|
|
|
const rl = readline.createInterface({ input, output, terminal: true });
|
|
try {
|
|
output.write(`${label}: `);
|
|
execFileSync('stty', ['-echo'], { stdio: 'inherit' });
|
|
const value = await rl.question('');
|
|
output.write('\n');
|
|
return value.trim();
|
|
} finally {
|
|
try {
|
|
execFileSync('stty', ['echo'], { stdio: 'inherit' });
|
|
} catch {
|
|
// best-effort only
|
|
}
|
|
rl.close();
|
|
}
|
|
}
|
|
|
|
async function promptForPassword(): Promise<string> {
|
|
const first = await promptHidden('Enter operator password');
|
|
if (!first) throw new Error('Password cannot be empty');
|
|
const second = await promptHidden('Confirm operator password');
|
|
if (first !== second) throw new Error('Password confirmation did not match');
|
|
return first;
|
|
}
|
|
|
|
export async function run(args: string[]): Promise<void> {
|
|
const opts = parseArgs(args);
|
|
|
|
const operatorName = (opts.name || CONTROLPLANE_HOST_LABEL).trim();
|
|
if (opts.email && !isValidEmail(opts.email.trim())) {
|
|
throw new Error(`Invalid operator email: ${opts.email}`);
|
|
}
|
|
const operatorEmail = deriveControlplaneOperatorEmail({
|
|
operatorName,
|
|
explicitEmail: opts.email,
|
|
internalDomain: CONTROLPLANE_INTERNAL_DOMAIN,
|
|
publicBase: PLATFORM_PUBLIC_BASE || undefined,
|
|
});
|
|
const operatorPassword = await promptForPassword();
|
|
try {
|
|
const result = await resetOperatorAuthCredentials({
|
|
operatorEmail,
|
|
operatorName,
|
|
operatorPassword,
|
|
});
|
|
emitStatus('SETUP_OPERATOR_AUTH', {
|
|
STATUS: 'success',
|
|
OPERATOR: result.operatorName,
|
|
OPERATOR_EMAIL: result.operatorEmail,
|
|
BETTER_AUTH: result.betterAuthStatus,
|
|
});
|
|
|
|
console.log(`\nOperator auth ready.`);
|
|
console.log(` Name: ${result.operatorName}`);
|
|
console.log(` Email: ${result.operatorEmail}`);
|
|
console.log(` Better Auth: ${result.betterAuthStatus}`);
|
|
console.log(' Saved CONTROLPLANE_OPERATOR_EMAIL to .env');
|
|
console.log(' Password was not written to disk; store it yourself.');
|
|
} catch (error) {
|
|
logger.error({ err: error }, 'operator-auth setup failed');
|
|
throw error;
|
|
}
|
|
}
|