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
6 changes: 3 additions & 3 deletions apps/cli/src/legacy/commands/sso/add/SIDE_EFFECTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ same shape via an inline anonymous struct with `Default *any`.
| `1` | `LegacySsoInvalidFlagValueError` — a `--type`/`--skip-url-validation`/`--name-id-format` occurrence pflag's `Value.Set` would reject (enum membership / `strconv.ParseBool`; fails before every validation; no request) |
| `1` | malformed CSV in a `--domains` value — fails during flag parsing, before the handler and telemetry, with pflag's exact diagnostic on stderr (e.g. `invalid argument "a\"b" for "--domains" flag: parse error on line 1, column 2: bare " in non-quoted-field`; a blank-only value fails with `EOF`) — CLI-2005 |
| `1` | `LegacySsoFlagNeedsArgumentError` — a bare value-taking flag is the final argv token (pflag `ValueRequiredError`, fails before every validation; no request) |
| `1` | `LegacySsoProfileError` — the pflag/viper-effective `--profile`/`SUPABASE_PROFILE` cannot be loaded the way Go's `LoadProfile` loads it (root `PersistentPreRunE`, before `ChangeWorkDir`; beats the workdir, required-flag, and mutex checks; no request) |
| `1` | `LegacySsoWorkdirError` — the pflag/viper-effective `--workdir`/`SUPABASE_WORKDIR` is not an existing directory (Go `ChangeWorkDir` in root `PersistentPreRunE`; beats the required-flag and mutex checks; no request) |
| `1` | `LegacyProfileLoadError` — the pflag/viper-effective `--profile`/`SUPABASE_PROFILE` cannot be loaded the way Go's `LoadProfile` loads it (root `PersistentPreRunE`, before `ChangeWorkDir`; beats the workdir, required-flag, and mutex checks; no request) |
| `1` | `LegacyPflagWorkdirError` — the pflag/viper-effective `--workdir`/`SUPABASE_WORKDIR` is not an existing directory (Go `ChangeWorkDir` in root `PersistentPreRunE`; beats the required-flag and mutex checks; no request) |
| `1` | `LegacySsoAddRequiredFlagError` — pflag consumed the `--type`/`-t` token as another flag's value (cobra `ValidateRequiredFlags`) |
| `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 |
Expand Down Expand Up @@ -88,7 +88,7 @@ Single `success` event with the parsed response as data.
- Flag values follow pflag's consumption rules, not the TS parser's: every value the handler acts on (`--project-ref`, `--metadata-file`, `--metadata-url`, `--attribute-mapping-file`, `--domains`, `--name-id-format`, `--skip-url-validation`) is reconciled against a pflag-faithful raw-argv scan. E.g. `--project-ref --metadata-file x.xml --metadata-url u` hands `--metadata-file` to `--project-ref` as its value and fails ref validation — the metadata file is never read (CLI-1982). Repeated flags resolve last-wins (pflag Sets every occurrence; the TS parser is first-wins), and an occurrence pflag's `Value.Set` would reject — `--type` outside `[ saml ]`, a boolean outside Go's `strconv.ParseBool` set (`--skip-url-validation=yes`), or a `--name-id-format` outside the enum — fails with pflag's exact `invalid argument …` message before every validation and request.
- Required-ness follows pflag too: when the `--type` token is itself consumed as another flag's value (`--domains --type saml`), the command fails with cobra's exact `required flag(s) "type" not set` before any request (cobra `ValidateRequiredFlags` runs before `ValidateFlagGroups`). `-t` shorthand occurrences are recognised by the scan and never trip this.
- The workdir follows pflag/viper too: Go's `ChangeWorkDir` (root `PersistentPreRunE`) chdir's to the effective `--workdir` (last occurrence, even a flag-shaped consumed token like `--workdir --metadata-file`) or `SUPABASE_WORKDIR`, and a missing directory aborts with Go's exact `failed to change workdir: chdir …` before the required-flag check, the mutex check, and any request. A changed-but-empty `--workdir=` shadows the env var and falls back to the always-valid project-root walk-up, exactly like viper.
- The profile follows pflag/viper too (PR #5974 round 7): whenever the pflag-effective `--profile`/`SUPABASE_PROFILE` token differs from the one the Effect parser gave the config layer (a `--profile` token consumed by another flag — `--domains --profile alternate.yml` targets the env/default profile, not `alternate.yml`; a flag-shaped consumed value — `--profile --metadata-url`; repeats, which pflag resolves last-wins; an explicit `--profile supabase` shadowing the env; an untrimmed/empty persisted `~/.supabase/profile` file), the handler re-runs Go's `LoadProfile` on the effective token (`sso.load-profile.ts`) — the POST targets that profile's `api_url`, and a token Go cannot load aborts with Go's error (`failed to read profile: …` / `failed to parse profile: …` / `invalid profile: …`, byte-exact for the deterministic classes) before the workdir check and any request. Where the scan and the parser agree — every normal invocation — the config layer's resolution (including its pre-existing lenient missing/malformed-file fallback, which predates CLI-1982 and applies shell-wide) is used unchanged. The upgrade-gate fallback GETs and the linked-project cache fill also target the reconciled host (Go's `CurrentProfile` is process-wide).
- The profile follows pflag/viper too (PR #5974 round 7): whenever the pflag-effective `--profile`/`SUPABASE_PROFILE` token differs from the one the Effect parser gave the config layer (a `--profile` token consumed by another flag — `--domains --profile alternate.yml` targets the env/default profile, not `alternate.yml`; a flag-shaped consumed value — `--profile --metadata-url`; repeats, which pflag resolves last-wins; an explicit `--profile supabase` shadowing the env; an untrimmed/empty persisted `~/.supabase/profile` file), the handler re-runs Go's `LoadProfile` on the effective token (`legacy-profile-load.ts`) — the POST targets that profile's `api_url`, and a token Go cannot load aborts with Go's error (`failed to read profile: …` / `failed to parse profile: …` / `invalid profile: …`, byte-exact for the deterministic classes) before the workdir check and any request. Where the scan and the parser agree — every normal invocation — the config layer's resolution (including its pre-existing lenient missing/malformed-file fallback, which predates CLI-1982 and applies shell-wide) is used unchanged. The upgrade-gate fallback GETs and the linked-project cache fill also target the reconciled host (Go's `CurrentProfile` is process-wide).
- Accepted micro-divergences of the profile emulation (each fail-closed: both CLIs exit 1 with zero requests; only stderr detail can differ): YAML parse-failure detail text (JS `yaml` vs go-yaml, shared `failed to read profile: While parsing config: ` prefix); non-YAML/JSON viper config types (`.toml`, `.env`, …) parsed as YAML; `http_url`/`hostname_rfc1123`/`uuid4` validator tags approximated; the final line of a padded multi-line error loses its trailing spaces to the shared error normalizer's trim. Also: when the effective and layer profiles differ AND the token is keyring-relevant, the keyring token lookup still uses the layer profile's name (env-token flows, e.g. the cli-e2e harness, are unaffected), and the upgrade-suggestion billing URL keeps the layer profile's dashboard host.
- `--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).
Expand Down
36 changes: 18 additions & 18 deletions apps/cli/src/legacy/commands/sso/add/add.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ import { legacyMissingAccessTokenMessage } from "../../../auth/legacy-access-tok
import { LegacyLinkedProjectCache } from "../../../telemetry/legacy-linked-project-cache.service.ts";
import { LegacyTelemetryState } from "../../../telemetry/legacy-telemetry-state.service.ts";
import { legacySuggestUpgrade } from "../../../shared/legacy-upgrade-suggest.ts";
import {
legacyPflagBoolValue,
legacyPflagEnumValue,
legacyPflagSliceValue,
legacyPflagStringValue,
legacyResolvePflagProfile,
legacyValidatePflagWorkdir,
} from "../../../shared/legacy-pflag-reconcile.ts";
import {
LegacySsoAddAttributeMappingFileError,
LegacySsoAddMetadataFileError,
Expand All @@ -40,14 +48,6 @@ import {
} from "../sso.errors.ts";
import { renderSingleProvider, toLegacySsoProviderView } from "../sso.format.ts";
import { validateMetadataUrl } from "../sso.metadata-url.ts";
import {
legacySsoPflagBoolValue,
legacySsoPflagEnumValue,
legacySsoPflagSliceValue,
legacySsoPflagStringValue,
legacySsoResolvePflagProfile,
legacySsoValidatePflagWorkdir,
} from "../sso.pflag-reconcile.ts";
import {
LEGACY_SSO_NAME_ID_FORMATS,
readAttributeMappingFile,
Expand Down Expand Up @@ -145,20 +145,20 @@ export const legacySsoAdd = Effect.fn("legacy.sso.add")(function* (flags: Legacy
// values; `--type`'s stays unused because every valid occurrence is the
// enum's single member, so the parsed `flags.type` is already
// pflag-effective whenever this validation passes.
yield* Result.match(legacySsoPflagEnumValue(occurrences, "type", ["saml"], "-t, --type"), {
yield* Result.match(legacyPflagEnumValue(occurrences, "type", ["saml"], "-t, --type"), {
onFailure: (message: string) => Effect.fail(new LegacySsoInvalidFlagValueError({ message })),
onSuccess: Effect.succeed,
});
const skipUrlValidation = yield* Result.match(
legacySsoPflagBoolValue(occurrences, "skip-url-validation"),
legacyPflagBoolValue(occurrences, "skip-url-validation"),
{
onFailure: (message: string) =>
Effect.fail(new LegacySsoInvalidFlagValueError({ message })),
onSuccess: Effect.succeed,
},
);
const nameIdFormat = yield* Result.match(
legacySsoPflagEnumValue(occurrences, "name-id-format", LEGACY_SSO_NAME_ID_FORMATS),
legacyPflagEnumValue(occurrences, "name-id-format", LEGACY_SSO_NAME_ID_FORMATS),
{
onFailure: (message: string) =>
Effect.fail(new LegacySsoInvalidFlagValueError({ message })),
Expand Down Expand Up @@ -193,7 +193,7 @@ export const legacySsoAdd = Effect.fn("legacy.sso.add")(function* (flags: Legacy
// contacts (binary-verified, PR #5974 review round 7). Where the scan
// and the parser agree, this resolves to `none` and the config layer's
// apiUrl below is already pflag-effective.
const reconciledProfile = yield* legacySsoResolvePflagProfile(scan);
const reconciledProfile = yield* legacyResolvePflagProfile(scan);
const profileApiUrl = Option.map(reconciledProfile, (profile) => profile.apiUrl);
// Reconciled-profile credentials, resolved ONCE for the main request and
// every auxiliary call (linked-project cache fill, upgrade-gate fallback
Expand Down Expand Up @@ -232,7 +232,7 @@ export const legacySsoAdd = Effect.fn("legacy.sso.add")(function* (flags: Legacy
// metadata — without this check the reconciliation below would silently
// drop the metadata source and POST a provider Go never creates
// (binary-verified, PR #5974 review round 6).
yield* legacySsoValidatePflagWorkdir(scan);
yield* legacyValidatePflagWorkdir(scan);

// `MarkFlagRequired("type")` (`cmd/sso.go:165`): when pflag consumed the
// `--type` or `-t` token as another flag's value (e.g. `--domains --type
Expand Down Expand Up @@ -271,11 +271,11 @@ export const legacySsoAdd = Effect.fn("legacy.sso.add")(function* (flags: Legacy
// required-flag check above). `--name-id-format` and
// `--skip-url-validation` were reconciled above, alongside their pflag
// value validation.
const projectRef = legacySsoPflagStringValue(occurrences, "project-ref");
const metadataFile = legacySsoPflagStringValue(occurrences, "metadata-file");
const metadataUrl = legacySsoPflagStringValue(occurrences, "metadata-url");
const attributeMappingFile = legacySsoPflagStringValue(occurrences, "attribute-mapping-file");
const domains = legacySsoPflagSliceValue(occurrences, "domains", flags.domains);
const projectRef = legacyPflagStringValue(occurrences, "project-ref");
const metadataFile = legacyPflagStringValue(occurrences, "metadata-file");
const metadataUrl = legacyPflagStringValue(occurrences, "metadata-url");
const attributeMappingFile = legacyPflagStringValue(occurrences, "attribute-mapping-file");
const domains = legacyPflagSliceValue(occurrences, "domains", flags.domains);

const ref = yield* resolver.resolve(projectRef);

Expand Down
10 changes: 5 additions & 5 deletions apps/cli/src/legacy/commands/sso/add/add.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -478,7 +478,7 @@ describe("legacy sso add integration", () => {
expect(Exit.isFailure(exit)).toBe(true);
if (Exit.isFailure(exit)) {
const dump = JSON.stringify(exit.cause);
expect(dump).toContain("LegacySsoWorkdirError");
expect(dump).toContain("LegacyPflagWorkdirError");
expect(dump).toContain(
"failed to change workdir: chdir --metadata-file: no such file or directory",
);
Expand Down Expand Up @@ -519,7 +519,7 @@ describe("legacy sso add integration", () => {
expect(Exit.isFailure(exit)).toBe(true);
if (Exit.isFailure(exit)) {
const dump = JSON.stringify(exit.cause);
expect(dump).toContain("LegacySsoWorkdirError");
expect(dump).toContain("LegacyPflagWorkdirError");
expect(dump).toContain(
"failed to change workdir: chdir /nonexistent-sso-add-workdir: no such file or directory",
);
Expand Down Expand Up @@ -1228,7 +1228,7 @@ describe("legacy sso add integration", () => {
expect(Exit.isFailure(exit)).toBe(true);
if (Exit.isFailure(exit)) {
const dump = JSON.stringify(exit.cause);
expect(dump).toContain("LegacySsoProfileError");
expect(dump).toContain("LegacyProfileLoadError");
expect(dump).toContain(`failed to read profile: Unsupported Config Type \\"\\"`);
}
expect(api.requests.length).toBe(0);
Expand Down Expand Up @@ -1288,8 +1288,8 @@ describe("legacy sso add integration", () => {
expect(Exit.isFailure(exit)).toBe(true);
if (Exit.isFailure(exit)) {
const dump = JSON.stringify(exit.cause);
expect(dump).toContain("LegacySsoProfileError");
expect(dump).not.toContain("LegacySsoWorkdirError");
expect(dump).toContain("LegacyProfileLoadError");
expect(dump).not.toContain("LegacyPflagWorkdirError");
expect(dump).not.toContain("LegacySsoAddRequiredFlagError");
expect(dump).not.toContain("LegacySsoMutexFlagError");
}
Expand Down
25 changes: 0 additions & 25 deletions apps/cli/src/legacy/commands/sso/sso.errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,31 +108,6 @@ export class LegacySsoAddRequiredFlagError extends Data.TaggedError(
readonly message: string;
}> {}

// Go's `ChangeWorkDir` (`internal/utils/misc.go:238-257`), run from the root
// `PersistentPreRunE` (`cmd/root.go:104`) — after `ParseFlags` and
// `ValidateArgs`, before `ValidateRequiredFlags`, `ValidateFlagGroups`, and
// `RunE` — so a missing workdir directory aborts with no API call ever made.
// Emulated for the pflag/viper-effective `--workdir`/`SUPABASE_WORKDIR` the
// Effect layer never validates (and, when `--workdir` consumed a flag-shaped
// token, never even saw — PR #5974 review round 6). Shared across add +
// update; message byte-matches Go's template.
export class LegacySsoWorkdirError extends Data.TaggedError("LegacySsoWorkdirError")<{
readonly message: string;
}> {}

// Go's `LoadProfile` (`internal/utils/profile.go:94-118`), run from the root
// `PersistentPreRunE` (`cmd/root.go:98-102`) immediately BEFORE
// `ChangeWorkDir` — so a profile Go cannot load aborts before the workdir
// check, `ValidateRequiredFlags`, `ValidateFlagGroups`, and `RunE`, with no
// API call ever made. Emulated for the pflag/viper-effective `--profile`/
// `SUPABASE_PROFILE` whenever it differs from the token the Effect config
// layer resolved (PR #5974 review round 7). Shared across add + update;
// message byte-matches Go for the deterministic failure classes (see
// `sso.load-profile.ts`).
export class LegacySsoProfileError extends Data.TaggedError("LegacySsoProfileError")<{
readonly message: string;
}> {}

// Shared across add + update — metadata URL validation.
export class LegacySsoMetadataUrlInvalidError extends Data.TaggedError(
"LegacySsoMetadataUrlInvalidError",
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/src/legacy/commands/sso/sso.saml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Effect, FileSystem } from "effect";
* (both commands bind the same Go `ssoNameIDFormat` enum var,
* `cmd/sso.go:158,176`). Order matters twice: it drives the CLI help text
* and it is joined verbatim into pflag's `invalid argument … must be one of
* [ … ]` error (`legacySsoPflagEnumValue`), which must byte-match Go.
* [ … ]` error (`legacyPflagEnumValue`), which must byte-match Go.
*/
export const LEGACY_SSO_NAME_ID_FORMATS = [
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
Expand Down
Loading
Loading