feat: read INITIAL_PASSWORD env var during setup (#8439)

* feat: read INITIAL_PASSWORD env var during setup

Allow users to set the admin password via the INITIAL_PASSWORD
environment variable instead of requiring the --password CLI flag
or interactive prompt. Falls between --password flag and interactive
prompt in resolution priority.

* test(cli): cover INITIAL_PASSWORD env var in setup resolvePassword

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: linh.doan <linh.doan@be.com.vn>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
This commit is contained in:
Harvey Doan
2026-07-26 13:53:28 +07:00
committed by GitHub
parent ca79344b40
commit a095ebc43d
2 changed files with 76 additions and 0 deletions

View File

@@ -26,6 +26,7 @@ function wantsProviderSetup(opts) {
async function resolvePassword(opts, prompt, nonInteractive) {
if (opts.password) return opts.password;
if (process.env.INITIAL_PASSWORD) return process.env.INITIAL_PASSWORD;
if (nonInteractive) return "";
const answer = await prompt.ask("Set an admin password now? [y/N]", "N");

View File

@@ -165,3 +165,78 @@ test("setup command can test provider and persist active status", async () => {
assert.equal(provider.last_error, null);
});
});
test("setup command reads the admin password from INITIAL_PASSWORD when --password is not set", async () => {
const ORIGINAL_INITIAL_PASSWORD = process.env.INITIAL_PASSWORD;
await withTempEnv(async (dataDir) => {
process.env.INITIAL_PASSWORD = "env-var-secret";
const loggedLines: string[] = [];
const originalConsoleLog = console.log;
console.log = (...args: unknown[]) => {
loggedLines.push(args.map(String).join(" "));
};
try {
const { runSetupCommand } = await import("../../bin/cli/commands/setup.mjs");
const exitCode = await runSetupCommand({ nonInteractive: true });
assert.equal(exitCode, 0);
const db = new Database(path.join(dataDir, "storage.sqlite"));
const rows = db
.prepare("SELECT key, value FROM key_value WHERE namespace = 'settings'")
.all() as Array<{ key: string; value: string }>;
const settings = Object.fromEntries(rows.map((row) => [row.key, JSON.parse(row.value)]));
db.close();
assert.equal(settings.requireLogin, true);
assert.equal(await bcrypt.compare("env-var-secret", settings.password as string), true);
// The raw password must never be echoed to stdout while resolving/setting it.
assert.ok(
!loggedLines.some((line) => line.includes("env-var-secret")),
"INITIAL_PASSWORD value must not be logged during setup"
);
} finally {
console.log = originalConsoleLog;
}
});
if (ORIGINAL_INITIAL_PASSWORD === undefined) {
delete process.env.INITIAL_PASSWORD;
} else {
process.env.INITIAL_PASSWORD = ORIGINAL_INITIAL_PASSWORD;
}
});
test("setup command prioritizes an explicit --password flag over INITIAL_PASSWORD", async () => {
const ORIGINAL_INITIAL_PASSWORD = process.env.INITIAL_PASSWORD;
await withTempEnv(async (dataDir) => {
process.env.INITIAL_PASSWORD = "env-var-should-lose";
const { runSetupCommand } = await import("../../bin/cli/commands/setup.mjs");
const exitCode = await runSetupCommand({
nonInteractive: true,
password: "flag-should-win",
});
assert.equal(exitCode, 0);
const db = new Database(path.join(dataDir, "storage.sqlite"));
const passwordRow = db
.prepare("SELECT value FROM key_value WHERE namespace = 'settings' AND key = 'password'")
.get() as { value: string };
db.close();
const storedHash = JSON.parse(passwordRow.value) as string;
assert.equal(await bcrypt.compare("flag-should-win", storedHash), true);
assert.equal(await bcrypt.compare("env-var-should-lose", storedHash), false);
});
if (ORIGINAL_INITIAL_PASSWORD === undefined) {
delete process.env.INITIAL_PASSWORD;
} else {
process.env.INITIAL_PASSWORD = ORIGINAL_INITIAL_PASSWORD;
}
});