Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
bb94ec7
fix(cli): use cobra mutual-exclusivity template in sso add (CLI-1982)
Coly010 Jul 28, 2026
820f0e7
fix(cli): reconcile sso flag values with pflag consumption semantics …
Coly010 Jul 29, 2026
887811b
Merge remote-tracking branch 'origin/develop' into columferry/cli-198…
Coly010 Jul 29, 2026
25df67c
fix(cli): emulate cobra arity and required-flag checks over pflag-eff…
Coly010 Jul 29, 2026
e2de912
fix(cli): close pflag scan gaps for consumed shorthands, missing valu…
Coly010 Jul 29, 2026
07b145b
fix(cli): reconcile enum and boolean flag values with pflag Set seman…
Coly010 Jul 29, 2026
7e21b4a
Merge remote-tracking branch 'origin/develop' into columferry/cli-198…
Coly010 Jul 30, 2026
6fb04b6
fix(cli): distinguish bare booleans from inline-empty values in sso p…
Coly010 Jul 30, 2026
050108e
fix(cli): emulate Go ChangeWorkDir for the pflag-effective workdir in…
Coly010 Jul 30, 2026
57e608c
fix(cli): emulate Go LoadProfile for the pflag-effective profile in s…
Coly010 Jul 30, 2026
22f27ac
fix(cli): decode, stitch, and route the reconciled-profile requests l…
Coly010 Jul 30, 2026
4b9e88f
fix(cli): resolve credentials for the pflag-reconciled profile (revie…
Coly010 Jul 30, 2026
8cc1bcc
fix(cli): pass reconciled-profile credentials to auxiliary requests (…
Coly010 Jul 30, 2026
600fe91
fix(cli): preserve pre-path --profile occurrences in the pflag scan (…
Coly010 Jul 31, 2026
c4d15a0
fix(cli): gate reconciled-profile requests on Go's missing-token abor…
Coly010 Jul 31, 2026
1bc9960
Merge remote-tracking branch 'origin/develop' into columferry/cli-198…
Coly010 Jul 31, 2026
fbc2bba
fix(cli): preserve pre-path --workdir occurrences in the pflag scan (…
Coly010 Jul 31, 2026
5e0a544
fix(cli): lowercase profile keys like viper before exact validation (…
Coly010 Jul 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions apps/cli/src/legacy/auth/legacy-access-token.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
import { Effect } from "effect";

import { legacyAqua } from "../shared/legacy-colors.ts";
import { LegacyInvalidAccessTokenError } from "./legacy-errors.ts";

/** Go's `utils.AccessTokenPattern` (`apps/cli-go/internal/utils/access_token.go:16`). */
export const LEGACY_ACCESS_TOKEN_PATTERN = /^sbp_(oauth_)?[a-f0-9]{40}$/;

/**
* Go's `utils.ErrMissingToken` message (`internal/utils/access_token.go:18`),
* with `supabase login` through the Aqua colour gate exactly like Go's
* `Aqua(...)`. Built lazily because the gate inspects the target stream at
* call time. Shared by `db advisors` and the sso reconciled-credentials gate.
*/
export const legacyMissingAccessTokenMessage = (): string =>
`Access token not provided. Supply an access token by running ${legacyAqua("supabase login")} or setting the SUPABASE_ACCESS_TOKEN environment variable.`;

/** Go's `utils.ErrInvalidToken` message (`internal/utils/access_token.go:17`). */
const LEGACY_INVALID_ACCESS_TOKEN_MESSAGE =
"Invalid access token format. Must be like `sbp_0102...1920`.";
Expand Down
138 changes: 109 additions & 29 deletions apps/cli/src/legacy/auth/legacy-credentials.layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import { Effect, FileSystem, Layer, Option, Path, Redacted, Result } from "effec

import { RuntimeInfo } from "../../shared/runtime/runtime-info.service.ts";
import { normalizeKeyringToken } from "../../shared/auth/keyring-token.ts";
import { LegacyDebugLogger } from "../shared/legacy-debug-logger.service.ts";
import {
LegacyDebugLogger,
type LegacyDebugLoggerShape,
} from "../shared/legacy-debug-logger.service.ts";
import { LegacyCliConfig } from "../config/legacy-cli-config.service.ts";
import { legacySupabaseHome } from "../config/legacy-profile-file.ts";
import { LEGACY_ACCESS_TOKEN_PATTERN, validateLegacyAccessToken } from "./legacy-access-token.ts";
Expand Down Expand Up @@ -345,59 +348,136 @@ const deleteAllKeyringEntries = (
}
});

const makeLegacyCredentials = Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const runtimeInfo = yield* RuntimeInfo;
const cliConfig = yield* LegacyCliConfig;
const debugLogger = yield* LegacyDebugLogger;
const profileAccount = cliConfig.profile;

// <SUPABASE_HOME or ~/.supabase>/access-token — fallback file path
const fallbackDir = legacySupabaseHome(runtimeInfo.homeDir);
const fallbackPath = path.join(fallbackDir, "access-token");

// `SUPABASE_NO_KEYRING=1` disables the OS keyring entirely (matches `next/`'s
// credentials layer and the cli-e2e harness, which sets it). Without this, any
// unconditional keyring access — e.g. `unlink`'s credential delete — blocks on a
// Keychain authorization prompt in non-interactive / CI contexts.
const noKeyring = process.env["SUPABASE_NO_KEYRING"] === "1";
const wsl = yield* detectWsl(fs);
const keyringModule =
wsl || noKeyring
// `SUPABASE_NO_KEYRING=1` disables the OS keyring entirely (matches `next/`'s
// credentials layer and the cli-e2e harness, which sets it). Without this, any
// unconditional keyring access — e.g. `unlink`'s credential delete — blocks on a
// Keychain authorization prompt in non-interactive / CI contexts.
const loadKeyringModule = (
fs: FileSystem.FileSystem,
): Effect.Effect<Option.Option<KeyringModule>> =>
Effect.gen(function* () {
const noKeyring = process.env["SUPABASE_NO_KEYRING"] === "1";
const wsl = yield* detectWsl(fs);
return wsl || noKeyring
? Option.none<KeyringModule>()
: yield* Effect.tryPromise(() => import("@napi-rs/keyring")).pipe(Effect.option);
});

const readKeyring = Effect.gen(function* () {
// Keyring chain for a given profile account: profile key first, then the
// legacy `access-token` key. The account parameter matters because Go keys
// the read on the RECONCILED `CurrentProfile.Name` (`access_token.go:43`).
const readKeyringForAccount = (
keyringModule: Option.Option<KeyringModule>,
profileAccount: string,
platform: RuntimePlatform,
debugLogger: LegacyDebugLoggerShape,
): Effect.Effect<Option.Option<string>> =>
Effect.gen(function* () {
if (Option.isNone(keyringModule)) return Option.none<string>();
const profileResult = yield* tryKeyringRead(
keyringModule.value,
profileAccount,
runtimeInfo.platform,
);
const profileResult = yield* tryKeyringRead(keyringModule.value, profileAccount, platform);
if (Option.isSome(profileResult)) {
yield* debugLogger.debug(`Using access token for profile: ${profileAccount}`);
return profileResult;
}
const legacyResult = yield* tryKeyringRead(
keyringModule.value,
LEGACY_KEYRING_ACCOUNT,
runtimeInfo.platform,
platform,
);
if (Option.isSome(legacyResult)) {
yield* debugLogger.debug("Using access token from credentials store...");
}
return legacyResult;
});

const readFile = Effect.gen(function* () {
const readFallbackFile = (
fs: FileSystem.FileSystem,
fallbackPath: string,
): Effect.Effect<Option.Option<string>> =>
Effect.gen(function* () {
const exists = yield* fs.exists(fallbackPath).pipe(Effect.orElseSucceed(() => false));
if (!exists) return Option.none<string>();
const content = yield* fs.readFileString(fallbackPath).pipe(Effect.orElseSucceed(() => ""));
const trimmed = content.trim();
return trimmed.length === 0 ? Option.none<string>() : Option.some(trimmed);
});

/**
* Token resolution for an explicit profile account, mirroring the service's
* `getAccessToken` chain exactly: env token → keyring (profile account, then
* legacy account) → fallback file. Go resolves credentials AFTER
* `LoadProfile`, so the keyring account is the reconciled
* `CurrentProfile.Name` (`access_token.go:43`) — but the `LegacyCredentials`
* service captures the config layer's profile at construction. Commands that
* reconcile a pflag-effective profile (sso add/update, PR #5974 round 9)
* resolve their token through this instead, keyed on the reconciled name.
* Fails with the same validation error as the service; callers absorb it the
* same way `resolveLegacyAccessToken` does.
*/
export const legacyAccessTokenForProfile = Effect.fnUntraced(function* (profileAccount: string) {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const runtimeInfo = yield* RuntimeInfo;
const cliConfig = yield* LegacyCliConfig;
// `serviceOption` keeps the logger optional (no-op outside the real CLI
// tree), same as the sso pflag-reconcile module's optional services.
const debugLogger: LegacyDebugLoggerShape = Option.getOrElse(
yield* Effect.serviceOption(LegacyDebugLogger),
() => ({ debug: () => Effect.void, http: () => Effect.void }),
);

if (Option.isSome(cliConfig.accessToken)) {
yield* debugLogger.debug("Using access token from env var...");
yield* validateLegacyAccessToken(Redacted.value(cliConfig.accessToken.value));
return Option.some(cliConfig.accessToken.value);
}

const keyringModule = yield* loadKeyringModule(fs);
const keyringValue = yield* readKeyringForAccount(
keyringModule,
profileAccount,
runtimeInfo.platform,
debugLogger,
);
if (Option.isSome(keyringValue)) {
yield* validateLegacyAccessToken(keyringValue.value);
return Option.some(Redacted.make(keyringValue.value));
}

const fallbackPath = path.join(legacySupabaseHome(runtimeInfo.homeDir), "access-token");
const fileValue = yield* readFallbackFile(fs, fallbackPath);
if (Option.isSome(fileValue)) {
yield* debugLogger.debug(`Using access token from file: ${fallbackPath}`);
yield* validateLegacyAccessToken(fileValue.value);
return Option.some(Redacted.make(fileValue.value));
}

return Option.none<Redacted.Redacted<string>>();
});

const makeLegacyCredentials = Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const runtimeInfo = yield* RuntimeInfo;
const cliConfig = yield* LegacyCliConfig;
const debugLogger = yield* LegacyDebugLogger;
const profileAccount = cliConfig.profile;

// <SUPABASE_HOME or ~/.supabase>/access-token — fallback file path
const fallbackDir = legacySupabaseHome(runtimeInfo.homeDir);
const fallbackPath = path.join(fallbackDir, "access-token");

const keyringModule = yield* loadKeyringModule(fs);

const readKeyring = readKeyringForAccount(
keyringModule,
profileAccount,
runtimeInfo.platform,
debugLogger,
);

const readFile = readFallbackFile(fs, fallbackPath);

return LegacyCredentials.of({
getAccessToken: Effect.gen(function* () {
// Env takes precedence (matches access_token.go:38).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { ProcessControl } from "../../../../shared/runtime/process-control.servi
import { LegacyCredentials } from "../../../auth/legacy-credentials.service.ts";
import { LegacyProjectRefResolver } from "../../../config/legacy-project-ref.service.ts";
import { legacyAqua } from "../../../shared/legacy-colors.ts";
import { legacyMissingAccessTokenMessage } from "../../../auth/legacy-access-token.ts";
import { legacyFailsOn } from "../../../shared/legacy-fail-on.ts";
import { LegacyIdentityStitch } from "../../../shared/legacy-identity-stitch.ts";
import { LegacyDbConfigResolver } from "../../../shared/legacy-db-config.service.ts";
Expand Down Expand Up @@ -36,10 +37,6 @@ import {
import { legacyFetchPerformanceAdvisors, legacyFetchSecurityAdvisors } from "./advisors.linked.ts";
import { splitLegacyLintsSql } from "./advisors.lints-sql.ts";

/** Go's `utils.ErrMissingToken` (`internal/utils/access_token.go:18`). */
const missingTokenMessage = (): string =>
`Access token not provided. Supply an access token by running ${legacyAqua("supabase login")} or setting the SUPABASE_ACCESS_TOKEN environment variable.`;

/** Go's advisors PreRunE `utils.CmdSuggestion` (`cmd/db.go`). */
const loginSuggestion = (): string => `Run ${legacyAqua("supabase login")} first.`;

Expand Down Expand Up @@ -164,7 +161,7 @@ const runLinked = Effect.fnUntraced(function* (
if (Option.isNone(tokenOpt)) {
return yield* Effect.fail(
new LegacyDbAdvisorsNotLoggedInError({
message: missingTokenMessage(),
message: legacyMissingAccessTokenMessage(),
suggestion: loginSuggestion(),
}),
);
Expand Down
Loading
Loading