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 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..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 @@ -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 { @@ -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,36 @@ 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.isFailure(exit)).toBe(true); + 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) } })), + ), + ); + + 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.isFailure(exit)).toBe(true); + 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) } })), + ), + ); + it.effect("falls back to the filesystem when keyring throws", () => { throwOnGetPasswordAccounts.add("Supabase CLI/supabase"); throwOnGetPasswordAccounts.add("Supabase CLI/access-token"); @@ -338,6 +377,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) |