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
12 changes: 6 additions & 6 deletions apps/cli/docs/go-cli-porting-status.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,12 +235,12 @@ Legend:
| `backups restore` | `ported` | [`../src/legacy/commands/backups/restore/restore.command.ts`](../src/legacy/commands/backups/restore/restore.command.ts) |
| `snippets list` | `wrapped` | [`../src/legacy/commands/snippets/list/list.command.ts`](../src/legacy/commands/snippets/list/list.command.ts) |
| `snippets download` | `wrapped` | [`../src/legacy/commands/snippets/download/download.command.ts`](../src/legacy/commands/snippets/download/download.command.ts) |
| `sso list` | `wrapped` | [`../src/legacy/commands/sso/list/list.command.ts`](../src/legacy/commands/sso/list/list.command.ts) |
| `sso add` | `wrapped` | [`../src/legacy/commands/sso/add/add.command.ts`](../src/legacy/commands/sso/add/add.command.ts) |
| `sso remove` | `wrapped` | [`../src/legacy/commands/sso/remove/remove.command.ts`](../src/legacy/commands/sso/remove/remove.command.ts) |
| `sso update` | `wrapped` | [`../src/legacy/commands/sso/update/update.command.ts`](../src/legacy/commands/sso/update/update.command.ts) |
| `sso show` | `wrapped` | [`../src/legacy/commands/sso/show/show.command.ts`](../src/legacy/commands/sso/show/show.command.ts) |
| `sso info` | `wrapped` | [`../src/legacy/commands/sso/info/info.command.ts`](../src/legacy/commands/sso/info/info.command.ts) |
| `sso list` | `ported` | [`../src/legacy/commands/sso/list/list.command.ts`](../src/legacy/commands/sso/list/list.command.ts) |
| `sso add` | `ported` | [`../src/legacy/commands/sso/add/add.command.ts`](../src/legacy/commands/sso/add/add.command.ts) |
| `sso remove` | `ported` | [`../src/legacy/commands/sso/remove/remove.command.ts`](../src/legacy/commands/sso/remove/remove.command.ts) |
| `sso update` | `ported` | [`../src/legacy/commands/sso/update/update.command.ts`](../src/legacy/commands/sso/update/update.command.ts) |
| `sso show` | `ported` | [`../src/legacy/commands/sso/show/show.command.ts`](../src/legacy/commands/sso/show/show.command.ts) |
| `sso info` | `ported` | [`../src/legacy/commands/sso/info/info.command.ts`](../src/legacy/commands/sso/info/info.command.ts) |
| `domains create` | `wrapped` | [`../src/legacy/commands/domains/create/create.command.ts`](../src/legacy/commands/domains/create/create.command.ts) |
| `domains get` | `wrapped` | [`../src/legacy/commands/domains/get/get.command.ts`](../src/legacy/commands/domains/get/get.command.ts) |
| `domains reverify` | `wrapped` | [`../src/legacy/commands/domains/reverify/reverify.command.ts`](../src/legacy/commands/domains/reverify/reverify.command.ts) |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,9 @@ import { mockAnalytics, mockOutput } from "../../../../../tests/helpers/mocks.ts
import {
buildLegacyTestRuntime,
legacyJsonResponse,
legacyStatusCodeFailure,
mockLegacyCliConfig,
mockLegacyLinkedProjectCacheTracked,
mockLegacyPlatformApi,
mockLegacyPlatformApiService,
mockLegacyTelemetryStateTracked,
useLegacyTempWorkdir,
} from "../../../../../tests/helpers/legacy-mocks.ts";
Expand All @@ -22,8 +20,7 @@ type UpdatedBranch = typeof V1UpdateABranchConfigOutput.Type;
// V1UpdateABranchConfigInput.branch_id_or_ref is a oneOf [project-ref, uuid] union.
// A 20-lowercase project ref matches BOTH branches → schema rejects.
// HTTP-level mock tests pass a v4 UUID so the schema picks exactly one branch.
// The upgrade-suggest test uses `mockLegacyPlatformApiService` to bypass schema
// validation entirely so it can exercise the production-shape branchRef path.
// HTTP-level mock tests pass a v4 UUID so the schema picks exactly one branch.
const BRANCH_UUID = "11111111-1111-4111-8111-111111111111";
const BRANCH_REF = "cccccccccccccccccccc";

Expand Down Expand Up @@ -272,39 +269,49 @@ describe("legacy branches update integration", () => {
it.live(
"fires cli_upgrade_suggested with the branch ref + branching_persistent on 4xx gated",
() => {
const captured: Array<{ method: string; input: unknown }> = [];
const out = mockOutput({ format: "text" });
const analytics = mockAnalytics();
const cliConfig = mockLegacyCliConfig({ workdir: tempRoot.current });

const apiMock = mockLegacyPlatformApiService({
v1: {
// Real `HttpClientError` with `StatusCodeError` reason so the handler's
// `HttpClientError.isHttpClientError(cause)` check + `cause.response.status`
// read see a 402.
updateABranchConfig: () => Effect.fail(legacyStatusCodeFailure(402)),
getProject: () => Effect.succeed(projectResponse(BRANCH_REF) as never),
getOrganizationEntitlements: () =>
Effect.succeed(
entitlementResponse({
featureKey: "branching_persistent",
hasAccess: false,
}) as never,
),
},
// `legacySuggestUpgrade` bypasses the typed Management API client to GET
// the project + entitlements (see its file-level comment — required so
// cli-e2e replay fixtures with `__PROJECT_REF__` placeholders don't trip
// strict schema decode). Route all three URLs through `mockLegacyPlatformApi`'s
// handler so the assertion covers the request log produced by the same
// HttpClient the production code uses.
const apiMock = mockLegacyPlatformApi({
handler: (request) =>
Effect.sync(() => {
if (request.method === "PATCH" && request.url.includes("/v1/branches/")) {
return legacyJsonResponse(request, 402, { message: "upgrade required" });
}
if (request.method === "GET" && request.url.endsWith(`/v1/projects/${BRANCH_REF}`)) {
return legacyJsonResponse(request, 200, projectResponse(BRANCH_REF));
}
if (
request.method === "GET" &&
request.url.endsWith(`/v1/organizations/${ORG_SLUG}/entitlements`)
) {
return legacyJsonResponse(
request,
200,
entitlementResponse({
featureKey: "branching_persistent",
hasAccess: false,
}),
);
}
return legacyJsonResponse(request, 200, null);
}),
});

const layer = buildLegacyTestRuntime({
out,
// The service mock's layer has no upstream error channel; the test
// runtime's typing allows either layer shape here.
api: { layer: apiMock.layer as never },
api: apiMock,
cliConfig,
analytics,
});

void captured;

return Effect.gen(function* () {
yield* Effect.exit(
legacyBranchesUpdate({
Expand All @@ -315,14 +322,21 @@ describe("legacy branches update integration", () => {
);
// The branch ref the resolver returned is what `legacySuggestUpgrade`
// should query getProject with — Go parity.
const projectCall = apiMock.requests.find((r) => r.method === "getProject");
expect(projectCall?.input).toMatchObject({ ref: BRANCH_REF });
const projectCall = apiMock.requests.find(
(r) => r.method === "GET" && r.url.endsWith(`/v1/projects/${BRANCH_REF}`),
);
expect(projectCall).toBeDefined();
const entitlementsCall = apiMock.requests.find((r) =>
r.url.endsWith(`/v1/organizations/${ORG_SLUG}/entitlements`),
);
expect(entitlementsCall).toBeDefined();
expect(analytics.captured).toEqual([
{
event: "cli_upgrade_suggested",
properties: { feature_key: "branching_persistent", org_slug: ORG_SLUG },
},
]);
expect(out.stderrText).toContain("Upgrade your plan:");
}).pipe(Effect.provide(layer));
},
);
Expand Down
84 changes: 62 additions & 22 deletions apps/cli/src/legacy/commands/sso/add/SIDE_EFFECTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,84 @@

## Files Read

| Path | Format | When |
| -------------------------- | ------------------------- | ---------------------------------------------------------- |
| `~/.supabase/access-token` | plain text (token string) | when `SUPABASE_ACCESS_TOKEN` unset and keyring unavailable |
| `<metadata-file>` | XML | when `--metadata-file` flag is provided |
| `<attribute-mapping-file>` | JSON | when `--attribute-mapping-file` flag is provided |
| Path | Format | When |
| ---------------------------------------------- | ------------------------- | --------------------------------------------------------------------------------------------- |
| keyring `"Supabase CLI"` / `<profile>` | OS keychain | when `SUPABASE_ACCESS_TOKEN` unset and keyring available; account = `LegacyCliConfig.profile` |
| keyring `"Supabase CLI"` / `access-token` | OS keychain | legacy-key fallback when the profile-keyed lookup misses |
| `~/.supabase/access-token` | plain text (token string) | last-resort fallback after env + keyring miss |
| `<workdir>/supabase/.temp/linked-project.json` | JSON | always — `linkedProjectCache` reads to decide whether to write |
| `<metadata-file>` | XML (UTF-8) | when `--metadata-file` is provided |
| `<attribute-mapping-file>` | JSON | when `--attribute-mapping-file` is provided |

## Files Written

| Path | Format | When |
| ---- | ------ | ---- |
| — | — | — |
| Path | Format | When |
| ---------------------------------------------- | ------ | ------------------------------------------------------------------- |
| `~/.supabase/telemetry.json` | JSON | always (`Effect.ensuring(telemetryState.flush)`) |
| `<workdir>/supabase/.temp/linked-project.json` | JSON | best-effort after `--project-ref` resolves (Go `PersistentPostRun`) |

## API Routes

| Method | Path | Auth | Request body | Response (used fields) |
| ------ | ---------------------------------------------- | ------------ | -------------------------------------------------------------------------------- | --------------------------------------------- |
| `POST` | `/v1/projects/{ref}/config/auth/sso/providers` | Bearer token | `{type, metadata_url, metadata_xml, domains, attribute_mapping, name_id_format}` | `{id, saml, domains, created_at, updated_at}` |
| Method | Path | Auth | Request body | Response (used fields) |
| ------ | ---------------------------------------------- | ------------ | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
| `POST` | `/v1/projects/{ref}/config/auth/sso/providers` | Bearer token | `{type, metadata_xml?, metadata_url?, domains?, attribute_mapping?, name_id_format?}` | `{id, saml?, domains?, created_at?, updated_at?}` (parsed loosely) |
| `GET` | `<metadata-url>` | none | `Accept: application/xml`, 10s timeout | XML body (UTF-8) — validation when `--skip-url-validation` not set |
| `GET` | `/v1/projects/{ref}` | Bearer token | none | `{organization_slug}` — upgrade-gate side-call on 4xx |
| `GET` | `/v1/organizations/{slug}/entitlements` | Bearer token | none | `{entitlements[].feature.key, .hasAccess}` — upgrade-gate |

Bypasses the typed Management API client for the POST so user-supplied keys inside
`attribute_mapping.keys.<x>` (e.g. `default`) are preserved verbatim — Go encodes the
same shape via an inline anonymous struct with `Default *any`.

## 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`) |
| `SUPABASE_PROFILE` | profile selector (built-in name or YAML file path) | no (defaults to `supabase`) |

## Exit Codes

| Code | Condition |
| ---- | ---------------------------------------------------- |
| `0` | success — provider created and details printed |
| `1` | authentication error — no valid token found |
| `1` | API error — non-2xx response from providers endpoint |
| `1` | validation error — invalid metadata URL or XML |
| `1` | network / connection failure |
| Code | Condition |
| ---- | -------------------------------------------------------------------------------------------------------------------- |
| `0` | success |
| `1` | `LegacySsoMutexFlagError` — `--metadata-file` and `--metadata-url` both set |
| `1` | `LegacySsoAddMetadataFileError` — metadata file unreadable, non-UTF-8, or metadata URL invalid/unreachable/non-UTF-8 |
| `1` | `LegacySsoAddAttributeMappingFileError` — JSON file unreadable or malformed |
| `1` | `LegacySsoAddSamlDisabledError` — 404 from POST |
| `1` | `LegacySsoAddUnexpectedStatusError` — other non-2xx |
| `1` | `LegacySsoAddNetworkError` — transport-level failure |

## Telemetry Events Fired

| Event | When | Notable properties |
| ----------------------- | ---------------------------------------------------- | ---------------------------------------------------------------------- |
| `cli_command_executed` | post-run, success or failure (via wrapper) | `exit_code`, `duration_ms`, `flags` (`--project-ref` allowed verbatim) |
| `cli_upgrade_suggested` | 4xx response **and** `auth.saml_2` entitlement gated | `feature_key: "auth.saml_2"`, `org_slug` |

## Output

### `--output-format text` / Go `--output pretty`

Glamour-styled property/value markdown table plus optional `## Attribute Mapping` and `## SAML 2.0 Metadata XML` sections (heading + fenced code block).

### `--output json` / `--output yaml` / `--output toml`

Response verbatim (Go-compatible alphabetised keys for JSON).

### `--output env`

No output (matches Go's `create.go:94`).

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

Single `success` event with the parsed response as data.

## Notes

- `--type saml` is required (currently the only supported type).
- `--type saml` is **required** (Go's `MarkFlagRequired("type")`).
- `--metadata-file` and `--metadata-url` are mutually exclusive.
- `--skip-url-validation` skips local DNS/HTTP validation of the metadata URL before sending to the API.
- Requires `--project-ref` or a linked project (`.supabase/config.json`).
- `--skip-url-validation` skips the HTTPS-only + 10s GET + UTF-8 body validation against the metadata URL.
- Metadata URL validation error message: `only HTTPS Metadata URLs are supported Use --skip-url-validation to suppress this error` (no trailing period — matches Go's `create.go:47`; differs from `sso update`'s variant).
- The `## Attribute Mapping` / `## SAML 2.0 Metadata XML` sections are emitted as plain markdown (heading + fence). Visual styling of the headings does not match Go's Glamour-rendered output; the XML body inside the fence is byte-parity via `formatSsoMetadataXml`.
- **Missing `--type` parser error**: the error message itself matches Go Cobra's `Error: required flag(s) "type" not set` verbatim (mapped in `shared/output/normalize-error.ts`). Effect CLI's parser however dumps the full help block to stdout _before_ the error, while Go Cobra shows usage only on explicit `--help`. The error string is parity; the surrounding help dump is an Effect CLI behavior that would require forking the CLI parser to suppress.
Loading
Loading