Skip to content

Commit dc95ef8

Browse files
committed
fix(cli): fail on unreadable project .env files in test db config
loadProjectEnv swallowed every .env read error as None, but Go's loadEnvIfExists ignores only os.ErrNotExist and returns other I/O errors, aborting before env() expansion. An unreadable .env (e.g. permissions) could make test db --local reject a valid env-backed port or use the wrong password while hiding the real error. Distinguish NotFound (skip) from other read errors (fail with LegacyDbConfigLoadError), matching the config.toml read path.
1 parent 85ba357 commit dc95ef8

2 files changed

Lines changed: 39 additions & 5 deletions

File tree

apps/cli/src/legacy/shared/legacy-db-config.toml-read.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,10 @@ const DEFAULT_SUPABASE_ENV = "development";
109109
* `.env.<env>`, then `.env` via `godotenv.Load`, which never overrides a value
110110
* already set. So the shell environment wins over the files, the `supabase/`
111111
* directory wins over the repo root, and earlier filenames win within a
112-
* directory. A malformed `.env` aborts (Go returns the parse error); the path is
113-
* named without leaking file contents (CWE-209-safe).
112+
* directory. A malformed `.env` — or one that exists but cannot be read —
113+
* aborts: Go's `loadEnvIfExists` swallows only `os.ErrNotExist` and returns
114+
* every other error. The path is named without leaking file contents
115+
* (CWE-209-safe).
114116
*/
115117
const loadProjectEnv = Effect.fnUntraced(function* (
116118
fs: FileSystem.FileSystem,
@@ -126,9 +128,21 @@ const loadProjectEnv = Effect.fnUntraced(function* (
126128
const loaded: Record<string, string> = {};
127129
for (const dir of dirs) {
128130
for (const name of filenames) {
129-
const content = yield* fs
130-
.readFileString(path.join(dir, name))
131-
.pipe(Effect.map(Option.some<string>), Effect.orElseSucceed(Option.none<string>));
131+
// Go's loadEnvIfExists ignores only os.ErrNotExist; any other read error
132+
// aborts rather than silently skipping the file (which would hide a broken
133+
// env-backed config). Effect surfaces "not found" as a NotFound PlatformError.
134+
const content = yield* fs.readFileString(path.join(dir, name)).pipe(
135+
Effect.map(Option.some<string>),
136+
Effect.catchTag("PlatformError", (error) =>
137+
error.reason._tag === "NotFound"
138+
? Effect.succeed(Option.none<string>())
139+
: Effect.fail(
140+
new LegacyDbConfigLoadError({
141+
message: `failed to read environment file: ${name}`,
142+
}),
143+
),
144+
),
145+
);
132146
if (Option.isNone(content)) continue;
133147
let parsed: Record<string, string>;
134148
try {

apps/cli/src/legacy/shared/legacy-db-config.toml-read.unit.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,26 @@ describe("legacyReadDbToml", () => {
266266
);
267267
});
268268

269+
it.effect("fails when a project .env file exists but cannot be read", () => {
270+
// Go's loadEnvIfExists swallows only os.ErrNotExist; any other read error
271+
// aborts rather than hiding a broken env-backed config. A directory at the
272+
// .env path yields a non-NotFound read error.
273+
const dir = withConfig(["[db]", "port = 5000", ""].join("\n"));
274+
mkdirSync(join(dir, "supabase", ".env"), { recursive: true });
275+
return read(dir).pipe(
276+
Effect.exit,
277+
Effect.tap((exit) =>
278+
Effect.sync(() => {
279+
expect(Exit.isFailure(exit)).toBe(true);
280+
if (Exit.isFailure(exit)) {
281+
expect(JSON.stringify(exit.cause)).toContain("failed to read environment file");
282+
}
283+
rmSync(dir, { recursive: true, force: true });
284+
}),
285+
),
286+
);
287+
});
288+
269289
it.effect("ignores a [db.pooler] connection_string in config.toml (Go reads .temp only)", () => {
270290
// The Go config field is tagged `toml:"-"`, so a connection_string in config.toml
271291
// is never honored; only supabase/.temp/pooler-url counts.

0 commit comments

Comments
 (0)