feat(core): support env-var overrides for webui port and bootstrap password
Two additions for trusted local launchers (notably SnowLumaDesktop) to configure core without writing to disk-shared config: - SNOWLUMA_WEBUI_PORT overrides runtime.json's webuiPort at startup without persisting; CLI/Docker users continue to drive it from the file. Mirrors the existing SNOWLUMA_HOOK_RUNTIME_DIR / *_TRUST_PROXY conventions. - SNOWLUMA_WEBUI_BOOTSTRAP_PASSWORD seeds the initial WebUI password on first launch (when webui.json doesn't yet exist), with mustChangePassword=false. After the file is persisted the env var is ignored — password rotation continues through the UI.
This commit is contained in:
@@ -21,26 +21,45 @@ export function loadRuntimeConfig(): RuntimeConfig {
|
||||
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
||||
|
||||
const loaded = tryLoadRuntimeConfig();
|
||||
let normalized: RuntimeConfig;
|
||||
if (!loaded) {
|
||||
saveRuntimeConfig(DEFAULT_RUNTIME_CONFIG);
|
||||
return { ...DEFAULT_RUNTIME_CONFIG };
|
||||
normalized = { ...DEFAULT_RUNTIME_CONFIG };
|
||||
} else {
|
||||
normalized = {
|
||||
webuiPort: normalizePort(loaded.webuiPort ?? 5099, 5099),
|
||||
hookAutoLoad: normalizeBool(loaded.hookAutoLoad, false),
|
||||
};
|
||||
if (
|
||||
normalized.webuiPort !== loaded.webuiPort
|
||||
|| normalized.hookAutoLoad !== loaded.hookAutoLoad
|
||||
) {
|
||||
saveRuntimeConfig(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
const normalized: RuntimeConfig = {
|
||||
webuiPort: normalizePort(loaded.webuiPort ?? 5099, 5099),
|
||||
hookAutoLoad: normalizeBool(loaded.hookAutoLoad, false),
|
||||
};
|
||||
|
||||
if (
|
||||
normalized.webuiPort !== loaded.webuiPort
|
||||
|| normalized.hookAutoLoad !== loaded.hookAutoLoad
|
||||
) {
|
||||
saveRuntimeConfig(normalized);
|
||||
// SNOWLUMA_WEBUI_PORT overrides the on-disk value at runtime without
|
||||
// rewriting runtime.json. Lets a trusted launcher (e.g. SnowLumaDesktop)
|
||||
// pick a free port per-launch while CLI/Docker users continue to drive
|
||||
// it from runtime.json.
|
||||
const envPort = envPortOverride();
|
||||
if (envPort !== undefined) {
|
||||
normalized = { ...normalized, webuiPort: envPort };
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function envPortOverride(): number | undefined {
|
||||
const raw = process.env.SNOWLUMA_WEBUI_PORT;
|
||||
if (!raw || typeof raw !== 'string') return undefined;
|
||||
const n = Number(raw.trim());
|
||||
if (!Number.isFinite(n)) return undefined;
|
||||
const port = Math.trunc(n);
|
||||
if (port <= 0 || port > 65535) return undefined;
|
||||
return port;
|
||||
}
|
||||
|
||||
function tryLoadRuntimeConfig(): RuntimeConfig | null {
|
||||
if (!fs.existsSync(RUNTIME_CONFIG_PATH)) return null;
|
||||
|
||||
|
||||
@@ -24,6 +24,16 @@ export function isDevAuthMode(): boolean {
|
||||
return process.env.SNOWLUMA_DEV_MODE === '1';
|
||||
}
|
||||
|
||||
function envBootstrapPassword(): string | null {
|
||||
const raw = process.env.SNOWLUMA_WEBUI_BOOTSTRAP_PASSWORD;
|
||||
if (!raw || typeof raw !== 'string') return null;
|
||||
// Minimum sanity: avoid accidental empty / 1-char seeds. Strength
|
||||
// rules still don't apply because we want trusted launchers to be
|
||||
// able to pick a high-entropy random secret (e.g. base64(16)).
|
||||
if (raw.length < 8) return null;
|
||||
return raw;
|
||||
}
|
||||
|
||||
export interface WebuiAuthState {
|
||||
passwordHash: string; // hex
|
||||
passwordSalt: string; // hex
|
||||
@@ -153,6 +163,27 @@ export class WebuiAuth {
|
||||
backupCorruptConfig();
|
||||
}
|
||||
}
|
||||
// SNOWLUMA_WEBUI_BOOTSTRAP_PASSWORD lets a trusted launcher seed the
|
||||
// password on first run instead of having to scrape it from logs.
|
||||
// Effect: write webui.json with the env-provided password, hashed,
|
||||
// and mustChangePassword=false. Only honoured when there is NO existing
|
||||
// webui.json — once persisted, password changes happen via the UI.
|
||||
const envPassword = envBootstrapPassword();
|
||||
if (envPassword !== null) {
|
||||
const salt = randomBytes(16);
|
||||
const hash = hashPassword(envPassword, salt);
|
||||
const now = new Date().toISOString();
|
||||
const state: WebuiAuthState = {
|
||||
passwordHash: hash.toString('hex'),
|
||||
passwordSalt: salt.toString('hex'),
|
||||
mustChangePassword: false,
|
||||
generatedAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
atomicWrite(state);
|
||||
log.info('webui credentials seeded from SNOWLUMA_WEBUI_BOOTSTRAP_PASSWORD');
|
||||
return new WebuiAuth(state, null, false);
|
||||
}
|
||||
const initialPassword = randomBytes(8).toString('hex');
|
||||
const state = generateInitialState(initialPassword);
|
||||
atomicWrite(state);
|
||||
|
||||
Reference in New Issue
Block a user