import { sdk } from '../sdk' import { configFile } from '../file-models/config.json' import { randomBytes, scryptSync } from 'crypto' const { InputSpec, Value } = sdk const SCRYPT_KEYLEN = 64 // Mirror of Recap's setAdminPassword — same shape so server-side // admin-auth code can be lifted with minimal change. const inputSpec = InputSpec.of({ relay_admin_username: Value.text({ name: 'Admin Username', description: 'Username for the relay admin dashboard. Defaults to "admin".', required: true, default: 'admin', minLength: 1, maxLength: 64, }), relay_admin_password: Value.text({ name: 'Admin Password', description: 'Password for the relay admin dashboard. Must be at least 8 characters. Leave blank to disable /admin entirely (useful while testing /relay/* endpoints).', required: false, default: null, masked: true, minLength: 0, maxLength: 256, }), relay_admin_password_confirm: Value.text({ name: 'Confirm Password', description: 'Re-enter the password to confirm.', required: false, default: null, masked: true, minLength: 0, maxLength: 256, }), }) export const setAdminPassword = sdk.Action.withInput( 'set-admin-password', async ({ effects }) => ({ name: 'Set Admin Password', description: "Gate the relay's /admin dashboard. The public /relay/* endpoints are unaffected — they're per-call authenticated via X-Recap-Install-Id + Authorization headers.", warning: null, allowedStatuses: 'any', group: 'Setup', visibility: 'enabled', }), inputSpec, async ({ effects }) => { const config = await configFile.read().once() return { relay_admin_username: config?.relay_admin_username || 'admin', relay_admin_password: undefined, relay_admin_password_confirm: undefined, } }, async ({ effects, input }) => { const username = (input.relay_admin_username || '').trim() const password = input.relay_admin_password || '' const confirm = input.relay_admin_password_confirm || '' if (!username) throw new Error('Username is required.') if (password === '' && confirm === '') { await configFile.merge(effects, { relay_admin_username: username, relay_admin_password_hash: '', relay_admin_password_salt: '', }) return null } if (password !== confirm) { throw new Error('Password and confirmation do not match.') } if (password.length < 8) { throw new Error('Password must be at least 8 characters.') } const salt = randomBytes(16).toString('hex') const hash = scryptSync(password, salt, SCRYPT_KEYLEN).toString('hex') const existing = await configFile.read().once() const sessionSecret = existing?.relay_admin_session_secret && existing.relay_admin_session_secret.length > 0 ? existing.relay_admin_session_secret : randomBytes(32).toString('hex') await configFile.merge(effects, { relay_admin_username: username, relay_admin_password_hash: hash, relay_admin_password_salt: salt, relay_admin_session_secret: sessionSecret, }) return null }, )