From 7c53f24eee3ede74f7a0dfe7a382421633e95c37 Mon Sep 17 00:00:00 2001 From: 7ttp <117663341+7ttp@users.noreply.github.com> Date: Thu, 27 Aug 2026 14:19:26 +0530 Subject: [PATCH 1/4] fix(cli): accept sbp_v0 access tokens --- .../src/legacy/auth/legacy-access-token.ts | 2 +- .../legacy-credentials.layer.unit.test.ts | 43 +++++++++++++++++++ .../commands/backups/list/SIDE_EFFECTS.md | 2 +- .../commands/backups/restore/SIDE_EFFECTS.md | 2 +- .../commands/secrets/list/SIDE_EFFECTS.md | 2 +- .../commands/secrets/set/SIDE_EFFECTS.md | 2 +- .../commands/secrets/unset/SIDE_EFFECTS.md | 2 +- 7 files changed, 49 insertions(+), 6 deletions(-) diff --git a/apps/cli/src/legacy/auth/legacy-access-token.ts b/apps/cli/src/legacy/auth/legacy-access-token.ts index 88b924af68..172b37d94d 100644 --- a/apps/cli/src/legacy/auth/legacy-access-token.ts +++ b/apps/cli/src/legacy/auth/legacy-access-token.ts @@ -3,7 +3,7 @@ import { Effect } from "effect"; import { legacyAqua } from "../shared/legacy-colors.ts"; import { LegacyInvalidAccessTokenError } from "./legacy-errors.ts"; -export const LEGACY_ACCESS_TOKEN_PATTERN = /^sbp_(oauth_)?[a-f0-9]{40}$/; +export const LEGACY_ACCESS_TOKEN_PATTERN = /^sbp_(oauth_|v0_)?[a-f0-9]{40}$/; /** * Message shown when no access token is available, passing `supabase login` diff --git a/apps/cli/src/legacy/auth/legacy-credentials.layer.unit.test.ts b/apps/cli/src/legacy/auth/legacy-credentials.layer.unit.test.ts index 533e30cca5..ffdd2aeab3 100644 --- a/apps/cli/src/legacy/auth/legacy-credentials.layer.unit.test.ts +++ b/apps/cli/src/legacy/auth/legacy-credentials.layer.unit.test.ts @@ -164,6 +164,7 @@ afterEach(() => { const VALID_TOKEN = "sbp_" + "a".repeat(40); const VALID_OAUTH_TOKEN = "sbp_oauth_" + "b".repeat(40); +const VALID_V0_TOKEN = "sbp_v0_" + "c".repeat(40); const encodeGoKeyringBase64 = (token: string) => `go-keyring-base64:${Buffer.from(token).toString("base64")}`; const goWindowsKey = (account: string) => `Supabase CLI:${account}/Supabase CLI/${account}`; @@ -197,6 +198,14 @@ describe("legacyCredentialsLayer.getAccessToken", () => { }).pipe(Effect.provide(makeLayer({ env: { SUPABASE_ACCESS_TOKEN: VALID_TOKEN } }))); }); + it.effect("returns a versioned-format (sbp_v0_) env token", () => + Effect.gen(function* () { + const { getAccessToken } = yield* LegacyCredentials; + const token = yield* getAccessToken; + expectSomeToken(token, VALID_V0_TOKEN); + }).pipe(Effect.provide(makeLayer({ env: { SUPABASE_ACCESS_TOKEN: VALID_V0_TOKEN } }))), + ); + it.effect("uses the keyring profile account when env is unset", () => { passwords.set("Supabase CLI/supabase", VALID_TOKEN); return Effect.gen(function* () { @@ -304,6 +313,32 @@ describe("legacyCredentialsLayer.getAccessToken", () => { }).pipe(Effect.provide(makeLayer())); }); + it.effect("rejects an unknown version prefix (sbp_v1_)", () => + Effect.gen(function* () { + const { getAccessToken } = yield* LegacyCredentials; + const exit = yield* Effect.exit(getAccessToken); + expect(exit._tag).toBe("Failure"); + if (exit._tag === "Failure") { + expect(JSON.stringify(exit.cause)).toContain("LegacyInvalidAccessTokenError"); + } + }).pipe( + Effect.provide(makeLayer({ env: { SUPABASE_ACCESS_TOKEN: "sbp_v1_" + "c".repeat(40) } })), + ), + ); + + it.effect("rejects a versioned-format (sbp_v0_) token with a truncated payload", () => + Effect.gen(function* () { + const { getAccessToken } = yield* LegacyCredentials; + const exit = yield* Effect.exit(getAccessToken); + expect(exit._tag).toBe("Failure"); + if (exit._tag === "Failure") { + expect(JSON.stringify(exit.cause)).toContain("LegacyInvalidAccessTokenError"); + } + }).pipe( + Effect.provide(makeLayer({ env: { SUPABASE_ACCESS_TOKEN: "sbp_v0_" + "c".repeat(39) } })), + ), + ); + it.effect("falls back to the filesystem when keyring throws", () => { throwOnGetPasswordAccounts.add("Supabase CLI/supabase"); throwOnGetPasswordAccounts.add("Supabase CLI/access-token"); @@ -338,6 +373,14 @@ describe("legacyCredentialsLayer.saveAccessToken", () => { }).pipe(Effect.provide(makeLayer())), ); + it.effect("saves a versioned-format (sbp_v0_) token", () => + Effect.gen(function* () { + const { saveAccessToken } = yield* LegacyCredentials; + yield* saveAccessToken(VALID_V0_TOKEN); + expect(passwords.get("Supabase CLI/supabase")).toBe(VALID_V0_TOKEN); + }).pipe(Effect.provide(makeLayer())), + ); + it.effect("writes Windows credentials where Go keyring reads them", () => Effect.gen(function* () { const { saveAccessToken } = yield* LegacyCredentials; diff --git a/apps/cli/src/legacy/commands/backups/list/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/backups/list/SIDE_EFFECTS.md index 3940a065e2..74f3d89b47 100644 --- a/apps/cli/src/legacy/commands/backups/list/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/backups/list/SIDE_EFFECTS.md @@ -38,7 +38,7 @@ | ---- | ------------------------------------------------------------------------------------------ | | `0` | success — backup list printed to stdout | | `1` | `LegacyPlatformAuthRequiredError` — no token in env/keyring/file | -| `1` | `LegacyInvalidAccessTokenError` — token violates `^sbp_(oauth_)?[a-f0-9]{40}$` | +| `1` | `LegacyInvalidAccessTokenError` — token violates `^sbp_(oauth_\|v0_)?[a-f0-9]{40}$` | | `1` | `LegacyProjectNotLinkedError` — `--project-ref` unset, env/file empty, and stdin not a TTY | | `1` | `LegacyInvalidProjectRefError` — resolved ref violates `^[a-z]{20}$` | | `1` | `LegacyBackupListUnexpectedStatusError` — non-2xx response from the backups endpoint | diff --git a/apps/cli/src/legacy/commands/backups/restore/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/backups/restore/SIDE_EFFECTS.md index 6d069d3e10..6060883272 100644 --- a/apps/cli/src/legacy/commands/backups/restore/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/backups/restore/SIDE_EFFECTS.md @@ -38,7 +38,7 @@ | ---- | ------------------------------------------------------------------------------------------ | | `0` | success — restore initiated | | `1` | `LegacyPlatformAuthRequiredError` — no token in env/keyring/file | -| `1` | `LegacyInvalidAccessTokenError` — token violates `^sbp_(oauth_)?[a-f0-9]{40}$` | +| `1` | `LegacyInvalidAccessTokenError` — token violates `^sbp_(oauth_\|v0_)?[a-f0-9]{40}$` | | `1` | `LegacyProjectNotLinkedError` — `--project-ref` unset, env/file empty, and stdin not a TTY | | `1` | `LegacyInvalidProjectRefError` — resolved ref violates `^[a-z]{20}$` | | `1` | `LegacyBackupRestoreUnexpectedStatusError` — non-201 response from the restore endpoint | diff --git a/apps/cli/src/legacy/commands/secrets/list/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/secrets/list/SIDE_EFFECTS.md index eb75d043ab..fa3ca80cd4 100644 --- a/apps/cli/src/legacy/commands/secrets/list/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/secrets/list/SIDE_EFFECTS.md @@ -39,7 +39,7 @@ | ---- | ------------------------------------------------------------------------------------------ | | `0` | success — secrets printed to stdout | | `1` | `LegacyPlatformAuthRequiredError` — no token in env/keyring/file | -| `1` | `LegacyInvalidAccessTokenError` — token violates `^sbp_(oauth_)?[a-f0-9]{40}$` | +| `1` | `LegacyInvalidAccessTokenError` — token violates `^sbp_(oauth_\|v0_)?[a-f0-9]{40}$` | | `1` | `LegacyProjectNotLinkedError` — `--project-ref` unset, env/file empty, and stdin not a TTY | | `1` | `LegacyInvalidProjectRefError` — resolved ref violates `^[a-z]{20}$` | | `1` | `LegacySecretsListUnexpectedStatusError` — non-2xx response from the secrets endpoint | diff --git a/apps/cli/src/legacy/commands/secrets/set/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/secrets/set/SIDE_EFFECTS.md index 9502785b68..adc4fdaa6c 100644 --- a/apps/cli/src/legacy/commands/secrets/set/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/secrets/set/SIDE_EFFECTS.md @@ -44,7 +44,7 @@ | ---- | -------------------------------------------------------------------------------------------- | | `0` | success — secrets set on the linked project | | `1` | `LegacyPlatformAuthRequiredError` — no token in env/keyring/file | -| `1` | `LegacyInvalidAccessTokenError` — token violates `^sbp_(oauth_)?[a-f0-9]{40}$` | +| `1` | `LegacyInvalidAccessTokenError` — token violates `^sbp_(oauth_\|v0_)?[a-f0-9]{40}$` | | `1` | `LegacyProjectNotLinkedError` — `--project-ref` unset, env/file empty, and stdin not a TTY | | `1` | `LegacyInvalidProjectRefError` — resolved ref violates `^[a-z]{20}$` | | `1` | `LegacySecretsNoArgumentsError` — no positional pairs and no entries from env-file or config | diff --git a/apps/cli/src/legacy/commands/secrets/unset/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/secrets/unset/SIDE_EFFECTS.md index 642941d0a9..525d66b315 100644 --- a/apps/cli/src/legacy/commands/secrets/unset/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/secrets/unset/SIDE_EFFECTS.md @@ -41,7 +41,7 @@ | `0` | success — secrets unset from the linked project | | `0` | empty-args path resolved to zero non-`SUPABASE_` secrets (stderr no-op, no DELETE call) | | `1` | `LegacyPlatformAuthRequiredError` — no token in env/keyring/file | -| `1` | `LegacyInvalidAccessTokenError` — token violates `^sbp_(oauth_)?[a-f0-9]{40}$` | +| `1` | `LegacyInvalidAccessTokenError` — token violates `^sbp_(oauth_\|v0_)?[a-f0-9]{40}$` | | `1` | `LegacyProjectNotLinkedError` — `--project-ref` unset, env/file empty, and stdin not a TTY | | `1` | `LegacyInvalidProjectRefError` — resolved ref violates `^[a-z]{20}$` | | `1` | `LegacySecretsListUnexpectedStatusError` — non-2xx response from GET (empty-args path) | From 4eb482c36de26b2321a3f19921b1bf8f0b554b4e Mon Sep 17 00:00:00 2001 From: 7ttp <117663341+7ttp@users.noreply.github.com> Date: Thu, 27 Aug 2026 16:44:33 +0530 Subject: [PATCH 2/4] test: use exit helpers in negative token cases --- .../legacy/auth/legacy-credentials.layer.unit.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/cli/src/legacy/auth/legacy-credentials.layer.unit.test.ts b/apps/cli/src/legacy/auth/legacy-credentials.layer.unit.test.ts index ffdd2aeab3..b530fbeaa5 100644 --- a/apps/cli/src/legacy/auth/legacy-credentials.layer.unit.test.ts +++ b/apps/cli/src/legacy/auth/legacy-credentials.layer.unit.test.ts @@ -12,7 +12,7 @@ import { join } from "node:path"; import { describe, expect, it } from "@effect/vitest"; import { BunServices } from "@effect/platform-bun"; -import { Effect, FileSystem, Layer, Option, PlatformError, Redacted } from "effect"; +import { Effect, Exit, FileSystem, Layer, Option, PlatformError, Redacted } from "effect"; import { afterEach, beforeEach, vi } from "vitest"; import { @@ -317,8 +317,8 @@ describe("legacyCredentialsLayer.getAccessToken", () => { Effect.gen(function* () { const { getAccessToken } = yield* LegacyCredentials; const exit = yield* Effect.exit(getAccessToken); - expect(exit._tag).toBe("Failure"); - if (exit._tag === "Failure") { + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { expect(JSON.stringify(exit.cause)).toContain("LegacyInvalidAccessTokenError"); } }).pipe( @@ -330,8 +330,8 @@ describe("legacyCredentialsLayer.getAccessToken", () => { Effect.gen(function* () { const { getAccessToken } = yield* LegacyCredentials; const exit = yield* Effect.exit(getAccessToken); - expect(exit._tag).toBe("Failure"); - if (exit._tag === "Failure") { + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { expect(JSON.stringify(exit.cause)).toContain("LegacyInvalidAccessTokenError"); } }).pipe( From 0dda2a90fd9300d22191ea331445aaaf2768c1e7 Mon Sep 17 00:00:00 2001 From: 7ttp <117663341+7ttp@users.noreply.github.com> Date: Thu, 27 Aug 2026 16:46:46 +0530 Subject: [PATCH 3/4] fix: accept sbp_v0 tokens in go sidecar --- apps/cli-go/internal/utils/access_token.go | 2 +- apps/cli-go/internal/utils/access_token_test.go | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/apps/cli-go/internal/utils/access_token.go b/apps/cli-go/internal/utils/access_token.go index fb6ddc4af7..79489b564e 100644 --- a/apps/cli-go/internal/utils/access_token.go +++ b/apps/cli-go/internal/utils/access_token.go @@ -13,7 +13,7 @@ import ( ) var ( - AccessTokenPattern = regexp.MustCompile(`^sbp_(oauth_)?[a-f0-9]{40}$`) + AccessTokenPattern = regexp.MustCompile(`^sbp_(oauth_|v0_)?[a-f0-9]{40}$`) ErrInvalidToken = errors.New("Invalid access token format. Must be like `sbp_0102...1920`.") ErrMissingToken = errors.Errorf("Access token not provided. Supply an access token by running %s or setting the SUPABASE_ACCESS_TOKEN environment variable.", Aqua("supabase login")) ErrNotLoggedIn = errors.New("You were not logged in, nothing to do.") diff --git a/apps/cli-go/internal/utils/access_token_test.go b/apps/cli-go/internal/utils/access_token_test.go index c829113fea..602dec1f6a 100644 --- a/apps/cli-go/internal/utils/access_token_test.go +++ b/apps/cli-go/internal/utils/access_token_test.go @@ -29,6 +29,23 @@ func TestLoadToken(t *testing.T) { assert.Equal(t, token, loaded) }) + t.Run("loads v0 token from env var", func(t *testing.T) { + v0Token := "sbp_v0_" + token[len("sbp_"):] + t.Setenv("SUPABASE_ACCESS_TOKEN", v0Token) + fsys := afero.NewMemMapFs() + loaded, err := LoadAccessTokenFS(fsys) + assert.NoError(t, err) + assert.Equal(t, v0Token, loaded) + }) + + t.Run("throws error on unknown version prefix", func(t *testing.T) { + t.Setenv("SUPABASE_ACCESS_TOKEN", "sbp_v1_"+token[len("sbp_"):]) + fsys := afero.NewMemMapFs() + loaded, err := LoadAccessTokenFS(fsys) + assert.ErrorIs(t, err, ErrInvalidToken) + assert.Empty(t, loaded) + }) + t.Run("throws error on invalid token", func(t *testing.T) { t.Setenv("SUPABASE_ACCESS_TOKEN", "invalid") // Setup in-memory fs From 34fa973977091cf3c5b330b1874279fef748bb86 Mon Sep 17 00:00:00 2001 From: 7ttp <117663341+7ttp@users.noreply.github.com> Date: Fri, 28 Aug 2026 22:15:09 +0530 Subject: [PATCH 4/4] Nit --- .../auth/legacy-credentials.layer.unit.test.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/apps/cli/src/legacy/auth/legacy-credentials.layer.unit.test.ts b/apps/cli/src/legacy/auth/legacy-credentials.layer.unit.test.ts index b530fbeaa5..674f92ce4c 100644 --- a/apps/cli/src/legacy/auth/legacy-credentials.layer.unit.test.ts +++ b/apps/cli/src/legacy/auth/legacy-credentials.layer.unit.test.ts @@ -318,8 +318,10 @@ describe("legacyCredentialsLayer.getAccessToken", () => { const { getAccessToken } = yield* LegacyCredentials; const exit = yield* Effect.exit(getAccessToken); expect(Exit.isFailure(exit)).toBe(true); - if (Exit.isFailure(exit)) { - expect(JSON.stringify(exit.cause)).toContain("LegacyInvalidAccessTokenError"); + const errorOption = Exit.findErrorOption(exit); + expect(Option.isSome(errorOption)).toBe(true); + if (Option.isSome(errorOption)) { + expect(errorOption.value).toBeInstanceOf(LegacyInvalidAccessTokenError); } }).pipe( Effect.provide(makeLayer({ env: { SUPABASE_ACCESS_TOKEN: "sbp_v1_" + "c".repeat(40) } })), @@ -331,8 +333,10 @@ describe("legacyCredentialsLayer.getAccessToken", () => { const { getAccessToken } = yield* LegacyCredentials; const exit = yield* Effect.exit(getAccessToken); expect(Exit.isFailure(exit)).toBe(true); - if (Exit.isFailure(exit)) { - expect(JSON.stringify(exit.cause)).toContain("LegacyInvalidAccessTokenError"); + const errorOption = Exit.findErrorOption(exit); + expect(Option.isSome(errorOption)).toBe(true); + if (Option.isSome(errorOption)) { + expect(errorOption.value).toBeInstanceOf(LegacyInvalidAccessTokenError); } }).pipe( Effect.provide(makeLayer({ env: { SUPABASE_ACCESS_TOKEN: "sbp_v0_" + "c".repeat(39) } })),