fix(cli): generate a UI password for bare --ui-password in daemon/serve mode

The grand tunnel restructuring removed the CLI's auto-generated UI
password, so `openchamber -d --ui-password` (no value) silently started
an unauthenticated server instead of creating a password as in 1.8.1.

Restore generation for an explicit --ui-password flag without a value:
the password is generated before either launch path, passed to the
daemon/foreground process via OPENCHAMBER_UI_PASSWORD, persisted in the
instance state file, and surfaced once in human/quiet/json output.

Refs OPE-216
This commit is contained in:
Serhii Dziupin
2026-08-05 11:24:10 +03:00
parent 34c221b07f
commit 41a2e3781d
5 changed files with 108 additions and 7 deletions
+39
View File
@@ -34,12 +34,14 @@ import {
discoverRunningInstances,
discoverUnconfirmedRegistryInstanceOnPort,
ensureTunnelProfilesMigrated,
generateUiPassword,
getInstanceFilePath,
getPidFilePath,
isOpenchamberCmdline,
isOpenchamberProcessRunning,
parseArgs,
resolveServeHost,
resolveServeUiPassword,
} from './cli.js';
async function withTempOpenChamberDataDir(fn) {
@@ -692,6 +694,43 @@ describe('network-exposed auth validation', () => {
});
});
describe('serve UI password resolution', () => {
it('keeps a configured password untouched', () => {
expect(resolveServeUiPassword({ uiPassword: 'secret', explicitUiPassword: true }))
.toEqual({ password: 'secret', generated: false });
});
it('generates a password for an explicit --ui-password flag without a value', () => {
const resolved = resolveServeUiPassword({ uiPassword: '', explicitUiPassword: true });
expect(resolved.generated).toBe(true);
expect(typeof resolved.password).toBe('string');
expect(resolved.password.length).toBe(16);
});
it('does not generate a password when the flag is absent', () => {
expect(resolveServeUiPassword({ uiPassword: undefined, explicitUiPassword: false }))
.toEqual({ password: undefined, generated: false });
});
it('generates passwords from an ambiguity-free charset', () => {
const resolved = resolveServeUiPassword({ uiPassword: '', explicitUiPassword: true });
expect(resolved.password).toMatch(/^[A-HJ-NP-Za-km-z2-9]{16}$/);
expect(resolved.password).not.toMatch(/[0O1Il]/);
});
it('generates distinct passwords on repeated calls', () => {
const a = generateUiPassword();
const b = generateUiPassword();
expect(a).not.toBe(b);
});
it('parses --ui-password without a value as explicit but empty', () => {
const parsed = parseArgs(['serve', '--ui-password']);
expect(parsed.options.explicitUiPassword).toBe(true);
expect(parsed.options.uiPassword).toBe('');
});
});
describe('serve host resolution', () => {
it('uses OPENCHAMBER_HOST when --host is not provided', () => {
const previous = process.env.OPENCHAMBER_HOST;