clawdie-ai/setup/operator-auth.ts
Operator & Codex 5c685f1285 Harden dashboard password reset flow
---
Build: pass | Tests: FAIL — 26 failed
2026-05-03 20:45:48 +02:00

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;
}
}