Skip to content

Commit ffc77b7

Browse files
committed
fix(sim-setup): only persist URL-safe shell passwords; honor an empty POSTGRES_USER export
DATABASE_URL embeds the password unescaped, so a shell-only password is persisted only when it is made of URL-unreserved characters. An empty POSTGRES_USER export resolves to the Compose default, matching ${POSTGRES_USER:-postgres}.
1 parent d4ee337 commit ffc77b7

2 files changed

Lines changed: 43 additions & 22 deletions

File tree

‎packages/sim-setup/src/compose-database.test.ts‎

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -93,17 +93,32 @@ describe('choosePostgresPassword', () => {
9393
}
9494
})
9595

96-
it.each(['pa ss', 'pass#word', 'pa"ss', "pa'ss", 'pa\\ss', 'pa$ss'])(
97-
'refuses to persist %s, which .env cannot store verbatim',
98-
(value) => {
99-
expect(() =>
100-
choosePostgresPassword(undefined, 'sim-abc', {
101-
shell: { POSTGRES_PASSWORD: value },
102-
hasDatabaseVolume: () => false,
103-
})
104-
).toThrow(/cannot store verbatim/)
105-
}
106-
)
96+
it.each([
97+
'pa ss',
98+
'pass#word',
99+
'pa"ss',
100+
"pa'ss",
101+
'pa\\ss',
102+
'pa$ss',
103+
'pa/ss',
104+
'pa?ss',
105+
'pa%ss',
106+
'pa@ss',
107+
'pa:ss',
108+
])('refuses to persist %s, which .env or DATABASE_URL would change', (value) => {
109+
expect(() =>
110+
choosePostgresPassword(undefined, 'sim-abc', {
111+
shell: { POSTGRES_PASSWORD: value },
112+
hasDatabaseVolume: () => false,
113+
})
114+
).toThrow(SetupError)
115+
})
116+
117+
it('persists a shell password made of URL-unreserved characters', () => {
118+
expect(
119+
choosePostgresPassword(undefined, 'sim-abc', { shell: { POSTGRES_PASSWORD: 'Ab9._~-z' } })
120+
).toEqual({ value: 'Ab9._~-z', source: 'environment' })
121+
})
107122

108123
it('reads the process environment by default', () => {
109124
vi.stubEnv('POSTGRES_PASSWORD', 'from-process')
@@ -114,10 +129,12 @@ describe('choosePostgresPassword', () => {
114129
})
115130

116131
describe('postgresUser', () => {
117-
it('prefers the shell, then .env, then the image default', () => {
132+
it('follows Compose: a shell export wins even when empty, then .env, then the default', () => {
118133
vi.stubEnv('POSTGRES_USER', 'from-shell')
119134
expect(postgresUser('from-env-file')).toBe('from-shell')
120135
vi.stubEnv('POSTGRES_USER', '')
136+
expect(postgresUser('from-env-file')).toBe('postgres')
137+
vi.stubEnv('POSTGRES_USER', undefined)
121138
expect(postgresUser('from-env-file')).toBe('from-env-file')
122139
expect(postgresUser(undefined)).toBe('postgres')
123140
})

‎packages/sim-setup/src/compose-database.ts‎

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,11 @@ export interface PostgresPasswordChoice {
3333
}
3434

3535
/**
36-
* Characters that `.env` would reinterpret — whitespace, comments, quotes,
37-
* escapes, and Compose's `$` interpolation — so a value containing one cannot
38-
* be written unquoted and read back unchanged.
36+
* A password the wizard can persist verbatim: Compose interpolates it unescaped
37+
* into `DATABASE_URL`, and `.env` reinterprets whitespace, comments, quotes and
38+
* `$`, so only URL-unreserved characters survive both unchanged.
3939
*/
40-
const DOTENV_UNSAFE = /[\s#'"\\$]/
40+
const PERSISTABLE_PASSWORD = /^[A-Za-z0-9._~-]+$/
4141

4242
interface ChooseOptions {
4343
/** The shell environment, whose `POSTGRES_PASSWORD` Compose interpolates over `.env`. */
@@ -88,12 +88,12 @@ export function choosePostgresPassword(
8888
]
8989
)
9090
}
91-
if (DOTENV_UNSAFE.test(shellValue)) {
91+
if (!PERSISTABLE_PASSWORD.test(shellValue)) {
9292
throw new SetupError(
93-
'POSTGRES_PASSWORD is exported only in the shell, and contains characters .env cannot store verbatim.',
93+
'POSTGRES_PASSWORD is exported only in the shell, and contains characters that cannot be stored in .env and embedded in DATABASE_URL unchanged.',
9494
[
95-
'add it to .env yourself, quoted, so later runs without the export still use it',
96-
'new installs: use a value from openssl rand -hex 24',
95+
'use only letters, digits and . _ ~ - (for a new install: openssl rand -hex 24)',
96+
'or add it to .env yourself if you have confirmed it works in a connection URL',
9797
]
9898
)
9999
}
@@ -179,7 +179,11 @@ export function reportPostgresPasswordChoice(
179179
)
180180
}
181181

182-
/** The Postgres role Compose initializes, which the shell overrides over `.env` like any variable. */
182+
/**
183+
* The Postgres role Compose initializes from `${POSTGRES_USER:-postgres}`. A shell
184+
* export overrides `.env` even when empty, and an empty value takes the default.
185+
*/
183186
export function postgresUser(envFileValue: string | undefined): string {
184-
return process.env.POSTGRES_USER || envFileValue || 'postgres'
187+
const shellUser = process.env.POSTGRES_USER
188+
return shellUser === undefined ? envFileValue || 'postgres' : shellUser || 'postgres'
185189
}

0 commit comments

Comments
 (0)