HuggingClaw-MissionControl / src /lib /auto-credentials.ts
Nyk
feat: add first-time setup wizard and zero-config startup
943fe08
import { randomBytes } from 'node:crypto'
import fs from 'node:fs'
import path from 'node:path'
import { config, ensureDirExists } from './config'
import { logger } from './logger'
function getGeneratedFilePath(): string {
return path.join(config.dataDir, '.auto-generated')
}
interface PersistedValues {
AUTH_SECRET?: string
API_KEY?: string
}
function readPersisted(): PersistedValues {
try {
if (!fs.existsSync(getGeneratedFilePath())) return {}
const raw = fs.readFileSync(getGeneratedFilePath(), 'utf8')
const values: PersistedValues = {}
for (const line of raw.split('\n')) {
const trimmed = line.trim()
if (!trimmed || trimmed.startsWith('#')) continue
const eqIdx = trimmed.indexOf('=')
if (eqIdx < 0) continue
const key = trimmed.slice(0, eqIdx).trim()
const value = trimmed.slice(eqIdx + 1).trim()
if (key === 'AUTH_SECRET' || key === 'API_KEY') {
values[key] = value
}
}
return values
} catch {
return {}
}
}
function writePersisted(values: PersistedValues): void {
try {
ensureDirExists(config.dataDir)
const lines = [
'# Auto-generated values. Overridden by env vars when set.',
]
if (values.AUTH_SECRET) lines.push(`AUTH_SECRET=${values.AUTH_SECRET}`)
if (values.API_KEY) lines.push(`API_KEY=${values.API_KEY}`)
fs.writeFileSync(getGeneratedFilePath(), lines.join('\n') + '\n', { mode: 0o600 })
} catch (err) {
logger.warn({ err }, 'Failed to persist auto-generated values')
}
}
function generate(): string {
return randomBytes(32).toString('hex')
}
// Known placeholder values from .env.example that should be replaced
const PLACEHOLDER_AUTH_SECRETS = new Set([
'random-secret-for-legacy-cookies',
])
const PLACEHOLDER_API_KEYS = new Set([
'generate-a-random-key',
])
/**
* Ensure AUTH_SECRET and API_KEY are available.
* Priority: env var > persisted file > auto-generate + persist.
* Sets process.env so downstream code picks them up.
*/
export function ensureAutoGeneratedCredentials(): void {
if (process.env.NEXT_PHASE === 'phase-production-build') return
const persisted = readPersisted()
let dirty = false
// AUTH_SECRET
const currentAuthSecret = (process.env.AUTH_SECRET || '').trim()
if (!currentAuthSecret || PLACEHOLDER_AUTH_SECRETS.has(currentAuthSecret)) {
if (persisted.AUTH_SECRET) {
process.env.AUTH_SECRET = persisted.AUTH_SECRET
} else {
const val = generate()
process.env.AUTH_SECRET = val
persisted.AUTH_SECRET = val
dirty = true
logger.info('Auto-generated AUTH_SECRET (persisted to .data/.auto-generated)')
}
}
// API_KEY
const currentApiKey = (process.env.API_KEY || '').trim()
if (!currentApiKey || PLACEHOLDER_API_KEYS.has(currentApiKey)) {
if (persisted.API_KEY) {
process.env.API_KEY = persisted.API_KEY
} else {
const val = generate()
process.env.API_KEY = val
persisted.API_KEY = val
dirty = true
logger.info('Auto-generated API_KEY (persisted to .data/.auto-generated)')
}
}
if (dirty) {
writePersisted(persisted)
}
}