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
4 changes: 2 additions & 2 deletions apps/cli/docs/go-cli-porting-status.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,8 +255,8 @@ Legend:
| `network-restrictions update` | `wrapped` | [`../src/legacy/commands/network-restrictions/update/update.command.ts`](../src/legacy/commands/network-restrictions/update/update.command.ts) |
| `encryption get-root-key` | `wrapped` | [`../src/legacy/commands/encryption/get-root-key/get-root-key.command.ts`](../src/legacy/commands/encryption/get-root-key/get-root-key.command.ts) |
| `encryption update-root-key` | `wrapped` | [`../src/legacy/commands/encryption/update-root-key/update-root-key.command.ts`](../src/legacy/commands/encryption/update-root-key/update-root-key.command.ts) |
| `ssl-enforcement get` | `wrapped` | [`../src/legacy/commands/ssl-enforcement/get/get.command.ts`](../src/legacy/commands/ssl-enforcement/get/get.command.ts) |
| `ssl-enforcement update` | `wrapped` | [`../src/legacy/commands/ssl-enforcement/update/update.command.ts`](../src/legacy/commands/ssl-enforcement/update/update.command.ts) |
| `ssl-enforcement get` | `ported` | [`../src/legacy/commands/ssl-enforcement/get/get.command.ts`](../src/legacy/commands/ssl-enforcement/get/get.command.ts) |
| `ssl-enforcement update` | `ported` | [`../src/legacy/commands/ssl-enforcement/update/update.command.ts`](../src/legacy/commands/ssl-enforcement/update/update.command.ts) |
| `postgres-config get` | `wrapped` | [`../src/legacy/commands/postgres-config/get/get.command.ts`](../src/legacy/commands/postgres-config/get/get.command.ts) |
| `postgres-config update` | `wrapped` | [`../src/legacy/commands/postgres-config/update/update.command.ts`](../src/legacy/commands/postgres-config/update/update.command.ts) |
| `postgres-config delete` | `wrapped` | [`../src/legacy/commands/postgres-config/delete/delete.command.ts`](../src/legacy/commands/postgres-config/delete/delete.command.ts) |
Expand Down
70 changes: 1 addition & 69 deletions apps/cli/src/legacy/commands/backups/backups.errors.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import type { SupabaseApiError } from "@supabase/api/effect";
import { Data, Effect } from "effect";
import * as HttpClientError from "effect/unstable/http/HttpClientError";
import { Data } from "effect";

export class LegacyBackupListNetworkError extends Data.TaggedError("LegacyBackupListNetworkError")<{
readonly message: string;
Expand All @@ -27,69 +25,3 @@ export class LegacyBackupRestoreUnexpectedStatusError extends Data.TaggedError(
readonly body: string;
readonly message: string;
}> {}

// HttpClientError reasons that indicate the server returned an actual response (vs a transport
// failure). Anything in this set surfaces as an `UnexpectedStatusError`; everything else maps
// to a `NetworkError`.
const RESPONSE_ERROR_TAGS: ReadonlySet<HttpClientError.HttpClientErrorReason["_tag"]> = new Set([
"StatusCodeError",
"DecodeError",
"EmptyBodyError",
]);

// Caps the response body that gets embedded in error structures. The Management API is
// trusted, but capping prevents oversized error envelopes from flooding `--output-format json`
// and avoids forwarding arbitrary bytes verbatim if the trust boundary ever changes.
const MAX_BODY_LEN = 1024;

type NetworkErrorFactory<E> = new (args: { readonly message: string }) => E;

type StatusErrorFactory<E> = new (args: {
readonly status: number;
readonly body: string;
readonly message: string;
}) => E;

/**
* Build an error mapper that classifies a `SupabaseApiError` into either a typed network
* error or a typed unexpected-status error. Pulled out of the handlers so both commands
* share the dispatch logic, the body truncation, and the `RESPONSE_ERROR_TAGS` policy.
*
* `networkMessage` and `statusMessage` are templates: they build the human-readable error
* string with the same exact phrasing the handlers used before, so existing error-message
* assertions (and Go parity for status messages) continue to hold.
*/
export function mapLegacyBackupHttpError<N, S>(opts: {
readonly networkError: NetworkErrorFactory<N>;
readonly statusError: StatusErrorFactory<S>;
readonly networkMessage: (cause: string) => string;
readonly statusMessage: (status: number, body: string) => string;
}): (cause: SupabaseApiError) => Effect.Effect<never, N | S> {
return (cause) =>
Effect.gen(function* () {
if (HttpClientError.isHttpClientError(cause)) {
if (RESPONSE_ERROR_TAGS.has(cause.reason._tag) && cause.response !== undefined) {
const status = cause.response.status;
const rawBody = yield* cause.response.text.pipe(
Effect.orElseSucceed(() => cause.reason.description ?? ""),
);
const body = rawBody.length > MAX_BODY_LEN ? rawBody.slice(0, MAX_BODY_LEN) : rawBody;
return yield* Effect.fail(
new opts.statusError({
status,
body,
message: opts.statusMessage(status, body),
}),
);
}
const description = cause.reason.description ?? cause.reason._tag;
return yield* Effect.fail(
new opts.networkError({ message: opts.networkMessage(description) }),
);
}
// SchemaError or HttpBodyError — treat as transport-level network error.
return yield* Effect.fail(
new opts.networkError({ message: opts.networkMessage(String(cause)) }),
);
});
}
47 changes: 0 additions & 47 deletions apps/cli/src/legacy/commands/backups/backups.layers.ts

This file was deleted.

4 changes: 2 additions & 2 deletions apps/cli/src/legacy/commands/backups/list/list.command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Command, Flag } from "effect/unstable/cli";

import { withJsonErrorHandling } from "../../../../shared/output/json-error-handling.ts";
import { withCommandInstrumentation } from "../../../../shared/telemetry/command-instrumentation.ts";
import { legacyBackupsRuntimeLayer } from "../backups.layers.ts";
import { legacyManagementApiRuntimeLayer } from "../../../shared/legacy-management-api-runtime.layer.ts";
import { legacyBackupsList } from "./list.handler.ts";

const config = {
Expand Down Expand Up @@ -31,5 +31,5 @@ export const legacyBackupsListCommand = Command.make("list", config).pipe(
Command.withHandler((flags) =>
legacyBackupsList(flags).pipe(withCommandInstrumentation(), withJsonErrorHandling),
),
Command.provide(legacyBackupsRuntimeLayer(["backups", "list"])),
Command.provide(legacyManagementApiRuntimeLayer(["backups", "list"])),
);
13 changes: 9 additions & 4 deletions apps/cli/src/legacy/commands/backups/list/list.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,20 @@ import { renderGlamourTable } from "../../../output/legacy-glamour-table.ts";
import {
LegacyBackupListNetworkError,
LegacyBackupListUnexpectedStatusError,
mapLegacyBackupHttpError,
} from "../backups.errors.ts";
import { encodeEnv, encodeGoJson, encodeToml, encodeYaml } from "../backups.encoders.ts";
import {
encodeEnv,
encodeGoJson,
encodeToml,
encodeYaml,
} from "../../../shared/legacy-go-output.encoders.ts";
import { mapLegacyHttpError } from "../../../shared/legacy-http-errors.ts";
import { formatBackupTimestamp, formatRegion } from "../backups.format.ts";
import type { LegacyBackupsListFlags } from "./list.command.ts";

type BackupsResponse = typeof V1ListAllBackupsOutput.Type;

const mapListError = mapLegacyBackupHttpError({
const mapListError = mapLegacyHttpError({
networkError: LegacyBackupListNetworkError,
statusError: LegacyBackupListUnexpectedStatusError,
networkMessage: (cause) => `failed to list physical backups: ${cause}`,
Expand Down Expand Up @@ -85,7 +90,7 @@ export const legacyBackupsList = Effect.fn("legacy.backups.list")(function* (
const goFmt = Option.getOrUndefined(goOutputFlag);

if (goFmt === "json") {
yield* output.raw(encodeGoJson(response));
yield* output.raw(encodeGoJson(response, { nullForEmptyArrays: ["backups"] }));
return;
}
if (goFmt === "yaml") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Command, Flag } from "effect/unstable/cli";

import { withJsonErrorHandling } from "../../../../shared/output/json-error-handling.ts";
import { withCommandInstrumentation } from "../../../../shared/telemetry/command-instrumentation.ts";
import { legacyBackupsRuntimeLayer } from "../backups.layers.ts";
import { legacyManagementApiRuntimeLayer } from "../../../shared/legacy-management-api-runtime.layer.ts";
import { legacyBackupsRestore } from "./restore.handler.ts";

const config = {
Expand Down Expand Up @@ -32,5 +32,5 @@ export const legacyBackupsRestoreCommand = Command.make("restore", config).pipe(
Command.withHandler((flags) =>
legacyBackupsRestore(flags).pipe(withCommandInstrumentation(), withJsonErrorHandling),
),
Command.provide(legacyBackupsRuntimeLayer(["backups", "restore"])),
Command.provide(legacyManagementApiRuntimeLayer(["backups", "restore"])),
);
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import { Output } from "../../../../shared/output/output.service.ts";
import {
LegacyBackupRestoreNetworkError,
LegacyBackupRestoreUnexpectedStatusError,
mapLegacyBackupHttpError,
} from "../backups.errors.ts";
import { mapLegacyHttpError } from "../../../shared/legacy-http-errors.ts";
import type { LegacyBackupsRestoreFlags } from "./restore.command.ts";

const mapRestoreError = mapLegacyBackupHttpError({
const mapRestoreError = mapLegacyHttpError({
networkError: LegacyBackupRestoreNetworkError,
statusError: LegacyBackupRestoreUnexpectedStatusError,
networkMessage: (cause) => `failed to restore backup: ${cause}`,
Expand Down
87 changes: 62 additions & 25 deletions apps/cli/src/legacy/commands/ssl-enforcement/get/SIDE_EFFECTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,56 +2,93 @@

## Files Read

| Path | Format | When |
| -------------------------- | ------------------------- | ---------------------------------------------------------- |
| `~/.supabase/access-token` | plain text (token string) | when `SUPABASE_ACCESS_TOKEN` unset and keyring unavailable |
| Path | Format | When |
| -------------------------------------- | ------------------------- | ------------------------------------------------------------- |
| `~/.supabase/access-token` | plain text (token string) | when `SUPABASE_ACCESS_TOKEN` unset and keyring unavailable |
| `<workdir>/supabase/.temp/project-ref` | plain text (project ref) | when `--project-ref` flag and `PROJECT_ID` env are both unset |

## Files Written

| Path | Format | When |
| ---- | ------ | ---- |
| — | — | — |
| Path | Format | When |
| ------------------------------------------------ | ------ | ----------------------------------------------------------------------------- |
| `~/.supabase/<workdir-hash>/linked-project.json` | JSON | always (after ref resolution), via `Effect.ensuring` — on success and failure |
| `~/.supabase/telemetry.json` | JSON | always, via `Effect.ensuring` — on success and failure |

## API Routes

| Method | Path | Auth | Request body | Response (used fields) |
| ------ | ------------------------------------------- | ------------ | ------------ | ------------------------------ |
| `GET` | `/v1/projects/{ref}/config/ssl-enforcement` | Bearer token | none | `{enforced, override_enabled}` |
| Method | Path | Auth | Request body | Response (used fields) |
| ------ | ------------------------------------ | ------------ | ------------ | -------------------------------------------------------------------- |
| `GET` | `/v1/projects/{ref}/ssl-enforcement` | Bearer token | none | `{currentConfig: {database: boolean}, appliedSuccessfully: boolean}` |

## Environment Variables

| Variable | Purpose | Required? |
| ----------------------- | ---------------------------------------------------- | ------------------------------------------------------- |
| `SUPABASE_ACCESS_TOKEN` | auth token (bypasses credential file/keyring lookup) | no (falls back to keyring → `~/.supabase/access-token`) |
| `SUPABASE_API_URL` | override Management API base URL | no (defaults to `https://api.supabase.com`) |
| Variable | Purpose | Required? |
| ----------------------- | ---------------------------------------------------- | -------------------------------------------------------- |
| `SUPABASE_ACCESS_TOKEN` | auth token (bypasses credential file/keyring lookup) | no (falls back to keyring → `~/.supabase/access-token`) |
| `SUPABASE_API_URL` | override Management API base URL | no (defaults to `https://api.supabase.com`) |
| `PROJECT_ID` | project ref fallback when `--project-ref` is unset | no (falls back to `supabase/.temp/project-ref` → prompt) |

## Exit Codes

| Code | Condition |
| ---- | ---------------------------------------------------------- |
| `0` | success — SSL enforcement config printed to stdout |
| `1` | authentication error — no valid token found |
| `1` | API error — non-2xx response from SSL enforcement endpoint |
| `1` | network / connection failure |
| Code | Condition |
| ---- | --------------------------------------------------------------------------------------- |
| `0` | success — SSL enforcement status printed to stdout |
| `1` | project ref unresolved (`LegacyProjectNotLinkedError` / `LegacyInvalidProjectRefError`) |
| `1` | API non-200 (`LegacySslEnforcementGetUnexpectedStatusError`) |
| `1` | transport failure (`LegacySslEnforcementGetNetworkError`) |

## Output

### `--output-format text` (Go CLI compatible)
### `--output-format text` (default) — Go CLI compatible

Prints SSL enforcement configuration to stdout.
Single status line to stdout:

```
SSL is being enforced.
```

or

```
SSL is *NOT* being enforced.
```

The "_NOT_" form is emitted when `currentConfig.database` is `false` **or** when
`appliedSuccessfully` is `false` (i.e. the requested config has not yet propagated).

### Go `--output {json,yaml,toml,env}`

Byte-identical to the Go CLI's encoders (`apps/cli-go/internal/utils/output.go`).

- `json` — alphabetical struct-field order with trailing newline.
- `yaml` — `stringifyYaml(response)`.
- `toml` — `stringifyToml(response)` with trailing newline.
- `env` — Viper-flattened SCREAMING_SNAKE_CASE keys (e.g.
`APPLIEDSUCCESSFULLY="true"\nCURRENTCONFIG_DATABASE="true"\n`).

### Go `--output pretty`

Same as `text` mode (Go's default).

### `--output-format json`

Single JSON object emitted to stdout on success.
The full response object emitted as the `success` event payload:

```json
{ "currentConfig": { "database": true }, "appliedSuccessfully": true }
```

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

One `result` event on success.
One `result` event:

```ndjson
{"type":"result","data":{...}}
{"type":"result","data":{"currentConfig":{"database":true},"appliedSuccessfully":true}}
```

## Notes

- Requires `--project-ref` or a linked project (`.supabase/config.json`).
- The Go `--output` flag wins over the TS `--output-format` flag when both are provided.
- `linked-project.json` is written **after** the project ref is resolved, regardless of
whether the subsequent API call succeeds (mirrors Go's `PersistentPostRun`).
- `telemetry.json` is written on every invocation, including failures.
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
import { Command, Flag } from "effect/unstable/cli";
import type * as CliCommand from "effect/unstable/cli/Command";

import { withJsonErrorHandling } from "../../../../shared/output/json-error-handling.ts";
import { withCommandInstrumentation } from "../../../../shared/telemetry/command-instrumentation.ts";
import { legacyManagementApiRuntimeLayer } from "../../../shared/legacy-management-api-runtime.layer.ts";
import { legacySslEnforcementGet } from "./get.handler.ts";

const config = {
projectRef: Flag.string("project-ref").pipe(
Flag.withDescription("Project ref of the Supabase project."),
Flag.optional,
),
};
} as const;

export type LegacySslEnforcementGetFlags = CliCommand.Command.Config.Infer<typeof config>;

export const legacySslEnforcementGetCommand = Command.make("get", config).pipe(
Command.withDescription("Get the current SSL enforcement configuration."),
Command.withShortDescription("Get SSL enforcement configuration"),
Command.withHandler((flags) => legacySslEnforcementGet(flags)),
Command.withHandler((flags) =>
legacySslEnforcementGet(flags).pipe(withCommandInstrumentation(), withJsonErrorHandling),
),
Command.provide(legacyManagementApiRuntimeLayer(["ssl-enforcement", "get"])),
);
Loading
Loading