Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
20 changes: 20 additions & 0 deletions apps/cli/src/commands/secrets/secrets.errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,26 @@ export class InvalidSecretPairError extends Data.TaggedError("InvalidSecretPairE

export class SecretsNoArgumentsError extends Data.TaggedError("SecretsNoArgumentsError")<{
readonly message: string;
readonly suggestion?: string;
}> {
get [ErrorActionabilityId](): CliErrorActionabilityDeclaration {
return actionability.provideFlags;
}
}

export class SecretsUnsetInputError extends Data.TaggedError("SecretsUnsetInputError")<{
readonly message: string;
}> {
get [ErrorActionabilityId](): CliErrorActionabilityDeclaration {
return actionability.invalidInput;
}
}

export class SecretsUnsetConfirmationRequiredError extends Data.TaggedError(
"SecretsUnsetConfirmationRequiredError",
)<{
readonly message: string;
readonly suggestion: string;
}> {
get [ErrorActionabilityId](): CliErrorActionabilityDeclaration {
return actionability.provideFlags;
Expand Down
78 changes: 49 additions & 29 deletions apps/cli/src/commands/secrets/unset/SIDE_EFFECTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,43 +12,49 @@

## Files Written

| Path | Format | When |
| ------------------------------------------------ | ------ | ------------------------------------------------------------ |
| `~/.supabase/<workdir-hash>/linked-project.json` | JSON | always (in `Effect.ensuring`) after `--project-ref` resolves |
| `~/.supabase/telemetry.json` | JSON | always (in `Effect.ensuring`) at end of command |
| Path | Format | When |
| ---------------------------------------------- | ------ | -------------------------------------------------------------------------------------------------- |
| `<workdir>/supabase/.temp/linked-project.json` | JSON | in `Effect.ensuring` after the ref resolves, on success and failure, only when the file is missing |
| `~/.supabase/telemetry.json` | JSON | always (in `Effect.ensuring`) at end of command |

## API Routes

| Method | Path | Auth | Request body | Response (used fields) |
| -------- | ---------------------------- | ------------ | --------------- | ------------------------------------------------------------- |
| `GET` | `/v1/projects/{ref}/secrets` | Bearer token | none | `[{name}]` — empty-args path only, filters `SUPABASE_` prefix |
| `DELETE` | `/v1/projects/{ref}/secrets` | Bearer token | `["NAME", ...]` | none (200 expected) |
| `GET` | `/v1/projects` | Bearer token | none | TTY-prompt fallback only |
| Method | Path | Auth | Request body | Response (used fields) |
| -------- | ---------------------------- | ------------ | --------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `GET` | `/v1/projects/{ref}/secrets` | Bearer token | none | `[{name}]` — `--all` path only, filters `SUPABASE_` prefix |
| `DELETE` | `/v1/projects/{ref}/secrets` | Bearer token | `["NAME", ...]` | none (200 expected; not called with `--dry-run`) |
| `GET` | `/v1/projects` | Bearer token | none | TTY-prompt fallback only |
| `GET` | `/v1/projects/{ref}` | Bearer token | none | linked-project cache fill in `Effect.ensuring`, on success and failure, only when `linked-project.json` is missing; best-effort |

## Environment Variables

| Variable | Purpose | Required? |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| `SUPABASE_ACCESS_TOKEN` | auth token (bypasses credential file/keyring lookup) | no (falls back to keyring → `~/.supabase/access-token`) |
| `SUPABASE_PROFILE` | selects API base URL: `supabase` → `api.supabase.com`, `supabase-staging` → `api.supabase.green`, `supabase-local` → `http://localhost:8080`. May alternatively be a filesystem path to a YAML profile with at least `api_url:` and optional `name:` (used by the cli-e2e test harness). | no (defaults to `supabase`) |
| `SUPABASE_YES` | confirms without a prompt, same as `--yes` | no |
| `SUPABASE_PROJECT_ID` | project ref fallback when `--project-ref` is unset | no (also reads `<workdir>/supabase/.temp/project-ref` then prompts on TTY) |
| `SUPABASE_WORKDIR` | base directory for the `.temp/project-ref` lookup | no (walks up from CWD looking for `supabase/config.toml` or `supabase/config.json`) |

## Exit Codes

| Code | Condition |
| ---- | --------------------------------------------------------------------------------------- |
| `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` | `AccessTokenRequiredError` — no token in env/keyring/file |
| `1` | `InvalidAccessTokenError` — token violates `^sbp_(oauth_\|v0_)?[a-f0-9]{40}$` |
| `1` | `ProjectRefNotLinkedError` — `--project-ref` unset, env/file empty, and stdin not a TTY |
| `1` | `InvalidProjectRefError` — resolved ref violates `^[a-z]{20}$` |
| `1` | `SecretsListUnexpectedStatusError` — non-2xx response from GET (empty-args path) |
| `1` | `SecretsListNetworkError` — GET transport failure (empty-args path) |
| `1` | `SecretsUnsetCancelledError` — user declined the confirmation prompt |
| `1` | `SecretsUnsetUnexpectedStatusError` — non-2xx response from DELETE |
| `1` | `SecretsUnsetNetworkError` — DELETE transport failure |
| Code | Condition |
| ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `0` | success — secrets unset from the linked project |
| `0` | `--dry-run` — the secrets that would be unset were printed, no DELETE call |
| `0` | `--all` resolved to zero non-`SUPABASE_` secrets (no DELETE call; text writes the stderr no-op, machine formats an empty result) |
| `1` | `SecretsNoArgumentsError` — no names and no `--all`; fails before the project ref resolves and before any API call |
| `1` | `SecretsUnsetInputError` — names given together with `--all`; fails before the project ref resolves and before any API call |
| `1` | `AccessTokenRequiredError` — no token in env/keyring/file |
| `1` | `InvalidAccessTokenError` — token violates `^sbp_(oauth_\|v0_)?[a-f0-9]{40}$` |
| `1` | `ProjectRefNotLinkedError` — `--project-ref` unset, env/file empty, and stdin not a TTY |
| `1` | `InvalidProjectRefError` — resolved ref violates `^[a-z]{20}$` |
| `1` | `SecretsUnsetConfirmationRequiredError` — no `--yes`/`SUPABASE_YES` and stdin is not a TTY or output is not interactive text; no secrets request (the linked-project cache fill may still run) |
| `1` | `SecretsListUnexpectedStatusError` — non-2xx response from GET (`--all` path) |
| `1` | `SecretsListNetworkError` — GET transport failure (`--all` path) |
| `1` | `SecretsUnsetCancelledError` — user declined the confirmation prompt |
| `1` | `SecretsUnsetUnexpectedStatusError` — non-2xx response from DELETE |
| `1` | `SecretsUnsetNetworkError` — DELETE transport failure |

## Telemetry Events Fired

Expand All @@ -60,21 +66,35 @@

### `--output pretty` (default) / `--output-format text`

Stdout: `Finished supabase secrets unset.\n`. Stderr: confirmation prompt label (when TTY without `--yes`) or `[Y/n] y` echo (with `--yes`) or the no-op message when empty-args resolves to no secrets.
Stdout: `Finished supabase secrets unset.\n`. Stderr: the confirmation prompt (`Do you want to unset these function secrets from project <ref>?`, the bulleted names, then `[y/N]`; default No) on an interactive TTY without `--yes`, the same label with `[y/N] y` echoed with `--yes`, or the no-op message when `--all` resolves to no secrets.

The `--output {json,yaml,toml,env}` flags all collapse to the same text-mode `Finished` message.
`--dry-run` stdout, with no `Finished` line and no prompt:

```
Would unset these function secrets from project <ref>:
• NAME_A
• NAME_B
```

The `--output {json,yaml,toml,env}` flags all collapse to the same text-mode output.

### `--output-format json`

Single JSON object emitted via `Output.success` with `{project_ref, count}` as the `data` field.
Single JSON object emitted via `Output.success` with `{project_ref, count, names, dry_run: false}` as the `data` field.

With `--dry-run`, the message is `Dry run: no secrets were unset.` and `data` is `{project_ref, count, names, dry_run: true}`.

When `--all` resolves to no secrets, the message is `You have not set any function secrets, nothing to do.` and `data` is `{project_ref, count: 0, names: [], dry_run}`.

### `--output-format stream-json`

One `result` NDJSON event on success containing `{project_ref, count}`.
One `result` NDJSON event on success containing `{project_ref, count, names, dry_run}`, with the same `--dry-run` and empty-`--all` messages as `json`.

## Notes

- When called without arguments, fetches the full secret list and unsets all entries that do not have a `SUPABASE_` prefix.
- `--yes` bypasses the confirmation prompt with a stderr label echo.
- Non-TTY without `--yes` auto-confirms silently, after a 100ms non-TTY read timeout.
- Names and `--all` are mutually exclusive; with neither, the command fails. `--all` unsets every secret that does not have a `SUPABASE_` prefix.
- Argument validation runs before the project ref resolves, so a usage error never opens the project picker and makes no API call.
- The prompt defaults to No and names the project ref. `--yes` (or `SUPABASE_YES`) skips the prompt with a stderr label echo.
- Without `--yes`, a run whose stdin is not a TTY, or whose output is not interactive text (`json`, `stream-json`), fails before any secrets request. Piped stdin answers are not read.
- `--dry-run` never prompts, never calls DELETE, and works without `--yes` in every output mode. With `--all` it still calls GET to list the secrets.
- Sends `User-Agent: SupabaseCLI/<version>` and Bearer auth. No `X-Supabase-Command` headers.
20 changes: 19 additions & 1 deletion apps/cli/src/commands/secrets/unset/unset.command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,16 @@ const config = {
Argument.withDescription("Secret names to unset."),
Argument.variadic(),
),
all: Flag.Boolean("all").pipe(
Flag.withDescription(
"Unset every secret except SUPABASE_ ones. Required when no names are given.",
),
Flag.withDefault(false),
),
dryRun: Flag.Boolean("dry-run").pipe(
Flag.withDescription("Print the secrets that would be unset without unsetting them."),
Flag.withDefault(false),
),
} as const;

export type SecretsUnsetFlags = CliCommand.Command.Config.Infer<typeof config>;
Expand All @@ -33,10 +43,18 @@ export const secretsUnsetCommand = Command.make("unset", config).pipe(
command: "supabase secrets unset MY_SECRET OTHER_SECRET",
description: "Unset multiple secrets",
},
{
command: "supabase secrets unset --all",
description: "Unset every custom secret after confirming",
},
{
command: "supabase secrets unset --all --dry-run",
description: "Preview which secrets --all would unset",
},
]),
Command.withHandler((flags) =>
secretsUnset(flags).pipe(withCommandTelemetry({ flags }), withJsonErrorHandling),
),
// `stdinLayer`: the confirmation prompt reads piped stdin via `promptYesNo` on a non-TTY stdin.
// `promptYesNo` requires `Stdin` even though the handler fails before its non-TTY branch.
Command.provide(Layer.mergeAll(managementApiRuntimeLayer(["secrets", "unset"]), stdinLayer)),
);
Loading
Loading