Skip to content

Commit bca0cc1

Browse files
committed
fix(cli): read linked DB password from project .env files
resolveLinked read SUPABASE_DB_PASSWORD only from process.env, but Go's loadNestedEnv populates the environment from supabase/.env* (and root .env*) before viper.GetString("DB_PASSWORD"), so a password defined only in a project .env was invisible and the linked path tried to mint a temp login role (requiring Management API auth) instead. Resolve the password from the shell env or the loaded project .env (legacyLoadProjectEnv, exported), with the shell value still winning via its no-override map.
1 parent df51f5e commit bca0cc1

3 files changed

Lines changed: 38 additions & 6 deletions

File tree

apps/cli/src/legacy/shared/legacy-db-config.layer.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import {
2828
redactLegacyConnectionString,
2929
} from "./legacy-db-config.parse.ts";
3030
import { LegacyDbConfigResolver, type LegacyDbConfigError } from "./legacy-db-config.service.ts";
31-
import { legacyReadDbToml } from "./legacy-db-config.toml-read.ts";
31+
import { legacyLoadProjectEnv, legacyReadDbToml } from "./legacy-db-config.toml-read.ts";
3232
import type { LegacyDbConfigFlags } from "./legacy-db-config.types.ts";
3333
import { LegacyDebugLogger } from "./legacy-debug-logger.service.ts";
3434
import { legacyGetHostname } from "./legacy-hostname.ts";
@@ -276,8 +276,13 @@ export const legacyDbConfigLayer = Layer.effect(
276276
): Effect.Effect<LegacyPgConnInput, LegacyDbConfigError, LegacyPlatformApi> =>
277277
Effect.gen(function* () {
278278
// Read lazily (per invocation) rather than at layer build, so tests and
279-
// env-substitution see the current value. Go: viper `DB_PASSWORD`.
280-
const dbPassword = process.env["SUPABASE_DB_PASSWORD"] ?? "";
279+
// env-substitution see the current value. Go reads viper `DB_PASSWORD`
280+
// after `loadNestedEnv` has populated the environment from the project
281+
// `.env*` files, so honor those too — `legacyLoadProjectEnv`'s map already
282+
// excludes keys present in the shell env, so the shell value still wins.
283+
const projectEnv = yield* legacyLoadProjectEnv(fs, path, cliConfig.workdir);
284+
const dbPassword =
285+
process.env["SUPABASE_DB_PASSWORD"] ?? projectEnv["SUPABASE_DB_PASSWORD"] ?? "";
281286
const host = `db.${ref}.${cliConfig.projectHost}`;
282287
const base: LegacyPgConnInput = {
283288
host,

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ const DEFAULT_SUPABASE_ENV = "development";
114114
* every other error. The path is named without leaking file contents
115115
* (CWE-209-safe).
116116
*/
117-
const loadProjectEnv = Effect.fnUntraced(function* (
117+
export const legacyLoadProjectEnv = Effect.fnUntraced(function* (
118118
fs: FileSystem.FileSystem,
119119
path: Path.Path,
120120
workdir: string,
@@ -225,7 +225,7 @@ export const legacyReadDbToml = Effect.fnUntraced(function* (
225225

226226
// Resolve `env(VAR)` against the shell env first, then the project `.env` files
227227
// (Go's `loadNestedEnv` populates the process env before `LoadEnvHook`).
228-
const projectEnv = yield* loadProjectEnv(fs, path, workdir);
228+
const projectEnv = yield* legacyLoadProjectEnv(fs, path, workdir);
229229
const lookup: EnvLookup = (name) => process.env[name] ?? projectEnv[name];
230230

231231
// Go's loader enables viper `SetEnvPrefix("SUPABASE")` + `EnvKeyReplacer(".",

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

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { BunServices } from "@effect/platform-bun";
55
import { describe, expect, it } from "@effect/vitest";
66
import { Effect, Exit, FileSystem, Option, Path } from "effect";
77

8-
import { legacyReadDbToml } from "./legacy-db-config.toml-read.ts";
8+
import { legacyLoadProjectEnv, legacyReadDbToml } from "./legacy-db-config.toml-read.ts";
99

1010
function withConfig(content: string | undefined, poolerUrl?: string) {
1111
const dir = mkdtempSync(join(tmpdir(), "legacy-db-toml-"));
@@ -27,6 +27,13 @@ const read = (workdir: string) =>
2727
return yield* legacyReadDbToml(fs, path, workdir);
2828
}).pipe(Effect.provide(BunServices.layer));
2929

30+
const loadEnv = (workdir: string) =>
31+
Effect.gen(function* () {
32+
const fs = yield* FileSystem.FileSystem;
33+
const path = yield* Path.Path;
34+
return yield* legacyLoadProjectEnv(fs, path, workdir);
35+
}).pipe(Effect.provide(BunServices.layer));
36+
3037
describe("legacyReadDbToml", () => {
3138
it.effect("returns defaults when config.toml is absent", () => {
3239
const dir = withConfig(undefined);
@@ -334,6 +341,26 @@ describe("legacyReadDbToml", () => {
334341
);
335342
});
336343

344+
it.effect(
345+
"legacyLoadProjectEnv surfaces SUPABASE_DB_PASSWORD from .env (linked-path source)",
346+
() => {
347+
// The --linked resolver reads SUPABASE_DB_PASSWORD via this map, so a value
348+
// defined only in supabase/.env must be visible (Go's loadNestedEnv parity).
349+
delete process.env["SUPABASE_DB_PASSWORD"];
350+
const dir = mkdtempSync(join(tmpdir(), "legacy-db-toml-"));
351+
mkdirSync(join(dir, "supabase"), { recursive: true });
352+
writeFileSync(join(dir, "supabase", ".env"), "SUPABASE_DB_PASSWORD=from-dotenv\n");
353+
return loadEnv(dir).pipe(
354+
Effect.tap((env) =>
355+
Effect.sync(() => {
356+
expect(env["SUPABASE_DB_PASSWORD"]).toBe("from-dotenv");
357+
rmSync(dir, { recursive: true, force: true });
358+
}),
359+
),
360+
);
361+
},
362+
);
363+
337364
it.effect("ignores a [db.pooler] connection_string in config.toml (Go reads .temp only)", () => {
338365
// The Go config field is tagged `toml:"-"`, so a connection_string in config.toml
339366
// is never honored; only supabase/.temp/pooler-url counts.

0 commit comments

Comments
 (0)