From 187f2cc2c5f95a0a1cba56e91fcb9d452dddbb23 Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Tue, 7 Jul 2026 11:13:40 +0100 Subject: [PATCH 01/79] fix(cli): read piped stdin for gen signing-key's overwrite prompt (Go parity) (#5794) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Current Behavior Go's overwrite-confirmation prompt for `gen signing-key` reads piped stdin even in non-TTY mode (`internal/utils/console.go`'s `PromptYesNo`/`ReadLine`, racing a 100ms timeout) and honors an explicit y/n answer. The TS port's `signing-key.handler.ts` had its own local `confirmOverwrite` that returned `true` unconditionally in non-TTY mode without reading stdin at all, so `echo n | supabase gen signing-key` silently overwrote the existing key file instead of canceling — a data-loss risk for scripted/CI usage. ## Expected Behavior - Deletes the local `confirmOverwrite` and switches the call site to the shared `legacyPromptYesNo` helper (already used by `seed buckets`, `config push`, `logout`, `storage rm`, `db pull`), which already correctly implements Go's non-TTY read-with-timeout-and-parse behavior. - Swaps `LegacyYesFlag` for `legacyResolveYes` (matching those same five callers), so `gen signing-key` now also honors `SUPABASE_YES` and an explicit `--yes=false`, matching Go's `viper.GetBool("YES")`. - Fails the overwrite closed (rather than silently defaulting to yes) when a real interactive TTY requests a non-text `--output-format` — this command has no structured json/stream-json payload (SIDE_EFFECTS.md), and the shared helper's own default-on-non-text short-circuit would otherwise silently overwrite irrecoverable key material with no prompt at all. A non-TTY caller (piped or not) is unaffected by this guard. Fixes CLI-1865 --- .../commands/gen/signing-key/SIDE_EFFECTS.md | 11 +- .../gen/signing-key/signing-key.command.ts | 5 + .../gen/signing-key/signing-key.e2e.test.ts | 63 +++++ .../gen/signing-key/signing-key.handler.ts | 50 ++-- .../signing-key.integration.test.ts | 229 +++++++++++++++++- 5 files changed, 323 insertions(+), 35 deletions(-) create mode 100644 apps/cli/src/legacy/commands/gen/signing-key/signing-key.e2e.test.ts diff --git a/apps/cli/src/legacy/commands/gen/signing-key/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/gen/signing-key/SIDE_EFFECTS.md index 8cf887643d..2ea04a7fab 100644 --- a/apps/cli/src/legacy/commands/gen/signing-key/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/gen/signing-key/SIDE_EFFECTS.md @@ -22,9 +22,9 @@ ## Environment Variables -| Variable | Purpose | Required? | -| -------- | ------- | --------- | -| - | - | - | +| Variable | Purpose | Required? | +| -------------- | ---------------------------------------------------------------------------------- | --------- | +| `SUPABASE_YES` | Auto-confirms the overwrite prompt, same as `--yes` (Go's `viper.GetBool("YES")`). | No | ## Exit Codes @@ -47,16 +47,17 @@ ### `--output-format json` -Not applicable; the command uses raw stdout and stderr text like the Go CLI. +Not applicable to output rendering; the command uses raw stdout and stderr text like the Go CLI. It does, however, affect the overwrite-confirmation prompt: since this command has no structured json/stream-json payload, requesting a non-text format from a real interactive terminal (no `--yes`, no piped stdin) fails the overwrite closed (`context canceled`) rather than silently defaulting to yes on a destructive, irreversible action. A non-TTY caller (piped or not) is unaffected — piped `y`/`n` answers are honored regardless of `--output-format`. ### `--output-format stream-json` -Not applicable; the command uses raw stdout and stderr text like the Go CLI. +Same as `--output-format json` above. ## Notes - `--algorithm` accepts `ES256` (default, recommended) or `RS256`. - `--append` appends the new key to an existing keys file instead of overwriting. +- The overwrite prompt honors `SUPABASE_YES` and an explicit `--yes=false` override, matching Go's `viper.GetBool("YES")` precedence (flag wins over env; an omitted flag falls back to the env var). On non-TTY stdin, a piped `y`/`n` line is read within a 100ms timeout and honored before falling back to the default (`y`), matching Go's `Console.ReadLine`/`PromptYesNo` — a piped answer other than an exact `y`/`yes`/`n`/`no` (case-insensitive) also falls back to the default. - `auth.signing_keys_path` is resolved relative to the active `supabase/config.toml` or `supabase/config.json`. - Generated keys are JWKs, not PEM files. - No network or Management API calls are involved. diff --git a/apps/cli/src/legacy/commands/gen/signing-key/signing-key.command.ts b/apps/cli/src/legacy/commands/gen/signing-key/signing-key.command.ts index 35a026f682..3cbedfbc01 100644 --- a/apps/cli/src/legacy/commands/gen/signing-key/signing-key.command.ts +++ b/apps/cli/src/legacy/commands/gen/signing-key/signing-key.command.ts @@ -3,6 +3,7 @@ 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 { commandRuntimeLayer } from "../../../../shared/runtime/command-runtime.layer.ts"; +import { stdinLayer } from "../../../../shared/runtime/stdin.layer.ts"; import { legacyCliConfigLayer } from "../../../config/legacy-cli-config.layer.ts"; import { legacyDebugLoggerLayer } from "../../../shared/legacy-debug-logger.layer.ts"; import { withLegacyCommandInstrumentation } from "../../../telemetry/legacy-command-instrumentation.ts"; @@ -29,6 +30,10 @@ const legacyGenSigningKeyRuntimeLayer = Layer.mergeAll( cliConfig, legacyTelemetryStateLayer, commandRuntimeLayer(["gen", "signing-key"]), + // The overwrite-confirmation prompt reads piped stdin via `legacyPromptYesNo` + // (`stdin.readLine`), same as `config push`, `seed buckets`, `storage rm`, `db pull`, + // and `logout` — all of which merge `stdinLayer` alongside their runtime layer. + stdinLayer, ); export const legacyGenSigningKeyCommand = Command.make("signing-key", config).pipe( diff --git a/apps/cli/src/legacy/commands/gen/signing-key/signing-key.e2e.test.ts b/apps/cli/src/legacy/commands/gen/signing-key/signing-key.e2e.test.ts new file mode 100644 index 0000000000..3a2b377ee4 --- /dev/null +++ b/apps/cli/src/legacy/commands/gen/signing-key/signing-key.e2e.test.ts @@ -0,0 +1,63 @@ +import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeEach, describe, expect, test } from "vitest"; + +import { runSupabase } from "../../../../../tests/helpers/cli.ts"; + +const E2E_TIMEOUT_MS = 30_000; + +/** + * Golden-path e2e for CLI-1865: exercises the real compiled-binary boundary — + * `signing-key.command.ts`'s actual production runtime layer, not the mocked + * `Stdin` the integration suite provides via `Layer.succeed`. A missing + * `stdinLayer` in that composition only surfaces as a "Service not found" defect + * at this boundary (see the legacy CLAUDE.md Go Parity Checklist item 5). Per-branch + * prompt/format coverage lives in the integration suite. + */ +describe("supabase gen signing-key (legacy)", () => { + let projectDir: string; + + beforeEach(() => { + projectDir = mkdtempSync(join(tmpdir(), "supabase-gen-signing-key-e2e-")); + mkdirSync(join(projectDir, "supabase"), { recursive: true }); + writeFileSync( + join(projectDir, "supabase", "config.toml"), + '[auth]\nsigning_keys_path = "./signing_keys.json"\n', + ); + writeFileSync(join(projectDir, "supabase", "signing_keys.json"), "[]\n"); + }); + + afterEach(() => { + rmSync(projectDir, { recursive: true, force: true }); + }); + + test( + "declines the overwrite on a piped 'n' without crashing or writing the file", + { timeout: E2E_TIMEOUT_MS }, + async () => { + const { exitCode, stderr } = await runSupabase(["gen", "signing-key"], { + entrypoint: "legacy", + cwd: projectDir, + stdin: "n\n", + }); + expect(exitCode).toBe(1); + expect(stderr).toContain("context canceled"); + expect(stderr).not.toContain("Service not found"); + const saved = readFileSync(join(projectDir, "supabase", "signing_keys.json"), "utf8"); + expect(JSON.parse(saved)).toEqual([]); + }, + ); + + test("overwrites on a piped 'y'", { timeout: E2E_TIMEOUT_MS }, async () => { + const { exitCode, stderr } = await runSupabase(["gen", "signing-key"], { + entrypoint: "legacy", + cwd: projectDir, + stdin: "y\n", + }); + expect(exitCode).toBe(0); + expect(stderr).toContain("JWT signing key appended to:"); + const saved = readFileSync(join(projectDir, "supabase", "signing_keys.json"), "utf8"); + expect(JSON.parse(saved)).toHaveLength(1); + }); +}); diff --git a/apps/cli/src/legacy/commands/gen/signing-key/signing-key.handler.ts b/apps/cli/src/legacy/commands/gen/signing-key/signing-key.handler.ts index 6396c658c6..775a39140e 100644 --- a/apps/cli/src/legacy/commands/gen/signing-key/signing-key.handler.ts +++ b/apps/cli/src/legacy/commands/gen/signing-key/signing-key.handler.ts @@ -7,8 +7,9 @@ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; import { LegacyCliConfig } from "../../../config/legacy-cli-config.service.ts"; import { findGitRootPath } from "../../../../shared/git/git-root.ts"; import { LegacyDebugLogger } from "../../../shared/legacy-debug-logger.service.ts"; +import { legacyPromptYesNo } from "../../../shared/legacy-prompt-yes-no.ts"; import { LegacyTelemetryState } from "../../../telemetry/legacy-telemetry-state.service.ts"; -import { LegacyYesFlag } from "../../../../shared/legacy/global-flags.ts"; +import { legacyResolveYes } from "../../../../shared/legacy/global-flags.ts"; import { Output } from "../../../../shared/output/output.service.ts"; import { Tty } from "../../../../shared/runtime/tty.service.ts"; import type { LegacyGenSigningKeyFlags } from "./signing-key.command.ts"; @@ -246,25 +247,6 @@ const isGitIgnored = Effect.fnUntraced(function* (filePath: string, searchFrom: .pipe(Effect.map((exitCode) => Option.some(Number(exitCode) === 0))); }); -const confirmOverwrite = Effect.fnUntraced(function* (title: string) { - const output = yield* Output; - const tty = yield* Tty; - const yes = yield* LegacyYesFlag; - if (yes) { - yield* output.raw(`${title} [Y/n] y\n`, "stderr"); - return true; - } - if (!tty.stdinIsTty) { - yield* output.raw(`${title} [Y/n] \n`, "stderr"); - return true; - } - // In json / stream-json mode `promptConfirm` fails with NonInteractiveError; treat that as a - // declined overwrite so the command cancels cleanly instead of corrupting the machine payload. - return yield* output - .promptConfirm(title, { defaultValue: true }) - .pipe(Effect.orElseSucceed(() => false)); -}); - export const legacyGenSigningKey = Effect.fn("legacy.gen.signing-key")(function* ( flags: LegacyGenSigningKeyFlags, ) { @@ -273,6 +255,7 @@ export const legacyGenSigningKey = Effect.fn("legacy.gen.signing-key")(function* const telemetryState = yield* LegacyTelemetryState; const output = yield* Output; const tty = yield* Tty; + const yes = yield* legacyResolveYes; const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const emphasize = (text: string) => styleIfTty(tty.stdoutIsTty, "bold", text); @@ -298,9 +281,30 @@ export const legacyGenSigningKey = Effect.fn("legacy.gen.signing-key")(function* const nextKeys = flags.append ? [...configured.value.existingKeys, key] : yield* Effect.gen(function* () { - const confirmed = yield* confirmOverwrite( - `Do you want to overwrite the existing ${emphasize(configured.value.displayPath)} file?`, - ); + // `legacyPromptYesNo` silently returns the default (true) for any non-text + // `--output-format`, but this command has no structured json/stream-json output + // (SIDE_EFFECTS.md) — that combination only arises from a real interactive TTY + // explicitly requesting machine output. Fail closed rather than silently + // overwriting irrecoverable key material. + const confirmed = + !yes && tty.stdinIsTty && output.format !== "text" + ? false + : yield* legacyPromptYesNo( + // `legacyPromptYesNo` checks `output.format !== "text"` BEFORE it checks + // TTY, so a non-TTY (piped or empty) invocation under `json`/`stream-json` + // would otherwise hit that check first and return the default without + // ever reading stdin. Go's `console.PromptYesNo` + // (apps/cli-go/internal/utils/console.go:64-82) has no concept of output + // format at all — it always reads piped stdin — so a piped `y`/`n` answer + // must be honored here the same as in text mode. Present a text-shaped + // view of `output` to reach that read; `raw`/`promptConfirm` write the + // prompt to stderr under every `Output` layer, so this never touches the + // machine-readable stdout payload. + output.format === "text" ? output : { ...output, format: "text" }, + yes, + `Do you want to overwrite the existing ${emphasize(configured.value.displayPath)} file?`, + true, + ); if (!confirmed) { return yield* Effect.fail( new LegacyGenSigningKeyCancelledError({ message: "context canceled" }), diff --git a/apps/cli/src/legacy/commands/gen/signing-key/signing-key.integration.test.ts b/apps/cli/src/legacy/commands/gen/signing-key/signing-key.integration.test.ts index 93ac85949d..549b3151f5 100644 --- a/apps/cli/src/legacy/commands/gen/signing-key/signing-key.integration.test.ts +++ b/apps/cli/src/legacy/commands/gen/signing-key/signing-key.integration.test.ts @@ -10,6 +10,7 @@ import { mockAnalytics, mockOutput, mockRuntimeInfo, + mockStdin, mockTty, processEnvLayer, } from "../../../../../tests/helpers/mocks.ts"; @@ -20,6 +21,7 @@ import { mockLegacyTelemetryStateTracked, useLegacyTempWorkdir, } from "../../../../../tests/helpers/legacy-mocks.ts"; +import { CliArgs } from "../../../../shared/cli/cli-args.service.ts"; import { LegacyDebugLogger } from "../../../shared/legacy-debug-logger.service.ts"; import { LEGACY_GLOBAL_FLAGS, LegacyYesFlag } from "../../../../shared/legacy/global-flags.ts"; import { textCliOutputFormatter } from "../../../../shared/output/text-formatter.ts"; @@ -32,6 +34,7 @@ import { legacyGenSigningKey } from "./signing-key.handler.ts"; const tempRoot = useLegacyTempWorkdir("supabase-gen-signing-key-int-"); interface SetupOptions { + readonly format?: "text" | "json" | "stream-json"; readonly stdinIsTty?: boolean; readonly yes?: boolean; readonly promptConfirmResponses?: ReadonlyArray; @@ -39,6 +42,10 @@ interface SetupOptions { // Exit code returned by the mocked `git check-ignore` subprocess. `0` means the path is // ignored, any non-zero code means it is not. Only consumed by the gitignore-warning branch. readonly gitCheckIgnoreExitCode?: number; + // Piped (non-TTY) stdin answer for the overwrite prompt (CLI-1865). + readonly pipedAnswer?: string; + // Raw argv for `legacyResolveYes`'s explicit `--yes=false` detection. + readonly cliArgs?: ReadonlyArray; } // `git check-ignore` is invoked via ChildProcessSpawner. Mock it with a controlled exit code so @@ -68,7 +75,7 @@ function mockGitCheckIgnore(exitCode: number) { function setup(options: SetupOptions = {}) { const out = mockOutput({ - format: "text", + format: options.format ?? "text", interactive: options.stdinIsTty ?? false, promptConfirmResponses: options.promptConfirmResponses, }); @@ -82,6 +89,8 @@ function setup(options: SetupOptions = {}) { const layer = Layer.mergeAll( buildLegacyTestRuntime({ out, api, cliConfig, tty, telemetry: telemetry?.layer }), Layer.succeed(LegacyYesFlag, options.yes ?? false), + Layer.succeed(CliArgs, { args: options.cliArgs ?? [] }), + mockStdin(options.stdinIsTty ?? false, options.pipedAnswer), Layer.succeed(LegacyDebugLogger, { debug: () => Effect.void, http: () => Effect.void, @@ -156,6 +165,8 @@ describe("legacy gen signing-key integration", () => { processEnvLayer({ SUPABASE_HOME: tempRoot.current }), mockRuntimeInfo({ cwd: tempRoot.current, homeDir: tempRoot.current }), mockTty({ stdinIsTty: false, stdoutIsTty: false }), + Layer.succeed(CliArgs, { args: [] }), + mockStdin(false), Layer.succeed( TelemetryRuntime, TelemetryRuntime.of({ @@ -203,8 +214,38 @@ describe("legacy gen signing-key integration", () => { }).pipe(Effect.provide(layer)); }); - it.live("overwrites the configured signing keys file and defaults to yes on non-tty", () => { - const { layer, out } = setup({ stdinIsTty: false }); + it.live( + "overwrites the configured signing keys file and defaults to yes on non-tty when stdin has no piped answer", + () => { + const { layer, out } = setup({ stdinIsTty: false }); + return Effect.gen(function* () { + yield* Effect.tryPromise(() => + writeConfig('[auth]\nsigning_keys_path = "./signing_keys.json"\n'), + ); + yield* Effect.tryPromise(() => + writeFile(join(tempRoot.current, "supabase", "signing_keys.json"), "[]\n"), + ); + + yield* legacyGenSigningKey({ algorithm: "RS256", append: false }); + + const saved = yield* Effect.tryPromise(() => + readFile(join(tempRoot.current, "supabase", "signing_keys.json"), "utf8"), + ); + const parsed = JSON.parse(saved) as ReadonlyArray>; + expect(parsed).toHaveLength(1); + expect(parsed[0]?.alg).toBe("RS256"); + expect(out.stderrText).toContain("Do you want to overwrite the existing"); + expect(out.stderrText).toContain("JWT signing key appended to: "); + expect(out.stderrText).toContain(join("supabase", "signing_keys.json")); + }).pipe(Effect.provide(layer)); + }, + ); + + // CLI-1865: Go's overwrite prompt reads piped stdin even in non-TTY mode and honors an + // explicit "n" — before this fix, TS returned `true` unconditionally without reading stdin at + // all, so `echo n | supabase gen signing-key` silently overwrote instead of canceling. + it.live("cancels the overwrite when a piped non-tty answer of 'n' is read", () => { + const { layer, out } = setup({ stdinIsTty: false, pipedAnswer: "n" }); return Effect.gen(function* () { yield* Effect.tryPromise(() => writeConfig('[auth]\nsigning_keys_path = "./signing_keys.json"\n'), @@ -213,17 +254,41 @@ describe("legacy gen signing-key integration", () => { writeFile(join(tempRoot.current, "supabase", "signing_keys.json"), "[]\n"), ); - yield* legacyGenSigningKey({ algorithm: "RS256", append: false }); + const exit = yield* Effect.exit(legacyGenSigningKey({ algorithm: "ES256", append: false })); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + const json = JSON.stringify(exit.cause); + expect(json).toContain("LegacyGenSigningKeyCancelledError"); + expect(json).toContain("context canceled"); + } + + const saved = yield* Effect.tryPromise(() => + readFile(join(tempRoot.current, "supabase", "signing_keys.json"), "utf8"), + ); + expect(JSON.parse(saved)).toEqual([]); + // Go's non-TTY prompt echoes the piped answer back to stderr after the label. + expect(out.stderrText).toContain("[Y/n] n\n"); + }).pipe(Effect.provide(layer)); + }); + + it.live("overwrites when a piped non-tty answer of 'y' is read", () => { + const { layer, out } = setup({ stdinIsTty: false, pipedAnswer: "y" }); + return Effect.gen(function* () { + yield* Effect.tryPromise(() => + writeConfig('[auth]\nsigning_keys_path = "./signing_keys.json"\n'), + ); + yield* Effect.tryPromise(() => + writeFile(join(tempRoot.current, "supabase", "signing_keys.json"), "[]\n"), + ); + + yield* legacyGenSigningKey({ algorithm: "ES256", append: false }); const saved = yield* Effect.tryPromise(() => readFile(join(tempRoot.current, "supabase", "signing_keys.json"), "utf8"), ); const parsed = JSON.parse(saved) as ReadonlyArray>; expect(parsed).toHaveLength(1); - expect(parsed[0]?.alg).toBe("RS256"); expect(out.stderrText).toContain("Do you want to overwrite the existing"); - expect(out.stderrText).toContain("JWT signing key appended to: "); - expect(out.stderrText).toContain(join("supabase", "signing_keys.json")); }).pipe(Effect.provide(layer)); }); @@ -440,6 +505,156 @@ describe("legacy gen signing-key integration", () => { }).pipe(Effect.provide(layer)); }); + // This command has no structured json/stream-json output (SIDE_EFFECTS.md), so a real TTY + // requesting machine output is an unsupported combination — fail closed on this destructive, + // irreversible overwrite rather than silently defaulting to yes with no prompt at all. + it.live( + "declines the overwrite without prompting on a tty when --output-format is not text", + () => { + const { layer, out } = setup({ format: "json", stdinIsTty: true }); + return Effect.gen(function* () { + yield* Effect.tryPromise(() => + writeConfig('[auth]\nsigning_keys_path = "./signing_keys.json"\n'), + ); + yield* Effect.tryPromise(() => + writeFile(join(tempRoot.current, "supabase", "signing_keys.json"), "[]\n"), + ); + + const exit = yield* Effect.exit(legacyGenSigningKey({ algorithm: "ES256", append: false })); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + const json = JSON.stringify(exit.cause); + expect(json).toContain("LegacyGenSigningKeyCancelledError"); + } + + const saved = yield* Effect.tryPromise(() => + readFile(join(tempRoot.current, "supabase", "signing_keys.json"), "utf8"), + ); + expect(JSON.parse(saved)).toEqual([]); + expect(out.promptConfirmCalls).toHaveLength(0); + }).pipe(Effect.provide(layer)); + }, + ); + + // CLI-1865 follow-up: `legacyPromptYesNo` checks `output.format !== "text"` BEFORE it + // checks TTY, so a non-TTY invocation under `json`/`stream-json` must not fall into that + // early return — this command has no structured json/stream-json payload, so a piped + // answer must be honored the same as text mode. Before this fix, a piped "n" here was + // silently ignored and the file was overwritten with the default (true). + it.live("honors a piped non-tty 'n' even when --output-format is json", () => { + const { layer, out } = setup({ format: "json", stdinIsTty: false, pipedAnswer: "n" }); + return Effect.gen(function* () { + yield* Effect.tryPromise(() => + writeConfig('[auth]\nsigning_keys_path = "./signing_keys.json"\n'), + ); + yield* Effect.tryPromise(() => + writeFile(join(tempRoot.current, "supabase", "signing_keys.json"), "[]\n"), + ); + + const exit = yield* Effect.exit(legacyGenSigningKey({ algorithm: "ES256", append: false })); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(JSON.stringify(exit.cause)).toContain("LegacyGenSigningKeyCancelledError"); + } + + const saved = yield* Effect.tryPromise(() => + readFile(join(tempRoot.current, "supabase", "signing_keys.json"), "utf8"), + ); + expect(JSON.parse(saved)).toEqual([]); + expect(out.promptConfirmCalls).toHaveLength(0); + }).pipe(Effect.provide(layer)); + }); + + it.live("honors a piped non-tty 'y' when --output-format is stream-json", () => { + const { layer } = setup({ format: "stream-json", stdinIsTty: false, pipedAnswer: "y" }); + return Effect.gen(function* () { + yield* Effect.tryPromise(() => + writeConfig('[auth]\nsigning_keys_path = "./signing_keys.json"\n'), + ); + yield* Effect.tryPromise(() => + writeFile(join(tempRoot.current, "supabase", "signing_keys.json"), "[]\n"), + ); + + yield* legacyGenSigningKey({ algorithm: "ES256", append: false }); + + const saved = yield* Effect.tryPromise(() => + readFile(join(tempRoot.current, "supabase", "signing_keys.json"), "utf8"), + ); + expect(JSON.parse(saved) as ReadonlyArray).toHaveLength(1); + }).pipe(Effect.provide(layer)); + }); + + it.live("honors SUPABASE_YES and overwrites even when a piped 'n' is present", () => { + // Go reads `viper.GetBool("YES")` (incl. the SUPABASE_YES env var) BEFORE scanning + // stdin (`console.go:71`), so `SUPABASE_YES=1 printf 'n\n' | supabase gen signing-key` + // auto-confirms and overwrites rather than consuming the piped `n`. The handler + // resolves `yes` via `legacyResolveYes`, not the raw --yes flag. + const prev = process.env["SUPABASE_YES"]; + process.env["SUPABASE_YES"] = "1"; + const { layer } = setup({ stdinIsTty: false, pipedAnswer: "n" }); + return Effect.gen(function* () { + yield* Effect.tryPromise(() => + writeConfig('[auth]\nsigning_keys_path = "./signing_keys.json"\n'), + ); + yield* Effect.tryPromise(() => + writeFile(join(tempRoot.current, "supabase", "signing_keys.json"), "[]\n"), + ); + + yield* legacyGenSigningKey({ algorithm: "ES256", append: false }); + + const saved = yield* Effect.tryPromise(() => + readFile(join(tempRoot.current, "supabase", "signing_keys.json"), "utf8"), + ); + const parsed = JSON.parse(saved) as ReadonlyArray>; + expect(parsed).toHaveLength(1); + }).pipe( + Effect.ensuring( + Effect.sync(() => { + if (prev === undefined) delete process.env["SUPABASE_YES"]; + else process.env["SUPABASE_YES"] = prev; + }), + ), + Effect.provide(layer), + ); + }); + + it.live("an explicit --yes=false overrides SUPABASE_YES and honors a piped 'n'", () => { + const prev = process.env["SUPABASE_YES"]; + process.env["SUPABASE_YES"] = "1"; + const { layer } = setup({ + stdinIsTty: false, + pipedAnswer: "n", + cliArgs: ["gen", "signing-key", "--yes=false"], + }); + return Effect.gen(function* () { + yield* Effect.tryPromise(() => + writeConfig('[auth]\nsigning_keys_path = "./signing_keys.json"\n'), + ); + yield* Effect.tryPromise(() => + writeFile(join(tempRoot.current, "supabase", "signing_keys.json"), "[]\n"), + ); + + const exit = yield* Effect.exit(legacyGenSigningKey({ algorithm: "ES256", append: false })); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(JSON.stringify(exit.cause)).toContain("LegacyGenSigningKeyCancelledError"); + } + + const saved = yield* Effect.tryPromise(() => + readFile(join(tempRoot.current, "supabase", "signing_keys.json"), "utf8"), + ); + expect(JSON.parse(saved)).toEqual([]); + }).pipe( + Effect.ensuring( + Effect.sync(() => { + if (prev === undefined) delete process.env["SUPABASE_YES"]; + else process.env["SUPABASE_YES"] = prev; + }), + ), + Effect.provide(layer), + ); + }); + it.live("flushes telemetry state after the command finishes", () => { const { layer, telemetry } = setup({ trackTelemetry: true }); return Effect.gen(function* () { From 4ade688c62fc5344674763cd42e9f815791a9adf Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Tue, 7 Jul 2026 11:37:38 +0100 Subject: [PATCH 02/79] docs(cli): correct inaccurate SIDE_EFFECTS.md and AGENTS.md claims (#5813) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What kind of change does this PR introduce? Docs fix — Legacy port SIDE_EFFECTS.md / AGENTS.md accuracy (F3-F10, F12 from the parity audit). ## What is the current behavior? 11 `SIDE_EFFECTS.md` files and `AGENTS.md` contained factual errors where the code was already correct but the documentation was wrong — wrong file paths, a fabricated stdout message, an inverted claim about whether a command writes the linked-project cache, a missing json/stream-json output envelope, a fictitious subprocess name, a backwards project-ref claim, and a fabricated telemetry-identity call. Each claim was independently re-verified against `apps/cli-go/` (and, for the `completion` exit-code claim, against the actual compiled Go binary and the actual TS CLI) before editing. ## What is the new behavior? - **F3**: `db/branch/{create,delete,list,switch}` — corrected the current-branch file path to `supabase/.branches/_current_branch`, matching `internal/utils/misc.go:99`. - **F4**: `db/remote/commit` — removed the false claim it prints `Finished supabase db pull.` (that belongs only to `db pull`). - **F5**: `db/lint` — corrected: it DOES write the linked-project cache on `--linked`, matching Go's `ensureProjectGroupsCached`. - **F6**: `db/schema/declarative/generate` — corrected: it has a real json/stream-json success envelope. - **F7**: `db/schema/declarative/sync` — replaced a fictitious `migration up --local` subprocess with the real `db reset --local` recovery-path subprocess. - **F8**: `completion` — the doc's exit-code claim was backwards. Go exits `0` on both bare `completion` and an unrecognized shell subcommand (verified against the compiled binary); the legacy TS shell currently exits `1` for both — a genuine, systemic exit-code bug in the shared CLI harness, not `completion`-specific (it reproduces on any bare/unrecognized group-command invocation, e.g. `branches`). Rather than fixing the code here or describing the bug as intended, the doc now states Go's true behavior and flags the current TS divergence, filed separately as CLI-1906. - **F9**: `config/push` + `AGENTS.md` — corrected the linked-project-cache path (was a fabricated `~/.supabase//...`, real path is `/supabase/.temp/linked-project.json`) and fixed an adjacent conflation where the project-ref-fallback file was wrongly attributed to the same cache file, when it's actually a separate file (`/supabase/.temp/project-ref`). - **F10**: `branches/update` — corrected: the upgrade-suggest call uses the branch's own resolved ref, not the parent `--project-ref`. - **F12**: `AGENTS.md` telemetry table — removed a fabricated `analytics.identify(gotrueId)` claim from the `login` row; Go's `StitchLogin` only aliases. A second, wider pre-existing doc bug was found during F9's verification (the same fabricated linked-project-cache path is copy-pasted across 29 other `SIDE_EFFECTS.md` files not touched here) — filed separately as CLI-1907 rather than expanding this diff. Docs-only change; no `.ts`/`.go` files touched. `pnpm check:all` passes. --- apps/cli/AGENTS.md | 18 ++++++------ .../commands/branches/update/SIDE_EFFECTS.md | 10 +++---- .../commands/completion/SIDE_EFFECTS.md | 24 +++++++++++----- .../commands/config/push/SIDE_EFFECTS.md | 23 +++++++-------- .../commands/db/branch/create/SIDE_EFFECTS.md | 6 ++-- .../commands/db/branch/delete/SIDE_EFFECTS.md | 6 ++-- .../commands/db/branch/list/SIDE_EFFECTS.md | 2 +- .../commands/db/branch/switch/SIDE_EFFECTS.md | 6 ++-- .../legacy/commands/db/lint/SIDE_EFFECTS.md | 13 +++++---- .../commands/db/remote/commit/SIDE_EFFECTS.md | 5 +++- .../declarative/generate/SIDE_EFFECTS.md | 12 ++++++-- .../schema/declarative/sync/SIDE_EFFECTS.md | 28 +++++++++---------- 12 files changed, 88 insertions(+), 65 deletions(-) diff --git a/apps/cli/AGENTS.md b/apps/cli/AGENTS.md index c30465cd85..2aee95eb10 100644 --- a/apps/cli/AGENTS.md +++ b/apps/cli/AGENTS.md @@ -90,14 +90,14 @@ Always check `src/shared/` before writing new infrastructure. Do not duplicate w Also check the following `legacy/` infrastructure before writing equivalent helpers from scratch: -| Path | What it provides | -| ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `legacy/config/legacy-cli-config.layer.ts` | `LegacyCliConfig` — resolves `SUPABASE_PROFILE` (built-in name **or** YAML file path), `--workdir`, `--experimental`, project-id from `supabase/config.toml` | -| `legacy/config/legacy-project-ref.layer.ts` | `LegacyProjectRefResolver` — `--project-ref` flag → env → linked-project.json → config fallback chain; matches Go's resolver order | -| `legacy/telemetry/legacy-telemetry-state.layer.ts` | `LegacyTelemetryState.flush` — writes `~/.supabase/telemetry.json`, runs in every command's `Effect.ensuring` | -| `legacy/telemetry/legacy-linked-project-cache.layer.ts` | `LegacyLinkedProjectCache.cache(ref)` — writes `~/.supabase//linked-project.json` after `--project-ref` resolves; bypasses generated schema validation (uses raw HTTP client) | -| `legacy/auth/legacy-http-debug.layer.ts` | `legacyHttpClientLayer` — wraps the HTTP transport with a `--debug` stderr logger in Go's `log.LstdFlags` format | -| `legacy/output/legacy-glamour-table.ts` | `renderGlamourTable(headers, rows)` — byte-exact ASCII match for Go's `glamour.RenderTable(..., AsciiStyle)` | +| Path | What it provides | +| ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `legacy/config/legacy-cli-config.layer.ts` | `LegacyCliConfig` — resolves `SUPABASE_PROFILE` (built-in name **or** YAML file path), `--workdir`, `--experimental`, project-id from `supabase/config.toml` | +| `legacy/config/legacy-project-ref.layer.ts` | `LegacyProjectRefResolver` — `--project-ref` flag → env → `supabase/.temp/project-ref` file → prompt; matches Go's resolver order | +| `legacy/telemetry/legacy-telemetry-state.layer.ts` | `LegacyTelemetryState.flush` — writes `~/.supabase/telemetry.json`, runs in every command's `Effect.ensuring` | +| `legacy/telemetry/legacy-linked-project-cache.layer.ts` | `LegacyLinkedProjectCache.cache(ref)` — writes `/supabase/.temp/linked-project.json` after `--project-ref` resolves; bypasses generated schema validation (uses raw HTTP client) | +| `legacy/auth/legacy-http-debug.layer.ts` | `legacyHttpClientLayer` — wraps the HTTP transport with a `--debug` stderr logger in Go's `log.LstdFlags` format | +| `legacy/output/legacy-glamour-table.ts` | `renderGlamourTable(headers, rows)` — byte-exact ASCII match for Go's `glamour.RenderTable(..., AsciiStyle)` | --- @@ -286,7 +286,7 @@ The legacy shell sends the same PostHog events to the same product analytics pip | Command | Event | Identity / groups | Go source | | ------------------------------------------------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------ | --------------------------------------------- | - | `login` | `cli_login_completed` | `analytics.alias(gotrueId, deviceId)` + `analytics.identify(gotrueId)` after token persists | `internal/login/login.go:283-296` | + | `login` | `cli_login_completed` | `analytics.alias(gotrueId, deviceId)` after token persists | `internal/login/login.go:283-296` | | `link` | `cli_project_linked` | `analytics.groupIdentify("organization", slug, …)` + `analytics.groupIdentify("project", ref, …)` after link write | `internal/link/link.go:60` | | `start` | `cli_stack_started` | none — fired after stack health check passes | `internal/start/start.go:1245` | | `sso/{list,create,update,remove}`, `branches/{create,update}` | `cli_upgrade_suggested` | none — payload is `{feature_key, org_slug}`, fired inside billing-gate error branch | 7 call-sites under `internal/{sso,branches}/` | diff --git a/apps/cli/src/legacy/commands/branches/update/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/branches/update/SIDE_EFFECTS.md index 09bab3fd8f..4983406350 100644 --- a/apps/cli/src/legacy/commands/branches/update/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/branches/update/SIDE_EFFECTS.md @@ -6,10 +6,10 @@ Same auth and project-ref resolution chain as every Management-API legacy comman ## Files Written -| Path | Format | When | -| ------------------------------------------------ | ------ | ------------------------------------------------------------------------ | -| `~/.supabase//linked-project.json` | JSON | always (in `Effect.ensuring`) after `--project-ref` resolves — Go parity | -| `~/.supabase/telemetry.json` | JSON | always (in `Effect.ensuring`) at end of command — Go parity | +| Path | Format | When | +| ---------------------------------------------- | ------ | ------------------------------------------------------------------------ | +| `/supabase/.temp/linked-project.json` | JSON | always (in `Effect.ensuring`) after `--project-ref` resolves — Go parity | +| `~/.supabase/telemetry.json` | JSON | always (in `Effect.ensuring`) at end of command — Go parity | ## API Routes @@ -51,4 +51,4 @@ In Go encoder modes, the header goes to stderr followed by the encoded payload o ## Notes -The upgrade-suggest call uses the parent project ref (resolved from `--project-ref`) rather than the branch's project ref. Both refs belong to the same organization, so the entitlement check returns the same `org_slug` either way; this also sidesteps a known API schema constraint where `getProject` strictly requires a `^[a-z]{20}$` ref. +The upgrade-suggest call uses the branch's own resolved project ref (`legacyResolveBranchProjectRef`), matching Go's `update.go:26` (`pause.GetBranchProjectRef`) — not the parent `--project-ref` value — so the entitlements check is scoped to the branch's org. diff --git a/apps/cli/src/legacy/commands/completion/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/completion/SIDE_EFFECTS.md index f25912c9c5..ed1b76d1c2 100644 --- a/apps/cli/src/legacy/commands/completion/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/completion/SIDE_EFFECTS.md @@ -26,10 +26,10 @@ ## Exit Codes -| Code | Condition | -| ---- | ------------------------------------------------------------------ | -| `0` | success — completion script for the chosen shell printed to stdout | -| `1` | invocation error (missing or unknown shell subcommand) | +| Code | Condition | +| ---- | -------------------------------------------------------------------------------------------------------------------- | +| `0` | success — completion script for the chosen shell printed to stdout | +| `1` | unknown shell subcommand, or bare `completion` with no shell subcommand — **known divergence, see Notes (CLI-1906)** | ## Output @@ -58,9 +58,19 @@ straight to the Go binary. - Effect CLI's `--completions` global flag remains exposed at the root for `next/` users; it does not satisfy the legacy parity contract and is not what this subcommand routes through. -- The Go CLI exits non-zero when called without a shell subcommand (e.g. - `supabase completion`). Effect CLI surfaces the same condition through its usual - "missing subcommand" help-with-exit-1 behavior. +- **Known divergence (CLI-1906):** Go's cobra CLI exits `0` on both bare + `completion` (no shell subcommand) AND `completion ` — cobra + treats an unrecognized subcommand name the same as a missing one: a + non-`Runnable()` command with no `RunE` returns `flag.ErrHelp`, which cobra + maps to printing help and returning a nil error (verified against the + compiled Go binary for both cases: `completion` and `completion +bogus-shell` both exit `0`). The legacy TS shell currently exits `1` for + both invocations; this is a real, systemic exit-code bug in the shared CLI + harness (`shared/cli/run.ts`), not `completion`-specific — it reproduces on + any bare or unrecognized-subcommand invocation of a group command with + subcommands (e.g. `branches`, `branches bogus-subcommand`). See CLI-1906 for + the fix; this doc describes current (buggy) behavior, not the intended + target. - Each of `bash`/`zsh`/`fish`/`powershell` declares `--no-descriptions` (cobra's auto-registered flag, `completions.go` in `spf13/cobra`) and forwards it to the Go binary, so the emitted script omits completion descriptions exactly as it diff --git a/apps/cli/src/legacy/commands/config/push/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/config/push/SIDE_EFFECTS.md index 8a15ea687b..660cddd1fe 100644 --- a/apps/cli/src/legacy/commands/config/push/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/config/push/SIDE_EFFECTS.md @@ -7,20 +7,21 @@ local → if changed, print the unified diff and confirm → PATCH/PUT/POST. ## Files Read -| Path | Format | When | -| ------------------------------------------------ | ------------------------- | --------------------------------------------------------------- | -| `/supabase/config.toml` | TOML | always, before any network call (parse error aborts, exit 1) | -| `/supabase/.env`, `.env.local` | dotenv | always, to resolve `env(VAR)` references inside `config.toml` | -| Auth email template HTML (`content_path`) | HTML | when `auth.enabled`; paths resolved per Go rules (see below) | -| `~/.supabase//linked-project.json` | JSON | project-ref fallback (flag → `SUPABASE_PROJECT_ID` → this file) | -| `~/.supabase/access-token` | plain text (token string) | when `SUPABASE_ACCESS_TOKEN` unset and keyring unavailable | +| Path | Format | When | +| ---------------------------------------------- | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `/supabase/config.toml` | TOML | always, before any network call (parse error aborts, exit 1) | +| `/supabase/.env`, `.env.local` | dotenv | always, to resolve `env(VAR)` references inside `config.toml` | +| Auth email template HTML (`content_path`) | HTML | when `auth.enabled`; paths resolved per Go rules (see below) | +| `/supabase/.temp/project-ref` | plain text | project-ref fallback (flag → `SUPABASE_PROJECT_ID` → this file) | +| `/supabase/.temp/linked-project.json` | JSON | existence check only, to decide whether the cache write below is skipped (mirrors Go's `ensureProjectGroupsCached` telemetry cache — see `db/lint`'s Notes for the full mechanism) | +| `~/.supabase/access-token` | plain text (token string) | when `SUPABASE_ACCESS_TOKEN` unset and keyring unavailable | ## Files Written -| Path | Format | When | -| ------------------------------------------------ | ------ | ---------------------------------------------------------------------- | -| `~/.supabase//linked-project.json` | JSON | `Effect.ensuring` after run (success **and** failure), if ref resolved | -| `~/.supabase/telemetry.json` | JSON | `Effect.ensuring` after run (success **and** failure) | +| Path | Format | When | +| ---------------------------------------------- | ------ | ---------------------------------------------------------------------- | +| `/supabase/.temp/linked-project.json` | JSON | `Effect.ensuring` after run (success **and** failure), if ref resolved | +| `~/.supabase/telemetry.json` | JSON | `Effect.ensuring` after run (success **and** failure) | No writes to `config.toml`. diff --git a/apps/cli/src/legacy/commands/db/branch/create/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/db/branch/create/SIDE_EFFECTS.md index 5987e44efc..bd930c4242 100644 --- a/apps/cli/src/legacy/commands/db/branch/create/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/db/branch/create/SIDE_EFFECTS.md @@ -8,9 +8,9 @@ ## Files Written -| Path | Format | When | -| ------------------------------------------------ | --------- | ------ | -| `/.branches//` (directory) | directory | always | +| Path | Format | When | +| --------------------------------------------------------- | --------- | ------ | +| `/supabase/.branches//` (directory) | directory | always | ## API Routes diff --git a/apps/cli/src/legacy/commands/db/branch/delete/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/db/branch/delete/SIDE_EFFECTS.md index 6b392e7110..97c31f4eb1 100644 --- a/apps/cli/src/legacy/commands/db/branch/delete/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/db/branch/delete/SIDE_EFFECTS.md @@ -8,9 +8,9 @@ ## Files Written -| Path | Format | When | -| ------------------------------------------------ | --------- | ------- | -| `/.branches//` (directory) | directory | removed | +| Path | Format | When | +| --------------------------------------------------------- | --------- | ------- | +| `/supabase/.branches//` (directory) | directory | removed | ## API Routes diff --git a/apps/cli/src/legacy/commands/db/branch/list/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/db/branch/list/SIDE_EFFECTS.md index 14e1803271..bbbe030daf 100644 --- a/apps/cli/src/legacy/commands/db/branch/list/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/db/branch/list/SIDE_EFFECTS.md @@ -48,4 +48,4 @@ Not applicable. ## Notes - Deprecated in the Go CLI: use `branches list` instead. -- This is a local-only operation listing branches in `/.branches/`. +- This is a local-only operation listing branches in `/supabase/.branches/`. diff --git a/apps/cli/src/legacy/commands/db/branch/switch/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/db/branch/switch/SIDE_EFFECTS.md index 5cce0f2423..aa1cd0cbf5 100644 --- a/apps/cli/src/legacy/commands/db/branch/switch/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/db/branch/switch/SIDE_EFFECTS.md @@ -8,9 +8,9 @@ ## Files Written -| Path | Format | When | -| ------------------------------------ | ---------- | ------ | -| `/.supabase/current-branch` | plain text | always | +| Path | Format | When | +| ---------------------------------------------- | ---------- | ------ | +| `/supabase/.branches/_current_branch` | plain text | always | ## API Routes diff --git a/apps/cli/src/legacy/commands/db/lint/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/db/lint/SIDE_EFFECTS.md index f76f966c3c..49f2aac8ed 100644 --- a/apps/cli/src/legacy/commands/db/lint/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/db/lint/SIDE_EFFECTS.md @@ -12,15 +12,18 @@ Native TypeScript port of Go's `internal/db/lint`. ## Files Written -| Path | Format | When | -| ---------------------------- | ------ | ---------------------------------------------------- | -| `~/.supabase/telemetry.json` | JSON | always (PostHog state flush, Go `PersistentPostRun`) | +| Path | Format | When | +| ---------------------------------------------- | ------ | ------------------------------------------------------------------------------------------------------------------- | +| `/supabase/.temp/linked-project.json` | JSON | `--linked` only, via `LegacyLinkedProjectCache.cache` (Go's `ensureProjectGroupsCached`, `cmd/root.go:174,212-234`) | +| `~/.supabase/telemetry.json` | JSON | always (PostHog state flush, Go `PersistentPostRun`) | No user data is written: the lint runs inside a transaction that is **always rolled back** (`BEGIN` … `ROLLBACK`), matching Go — including `CREATE EXTENSION plpgsql_check`, which is issued on the same connection inside -the open transaction and so is rolled back too. `db lint` does not write the -linked-project cache (it has no `LegacyLinkedProjectCache` dependency). +the open transaction and so is rolled back too. `db lint` DOES write the +linked-project cache when run with `--linked` (matching Go's +`ensureProjectGroupsCached`); the default `--local` and `--db-url` paths never +populate a project ref, so no cache write occurs there. ## API Routes diff --git a/apps/cli/src/legacy/commands/db/remote/commit/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/db/remote/commit/SIDE_EFFECTS.md index 2b2e6ad3d2..88785a5530 100644 --- a/apps/cli/src/legacy/commands/db/remote/commit/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/db/remote/commit/SIDE_EFFECTS.md @@ -37,7 +37,10 @@ ### `--output-format text` (Go CLI compatible) -Prints `Finished supabase db pull.` on success. +Prints `Schema written to ` to stderr on success (from the shared +`pull.Run`, `internal/db/pull/pull.go:72`); no stdout confirmation message is +printed. The `Finished supabase db pull.` PostRun message belongs only to +`db pull` (`cmd/db.go:198-200`), not `db remote commit`. ### `--output-format json` diff --git a/apps/cli/src/legacy/commands/db/schema/declarative/generate/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/db/schema/declarative/generate/SIDE_EFFECTS.md index 5df90ac1a9..de3e3b967f 100644 --- a/apps/cli/src/legacy/commands/db/schema/declarative/generate/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/db/schema/declarative/generate/SIDE_EFFECTS.md @@ -54,9 +54,15 @@ pg-delta catalog (source) against the target database's catalog (target). ## Output -Text mode only (no machine envelope). Diagnostics + the final -`Declarative schema written to ` go to stderr; the PostRun prints -`Finished supabase db schema declarative generate.` to stdout on success. +Diagnostics (target resolution, prompts, `Declarative schema written to `) +always go to stderr, in every `--output-format`. On success: + +- `text` mode prints `Finished supabase db schema declarative generate.` to + stdout (matches Go's PostRun `fmt.Println`, `cmd/db_schema_declarative.go:116-118`). +- `json`/`stream-json` mode instead emits a structured success envelope + (`output.success("Finished supabase db schema declarative generate.")`) so + the machine stdout payload isn't corrupted by a bare human line + (`generate.command.ts:74-90`, CLI-1546 invariant). ## Notes diff --git a/apps/cli/src/legacy/commands/db/schema/declarative/sync/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/db/schema/declarative/sync/SIDE_EFFECTS.md index 53d1a64fad..83250c0487 100644 --- a/apps/cli/src/legacy/commands/db/schema/declarative/sync/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/db/schema/declarative/sync/SIDE_EFFECTS.md @@ -23,12 +23,12 @@ as a new timestamped migration. ## Subprocesses / Containers -| What | When | -| -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | -| `supabase-go db schema declarative __catalog --mode migrations --experimental` (seam) — shadow Postgres + `SetupDatabase` + apply migrations → catalog | always | -| `supabase-go db schema declarative __catalog --mode declarative --experimental` (seam) — shadow Postgres + `SetupDatabase` + apply declarative → catalog | always | -| Edge-runtime container running the pg-delta diff Deno script | always | -| `supabase-go migration up --local` | when the migration is applied (`--apply` / prompt / `--yes`) | +| What | When | +| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | +| `supabase-go db schema declarative __catalog --mode migrations --experimental` (seam) — shadow Postgres + `SetupDatabase` + apply migrations → catalog | always | +| `supabase-go db schema declarative __catalog --mode declarative --experimental` (seam) — shadow Postgres + `SetupDatabase` + apply declarative → catalog | always | +| Edge-runtime container running the pg-delta diff Deno script | always | +| `supabase-go db reset --local [--network-id ]` (seam) — only on the failed-apply recovery path; `db reset` is still Go-proxied (`wrapped`), so the reset itself shells out to the bundled binary | TTY only, apply failed, and the user confirms "reset and reapply" | ## Environment Variables @@ -42,14 +42,14 @@ as a new timestamped migration. ## Exit Codes -| Code | Condition | -| ---- | ------------------------------------------------------------------ | -| `0` | success (migration created, applied, or "No schema changes found") | -| `1` | conflicting `--apply`/`--no-apply` (mutually exclusive) | -| `1` | pg-delta not enabled | -| `1` | no declarative schema files found | -| `1` | shadow-database / edge-runtime / diff failure | -| `1` | apply failure (when applied) — propagated from `migration up` | +| Code | Condition | +| ---- | --------------------------------------------------------------------------------------------------- | +| `0` | success (migration created, applied, or "No schema changes found") | +| `1` | conflicting `--apply`/`--no-apply` (mutually exclusive) | +| `1` | pg-delta not enabled | +| `1` | no declarative schema files found | +| `1` | shadow-database / edge-runtime / diff failure | +| `1` | apply failure (when applied) — propagated from the native migration apply (`applyMigrationToLocal`) | ## Output From 47e9a2c61b1e83b4d96e3407d973cb8b66c43575 Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Tue, 7 Jul 2026 11:37:42 +0100 Subject: [PATCH 03/79] docs(cli): fix fabricated SUPABASE_API_URL and bare PROJECT_ID env-var references (#5811) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What kind of change does this PR introduce? Docs fix — Legacy port SIDE_EFFECTS.md accuracy. ## What is the current behavior? Two systemic env-var documentation errors across `src/legacy/**/SIDE_EFFECTS.md`, found in the parity audit: - **~36 files** document `SUPABASE_API_URL` as a real override for the Management API base URL. No such env var exists in Go: `SetEnvPrefix("SUPABASE")` + `AutomaticEnv()` never binds `API_URL`, there's no `os.Getenv("SUPABASE_API_URL")` anywhere, and the `Profile.api_url` field loads on a separate, unbound `viper.New()` instance (`internal/utils/profile.go:99`). The real override is `SUPABASE_PROFILE` (built-in name or a path to a YAML profile file). - **~18 files** document bare `PROJECT_ID` as if it were itself a settable env var. It's Go's internal viper key name — the real, user-facing env var is `SUPABASE_PROJECT_ID` (`viper.GetString("PROJECT_ID")` under the global `SetEnvPrefix("SUPABASE")` singleton, `internal/utils/flags/project_ref.go:62`). Both claims were independently verified against the real Go source and, for the viper-specific mechanics, a compiled test program against the exact pinned `github.com/spf13/viper v1.21.0`. ## What is the new behavior? - Removed the fabricated `SUPABASE_API_URL` rows; where a file didn't already document `SUPABASE_PROFILE`, added a real row for it (including the persisted `~/.supabase/profile` fallback step in the resolution chain, matching `legacy-cli-config.layer.ts`). - Corrected every bare `PROJECT_ID` mention (Environment Variables tables, Files Read "When" columns, and prose) to `SUPABASE_PROJECT_ID`, converging on the dominant convention already used correctly across ~15 sibling files. - Left `db/advisors/SIDE_EFFECTS.md` untouched — it already correctly documents that `SUPABASE_API_URL` is not honored. - Filed follow-ups for two related-but-out-of-scope findings surfaced during review: CLI-1904 (global choice-flag telemetry gap, unrelated) and CLI-1905 (bare `DB_PASSWORD` has the same bug class as this PR's F2, in a set of files this PR doesn't touch). Docs-only change; no `.ts` files touched. `pnpm check:all` passes. --- apps/cli/src/legacy/SIDE_EFFECTS_TEMPLATE.md | 2 +- .../commands/backups/list/SIDE_EFFECTS.md | 1 - .../commands/backups/restore/SIDE_EFFECTS.md | 1 - .../legacy/commands/bootstrap/SIDE_EFFECTS.md | 14 +++++------ .../commands/config/push/SIDE_EFFECTS.md | 1 - .../legacy/commands/domains/SIDE_EFFECTS.md | 10 ++++---- .../commands/encryption/SIDE_EFFECTS.md | 23 +++++++++---------- .../commands/functions/delete/SIDE_EFFECTS.md | 2 +- .../functions/download/SIDE_EFFECTS.md | 2 +- .../commands/functions/list/SIDE_EFFECTS.md | 1 - .../legacy/commands/gen/keys/SIDE_EFFECTS.md | 8 +++---- .../legacy/commands/gen/types/SIDE_EFFECTS.md | 2 +- .../commands/inspect/db/SIDE_EFFECTS.md | 2 +- .../commands/inspect/report/SIDE_EFFECTS.md | 12 +++++----- .../commands/network-bans/get/SIDE_EFFECTS.md | 12 +++++----- .../network-bans/remove/SIDE_EFFECTS.md | 12 +++++----- .../network-restrictions/get/SIDE_EFFECTS.md | 12 +++++----- .../update/SIDE_EFFECTS.md | 12 +++++----- .../postgres-config/delete/SIDE_EFFECTS.md | 12 +++++----- .../postgres-config/get/SIDE_EFFECTS.md | 12 +++++----- .../postgres-config/update/SIDE_EFFECTS.md | 12 +++++----- .../legacy/commands/projects/SIDE_EFFECTS.md | 2 +- .../projects/api-keys/SIDE_EFFECTS.md | 2 +- .../commands/projects/create/SIDE_EFFECTS.md | 2 +- .../commands/projects/delete/SIDE_EFFECTS.md | 2 +- .../commands/projects/list/SIDE_EFFECTS.md | 2 +- .../commands/secrets/list/SIDE_EFFECTS.md | 1 - .../commands/secrets/set/SIDE_EFFECTS.md | 1 - .../commands/secrets/unset/SIDE_EFFECTS.md | 1 - .../legacy/commands/services/SIDE_EFFECTS.md | 2 +- .../snippets/download/SIDE_EFFECTS.md | 5 ++-- .../commands/snippets/list/SIDE_EFFECTS.md | 5 ++-- .../ssl-enforcement/get/SIDE_EFFECTS.md | 12 +++++----- .../ssl-enforcement/update/SIDE_EFFECTS.md | 12 +++++----- .../activate/SIDE_EFFECTS.md | 12 +++++----- .../check-availability/SIDE_EFFECTS.md | 12 +++++----- .../vanity-subdomains/delete/SIDE_EFFECTS.md | 12 +++++----- .../vanity-subdomains/get/SIDE_EFFECTS.md | 12 +++++----- 38 files changed, 126 insertions(+), 136 deletions(-) diff --git a/apps/cli/src/legacy/SIDE_EFFECTS_TEMPLATE.md b/apps/cli/src/legacy/SIDE_EFFECTS_TEMPLATE.md index a27b7f3ab6..9ab7ef9ade 100644 --- a/apps/cli/src/legacy/SIDE_EFFECTS_TEMPLATE.md +++ b/apps/cli/src/legacy/SIDE_EFFECTS_TEMPLATE.md @@ -61,7 +61,7 @@ | 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` | built-in profile name or YAML file path | no (falls back to `~/.supabase/profile` -> `supabase`) | ## Exit Codes 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 f69b0219d6..cff8a366c1 100644 --- a/apps/cli/src/legacy/commands/backups/list/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/backups/list/SIDE_EFFECTS.md @@ -31,7 +31,6 @@ | `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:` (Go parity — used by the cli-e2e test harness). | no (defaults to `supabase`) | | `SUPABASE_PROJECT_ID` | project ref fallback when `--project-ref` is unset | no (also reads `/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`) | -| ~~`SUPABASE_API_URL`~~ | **not honored** — Go parity. Use `SUPABASE_PROFILE` to override the API base URL. | — | ## Exit Codes 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 8b08e2727b..768bb9b728 100644 --- a/apps/cli/src/legacy/commands/backups/restore/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/backups/restore/SIDE_EFFECTS.md @@ -31,7 +31,6 @@ | `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:` (Go parity — used by the cli-e2e test harness). | no (defaults to `supabase`) | | `SUPABASE_PROJECT_ID` | project ref fallback when `--project-ref` is unset | no (also reads `/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`) | -| ~~`SUPABASE_API_URL`~~ | **not honored** — Go parity. Use `SUPABASE_PROFILE` to override the API base URL. | — | ## Exit Codes diff --git a/apps/cli/src/legacy/commands/bootstrap/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/bootstrap/SIDE_EFFECTS.md index a6428d8f47..6c5c7cefbe 100644 --- a/apps/cli/src/legacy/commands/bootstrap/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/bootstrap/SIDE_EFFECTS.md @@ -46,13 +46,13 @@ leaking the change to the surrounding process. ## Environment Variables -| Variable | Purpose | Required? | -| -------------------------------------- | -------------------------------------------------- | --------- | -| `SUPABASE_WORKDIR` | target dir (`--workdir` flag → env → prompt → cwd) | no | -| `SUPABASE_DB_PASSWORD` | DB password (`-p` flag → env → prompt/generate) | no | -| `GITHUB_TOKEN` | raise the GitHub API rate limit for template fetch | no | -| `SUPABASE_ACCESS_TOKEN` | auth bypass for ensure-login | no | -| `SUPABASE_API_URL`, `SUPABASE_PROFILE` | API host / profile | no | +| Variable | Purpose | Required? | +| ----------------------- | ------------------------------------------------------------ | --------- | +| `SUPABASE_WORKDIR` | target dir (`--workdir` flag → env → prompt → cwd) | no | +| `SUPABASE_DB_PASSWORD` | DB password (`-p` flag → env → prompt/generate) | no | +| `GITHUB_TOKEN` | raise the GitHub API rate limit for template fetch | no | +| `SUPABASE_ACCESS_TOKEN` | auth bypass for ensure-login | no | +| `SUPABASE_PROFILE` | profile name/path (env → `~/.supabase/profile` → `supabase`) | no | ## Exit Codes diff --git a/apps/cli/src/legacy/commands/config/push/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/config/push/SIDE_EFFECTS.md index 660cddd1fe..374a785a22 100644 --- a/apps/cli/src/legacy/commands/config/push/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/config/push/SIDE_EFFECTS.md @@ -56,7 +56,6 @@ when its local gate is off. | `SUPABASE_PROJECT_ID` | project ref (flag → this → `.temp/project-ref` → prompt) | no | | `SUPABASE_YES` | auto-confirm prompts (`--yes`) | no | | `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` | API profile selection | no | | `env(VAR)` references | interpolated into `config.toml` values at load | no | diff --git a/apps/cli/src/legacy/commands/domains/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/domains/SIDE_EFFECTS.md index 90660186db..5dada1097d 100644 --- a/apps/cli/src/legacy/commands/domains/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/domains/SIDE_EFFECTS.md @@ -6,10 +6,10 @@ Cloudflare DNS-over-HTTPS CNAME pre-check. ## Files Read -| Path | Format | When | -| -------------------------------------- | ------------------------- | ---------------------------------------------------------- | -| `~/.supabase/access-token` | plain text (token string) | when `SUPABASE_ACCESS_TOKEN` unset and keyring unavailable | -| `/supabase/.temp/project-ref` | plain text | when `--project-ref` flag and `PROJECT_ID` env are unset | +| Path | Format | When | +| -------------------------------------- | ------------------------- | ----------------------------------------------------------------- | +| `~/.supabase/access-token` | plain text (token string) | when `SUPABASE_ACCESS_TOKEN` unset and keyring unavailable | +| `/supabase/.temp/project-ref` | plain text | when `--project-ref` flag and `SUPABASE_PROJECT_ID` env are unset | ## Files Written @@ -92,7 +92,7 @@ suppressed on stderr. `delete` ignores `-o`. - `--custom-hostname` is required for `create`. - `create` validates the CNAME via Cloudflare DNS-over-HTTPS (`https://1.1.1.1`, 10s timeout) before initializing; on failure it short-circuits before any POST. -- All subcommands resolve the ref via `--project-ref` → `PROJECT_ID` env → linked-project file, matching Go. +- All subcommands resolve the ref via `--project-ref` → `SUPABASE_PROJECT_ID` env → linked-project file, matching Go. - The project-ref fallback env var is `SUPABASE_PROJECT_ID`, matching Go (Go calls `viper.GetString("PROJECT_ID")` under `viper.SetEnvPrefix("SUPABASE")`, which resolves to the `SUPABASE_PROJECT_ID` environment variable). - **Documented divergences from Go (intentional):** - `--include-raw-output` is declared as a normal boolean **on each subcommand** (Go declares it as a persistent flag on the `domains` group). Two consequences: (a) it must appear after the subcommand name (`domains get --include-raw-output`) rather than before it (`domains --include-raw-output get`), matching how `--project-ref` is already handled shell-wide; (b) it cannot reproduce Cobra's help-hiding or the `Flag --include-raw-output has been deprecated` stderr warning, which Effect CLI has no hook for. It still reproduces the behavioral effect (forces `-o json` when `-o` is unset/pretty); on `delete` it is inert, matching Go. diff --git a/apps/cli/src/legacy/commands/encryption/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/encryption/SIDE_EFFECTS.md index 458b7aeb86..770358b237 100644 --- a/apps/cli/src/legacy/commands/encryption/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/encryption/SIDE_EFFECTS.md @@ -6,11 +6,11 @@ additionally reads the new key from stdin. ## Files Read -| Path | Format | When | -| ------------------------------------------------ | ------------------------- | ------------------------------------------------------------------ | -| `~/.supabase/access-token` | plain text (token string) | when `SUPABASE_ACCESS_TOKEN` unset and keyring unavailable | -| `~/.supabase//linked-project.json` | JSON | when `--project-ref` / `PROJECT_ID` unset, to resolve linked ref | -| stdin | raw bytes / masked TTY | `update-root-key` only — masked TTY input or piped bytes (the key) | +| Path | Format | When | +| ------------------------------------------------ | ------------------------- | ------------------------------------------------------------------------- | +| `~/.supabase/access-token` | plain text (token string) | when `SUPABASE_ACCESS_TOKEN` unset and keyring unavailable | +| `~/.supabase//linked-project.json` | JSON | when `--project-ref` / `SUPABASE_PROJECT_ID` unset, to resolve linked ref | +| stdin | raw bytes / masked TTY | `update-root-key` only — masked TTY input or piped bytes (the key) | ## Files Written @@ -30,12 +30,11 @@ additionally reads the new key from stdin. ## Environment Variables -| Variable | Purpose | Required? | -| ------------------------------------ | ---------------------------------------------------- | ------------------------------------------------------- | -| `SUPABASE_ACCESS_TOKEN` | auth token (bypasses credential file/keyring lookup) | no (falls back to keyring → `~/.supabase/access-token`) | -| `SUPABASE_PROJECT_ID` / `PROJECT_ID` | project ref (fallback when `--project-ref` unset) | no (falls back to linked-project file → prompt) | -| `SUPABASE_API_URL` | override Management API base URL | no (defaults to `https://api.supabase.com`) | -| `SUPABASE_PROFILE` | built-in profile name or YAML file path | no (defaults to `supabase`) | +| Variable | Purpose | Required? | +| ----------------------- | ---------------------------------------------------- | ------------------------------------------------------- | +| `SUPABASE_ACCESS_TOKEN` | auth token (bypasses credential file/keyring lookup) | no (falls back to keyring → `~/.supabase/access-token`) | +| `SUPABASE_PROJECT_ID` | project ref (fallback when `--project-ref` unset) | no (falls back to linked-project file → prompt) | +| `SUPABASE_PROFILE` | built-in profile name or YAML file path | no (falls back to `~/.supabase/profile` -> `supabase`) | ## Exit Codes @@ -76,7 +75,7 @@ One `result` event carrying `{root_key}` (both subcommands). ## Notes -- Requires `--project-ref`, `SUPABASE_PROJECT_ID`/`PROJECT_ID`, or a linked project. +- Requires `--project-ref`, `SUPABASE_PROJECT_ID`, or a linked project. - `update-root-key` reads the key from stdin: a real TTY is read with a masked prompt; piped stdin is decoded as UTF-8 and whitespace-trimmed. An empty or whitespace-only key sends an empty `root_key`, matching Go's `io.Copy` + diff --git a/apps/cli/src/legacy/commands/functions/delete/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/functions/delete/SIDE_EFFECTS.md index f3914b1b22..e3c3e92dd6 100644 --- a/apps/cli/src/legacy/commands/functions/delete/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/functions/delete/SIDE_EFFECTS.md @@ -23,7 +23,7 @@ | 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` | built-in profile name or YAML file path | no (falls back to `~/.supabase/profile` -> `supabase`) | ## Exit Codes diff --git a/apps/cli/src/legacy/commands/functions/download/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/functions/download/SIDE_EFFECTS.md index 7a70bcb517..ff2a034c35 100644 --- a/apps/cli/src/legacy/commands/functions/download/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/functions/download/SIDE_EFFECTS.md @@ -31,7 +31,7 @@ | 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` | built-in profile name or YAML file path | no (falls back to `~/.supabase/profile` -> `supabase`) | ## Exit Codes diff --git a/apps/cli/src/legacy/commands/functions/list/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/functions/list/SIDE_EFFECTS.md index b6764b7dfc..f1613f83fe 100644 --- a/apps/cli/src/legacy/commands/functions/list/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/functions/list/SIDE_EFFECTS.md @@ -34,7 +34,6 @@ | `SUPABASE_PROFILE` | select a built-in profile or YAML profile file with `api_url:` | no (falls back to `~/.supabase/profile` -> `supabase`) | | `SUPABASE_PROJECT_ID` | provides the project ref when `--project-ref` is unset | no (falls back to `/supabase/.temp/project-ref`) | | `SUPABASE_WORKDIR` | sets `` for local Supabase temp files | no (falls back to `--workdir` -> current working dir) | -| ~~`SUPABASE_API_URL`~~ | **not honored** - Go parity. Use `SUPABASE_PROFILE` instead. | - | ## Exit Codes diff --git a/apps/cli/src/legacy/commands/gen/keys/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/gen/keys/SIDE_EFFECTS.md index 325e8d58ed..00f28f8924 100644 --- a/apps/cli/src/legacy/commands/gen/keys/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/gen/keys/SIDE_EFFECTS.md @@ -20,10 +20,10 @@ ## Environment Variables -| Variable | Purpose | Required? | -| ----------------------- | -------------------------------- | ------------------------------------------------------- | -| `SUPABASE_ACCESS_TOKEN` | auth token | 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 | no (falls back to keyring → `~/.supabase/access-token`) | +| `SUPABASE_PROFILE` | built-in profile name or YAML file path | no (falls back to `~/.supabase/profile` -> `supabase`) | ## Exit Codes diff --git a/apps/cli/src/legacy/commands/gen/types/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/gen/types/SIDE_EFFECTS.md index 1bc6151b77..c7accf16f1 100644 --- a/apps/cli/src/legacy/commands/gen/types/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/gen/types/SIDE_EFFECTS.md @@ -59,7 +59,7 @@ default 10s pg-delta probe timeout. | Variable | Purpose | Required? | | ---------------------------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | | `SUPABASE_ACCESS_TOKEN` | auth token for linked/project-id mode | 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` | built-in profile name or YAML file path | no (falls back to `~/.supabase/profile` -> `supabase`) | | `SUPABASE_DB_PASSWORD` | database password for `--local` and the `--linked` workdir project | no (defaults to `postgres`; **ignored** for ad-hoc `--project-id`, which always mints a temporary login role) | | `SUPABASE_SERVICES_HOSTNAME` | host used for the local TLS probe | no (defaults to `127.0.0.1`) | | `SUPABASE_INTERNAL_IMAGE_REGISTRY` | pg-meta image registry override (`docker.io` → Docker Hub; any other value → that registry) | no (defaults to the ECR registry) | diff --git a/apps/cli/src/legacy/commands/inspect/db/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/inspect/db/SIDE_EFFECTS.md index f5728c86be..7bfc41e7a1 100644 --- a/apps/cli/src/legacy/commands/inspect/db/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/inspect/db/SIDE_EFFECTS.md @@ -40,7 +40,7 @@ no new config reads. | ---------------------------------------------------- | --------------------------------- | --------------------------------------- | | `SUPABASE_DB_PASSWORD` / `DB_PASSWORD` | database password (linked/local) | no (prompts / config fallback) | | `SUPABASE_ACCESS_TOKEN` | Management API auth (linked only) | no (falls back to keyring / token file) | -| `PROJECT_ID` | project ref fallback (linked) | no (config resolution fallback) | +| `SUPABASE_PROJECT_ID` | project ref fallback (linked) | no (config resolution fallback) | | libpq vars (`PGSSLROOTCERT`, `PGCONNECT_TIMEOUT`, …) | honored when `--db-url` is used | no | ## Database Queries diff --git a/apps/cli/src/legacy/commands/inspect/report/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/inspect/report/SIDE_EFFECTS.md index 3da2b98fdb..9307ddcd2a 100644 --- a/apps/cli/src/legacy/commands/inspect/report/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/inspect/report/SIDE_EFFECTS.md @@ -54,12 +54,12 @@ resolve the connection (via `LegacyDbConfigResolver`). ## Environment Variables -| Variable | Purpose | Required? | -| ----------------------- | ---------------------------------------------------- | --------- | -| `SUPABASE_ACCESS_TOKEN` | auth token (bypasses credential file/keyring lookup) | no | -| `SUPABASE_API_URL` | override Management API base URL | no | -| `SUPABASE_DB_*` | override `[db]` port / shadow_port / password | no | -| `SUPABASE_ENV` | selects which project `.env` files load | no | +| Variable | Purpose | Required? | +| ----------------------- | ---------------------------------------------------- | ------------------------------------------------------ | +| `SUPABASE_ACCESS_TOKEN` | auth token (bypasses credential file/keyring lookup) | no | +| `SUPABASE_PROFILE` | built-in profile name or YAML file path | no (falls back to `~/.supabase/profile` -> `supabase`) | +| `SUPABASE_DB_*` | override `[db]` port / shadow_port / password | no | +| `SUPABASE_ENV` | selects which project `.env` files load | no | ## Exit Codes diff --git a/apps/cli/src/legacy/commands/network-bans/get/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/network-bans/get/SIDE_EFFECTS.md index f9042c22a7..edfa550b89 100644 --- a/apps/cli/src/legacy/commands/network-bans/get/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/network-bans/get/SIDE_EFFECTS.md @@ -2,10 +2,10 @@ ## Files Read -| Path | Format | When | -| -------------------------------------- | ------------------------- | ------------------------------------------------------------- | -| `~/.supabase/access-token` | plain text (token string) | when `SUPABASE_ACCESS_TOKEN` unset and keyring unavailable | -| `/supabase/.temp/project-ref` | plain text (project ref) | when `--project-ref` flag and `PROJECT_ID` env are both unset | +| Path | Format | When | +| -------------------------------------- | ------------------------- | ---------------------------------------------------------------------- | +| `~/.supabase/access-token` | plain text (token string) | when `SUPABASE_ACCESS_TOKEN` unset and keyring unavailable | +| `/supabase/.temp/project-ref` | plain text (project ref) | when `--project-ref` flag and `SUPABASE_PROJECT_ID` env are both unset | ## Files Written @@ -27,8 +27,8 @@ The Management API exposes this read operation as `POST .../network-bans/retriev | 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) | +| `SUPABASE_PROFILE` | built-in profile name or YAML file path | no (falls back to `~/.supabase/profile` -> `supabase`) | +| `SUPABASE_PROJECT_ID` | project ref fallback when `--project-ref` is unset | no (falls back to `supabase/.temp/project-ref` → prompt) | | `SUPABASE_EXPERIMENTAL` | enables `--experimental`-gated commands without the flag | no (pass `--experimental` instead; one of the two is required) | ## Exit Codes diff --git a/apps/cli/src/legacy/commands/network-bans/remove/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/network-bans/remove/SIDE_EFFECTS.md index 333bf103a1..937970c60d 100644 --- a/apps/cli/src/legacy/commands/network-bans/remove/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/network-bans/remove/SIDE_EFFECTS.md @@ -2,10 +2,10 @@ ## Files Read -| Path | Format | When | -| -------------------------------------- | ------------------------- | ------------------------------------------------------------- | -| `~/.supabase/access-token` | plain text (token string) | when `SUPABASE_ACCESS_TOKEN` unset and keyring unavailable | -| `/supabase/.temp/project-ref` | plain text (project ref) | when `--project-ref` flag and `PROJECT_ID` env are both unset | +| Path | Format | When | +| -------------------------------------- | ------------------------- | ---------------------------------------------------------------------- | +| `~/.supabase/access-token` | plain text (token string) | when `SUPABASE_ACCESS_TOKEN` unset and keyring unavailable | +| `/supabase/.temp/project-ref` | plain text (project ref) | when `--project-ref` flag and `SUPABASE_PROJECT_ID` env are both unset | ## Files Written @@ -27,8 +27,8 @@ | 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) | +| `SUPABASE_PROFILE` | built-in profile name or YAML file path | no (falls back to `~/.supabase/profile` -> `supabase`) | +| `SUPABASE_PROJECT_ID` | project ref fallback when `--project-ref` is unset | no (falls back to `supabase/.temp/project-ref` → prompt) | | `SUPABASE_EXPERIMENTAL` | enables `--experimental`-gated commands without the flag | no (pass `--experimental` instead; one of the two is required) | ## Exit Codes diff --git a/apps/cli/src/legacy/commands/network-restrictions/get/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/network-restrictions/get/SIDE_EFFECTS.md index 7c746dbd01..f39c04d7c9 100644 --- a/apps/cli/src/legacy/commands/network-restrictions/get/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/network-restrictions/get/SIDE_EFFECTS.md @@ -2,10 +2,10 @@ ## Files Read -| Path | Format | When | -| -------------------------------------- | ------------------------- | ------------------------------------------------------------- | -| `~/.supabase/access-token` | plain text (token string) | when `SUPABASE_ACCESS_TOKEN` unset and keyring unavailable | -| `/supabase/.temp/project-ref` | plain text (project ref) | when `--project-ref` flag and `PROJECT_ID` env are both unset | +| Path | Format | When | +| -------------------------------------- | ------------------------- | ---------------------------------------------------------------------- | +| `~/.supabase/access-token` | plain text (token string) | when `SUPABASE_ACCESS_TOKEN` unset and keyring unavailable | +| `/supabase/.temp/project-ref` | plain text (project ref) | when `--project-ref` flag and `SUPABASE_PROJECT_ID` env are both unset | ## Files Written @@ -25,8 +25,8 @@ | 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) | +| `SUPABASE_PROFILE` | built-in profile name or YAML file path | no (falls back to `~/.supabase/profile` -> `supabase`) | +| `SUPABASE_PROJECT_ID` | project ref fallback when `--project-ref` is unset | no (falls back to `supabase/.temp/project-ref` → prompt) | ## Exit Codes diff --git a/apps/cli/src/legacy/commands/network-restrictions/update/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/network-restrictions/update/SIDE_EFFECTS.md index fb31633592..f30b5ed87e 100644 --- a/apps/cli/src/legacy/commands/network-restrictions/update/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/network-restrictions/update/SIDE_EFFECTS.md @@ -2,10 +2,10 @@ ## Files Read -| Path | Format | When | -| -------------------------------------- | ------------------------- | ------------------------------------------------------------- | -| `~/.supabase/access-token` | plain text (token string) | when `SUPABASE_ACCESS_TOKEN` unset and keyring unavailable | -| `/supabase/.temp/project-ref` | plain text (project ref) | when `--project-ref` flag and `PROJECT_ID` env are both unset | +| Path | Format | When | +| -------------------------------------- | ------------------------- | ---------------------------------------------------------------------- | +| `~/.supabase/access-token` | plain text (token string) | when `SUPABASE_ACCESS_TOKEN` unset and keyring unavailable | +| `/supabase/.temp/project-ref` | plain text (project ref) | when `--project-ref` flag and `SUPABASE_PROJECT_ID` env are both unset | ## Files Written @@ -31,8 +31,8 @@ when no `--db-allow-cidr` was supplied), matching Go's `&[]string{}` initializat | 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) | +| `SUPABASE_PROFILE` | built-in profile name or YAML file path | no (falls back to `~/.supabase/profile` -> `supabase`) | +| `SUPABASE_PROJECT_ID` | project ref fallback when `--project-ref` is unset | no (falls back to `supabase/.temp/project-ref` → prompt) | ## Exit Codes diff --git a/apps/cli/src/legacy/commands/postgres-config/delete/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/postgres-config/delete/SIDE_EFFECTS.md index 69bd23294e..97168c0975 100644 --- a/apps/cli/src/legacy/commands/postgres-config/delete/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/postgres-config/delete/SIDE_EFFECTS.md @@ -2,10 +2,10 @@ ## Files Read -| Path | Format | When | -| -------------------------------------- | ------------------------- | ------------------------------------------------------------- | -| `~/.supabase/access-token` | plain text (token string) | when `SUPABASE_ACCESS_TOKEN` unset and keyring unavailable | -| `/supabase/.temp/project-ref` | plain text (project ref) | when `--project-ref` flag and `PROJECT_ID` env are both unset | +| Path | Format | When | +| -------------------------------------- | ------------------------- | ---------------------------------------------------------------------- | +| `~/.supabase/access-token` | plain text (token string) | when `SUPABASE_ACCESS_TOKEN` unset and keyring unavailable | +| `/supabase/.temp/project-ref` | plain text (project ref) | when `--project-ref` flag and `SUPABASE_PROJECT_ID` env are both unset | ## Files Written @@ -28,8 +28,8 @@ This command does not call a delete endpoint. It mirrors Go: fetch current confi | 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) | +| `SUPABASE_PROFILE` | built-in profile name or YAML file path | no (falls back to `~/.supabase/profile` -> `supabase`) | +| `SUPABASE_PROJECT_ID` | project ref fallback when `--project-ref` is unset | no (falls back to `supabase/.temp/project-ref` -> prompt) | | `SUPABASE_EXPERIMENTAL` | enables `--experimental`-gated commands without the flag | no (pass `--experimental` instead; one of the two is required) | ## Exit Codes diff --git a/apps/cli/src/legacy/commands/postgres-config/get/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/postgres-config/get/SIDE_EFFECTS.md index 6582161bd1..d969c35982 100644 --- a/apps/cli/src/legacy/commands/postgres-config/get/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/postgres-config/get/SIDE_EFFECTS.md @@ -2,10 +2,10 @@ ## Files Read -| Path | Format | When | -| -------------------------------------- | ------------------------- | ------------------------------------------------------------- | -| `~/.supabase/access-token` | plain text (token string) | when `SUPABASE_ACCESS_TOKEN` unset and keyring unavailable | -| `/supabase/.temp/project-ref` | plain text (project ref) | when `--project-ref` flag and `PROJECT_ID` env are both unset | +| Path | Format | When | +| -------------------------------------- | ------------------------- | ---------------------------------------------------------------------- | +| `~/.supabase/access-token` | plain text (token string) | when `SUPABASE_ACCESS_TOKEN` unset and keyring unavailable | +| `/supabase/.temp/project-ref` | plain text (project ref) | when `--project-ref` flag and `SUPABASE_PROJECT_ID` env are both unset | ## Files Written @@ -25,8 +25,8 @@ | 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) | +| `SUPABASE_PROFILE` | built-in profile name or YAML file path | no (falls back to `~/.supabase/profile` -> `supabase`) | +| `SUPABASE_PROJECT_ID` | project ref fallback when `--project-ref` is unset | no (falls back to `supabase/.temp/project-ref` -> prompt) | | `SUPABASE_EXPERIMENTAL` | enables `--experimental`-gated commands without the flag | no (pass `--experimental` instead; one of the two is required) | ## Exit Codes diff --git a/apps/cli/src/legacy/commands/postgres-config/update/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/postgres-config/update/SIDE_EFFECTS.md index edd0de4c4a..8cfba86318 100644 --- a/apps/cli/src/legacy/commands/postgres-config/update/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/postgres-config/update/SIDE_EFFECTS.md @@ -2,10 +2,10 @@ ## Files Read -| Path | Format | When | -| -------------------------------------- | ------------------------- | ------------------------------------------------------------- | -| `~/.supabase/access-token` | plain text (token string) | when `SUPABASE_ACCESS_TOKEN` unset and keyring unavailable | -| `/supabase/.temp/project-ref` | plain text (project ref) | when `--project-ref` flag and `PROJECT_ID` env are both unset | +| Path | Format | When | +| -------------------------------------- | ------------------------- | ---------------------------------------------------------------------- | +| `~/.supabase/access-token` | plain text (token string) | when `SUPABASE_ACCESS_TOKEN` unset and keyring unavailable | +| `/supabase/.temp/project-ref` | plain text (project ref) | when `--project-ref` flag and `SUPABASE_PROJECT_ID` env are both unset | ## Files Written @@ -28,8 +28,8 @@ The initial `GET` is skipped when `--replace-existing-overrides` is set. Otherwi | 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) | +| `SUPABASE_PROFILE` | built-in profile name or YAML file path | no (falls back to `~/.supabase/profile` -> `supabase`) | +| `SUPABASE_PROJECT_ID` | project ref fallback when `--project-ref` is unset | no (falls back to `supabase/.temp/project-ref` -> prompt) | | `SUPABASE_EXPERIMENTAL` | enables `--experimental`-gated commands without the flag | no (pass `--experimental` instead; one of the two is required) | ## Exit Codes diff --git a/apps/cli/src/legacy/commands/projects/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/projects/SIDE_EFFECTS.md index 23e0b9711e..fa2e82029d 100644 --- a/apps/cli/src/legacy/commands/projects/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/projects/SIDE_EFFECTS.md @@ -41,7 +41,7 @@ subcommand's own `SIDE_EFFECTS.md`. | ----------------------- | ---------------------------------------------------- | ------------------------------------------------------------- | | `SUPABASE_ACCESS_TOKEN` | auth token (bypasses credential file/keyring lookup) | no (falls back to keyring → `~/.supabase/access-token`) | | `SUPABASE_PROJECT_REF` | linked project ref (via the config layer) | no (used by `list` marker / `api-keys` ref / `delete` unlink) | -| `SUPABASE_API_URL` | override Management API base URL | no (defaults to `https://api.supabase.com`) | +| `SUPABASE_PROFILE` | built-in profile name or YAML file path | no (falls back to `~/.supabase/profile` -> `supabase`) | > `DB_PASSWORD` is **not** consumed. In Go it only mirrors `--db-password` via a > viper binding for downstream local-stack use; `projects create` never reads it. diff --git a/apps/cli/src/legacy/commands/projects/api-keys/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/projects/api-keys/SIDE_EFFECTS.md index 79a7642c2c..b0207e720a 100644 --- a/apps/cli/src/legacy/commands/projects/api-keys/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/projects/api-keys/SIDE_EFFECTS.md @@ -28,7 +28,7 @@ Management API to return the full secret keys (`sb_secret_...`) in `api_key` ins | 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` | built-in profile name or YAML file path | no (falls back to `~/.supabase/profile` -> `supabase`) | ## Flags diff --git a/apps/cli/src/legacy/commands/projects/create/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/projects/create/SIDE_EFFECTS.md index 0726aaa348..73d151ebd5 100644 --- a/apps/cli/src/legacy/commands/projects/create/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/projects/create/SIDE_EFFECTS.md @@ -24,7 +24,7 @@ | 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` | built-in profile name or YAML file path | no (falls back to `~/.supabase/profile` -> `supabase`) | | `DB_PASSWORD` | **not consumed** — Go only mirrors `--db-password` into viper for local-stack reuse; `projects create` never reads it | n/a | ## Exit Codes diff --git a/apps/cli/src/legacy/commands/projects/delete/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/projects/delete/SIDE_EFFECTS.md index f4e46eab50..3d94401b92 100644 --- a/apps/cli/src/legacy/commands/projects/delete/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/projects/delete/SIDE_EFFECTS.md @@ -30,7 +30,7 @@ | 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` | built-in profile name or YAML file path | no (falls back to `~/.supabase/profile` -> `supabase`) | ## Exit Codes diff --git a/apps/cli/src/legacy/commands/projects/list/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/projects/list/SIDE_EFFECTS.md index f4785c0f14..8ddce1209a 100644 --- a/apps/cli/src/legacy/commands/projects/list/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/projects/list/SIDE_EFFECTS.md @@ -24,7 +24,7 @@ | 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` | built-in profile name or YAML file path | no (falls back to `~/.supabase/profile` -> `supabase`) | ## Exit Codes 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 a7a248607e..9a1ce5d299 100644 --- a/apps/cli/src/legacy/commands/secrets/list/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/secrets/list/SIDE_EFFECTS.md @@ -32,7 +32,6 @@ | `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:` (Go parity — used by the cli-e2e test harness). | no (defaults to `supabase`) | | `SUPABASE_PROJECT_ID` | project ref fallback when `--project-ref` is unset | no (also reads `/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`) | -| ~~`SUPABASE_API_URL`~~ | **not honored** — Go parity. Use `SUPABASE_PROFILE` to override the API base URL. | — | ## Exit Codes 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 c99884485e..e9b725e37b 100644 --- a/apps/cli/src/legacy/commands/secrets/set/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/secrets/set/SIDE_EFFECTS.md @@ -37,7 +37,6 @@ | `SUPABASE_PROJECT_ID` | project ref fallback when `--project-ref` is unset | no (also reads `/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`) | | `env(VAR)` references | values matching `env(NAME)` in `[edge_runtime.secrets]` are resolved against the loaded env. Missing variables preserve the literal verbatim (Go parity). | — | -| ~~`SUPABASE_API_URL`~~ | **not honored** — Go parity. Use `SUPABASE_PROFILE` to override the API base URL. | — | ## Exit Codes 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 504a4d99f1..873a57cf6d 100644 --- a/apps/cli/src/legacy/commands/secrets/unset/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/secrets/unset/SIDE_EFFECTS.md @@ -33,7 +33,6 @@ | `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:` (Go parity — used by the cli-e2e test harness). | no (defaults to `supabase`) | | `SUPABASE_PROJECT_ID` | project ref fallback when `--project-ref` is unset | no (also reads `/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`) | -| ~~`SUPABASE_API_URL`~~ | **not honored** — Go parity. Use `SUPABASE_PROFILE` to override the API base URL. | — | ## Exit Codes diff --git a/apps/cli/src/legacy/commands/services/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/services/SIDE_EFFECTS.md index 07092ca913..dfef6aa082 100644 --- a/apps/cli/src/legacy/commands/services/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/services/SIDE_EFFECTS.md @@ -36,7 +36,7 @@ matching `apps/cli-go/pkg/fetcher/gateway.go`. | Variable | Purpose | Required? | | ----------------------- | --------------------------------------------------- | ----------------------------------------------------------- | | `SUPABASE_ACCESS_TOKEN` | auth token for Management API linked-version checks | no (falls back to keyring, then `~/.supabase/access-token`) | -| `SUPABASE_API_URL` | override Management API base URL | no (defaults to `https://api.supabase.com`) | +| `SUPABASE_PROFILE` | built-in profile name or YAML file path | no (falls back to `~/.supabase/profile` -> `supabase`) | ## Exit Codes diff --git a/apps/cli/src/legacy/commands/snippets/download/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/snippets/download/SIDE_EFFECTS.md index 19f3323f9e..ec7b0f9f11 100644 --- a/apps/cli/src/legacy/commands/snippets/download/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/snippets/download/SIDE_EFFECTS.md @@ -7,7 +7,7 @@ | keyring `"Supabase CLI"` / `` | 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 | -| `/supabase/.temp/project-ref` | plain text | when `--project-ref` flag and `PROJECT_ID` env are unset | +| `/supabase/.temp/project-ref` | plain text | when `--project-ref` flag and `SUPABASE_PROJECT_ID` env are unset | | `/supabase/.temp/linked-project.json` | JSON | always — `linkedProjectCache` reads to decide whether to write | ## Files Written @@ -30,8 +30,7 @@ Only `content.sql` is rendered in text mode. The full payload is exposed via `-- | Variable | Purpose | Required? | | ----------------------- | ---------------------------------------------------- | ------------------------------------------------------- | | `SUPABASE_ACCESS_TOKEN` | auth token (bypasses credential file/keyring lookup) | no (falls back to keyring → `~/.supabase/access-token`) | -| `PROJECT_ID` | project ref fallback when `--project-ref` is unset | no (falls back to `supabase/.temp/project-ref`) | -| `SUPABASE_API_URL` | override Management API base URL | no (defaults to `https://api.supabase.com`) | +| `SUPABASE_PROJECT_ID` | project ref fallback when `--project-ref` is unset | no (falls back to `supabase/.temp/project-ref`) | | `SUPABASE_PROFILE` | profile selector (built-in name or YAML file path) | no (defaults to `supabase`) | ## Exit Codes diff --git a/apps/cli/src/legacy/commands/snippets/list/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/snippets/list/SIDE_EFFECTS.md index 867b2331ac..85e3717180 100644 --- a/apps/cli/src/legacy/commands/snippets/list/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/snippets/list/SIDE_EFFECTS.md @@ -7,7 +7,7 @@ | keyring `"Supabase CLI"` / `` | 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 | -| `/supabase/.temp/project-ref` | plain text | when `--project-ref` flag and `PROJECT_ID` env are unset | +| `/supabase/.temp/project-ref` | plain text | when `--project-ref` flag and `SUPABASE_PROJECT_ID` env are unset | | `/supabase/.temp/linked-project.json` | JSON | always — `linkedProjectCache` reads to decide whether to write | ## Files Written @@ -28,8 +28,7 @@ | Variable | Purpose | Required? | | ----------------------- | ---------------------------------------------------- | ------------------------------------------------------- | | `SUPABASE_ACCESS_TOKEN` | auth token (bypasses credential file/keyring lookup) | no (falls back to keyring → `~/.supabase/access-token`) | -| `PROJECT_ID` | project ref fallback when `--project-ref` is unset | no (falls back to `supabase/.temp/project-ref`) | -| `SUPABASE_API_URL` | override Management API base URL | no (defaults to `https://api.supabase.com`) | +| `SUPABASE_PROJECT_ID` | project ref fallback when `--project-ref` is unset | no (falls back to `supabase/.temp/project-ref`) | | `SUPABASE_PROFILE` | profile selector (built-in name or YAML file path) | no (defaults to `supabase`) | ## Exit Codes diff --git a/apps/cli/src/legacy/commands/ssl-enforcement/get/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/ssl-enforcement/get/SIDE_EFFECTS.md index ccf0e03d43..d56ed25db2 100644 --- a/apps/cli/src/legacy/commands/ssl-enforcement/get/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/ssl-enforcement/get/SIDE_EFFECTS.md @@ -2,10 +2,10 @@ ## Files Read -| Path | Format | When | -| -------------------------------------- | ------------------------- | ------------------------------------------------------------- | -| `~/.supabase/access-token` | plain text (token string) | when `SUPABASE_ACCESS_TOKEN` unset and keyring unavailable | -| `/supabase/.temp/project-ref` | plain text (project ref) | when `--project-ref` flag and `PROJECT_ID` env are both unset | +| Path | Format | When | +| -------------------------------------- | ------------------------- | ---------------------------------------------------------------------- | +| `~/.supabase/access-token` | plain text (token string) | when `SUPABASE_ACCESS_TOKEN` unset and keyring unavailable | +| `/supabase/.temp/project-ref` | plain text (project ref) | when `--project-ref` flag and `SUPABASE_PROJECT_ID` env are both unset | ## Files Written @@ -25,8 +25,8 @@ | 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) | +| `SUPABASE_PROFILE` | built-in profile name or YAML file path | no (falls back to `~/.supabase/profile` -> `supabase`) | +| `SUPABASE_PROJECT_ID` | project ref fallback when `--project-ref` is unset | no (falls back to `supabase/.temp/project-ref` → prompt) | | `SUPABASE_EXPERIMENTAL` | enables `--experimental`-gated commands without the flag | no (pass `--experimental` instead; one of the two is required) | ## Exit Codes diff --git a/apps/cli/src/legacy/commands/ssl-enforcement/update/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/ssl-enforcement/update/SIDE_EFFECTS.md index 6f067ad6b5..2a5657772a 100644 --- a/apps/cli/src/legacy/commands/ssl-enforcement/update/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/ssl-enforcement/update/SIDE_EFFECTS.md @@ -2,10 +2,10 @@ ## Files Read -| Path | Format | When | -| -------------------------------------- | ------------------------- | ------------------------------------------------------------- | -| `~/.supabase/access-token` | plain text (token string) | when `SUPABASE_ACCESS_TOKEN` unset and keyring unavailable | -| `/supabase/.temp/project-ref` | plain text (project ref) | when `--project-ref` flag and `PROJECT_ID` env are both unset | +| Path | Format | When | +| -------------------------------------- | ------------------------- | ---------------------------------------------------------------------- | +| `~/.supabase/access-token` | plain text (token string) | when `SUPABASE_ACCESS_TOKEN` unset and keyring unavailable | +| `/supabase/.temp/project-ref` | plain text (project ref) | when `--project-ref` flag and `SUPABASE_PROJECT_ID` env are both unset | ## Files Written @@ -25,8 +25,8 @@ | 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) | +| `SUPABASE_PROFILE` | built-in profile name or YAML file path | no (falls back to `~/.supabase/profile` -> `supabase`) | +| `SUPABASE_PROJECT_ID` | project ref fallback when `--project-ref` is unset | no (falls back to `supabase/.temp/project-ref` → prompt) | | `SUPABASE_EXPERIMENTAL` | enables `--experimental`-gated commands without the flag | no (pass `--experimental` instead; one of the two is required) | ## Exit Codes diff --git a/apps/cli/src/legacy/commands/vanity-subdomains/activate/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/vanity-subdomains/activate/SIDE_EFFECTS.md index 23fbe98051..aace0d1a0b 100644 --- a/apps/cli/src/legacy/commands/vanity-subdomains/activate/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/vanity-subdomains/activate/SIDE_EFFECTS.md @@ -2,10 +2,10 @@ ## Files Read -| Path | Format | When | -| -------------------------------------- | ------------------------- | ------------------------------------------------------------- | -| `~/.supabase/access-token` | plain text (token string) | when `SUPABASE_ACCESS_TOKEN` unset and keyring unavailable | -| `/supabase/.temp/project-ref` | plain text (project ref) | when `--project-ref` flag and `PROJECT_ID` env are both unset | +| Path | Format | When | +| -------------------------------------- | ------------------------- | ---------------------------------------------------------------------- | +| `~/.supabase/access-token` | plain text (token string) | when `SUPABASE_ACCESS_TOKEN` unset and keyring unavailable | +| `/supabase/.temp/project-ref` | plain text (project ref) | when `--project-ref` flag and `SUPABASE_PROJECT_ID` env are both unset | ## Files Written @@ -25,8 +25,8 @@ | Variable | Purpose | Required? | | ----------------------- | ---------------------------------------------------- | ---------------------------------------------------------- | | `SUPABASE_ACCESS_TOKEN` | auth token (bypasses credential file/keyring lookup) | no (falls back to keyring then `~/.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`) | +| `SUPABASE_PROFILE` | built-in profile name or YAML file path | no (falls back to `~/.supabase/profile` -> `supabase`) | +| `SUPABASE_PROJECT_ID` | project ref fallback when `--project-ref` is unset | no (falls back to `supabase/.temp/project-ref`) | ## Exit Codes diff --git a/apps/cli/src/legacy/commands/vanity-subdomains/check-availability/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/vanity-subdomains/check-availability/SIDE_EFFECTS.md index dc361d9fbc..9fc14225dc 100644 --- a/apps/cli/src/legacy/commands/vanity-subdomains/check-availability/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/vanity-subdomains/check-availability/SIDE_EFFECTS.md @@ -2,10 +2,10 @@ ## Files Read -| Path | Format | When | -| -------------------------------------- | ------------------------- | ------------------------------------------------------------- | -| `~/.supabase/access-token` | plain text (token string) | when `SUPABASE_ACCESS_TOKEN` unset and keyring unavailable | -| `/supabase/.temp/project-ref` | plain text (project ref) | when `--project-ref` flag and `PROJECT_ID` env are both unset | +| Path | Format | When | +| -------------------------------------- | ------------------------- | ---------------------------------------------------------------------- | +| `~/.supabase/access-token` | plain text (token string) | when `SUPABASE_ACCESS_TOKEN` unset and keyring unavailable | +| `/supabase/.temp/project-ref` | plain text (project ref) | when `--project-ref` flag and `SUPABASE_PROJECT_ID` env are both unset | ## Files Written @@ -25,8 +25,8 @@ | Variable | Purpose | Required? | | ----------------------- | ---------------------------------------------------- | ---------------------------------------------------------- | | `SUPABASE_ACCESS_TOKEN` | auth token (bypasses credential file/keyring lookup) | no (falls back to keyring then `~/.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`) | +| `SUPABASE_PROFILE` | built-in profile name or YAML file path | no (falls back to `~/.supabase/profile` -> `supabase`) | +| `SUPABASE_PROJECT_ID` | project ref fallback when `--project-ref` is unset | no (falls back to `supabase/.temp/project-ref`) | ## Exit Codes diff --git a/apps/cli/src/legacy/commands/vanity-subdomains/delete/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/vanity-subdomains/delete/SIDE_EFFECTS.md index 2596dc4cb6..5e67410233 100644 --- a/apps/cli/src/legacy/commands/vanity-subdomains/delete/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/vanity-subdomains/delete/SIDE_EFFECTS.md @@ -2,10 +2,10 @@ ## Files Read -| Path | Format | When | -| -------------------------------------- | ------------------------- | ------------------------------------------------------------- | -| `~/.supabase/access-token` | plain text (token string) | when `SUPABASE_ACCESS_TOKEN` unset and keyring unavailable | -| `/supabase/.temp/project-ref` | plain text (project ref) | when `--project-ref` flag and `PROJECT_ID` env are both unset | +| Path | Format | When | +| -------------------------------------- | ------------------------- | ---------------------------------------------------------------------- | +| `~/.supabase/access-token` | plain text (token string) | when `SUPABASE_ACCESS_TOKEN` unset and keyring unavailable | +| `/supabase/.temp/project-ref` | plain text (project ref) | when `--project-ref` flag and `SUPABASE_PROJECT_ID` env are both unset | ## Files Written @@ -25,8 +25,8 @@ | Variable | Purpose | Required? | | ----------------------- | ---------------------------------------------------- | ---------------------------------------------------------- | | `SUPABASE_ACCESS_TOKEN` | auth token (bypasses credential file/keyring lookup) | no (falls back to keyring then `~/.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`) | +| `SUPABASE_PROFILE` | built-in profile name or YAML file path | no (falls back to `~/.supabase/profile` -> `supabase`) | +| `SUPABASE_PROJECT_ID` | project ref fallback when `--project-ref` is unset | no (falls back to `supabase/.temp/project-ref`) | ## Exit Codes diff --git a/apps/cli/src/legacy/commands/vanity-subdomains/get/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/vanity-subdomains/get/SIDE_EFFECTS.md index 15c79abde7..73b367ec63 100644 --- a/apps/cli/src/legacy/commands/vanity-subdomains/get/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/vanity-subdomains/get/SIDE_EFFECTS.md @@ -2,10 +2,10 @@ ## Files Read -| Path | Format | When | -| -------------------------------------- | ------------------------- | ------------------------------------------------------------- | -| `~/.supabase/access-token` | plain text (token string) | when `SUPABASE_ACCESS_TOKEN` unset and keyring unavailable | -| `/supabase/.temp/project-ref` | plain text (project ref) | when `--project-ref` flag and `PROJECT_ID` env are both unset | +| Path | Format | When | +| -------------------------------------- | ------------------------- | ---------------------------------------------------------------------- | +| `~/.supabase/access-token` | plain text (token string) | when `SUPABASE_ACCESS_TOKEN` unset and keyring unavailable | +| `/supabase/.temp/project-ref` | plain text (project ref) | when `--project-ref` flag and `SUPABASE_PROJECT_ID` env are both unset | ## Files Written @@ -25,8 +25,8 @@ | Variable | Purpose | Required? | | ----------------------- | ---------------------------------------------------- | ---------------------------------------------------------- | | `SUPABASE_ACCESS_TOKEN` | auth token (bypasses credential file/keyring lookup) | no (falls back to keyring then `~/.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`) | +| `SUPABASE_PROFILE` | built-in profile name or YAML file path | no (falls back to `~/.supabase/profile` -> `supabase`) | +| `SUPABASE_PROJECT_ID` | project ref fallback when `--project-ref` is unset | no (falls back to `supabase/.temp/project-ref`) | ## Exit Codes From 973ed75950553761d69d2f57342afe8c75b4c576 Mon Sep 17 00:00:00 2001 From: Andrew Valleteau Date: Tue, 7 Jul 2026 12:49:16 +0200 Subject: [PATCH 04/79] feat(cli): port db push, db reset, and db start to native TypeScript (#5715) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ports the `db push`, `db reset`, and `db start` commands of the legacy CLI shell from Go-binary proxies to native TypeScript (CLI-1325), and introduces a hidden Go seam for the container-bootstrap primitives that aren't ported. ## What changed **`db push`** — fully native: pending-migration reconciliation, seed-file ops with `seed_files` hash tracking, `[db.vault]` upsert, `--include-roles`/`--include-seed`/`--dry-run`, against local / linked / `--db-url`. **`db reset`** — native on both legs: - Remote (`--linked` / remote `--db-url`): drop user schemas → vault upsert → migrate + seed, `--version`/`--last`, and the Go-parity `--sql-paths` seed override (merged from develop, mutually exclusive with `--no-seed`). - Local (`--local` / local `--db-url`): running check, `Resetting local database…`, container recreate + migrate + seed via the seam, storage-gated bucket seeding (reuses the ported `seed buckets` core), and the `Finished … on branch .` line. - Only the niche `--experimental` remote schema-files path still delegates to the Go binary. **`db start`** — native: config validation, the `AssertSupabaseDbIsRunning` check (prints Go's "already running" line), else container bootstrap via the seam. No status table and no `cli_stack_started` — those belong to the top-level `supabase start`, not `db start`. **Hidden Go seam (`db __db-bootstrap`)** — mirrors the existing `db __shadow` seam. Exposes the un-ported container primitives (`StartDatabase` + `DockerRemoveAll` cleanup, the PG14/PG15 reset recreate, the storage health gate) behind `--mode {start|recreate|await-storage}`. The TS side orchestrates everything else (messages, version resolution, bucket seeding, the git-branch line, telemetry, `--output-format` shaping); the seam runs with telemetry disabled and stderr inherited, like `__shadow`. **Config loading (review follow-ups)** — `db push` / `db reset` / `db start` load `config.toml` through the legacy Go-parity reader (`legacyCheckDbToml`, Go's `flags.LoadConfig` → `config.Load` + `Validate`) rather than `@supabase/config`, so their config semantics match the Go CLI exactly: - Go-style env-reference booleans (e.g. `[db.seed] enabled = "env(SEED_ENABLED)"`) load like Go (env-expand → `strconv.ParseBool`) instead of failing with a parse error. - a matched `[remotes.]` block's `db.migrations.enabled` / `db.seed.enabled` beats the `SUPABASE_DB_*_ENABLED` env var (Go's `v.Set` override tier sits above AutomaticEnv), closing the last remote-vs-env precedence gap. - `encrypted:` `[db.vault]` secrets are decrypted at config-load time with the shell **and** project-`.env` `DOTENV_PRIVATE_KEY*` keys and fail fast (before any connect / schema drop) on an undecryptable value, matching Go's `DecryptSecretHookFunc`. - seed `sql_paths` (config and `--sql-paths`) resolve once to Go's config-load form; `db start` validates config before the already-running check. Malformed config now surfaces the reader's Go message (`failed to load config`), consistent with `db diff` / `dump` / `pull` / `migration`. ## Why `db start` / `db reset --local` need container lifecycle (create/recreate, image pull, health checks, init schema, service restarts) that is impractical to reimplement in TS. The seam keeps that lifecycle in Go while moving orchestration, output parity, telemetry, and `--output-format` handling to native TS — matching the approach already used for `db diff` / `db pull`. ## Reviewer notes - The seam is the only `db reset`/`db start` boundary the in-process integration suites mock; it's covered end-to-end by the cli-e2e live suite (`db-reset-start.live.e2e.test.ts`, run via `pnpm --filter @supabase/cli-e2e test:e2e:live`), which exercises the local leg against a real Docker socket and the remote `db reset` leg against the staging session pooler. Both `describe`s skip the `ts-next` target (the next shell has no `db` group). - `db reset --local` recreate forwards `--no-seed` / `--sql-paths` to the seam, which applies them via the same `applyDbResetSeedFlags` helper `db reset` uses, so seed handling stays identical across local and remote. - The bundled `supabase-go` binary must be rebuilt (`pnpm build:go-sidecar` copies it into `dist/`) since the seam adds the `db __db-bootstrap` command. - `docs/go-cli-porting-status.md` flips `db push` / `db reset` / `db start` to `ported`; each command's `SIDE_EFFECTS.md` documents the files, subprocesses, DB mutations, env vars, and exit codes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude --- .../live/db-reset-start.live.e2e.test.ts | 81 ++ apps/cli-go/cmd/db.go | 74 ++ apps/cli-go/internal/db/reset/reset.go | 32 + apps/cli/docs/go-cli-porting-status.md | 300 ++--- .../legacy-platform-api.layer.unit.test.ts | 1 + .../legacy/commands/db/push/SIDE_EFFECTS.md | 109 +- .../legacy/commands/db/push/push.command.ts | 25 +- .../legacy/commands/db/push/push.errors.ts | 56 + .../legacy/commands/db/push/push.handler.ts | 349 ++++- .../commands/db/push/push.integration.test.ts | 826 ++++++++++++ .../legacy/commands/db/push/push.layers.ts | 78 ++ .../legacy/commands/db/reset/SIDE_EFFECTS.md | 145 ++- .../legacy/commands/db/reset/reset.command.ts | 25 +- .../legacy/commands/db/reset/reset.errors.ts | 88 ++ .../legacy/commands/db/reset/reset.handler.ts | 430 +++++- .../db/reset/reset.integration.test.ts | 1153 +++++++++++++++-- .../legacy/commands/db/reset/reset.layers.ts | 80 ++ .../db/shared/legacy-db-bootstrap.errors.ts | 20 + .../shared/legacy-db-bootstrap.seam.layer.ts | 241 ++++ .../legacy-db-bootstrap.seam.service.ts | 68 + .../commands/db/shared/legacy-drop-schemas.ts | 169 +++ .../db/shared/legacy-migration-pending.ts | 123 ++ .../legacy-migration-pending.unit.test.ts | 83 ++ .../db/shared/legacy-pgdelta.seam.layer.ts | 8 +- .../commands/db/shared/legacy-seed-ops.ts | 372 ++++++ .../db/shared/legacy-seed-ops.unit.test.ts | 126 ++ .../legacy/commands/db/start/SIDE_EFFECTS.md | 67 +- .../legacy/commands/db/start/start.command.ts | 16 +- .../legacy/commands/db/start/start.handler.ts | 87 +- .../db/start/start.integration.test.ts | 240 ++++ .../legacy/commands/db/start/start.layers.ts | 27 + .../commands/seed/buckets/buckets.handler.ts | 560 +------- .../seed/buckets/buckets.integration.test.ts | 65 + .../services/services.integration.test.ts | 1 + .../legacy/config/legacy-cli-config.layer.ts | 19 +- .../legacy-cli-config.layer.unit.test.ts | 9 +- .../config/legacy-cli-config.service.ts | 8 + .../legacy-project-ref.layer.unit.test.ts | 1 + .../legacy/shared/legacy-connect-errors.ts | 91 ++ .../shared/legacy-connect-errors.unit.test.ts | 74 ++ .../legacy-db-config.integration.test.ts | 27 + .../legacy/shared/legacy-db-config.layer.ts | 33 +- .../shared/legacy-db-config.toml-read.ts | 268 +++- .../legacy-db-config.toml-read.unit.test.ts | 160 +++ .../shared/legacy-db-connection.service.ts | 8 + .../legacy-db-connection.sql-pg.layer.ts | 20 +- .../legacy/shared/legacy-db-target-flags.ts | 4 +- .../legacy/shared/legacy-docker-run.layer.ts | 11 +- .../legacy/shared/legacy-docker-suggest.ts | 21 + .../shared/legacy-docker-suggest.unit.test.ts | 31 + .../legacy/shared/legacy-migration-apply.ts | 150 ++- .../legacy-migration-apply.unit.test.ts | 26 + .../src/legacy/shared/legacy-prompt-yes-no.ts | 11 +- .../src/legacy/shared/legacy-seed-buckets.ts | 584 +++++++++ apps/cli/src/shared/cli/run.ts | 4 + apps/cli/src/shared/cli/run.unit.test.ts | 3 + apps/cli/src/shared/legacy/global-flags.ts | 19 + apps/cli/tests/helpers/legacy-mocks.ts | 2 + packages/cli-test-helpers/src/normalize.ts | 13 + .../src/normalize.unit.test.ts | 21 + 60 files changed, 6716 insertions(+), 1027 deletions(-) create mode 100644 apps/cli-e2e/src/tests/live/db-reset-start.live.e2e.test.ts create mode 100644 apps/cli/src/legacy/commands/db/push/push.errors.ts create mode 100644 apps/cli/src/legacy/commands/db/push/push.integration.test.ts create mode 100644 apps/cli/src/legacy/commands/db/push/push.layers.ts create mode 100644 apps/cli/src/legacy/commands/db/reset/reset.errors.ts create mode 100644 apps/cli/src/legacy/commands/db/reset/reset.layers.ts create mode 100644 apps/cli/src/legacy/commands/db/shared/legacy-db-bootstrap.errors.ts create mode 100644 apps/cli/src/legacy/commands/db/shared/legacy-db-bootstrap.seam.layer.ts create mode 100644 apps/cli/src/legacy/commands/db/shared/legacy-db-bootstrap.seam.service.ts create mode 100644 apps/cli/src/legacy/commands/db/shared/legacy-drop-schemas.ts create mode 100644 apps/cli/src/legacy/commands/db/shared/legacy-migration-pending.ts create mode 100644 apps/cli/src/legacy/commands/db/shared/legacy-migration-pending.unit.test.ts create mode 100644 apps/cli/src/legacy/commands/db/shared/legacy-seed-ops.ts create mode 100644 apps/cli/src/legacy/commands/db/shared/legacy-seed-ops.unit.test.ts create mode 100644 apps/cli/src/legacy/commands/db/start/start.integration.test.ts create mode 100644 apps/cli/src/legacy/commands/db/start/start.layers.ts create mode 100644 apps/cli/src/legacy/shared/legacy-docker-suggest.ts create mode 100644 apps/cli/src/legacy/shared/legacy-docker-suggest.unit.test.ts create mode 100644 apps/cli/src/legacy/shared/legacy-seed-buckets.ts diff --git a/apps/cli-e2e/src/tests/live/db-reset-start.live.e2e.test.ts b/apps/cli-e2e/src/tests/live/db-reset-start.live.e2e.test.ts new file mode 100644 index 0000000000..9c2ec6f8e7 --- /dev/null +++ b/apps/cli-e2e/src/tests/live/db-reset-start.live.e2e.test.ts @@ -0,0 +1,81 @@ +import { mkdirSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { describe, expect } from "vitest"; +import { TARGET } from "../env.ts"; +import { testLive } from "./live-context.ts"; + +// Real-backend live coverage for the native `db start` / `db reset` ports. +// +// `db start` / `db reset` live only in the `go` reference and the `ts-legacy` +// port (the `next` shell has no `db` group), so skip the `ts-next` target. +// +// The live suite runs serially (`fileParallelism: false`, `maxWorkers: 1`), so the +// destructive remote reset below is safe against the throwaway per-run project. + +// --- Local leg: db start + db reset --local against the real Docker socket ----- +// Exercises the hidden `db __db-bootstrap` Go seam end-to-end — the boundary the +// in-process integration suites mock. The start → already-running → reset cycle +// runs in one test so it shares a single booted stack, and `finally` stops it +// (legacy proxies `stop` to Go) so the run never leaves containers behind. +describe.skipIf(TARGET === "ts-next")("db start / db reset --local (live, local Docker)", () => { + testLive( + "db start boots, is idempotent, and db reset --local recreates", + { timeout: 600_000 }, + async ({ run }) => { + try { + const start = await run(["db", "start"]); + expect(start.exitCode, start.stderr).toBe(0); + // Go tees bootstrap progress to stderr (mode-independent). + expect(`${start.stdout}${start.stderr}`).toMatch(/Starting database|Initialising schema/i); + + // Second start is a no-op: the db is already running, exit 0. + const again = await run(["db", "start"]); + expect(again.exitCode, again.stderr).toBe(0); + expect(`${again.stdout}${again.stderr}`).toMatch(/already[\s-]running/i); + + // Local reset recreates the container and prints the git-branch line. + const reset = await run(["db", "reset", "--local"]); + expect(reset.exitCode, reset.stderr).toBe(0); + expect(reset.stderr).toContain("on branch "); + } finally { + await run(["stop", "--no-backup"]).catch(() => undefined); + } + }, + ); +}); + +// --- Remote leg: db reset against the staging project over the session pooler --- +// Exercises the native remote reset path (drop user schemas → apply local +// migrations → seed) against a real Postgres, no Docker. `--yes` auto-accepts the +// confirmation prompt (the non-interactive default is decline). Mutates the +// throwaway project's schema — deleted on teardown. The IPv4 session pooler +// `dbUrl` is used because the direct host is IPv6-only and unreachable from +// IPv4-only CI runners. +describe.skipIf(TARGET === "ts-next")("db reset (live, remote session pooler)", () => { + testLive( + "resets the remote schema and re-applies a local migration", + { timeout: 600_000 }, + async ({ run, dbUrl, workspace }) => { + const migrations = join(workspace.path, "supabase", "migrations"); + mkdirSync(migrations, { recursive: true }); + writeFileSync( + join(migrations, "20240101000000_e2e_reset.sql"), + "create table if not exists e2e_reset (id int);\n", + ); + + const reset = await run(["db", "reset", "--db-url", dbUrl, "--yes"]); + expect(reset.exitCode, reset.stderr).toBe(0); + expect(reset.stderr).toContain("Resetting remote database"); + // A real connection failure must never be mistaken for a benign outcome. + expect(`${reset.stdout}${reset.stderr}`, "db reset hit a connection error").not.toMatch( + /dial|no route|connection refused|could not connect|server closed the connection|i\/o timeout/i, + ); + + // The migration history shows the re-applied version → proves the drop + + // migrate ran against the remote database. + const listed = await run(["migration", "list", "--db-url", dbUrl]); + expect(listed.exitCode, listed.stderr).toBe(0); + expect(listed.stdout).toContain("20240101000000"); + }, + ); +}); diff --git a/apps/cli-go/cmd/db.go b/apps/cli-go/cmd/db.go index df04364e44..66df4311cc 100644 --- a/apps/cli-go/cmd/db.go +++ b/apps/cli-go/cmd/db.go @@ -1,6 +1,7 @@ package cmd import ( + "context" "errors" "fmt" "os" @@ -270,6 +271,71 @@ var ( }, } + bootstrapMode string + bootstrapSqlPaths []string + bootstrapFromBackup string + bootstrapVersion string + bootstrapNoSeed bool + + // dbBootstrapCmd is a hidden seam used by the native-TypeScript `db start` and + // `db reset --local` commands to drive the container-bootstrap primitives that + // are not yet ported to TypeScript: creating/recreating the local Postgres + // container, applying the initial schema, and the storage health gate. The TS + // caller orchestrates everything else (the "already running?" check and its + // message, version/last resolution, bucket seeding, the git-branch "Finished…" + // line, telemetry, and --output-format shaping); the seam stays in Go only for + // the Docker lifecycle. It mirrors the existing db __shadow seam: it carries no + // db-url/local/linked target flags, so it loads supabase/config.toml explicitly + // (the root PersistentPreRunE only loads it when a target flag is set). Progress + // goes to stderr; the only stdout output is a single machine-parseable marker + // for --mode await-storage ("ready" or "absent"). + dbBootstrapCmd = &cobra.Command{ + Use: "__db-bootstrap", + Hidden: true, + Short: "Internal: container bootstrap for the native db start / db reset commands", + RunE: func(cmd *cobra.Command, args []string) error { + fsys := afero.NewOsFs() + if err := flags.LoadConfig(fsys); err != nil { + return err + } + switch bootstrapMode { + case "start": + // Mirror start.Run minus the "already running?" check, which the TS + // caller performs (and prints "Postgres database is already running."). + if err := start.StartDatabase(cmd.Context(), bootstrapFromBackup, fsys, os.Stderr); err != nil { + if rmErr := utils.DockerRemoveAll(context.Background(), os.Stderr, utils.Config.ProjectId); rmErr != nil { + fmt.Fprintln(os.Stderr, rmErr) + } + return err + } + return nil + case "recreate": + // The PG14/PG15 container-recreate half of local db reset. The TS + // caller has already printed "Resetting local database…" and validated + // the flags. Apply the same seed handling as `db reset` (dbResetCmd): + // `--no-seed` disables the seed, `--sql-paths` overrides the seed paths, + // before MigrateAndSeed runs inside the recreate. + if err := applyDbResetSeedFlags(bootstrapNoSeed, bootstrapSqlPaths); err != nil { + return err + } + return reset.RecreateLocalDatabase(cmd.Context(), bootstrapVersion, fsys) + case "await-storage": + ready, err := reset.AwaitStorageReady(cmd.Context()) + if err != nil { + return err + } + if ready { + fmt.Println("ready") + } else { + fmt.Println("absent") + } + return nil + default: + return fmt.Errorf("unknown bootstrap mode: %s", bootstrapMode) + } + }, + } + dbRemoteCmd = &cobra.Command{ Hidden: true, Use: "remote", @@ -620,6 +686,14 @@ func init() { shadowFlags.StringSliceVarP(&shadowSchema, "schema", "s", []string{}, "Comma separated list of schema to include.") shadowFlags.StringVar(&shadowProjectRef, "project-ref", "", "Linked project ref, so the shadow merges the matching [remotes.] config override.") dbCmd.AddCommand(dbShadowCmd) + // Build hidden container-bootstrap seam command (native db start / db reset) + bootstrapFlags := dbBootstrapCmd.Flags() + bootstrapFlags.StringVar(&bootstrapMode, "mode", "start", "Bootstrap mode: start, recreate, or await-storage.") + bootstrapFlags.StringVar(&bootstrapFromBackup, "from-backup", "", "Path to a logical backup file (start mode).") + bootstrapFlags.StringVar(&bootstrapVersion, "version", "", "Reset up to the specified version (recreate mode).") + bootstrapFlags.BoolVar(&bootstrapNoSeed, "no-seed", false, "Skip the seed script after recreate (recreate mode).") + bootstrapFlags.StringArrayVar(&bootstrapSqlPaths, "sql-paths", nil, "Override [db.seed].sql_paths for the recreate (recreate mode).") + dbCmd.AddCommand(dbBootstrapCmd) // Build remote command remoteFlags := dbRemoteCmd.PersistentFlags() remoteFlags.StringSliceVarP(&schema, "schema", "s", []string{}, "Comma separated list of schema to include.") diff --git a/apps/cli-go/internal/db/reset/reset.go b/apps/cli-go/internal/db/reset/reset.go index 7d841f4ba3..765153d6d7 100644 --- a/apps/cli-go/internal/db/reset/reset.go +++ b/apps/cli-go/internal/db/reset/reset.go @@ -92,6 +92,38 @@ func toLogMessage(version string) string { return "..." } +// RecreateLocalDatabase is the container-lifecycle half of a local `db reset`, +// exposed for the native-TypeScript `db reset --local` seam (cmd db __db-bootstrap). +// It performs the PG14/PG15 branch — recreate the db container/volume, init schema, +// migrate + seed, and restart the satellite containers — WITHOUT the leading +// "Resetting local database…" line, which the TS caller prints itself. Mirrors +// resetDatabase (above) minus that message. +func RecreateLocalDatabase(ctx context.Context, version string, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error { + if utils.Config.Db.MajorVersion <= 14 { + return resetDatabase14(ctx, version, fsys, options...) + } + return resetDatabase15(ctx, version, fsys, options...) +} + +// AwaitStorageReady mirrors the storage-health gate that local `db reset` runs +// before seeding buckets (Run, above): if the storage container exists but is not +// healthy, wait up to 30s for it. It reports whether the storage container exists +// so the native-TypeScript caller knows whether to run the (already-ported) bucket +// seeding. Any inspect error is treated as "storage not running" → false, matching +// Go's `err == nil` gate, which silently skips buckets on any inspect failure. +func AwaitStorageReady(ctx context.Context) (bool, error) { + resp, err := utils.Docker.ContainerInspect(ctx, utils.StorageId) + if err != nil { + return false, nil + } + if resp.State.Health == nil || resp.State.Health.Status != types.Healthy { + if err := start.WaitForHealthyService(ctx, 30*time.Second, utils.StorageId); err != nil { + return false, err + } + } + return true, nil +} + func resetDatabase14(ctx context.Context, version string, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error { if err := recreateDatabase(ctx, options...); err != nil { return err diff --git a/apps/cli/docs/go-cli-porting-status.md b/apps/cli/docs/go-cli-porting-status.md index 42d783057c..e8334c52cf 100644 --- a/apps/cli/docs/go-cli-porting-status.md +++ b/apps/cli/docs/go-cli-porting-status.md @@ -80,51 +80,51 @@ These commands exist in the TS CLI today but have no direct top-level equivalent ## Database -| Old command | TS status | TS command path or `missing` | Missing flags/params | Extra TS flags/params | Notes | -| --------------------------------- | --------- | -------------------------------------------------- | -------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `db diff` | `ported` | `legacy/commands/db/diff/` | `n/a` | `n/a` | Native TS port. Native pg-delta / migra diff via edge-runtime against a Go-seam-provisioned live shadow (`db __shadow`); `--use-pgadmin` / `--use-pg-schema` delegate to the Go binary. | -| `db dump` | `ported` | `legacy/commands/db/dump/` | `n/a` | `n/a` | Native TS port. Streams `pg_dump`/`pg_dumpall` via a Docker container (`LegacyDockerRun`); schema/data/role modes, `--dry-run` script print, IPv4 transaction-pooler fallback. | -| `db lint` | `ported` | `legacy/commands/db/lint/` | `n/a` | `n/a` | Native TS port. Runs `plpgsql_check` in a rolled-back transaction via LegacyDbConnection; emits Go-parity pretty JSON. | -| `db pull` | `ported` | `legacy/commands/db/pull/` | `n/a` | `n/a` | Native TS port. Native pg-delta / migra migration + `--declarative` pg-delta export; reconciles `schema_migrations`. The initial-migra pull dumps the remote schema natively (`pg_dump`) then appends the migra diff. Only `--experimental` (structured dump) still delegates to Go, pending a TS PostgreSQL DDL parser for `format.WriteStructuredSchemas`. | -| `db push` | `missing` | `missing` | `n/a` | `n/a` | No native TS implementation yet. Wrapped in legacy shell. | -| `db reset` | `missing` | `missing` | `n/a` | `n/a` | No native TS implementation yet. Wrapped in legacy shell. | -| `db start` | `missing` | `missing` | `n/a` | `n/a` | No native TS implementation yet. Wrapped in legacy shell. | -| `inspect report` | `ported` | `legacy/commands/inspect/report/` | `n/a` | `n/a` | Native TS port. Runs every inspect query via server-side `COPY ... CSV`, writes 14 CSVs under `//`, then renders a Go-parity Glamour rules summary (bounded csvq-subset evaluator; custom `[experimental.inspect.rules]` supported). | -| `inspect db db-stats` | `ported` | `legacy/commands/inspect/db/db-stats/` | `n/a` | `n/a` | Native TS port. Queries Postgres directly via LegacyDbConnection; renders Go-parity Glamour tables. | -| `inspect db replication-slots` | `ported` | `legacy/commands/inspect/db/replication-slots/` | `n/a` | `n/a` | Native TS port. Queries Postgres directly via LegacyDbConnection; renders Go-parity Glamour tables. | -| `inspect db locks` | `ported` | `legacy/commands/inspect/db/locks/` | `n/a` | `n/a` | Native TS port. Queries Postgres directly via LegacyDbConnection; renders Go-parity Glamour tables. | -| `inspect db blocking` | `ported` | `legacy/commands/inspect/db/blocking/` | `n/a` | `n/a` | Native TS port. Queries Postgres directly via LegacyDbConnection; renders Go-parity Glamour tables. | -| `inspect db outliers` | `ported` | `legacy/commands/inspect/db/outliers/` | `n/a` | `n/a` | Native TS port. Queries Postgres directly via LegacyDbConnection; renders Go-parity Glamour tables. | -| `inspect db calls` | `ported` | `legacy/commands/inspect/db/calls/` | `n/a` | `n/a` | Native TS port. Queries Postgres directly via LegacyDbConnection; renders Go-parity Glamour tables. | -| `inspect db index-stats` | `ported` | `legacy/commands/inspect/db/index-stats/` | `n/a` | `n/a` | Native TS port. Queries Postgres directly via LegacyDbConnection; renders Go-parity Glamour tables. | -| `inspect db long-running-queries` | `ported` | `legacy/commands/inspect/db/long-running-queries/` | `n/a` | `n/a` | Native TS port. Queries Postgres directly via LegacyDbConnection; renders Go-parity Glamour tables. | -| `inspect db bloat` | `ported` | `legacy/commands/inspect/db/bloat/` | `n/a` | `n/a` | Native TS port. Queries Postgres directly via LegacyDbConnection; renders Go-parity Glamour tables. | -| `inspect db role-stats` | `ported` | `legacy/commands/inspect/db/role-stats/` | `n/a` | `n/a` | Native TS port. Queries Postgres directly via LegacyDbConnection; renders Go-parity Glamour tables. | -| `inspect db vacuum-stats` | `ported` | `legacy/commands/inspect/db/vacuum-stats/` | `n/a` | `n/a` | Native TS port. Queries Postgres directly via LegacyDbConnection; renders Go-parity Glamour tables. | -| `inspect db table-stats` | `ported` | `legacy/commands/inspect/db/table-stats/` | `n/a` | `n/a` | Native TS port. Queries Postgres directly via LegacyDbConnection; renders Go-parity Glamour tables. | -| `inspect db traffic-profile` | `ported` | `legacy/commands/inspect/db/traffic-profile/` | `n/a` | `n/a` | Native TS port. Queries Postgres directly via LegacyDbConnection; renders Go-parity Glamour tables. | -| `inspect db cache-hit` | `ported` | `legacy/commands/inspect/db/cache-hit/` | `n/a` | `n/a` | Native TS port. Deprecated (use db-stats); routes to the active query. | -| `inspect db index-usage` | `ported` | `legacy/commands/inspect/db/index-usage/` | `n/a` | `n/a` | Native TS port. Deprecated (use index-stats); routes to the active query. | -| `inspect db total-index-size` | `ported` | `legacy/commands/inspect/db/total-index-size/` | `n/a` | `n/a` | Native TS port. Deprecated (use index-stats); routes to the active query. | -| `inspect db index-sizes` | `ported` | `legacy/commands/inspect/db/index-sizes/` | `n/a` | `n/a` | Native TS port. Deprecated (use index-stats); routes to the active query. | -| `inspect db table-sizes` | `ported` | `legacy/commands/inspect/db/table-sizes/` | `n/a` | `n/a` | Native TS port. Deprecated (use table-stats); routes to the active query. | -| `inspect db table-index-sizes` | `ported` | `legacy/commands/inspect/db/table-index-sizes/` | `n/a` | `n/a` | Native TS port. Deprecated (use table-stats); routes to the active query. | -| `inspect db total-table-sizes` | `ported` | `legacy/commands/inspect/db/total-table-sizes/` | `n/a` | `n/a` | Native TS port. Deprecated (use table-stats); routes to the active query. | -| `inspect db unused-indexes` | `ported` | `legacy/commands/inspect/db/unused-indexes/` | `n/a` | `n/a` | Native TS port. Deprecated (use index-stats); routes to the active query. | -| `inspect db table-record-counts` | `ported` | `legacy/commands/inspect/db/table-record-counts/` | `n/a` | `n/a` | Native TS port. Deprecated (use table-stats); routes to the active query. | -| `inspect db seq-scans` | `ported` | `legacy/commands/inspect/db/seq-scans/` | `n/a` | `n/a` | Native TS port. Deprecated (use index-stats); routes to the active query. | -| `inspect db role-configs` | `ported` | `legacy/commands/inspect/db/role-configs/` | `n/a` | `n/a` | Native TS port. Deprecated (use role-stats); routes to the active query. | -| `inspect db role-connections` | `ported` | `legacy/commands/inspect/db/role-connections/` | `n/a` | `n/a` | Native TS port. Deprecated (use role-stats); routes to the active query. | -| `migration down` | `ported` | `legacy/commands/migration/down/` | `n/a` | `n/a` | Native TS port. Revert prompt → drop user schemas → vault upsert → migrate&seed to the target version; defaults to `--local`. Skips Go's pgcache catalog write. | -| `migration fetch` | `ported` | `legacy/commands/migration/fetch/` | `n/a` | `n/a` | Native TS port. Reads `schema_migrations` and writes `supabase/migrations/_.sql`; overwrite prompt for a non-empty dir. | -| `migration list` | `ported` | `legacy/commands/migration/list/` | `n/a` | `n/a` | Native TS port. Merges remote `schema_migrations` with local files into a Glamour ASCII table (Local / Remote / Time-UTC columns); defaults to `--linked`. | -| `migration new` | `ported` | `legacy/commands/migration/new/` | `n/a` | `n/a` | Native TS port. Writes `supabase/migrations/_.sql` (mode 0644) from piped stdin; no DB/API. | -| `migration repair` | `ported` | `legacy/commands/migration/repair/` | `n/a` | `n/a` | Native TS port. Transactional create-table + TRUNCATE/UPSERT/DELETE; applied mode reads local files; repair-all prompt; defaults to `--linked`. | -| `migration squash` | `missing` | `missing` | `n/a` | `n/a` | No native TS implementation yet. Wrapped in legacy shell. | -| `migration up` | `ported` | `legacy/commands/migration/up/` | `n/a` | `n/a` | Native TS port. Computes pending migrations, upserts `[db.vault]`, applies each transactionally; `--include-all` for out-of-order; defaults to `--local`. Does not seed (matches Go). | -| `seed buckets` | `ported` | `legacy/commands/seed/buckets/` | `n/a` | `n/a` | Native TS port. Local-only (Go's `seed` defines no `--project-ref`, so the ref is always empty): seeds `[storage.buckets]` + `[storage.vector]` against the local Storage service gateway; remote/analytics paths are unreachable and omitted. `--linked`/`--local` accepted for surface parity (both seed local). Vector graceful-skip WARNINGs ported. | -| `test db` | `ported` | `legacy/commands/test/db/` | `n/a` | `n/a` | Native TS port. `--db-url`/`--local`/`--linked` + variadic paths; runs `supabase/pg_prove:3.36` via `docker run`; pgTAP enable/disable via `@effect/sql-pg`. `--network-id` override and `[images]` config override not modeled (documented divergences). | -| `test new` | `ported` | `legacy/commands/test/new/` | `n/a` | `n/a` | Native TS port. Writes `supabase/tests/_test.sql` from the embedded pgtap template; `--template` (pgtap). | +| Old command | TS status | TS command path or `missing` | Missing flags/params | Extra TS flags/params | Notes | +| --------------------------------- | --------- | -------------------------------------------------- | -------------------- | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `db diff` | `ported` | `legacy/commands/db/diff/` | `n/a` | `n/a` | Native TS port. Native pg-delta / migra diff via edge-runtime against a Go-seam-provisioned live shadow (`db __shadow`); `--use-pgadmin` / `--use-pg-schema` delegate to the Go binary. | +| `db dump` | `ported` | `legacy/commands/db/dump/` | `n/a` | `n/a` | Native TS port. Streams `pg_dump`/`pg_dumpall` via a Docker container (`LegacyDockerRun`); schema/data/role modes, `--dry-run` script print, IPv4 transaction-pooler fallback. | +| `db lint` | `ported` | `legacy/commands/db/lint/` | `n/a` | `n/a` | Native TS port. Runs `plpgsql_check` in a rolled-back transaction via LegacyDbConnection; emits Go-parity pretty JSON. | +| `db pull` | `ported` | `legacy/commands/db/pull/` | `n/a` | `n/a` | Native TS port. Native pg-delta / migra migration + `--declarative` pg-delta export; reconciles `schema_migrations`. The initial-migra pull dumps the remote schema natively (`pg_dump`) then appends the migra diff. Only `--experimental` (structured dump) still delegates to Go, pending a TS PostgreSQL DDL parser for `format.WriteStructuredSchemas`. | +| `db push` | `ported` | `legacy/commands/db/push/` | `n/a` | `n/a` | Native TS port. Connects local/linked/`--db-url`; pushes pending migrations, `--include-seed` seeds (`seed_files` hash tracking), `--include-roles`, `[db.vault]` secrets; `--dry-run`. `encrypted:` vault secrets + best-effort pg-delta catalog cache not ported (no output impact). | +| `db reset` | `ported` | `legacy/commands/db/reset/` | `n/a` | `n/a` | Remote path native (drop user schemas, vault upsert, MigrateAndSeed, `--version`/`--last`, `--sql-paths` seed override). Local path native: running check, recreate + migrate + seed via the hidden Go `db __db-bootstrap` seam, storage-gated bucket seeding (reuses `seed buckets`), git-branch `Finished…` line. Only the niche `--experimental` remote schema-files path still delegates to the Go binary (telemetry-disabled). | +| `db start` | `ported` | `legacy/commands/db/start/` | `n/a` | `n/a` | Native TS port. Validates config, checks "already running" (prints Go's line), else delegates the container bootstrap (create + health + initial schema/roles/migrations/seed + `_current_branch`) to the hidden Go `db __db-bootstrap --mode start` seam. No status table / `cli_stack_started` (those are `supabase start`). `--from-backup` supported. | +| `inspect report` | `ported` | `legacy/commands/inspect/report/` | `n/a` | `n/a` | Native TS port. Runs every inspect query via server-side `COPY ... CSV`, writes 14 CSVs under `//`, then renders a Go-parity Glamour rules summary (bounded csvq-subset evaluator; custom `[experimental.inspect.rules]` supported). | +| `inspect db db-stats` | `ported` | `legacy/commands/inspect/db/db-stats/` | `n/a` | `n/a` | Native TS port. Queries Postgres directly via LegacyDbConnection; renders Go-parity Glamour tables. | +| `inspect db replication-slots` | `ported` | `legacy/commands/inspect/db/replication-slots/` | `n/a` | `n/a` | Native TS port. Queries Postgres directly via LegacyDbConnection; renders Go-parity Glamour tables. | +| `inspect db locks` | `ported` | `legacy/commands/inspect/db/locks/` | `n/a` | `n/a` | Native TS port. Queries Postgres directly via LegacyDbConnection; renders Go-parity Glamour tables. | +| `inspect db blocking` | `ported` | `legacy/commands/inspect/db/blocking/` | `n/a` | `n/a` | Native TS port. Queries Postgres directly via LegacyDbConnection; renders Go-parity Glamour tables. | +| `inspect db outliers` | `ported` | `legacy/commands/inspect/db/outliers/` | `n/a` | `n/a` | Native TS port. Queries Postgres directly via LegacyDbConnection; renders Go-parity Glamour tables. | +| `inspect db calls` | `ported` | `legacy/commands/inspect/db/calls/` | `n/a` | `n/a` | Native TS port. Queries Postgres directly via LegacyDbConnection; renders Go-parity Glamour tables. | +| `inspect db index-stats` | `ported` | `legacy/commands/inspect/db/index-stats/` | `n/a` | `n/a` | Native TS port. Queries Postgres directly via LegacyDbConnection; renders Go-parity Glamour tables. | +| `inspect db long-running-queries` | `ported` | `legacy/commands/inspect/db/long-running-queries/` | `n/a` | `n/a` | Native TS port. Queries Postgres directly via LegacyDbConnection; renders Go-parity Glamour tables. | +| `inspect db bloat` | `ported` | `legacy/commands/inspect/db/bloat/` | `n/a` | `n/a` | Native TS port. Queries Postgres directly via LegacyDbConnection; renders Go-parity Glamour tables. | +| `inspect db role-stats` | `ported` | `legacy/commands/inspect/db/role-stats/` | `n/a` | `n/a` | Native TS port. Queries Postgres directly via LegacyDbConnection; renders Go-parity Glamour tables. | +| `inspect db vacuum-stats` | `ported` | `legacy/commands/inspect/db/vacuum-stats/` | `n/a` | `n/a` | Native TS port. Queries Postgres directly via LegacyDbConnection; renders Go-parity Glamour tables. | +| `inspect db table-stats` | `ported` | `legacy/commands/inspect/db/table-stats/` | `n/a` | `n/a` | Native TS port. Queries Postgres directly via LegacyDbConnection; renders Go-parity Glamour tables. | +| `inspect db traffic-profile` | `ported` | `legacy/commands/inspect/db/traffic-profile/` | `n/a` | `n/a` | Native TS port. Queries Postgres directly via LegacyDbConnection; renders Go-parity Glamour tables. | +| `inspect db cache-hit` | `ported` | `legacy/commands/inspect/db/cache-hit/` | `n/a` | `n/a` | Native TS port. Deprecated (use db-stats); routes to the active query. | +| `inspect db index-usage` | `ported` | `legacy/commands/inspect/db/index-usage/` | `n/a` | `n/a` | Native TS port. Deprecated (use index-stats); routes to the active query. | +| `inspect db total-index-size` | `ported` | `legacy/commands/inspect/db/total-index-size/` | `n/a` | `n/a` | Native TS port. Deprecated (use index-stats); routes to the active query. | +| `inspect db index-sizes` | `ported` | `legacy/commands/inspect/db/index-sizes/` | `n/a` | `n/a` | Native TS port. Deprecated (use index-stats); routes to the active query. | +| `inspect db table-sizes` | `ported` | `legacy/commands/inspect/db/table-sizes/` | `n/a` | `n/a` | Native TS port. Deprecated (use table-stats); routes to the active query. | +| `inspect db table-index-sizes` | `ported` | `legacy/commands/inspect/db/table-index-sizes/` | `n/a` | `n/a` | Native TS port. Deprecated (use table-stats); routes to the active query. | +| `inspect db total-table-sizes` | `ported` | `legacy/commands/inspect/db/total-table-sizes/` | `n/a` | `n/a` | Native TS port. Deprecated (use table-stats); routes to the active query. | +| `inspect db unused-indexes` | `ported` | `legacy/commands/inspect/db/unused-indexes/` | `n/a` | `n/a` | Native TS port. Deprecated (use index-stats); routes to the active query. | +| `inspect db table-record-counts` | `ported` | `legacy/commands/inspect/db/table-record-counts/` | `n/a` | `n/a` | Native TS port. Deprecated (use table-stats); routes to the active query. | +| `inspect db seq-scans` | `ported` | `legacy/commands/inspect/db/seq-scans/` | `n/a` | `n/a` | Native TS port. Deprecated (use index-stats); routes to the active query. | +| `inspect db role-configs` | `ported` | `legacy/commands/inspect/db/role-configs/` | `n/a` | `n/a` | Native TS port. Deprecated (use role-stats); routes to the active query. | +| `inspect db role-connections` | `ported` | `legacy/commands/inspect/db/role-connections/` | `n/a` | `n/a` | Native TS port. Deprecated (use role-stats); routes to the active query. | +| `migration down` | `ported` | `legacy/commands/migration/down/` | `n/a` | `n/a` | Native TS port. Revert prompt → drop user schemas → vault upsert → migrate&seed to the target version; defaults to `--local`. Skips Go's pgcache catalog write. | +| `migration fetch` | `ported` | `legacy/commands/migration/fetch/` | `n/a` | `n/a` | Native TS port. Reads `schema_migrations` and writes `supabase/migrations/_.sql`; overwrite prompt for a non-empty dir. | +| `migration list` | `ported` | `legacy/commands/migration/list/` | `n/a` | `n/a` | Native TS port. Merges remote `schema_migrations` with local files into a Glamour ASCII table (Local / Remote / Time-UTC columns); defaults to `--linked`. | +| `migration new` | `ported` | `legacy/commands/migration/new/` | `n/a` | `n/a` | Native TS port. Writes `supabase/migrations/_.sql` (mode 0644) from piped stdin; no DB/API. | +| `migration repair` | `ported` | `legacy/commands/migration/repair/` | `n/a` | `n/a` | Native TS port. Transactional create-table + TRUNCATE/UPSERT/DELETE; applied mode reads local files; repair-all prompt; defaults to `--linked`. | +| `migration squash` | `missing` | `missing` | `n/a` | `n/a` | No native TS implementation yet. Wrapped in legacy shell. | +| `migration up` | `ported` | `legacy/commands/migration/up/` | `n/a` | `n/a` | Native TS port. Computes pending migrations, upserts `[db.vault]`, applies each transactionally; `--include-all` for out-of-order; defaults to `--local`. Does not seed (matches Go). | +| `seed buckets` | `ported` | `legacy/commands/seed/buckets/` | `n/a` | `n/a` | Native TS port. Local-only (Go's `seed` defines no `--project-ref`, so the ref is always empty): seeds `[storage.buckets]` + `[storage.vector]` against the local Storage service gateway; remote/analytics paths are unreachable and omitted. `--linked`/`--local` accepted for surface parity (both seed local). Vector graceful-skip WARNINGs ported. | +| `test db` | `ported` | `legacy/commands/test/db/` | `n/a` | `n/a` | Native TS port. `--db-url`/`--local`/`--linked` + variadic paths; runs `supabase/pg_prove:3.36` via `docker run`; pgTAP enable/disable via `@effect/sql-pg`. `--network-id` override and `[images]` config override not modeled (documented divergences). | +| `test new` | `ported` | `legacy/commands/test/new/` | `n/a` | `n/a` | Native TS port. Writes `supabase/tests/_test.sql` from the embedded pgtap template; `--template` (pgtap). | ## Code Generation @@ -211,111 +211,111 @@ Legend: - `wrapped`: Phase 0 proxy wrapper exists in the legacy shell - `missing`: no legacy shell command yet -| Command | Legacy status | Legacy command path | -| -------------------------------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `orgs list` | `ported` | [`../src/legacy/commands/orgs/list/list.command.ts`](../src/legacy/commands/orgs/list/list.command.ts) | -| `orgs create` | `ported` | [`../src/legacy/commands/orgs/create/create.command.ts`](../src/legacy/commands/orgs/create/create.command.ts) | -| `projects list` | `ported` | [`../src/legacy/commands/projects/list/list.command.ts`](../src/legacy/commands/projects/list/list.command.ts) | -| `projects create` | `ported` | [`../src/legacy/commands/projects/create/create.command.ts`](../src/legacy/commands/projects/create/create.command.ts) | -| `projects delete` | `ported` | [`../src/legacy/commands/projects/delete/delete.command.ts`](../src/legacy/commands/projects/delete/delete.command.ts) | -| `projects api-keys` | `ported` | [`../src/legacy/commands/projects/api-keys/api-keys.command.ts`](../src/legacy/commands/projects/api-keys/api-keys.command.ts) | -| `branches list` | `ported` | [`../src/legacy/commands/branches/list/list.command.ts`](../src/legacy/commands/branches/list/list.command.ts) | -| `branches create` | `ported` | [`../src/legacy/commands/branches/create/create.command.ts`](../src/legacy/commands/branches/create/create.command.ts) | -| `branches get` | `ported` | [`../src/legacy/commands/branches/get/get.command.ts`](../src/legacy/commands/branches/get/get.command.ts) | -| `branches update` | `ported` | [`../src/legacy/commands/branches/update/update.command.ts`](../src/legacy/commands/branches/update/update.command.ts) | -| `branches pause` | `ported` | [`../src/legacy/commands/branches/pause/pause.command.ts`](../src/legacy/commands/branches/pause/pause.command.ts) | -| `branches unpause` | `ported` | [`../src/legacy/commands/branches/unpause/unpause.command.ts`](../src/legacy/commands/branches/unpause/unpause.command.ts) | -| `branches delete` | `ported` | [`../src/legacy/commands/branches/delete/delete.command.ts`](../src/legacy/commands/branches/delete/delete.command.ts) | -| `branches disable` | `ported` | [`../src/legacy/commands/branches/disable/disable.command.ts`](../src/legacy/commands/branches/disable/disable.command.ts) | -| `secrets list` | `ported` | [`../src/legacy/commands/secrets/list/list.command.ts`](../src/legacy/commands/secrets/list/list.command.ts) | -| `secrets set` | `ported` | [`../src/legacy/commands/secrets/set/set.command.ts`](../src/legacy/commands/secrets/set/set.command.ts) | -| `secrets unset` | `ported` | [`../src/legacy/commands/secrets/unset/unset.command.ts`](../src/legacy/commands/secrets/unset/unset.command.ts) | -| `config push` | `ported` | [`../src/legacy/commands/config/push/push.command.ts`](../src/legacy/commands/config/push/push.command.ts) | -| `backups list` | `ported` | [`../src/legacy/commands/backups/list/list.command.ts`](../src/legacy/commands/backups/list/list.command.ts) | -| `backups restore` | `ported` | [`../src/legacy/commands/backups/restore/restore.command.ts`](../src/legacy/commands/backups/restore/restore.command.ts) | -| `snippets list` | `ported` | [`../src/legacy/commands/snippets/list/list.command.ts`](../src/legacy/commands/snippets/list/list.command.ts) | -| `snippets download` | `ported` | [`../src/legacy/commands/snippets/download/download.command.ts`](../src/legacy/commands/snippets/download/download.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` | `ported` | [`../src/legacy/commands/domains/create/create.command.ts`](../src/legacy/commands/domains/create/create.command.ts) | -| `domains get` | `ported` | [`../src/legacy/commands/domains/get/get.command.ts`](../src/legacy/commands/domains/get/get.command.ts) | -| `domains reverify` | `ported` | [`../src/legacy/commands/domains/reverify/reverify.command.ts`](../src/legacy/commands/domains/reverify/reverify.command.ts) | -| `domains activate` | `ported` | [`../src/legacy/commands/domains/activate/activate.command.ts`](../src/legacy/commands/domains/activate/activate.command.ts) | -| `domains delete` | `ported` | [`../src/legacy/commands/domains/delete/delete.command.ts`](../src/legacy/commands/domains/delete/delete.command.ts) | -| `vanity-subdomains get` | `ported` | [`../src/legacy/commands/vanity-subdomains/get/get.command.ts`](../src/legacy/commands/vanity-subdomains/get/get.command.ts) | -| `vanity-subdomains check-availability` | `ported` | [`../src/legacy/commands/vanity-subdomains/check-availability/check-availability.command.ts`](../src/legacy/commands/vanity-subdomains/check-availability/check-availability.command.ts) | -| `vanity-subdomains activate` | `ported` | [`../src/legacy/commands/vanity-subdomains/activate/activate.command.ts`](../src/legacy/commands/vanity-subdomains/activate/activate.command.ts) | -| `vanity-subdomains delete` | `ported` | [`../src/legacy/commands/vanity-subdomains/delete/delete.command.ts`](../src/legacy/commands/vanity-subdomains/delete/delete.command.ts) | -| `network-bans get` | `ported` | [`../src/legacy/commands/network-bans/get/get.command.ts`](../src/legacy/commands/network-bans/get/get.command.ts) | -| `network-bans remove` | `ported` | [`../src/legacy/commands/network-bans/remove/remove.command.ts`](../src/legacy/commands/network-bans/remove/remove.command.ts) | -| `network-restrictions get` | `ported` | [`../src/legacy/commands/network-restrictions/get/get.command.ts`](../src/legacy/commands/network-restrictions/get/get.command.ts) | -| `network-restrictions update` | `ported` | [`../src/legacy/commands/network-restrictions/update/update.command.ts`](../src/legacy/commands/network-restrictions/update/update.command.ts) | -| `encryption get-root-key` | `ported` | [`../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` | `ported` | [`../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` | `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` | `ported` | [`../src/legacy/commands/postgres-config/get/get.command.ts`](../src/legacy/commands/postgres-config/get/get.command.ts) | -| `postgres-config update` | `ported` | [`../src/legacy/commands/postgres-config/update/update.command.ts`](../src/legacy/commands/postgres-config/update/update.command.ts) | -| `postgres-config delete` | `ported` | [`../src/legacy/commands/postgres-config/delete/delete.command.ts`](../src/legacy/commands/postgres-config/delete/delete.command.ts) | -| `login` | `ported` | [`../src/legacy/commands/login/login.command.ts`](../src/legacy/commands/login/login.command.ts) | -| `logout` | `ported` | [`../src/legacy/commands/logout/logout.command.ts`](../src/legacy/commands/logout/logout.command.ts) | -| `link` | `ported` | [`../src/legacy/commands/link/link.command.ts`](../src/legacy/commands/link/link.command.ts) | -| `unlink` | `ported` | [`../src/legacy/commands/unlink/unlink.command.ts`](../src/legacy/commands/unlink/unlink.command.ts) | -| `bootstrap` | `ported` | [`../src/legacy/commands/bootstrap/bootstrap.command.ts`](../src/legacy/commands/bootstrap/bootstrap.command.ts) (native; `db push` step delegated to the Go binary — interim) | -| `init` | `ported` | [`../src/legacy/commands/init/init.command.ts`](../src/legacy/commands/init/init.command.ts) | -| `services` | `ported` | [`../src/legacy/commands/services/services.command.ts`](../src/legacy/commands/services/services.command.ts) | -| `start` | `wrapped` | [`../src/legacy/commands/start/start.command.ts`](../src/legacy/commands/start/start.command.ts) | -| `stop` | `wrapped` | [`../src/legacy/commands/stop/stop.command.ts`](../src/legacy/commands/stop/stop.command.ts) | -| `status` | `wrapped` | [`../src/legacy/commands/status/status.command.ts`](../src/legacy/commands/status/status.command.ts) | -| `telemetry enable` | `ported` | [`../src/legacy/commands/telemetry/enable/enable.command.ts`](../src/legacy/commands/telemetry/enable/enable.command.ts) | -| `telemetry disable` | `ported` | [`../src/legacy/commands/telemetry/disable/disable.command.ts`](../src/legacy/commands/telemetry/disable/disable.command.ts) | -| `telemetry status` | `ported` | [`../src/legacy/commands/telemetry/status/status.command.ts`](../src/legacy/commands/telemetry/status/status.command.ts) | -| `migration list` | `ported` | [`../src/legacy/commands/migration/list/list.command.ts`](../src/legacy/commands/migration/list/list.command.ts) — native; merged Local/Remote/Time-UTC Glamour table | -| `migration new` | `ported` | [`../src/legacy/commands/migration/new/new.command.ts`](../src/legacy/commands/migration/new/new.command.ts) — native; writes `supabase/migrations/_.sql` from piped stdin | -| `migration repair` | `ported` | [`../src/legacy/commands/migration/repair/repair.command.ts`](../src/legacy/commands/migration/repair/repair.command.ts) — native; transactional TRUNCATE/UPSERT/DELETE, repair-all prompt | -| `migration squash` | `wrapped` | [`../src/legacy/commands/migration/squash/squash.command.ts`](../src/legacy/commands/migration/squash/squash.command.ts) | -| `migration up` | `ported` | [`../src/legacy/commands/migration/up/up.command.ts`](../src/legacy/commands/migration/up/up.command.ts) — native; pending compute + vault upsert + per-file apply | -| `migration down` | `ported` | [`../src/legacy/commands/migration/down/down.command.ts`](../src/legacy/commands/migration/down/down.command.ts) — native; drop + vault + migrate&seed to target version | -| `migration fetch` | `ported` | [`../src/legacy/commands/migration/fetch/fetch.command.ts`](../src/legacy/commands/migration/fetch/fetch.command.ts) — native; writes history rows to `supabase/migrations/` | -| `gen types` | `ported` | [`../src/legacy/commands/gen/types/types.command.ts`](../src/legacy/commands/gen/types/types.command.ts) - native; non-TypeScript project refs use pg-meta with IPv4 pooler retry instead of Go's "Try using --db-url" failure; `--swift-access-control` requires `--lang swift`; explicit source flags with `--query-timeout` on the remote TypeScript path error, while the implicit linked TypeScript path warns and continues | -| `gen signing-key` | `ported` | [`../src/legacy/commands/gen/signing-key/signing-key.command.ts`](../src/legacy/commands/gen/signing-key/signing-key.command.ts) | -| `gen bearer-jwt` | `wrapped` | [`../src/legacy/commands/gen/bearer-jwt/bearer-jwt.command.ts`](../src/legacy/commands/gen/bearer-jwt/bearer-jwt.command.ts) | -| `gen keys` | `wrapped` | [`../src/legacy/commands/gen/keys/keys.command.ts`](../src/legacy/commands/gen/keys/keys.command.ts) | -| `functions list` | `wrapped` | [`../src/legacy/commands/functions/list/list.command.ts`](../src/legacy/commands/functions/list/list.command.ts) | -| `functions delete` | `ported` | [`../src/legacy/commands/functions/delete/delete.command.ts`](../src/legacy/commands/functions/delete/delete.command.ts) | -| `functions download` | `ported` | [`../src/legacy/commands/functions/download/download.command.ts`](../src/legacy/commands/functions/download/download.command.ts) | -| `functions deploy` | `ported` | [`../src/legacy/commands/functions/deploy/deploy.command.ts`](../src/legacy/commands/functions/deploy/deploy.command.ts) | -| `functions new` | `ported` | [`../src/legacy/commands/functions/new/new.command.ts`](../src/legacy/commands/functions/new/new.command.ts) | -| `functions serve` | `ported` | [`../src/legacy/commands/functions/serve/serve.command.ts`](../src/legacy/commands/functions/serve/serve.command.ts) | -| `storage ls` | `ported` | [`../src/legacy/commands/storage/ls/ls.command.ts`](../src/legacy/commands/storage/ls/ls.command.ts) | -| `storage cp` | `ported` | [`../src/legacy/commands/storage/cp/cp.command.ts`](../src/legacy/commands/storage/cp/cp.command.ts) | -| `storage mv` | `ported` | [`../src/legacy/commands/storage/mv/mv.command.ts`](../src/legacy/commands/storage/mv/mv.command.ts) | -| `storage rm` | `ported` | [`../src/legacy/commands/storage/rm/rm.command.ts`](../src/legacy/commands/storage/rm/rm.command.ts) | -| `test db` | `ported` | [`../src/legacy/commands/test/db/db.command.ts`](../src/legacy/commands/test/db/db.command.ts) | -| `test new` | `ported` | [`../src/legacy/commands/test/new/new.command.ts`](../src/legacy/commands/test/new/new.command.ts) | -| `seed buckets` | `ported` | [`../src/legacy/commands/seed/buckets/buckets.command.ts`](../src/legacy/commands/seed/buckets/buckets.command.ts) | -| `db diff` | `ported` | [`../src/legacy/commands/db/diff/diff.command.ts`](../src/legacy/commands/db/diff/diff.command.ts) — native pg-delta / migra; `--use-pgadmin` / `--use-pg-schema` delegate to Go | -| `db dump` | `ported` | [`../src/legacy/commands/db/dump/dump.command.ts`](../src/legacy/commands/db/dump/dump.command.ts) | -| `db push` | `wrapped` | [`../src/legacy/commands/db/push/push.command.ts`](../src/legacy/commands/db/push/push.command.ts) | -| `db pull` | `ported` | [`../src/legacy/commands/db/pull/pull.command.ts`](../src/legacy/commands/db/pull/pull.command.ts) — native pg-delta / migra; `--declarative` (deprecated alias `--use-pg-delta`) + `--diff-engine` (migra\|pg-delta); initial-migra pull dumps the schema natively (`pg_dump`) + appends the diff; only `--experimental` structured dump still delegates to Go (needs a TS DDL parser for `WriteStructuredSchemas`) | -| `db reset` | `wrapped` | [`../src/legacy/commands/db/reset/reset.command.ts`](../src/legacy/commands/db/reset/reset.command.ts) — includes Go-parity `--sql-paths` override for `[db.seed].sql_paths` | -| `db lint` | `ported` | [`../src/legacy/commands/db/lint/lint.command.ts`](../src/legacy/commands/db/lint/lint.command.ts) | -| `db start` | `wrapped` | [`../src/legacy/commands/db/start/start.command.ts`](../src/legacy/commands/db/start/start.command.ts) | -| `db query` | `ported` | [`../src/legacy/commands/db/query/query.command.ts`](../src/legacy/commands/db/query/query.command.ts) | -| `db advisors` | `ported` | [`../src/legacy/commands/db/advisors/advisors.command.ts`](../src/legacy/commands/db/advisors/advisors.command.ts) | -| `db test` | `wrapped` | [`../src/legacy/commands/db/test/test.command.ts`](../src/legacy/commands/db/test/test.command.ts) | -| `db branch create` | `wrapped` | [`../src/legacy/commands/db/branch/create/create.command.ts`](../src/legacy/commands/db/branch/create/create.command.ts) | -| `db branch delete` | `wrapped` | [`../src/legacy/commands/db/branch/delete/delete.command.ts`](../src/legacy/commands/db/branch/delete/delete.command.ts) | -| `db branch list` | `wrapped` | [`../src/legacy/commands/db/branch/list/list.command.ts`](../src/legacy/commands/db/branch/list/list.command.ts) | -| `db branch switch` | `wrapped` | [`../src/legacy/commands/db/branch/switch/switch.command.ts`](../src/legacy/commands/db/branch/switch/switch.command.ts) | -| `db remote changes` | `wrapped` | [`../src/legacy/commands/db/remote/changes/changes.command.ts`](../src/legacy/commands/db/remote/changes/changes.command.ts) | -| `db remote commit` | `wrapped` | [`../src/legacy/commands/db/remote/commit/commit.command.ts`](../src/legacy/commands/db/remote/commit/commit.command.ts) | -| `db schema declarative sync` | `ported` | [`../src/legacy/commands/db/schema/declarative/sync/sync.command.ts`](../src/legacy/commands/db/schema/declarative/sync/sync.command.ts) | -| `db schema declarative generate` | `ported` | [`../src/legacy/commands/db/schema/declarative/generate/generate.command.ts`](../src/legacy/commands/db/schema/declarative/generate/generate.command.ts) | +| Command | Legacy status | Legacy command path | +| -------------------------------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `orgs list` | `ported` | [`../src/legacy/commands/orgs/list/list.command.ts`](../src/legacy/commands/orgs/list/list.command.ts) | +| `orgs create` | `ported` | [`../src/legacy/commands/orgs/create/create.command.ts`](../src/legacy/commands/orgs/create/create.command.ts) | +| `projects list` | `ported` | [`../src/legacy/commands/projects/list/list.command.ts`](../src/legacy/commands/projects/list/list.command.ts) | +| `projects create` | `ported` | [`../src/legacy/commands/projects/create/create.command.ts`](../src/legacy/commands/projects/create/create.command.ts) | +| `projects delete` | `ported` | [`../src/legacy/commands/projects/delete/delete.command.ts`](../src/legacy/commands/projects/delete/delete.command.ts) | +| `projects api-keys` | `ported` | [`../src/legacy/commands/projects/api-keys/api-keys.command.ts`](../src/legacy/commands/projects/api-keys/api-keys.command.ts) | +| `branches list` | `ported` | [`../src/legacy/commands/branches/list/list.command.ts`](../src/legacy/commands/branches/list/list.command.ts) | +| `branches create` | `ported` | [`../src/legacy/commands/branches/create/create.command.ts`](../src/legacy/commands/branches/create/create.command.ts) | +| `branches get` | `ported` | [`../src/legacy/commands/branches/get/get.command.ts`](../src/legacy/commands/branches/get/get.command.ts) | +| `branches update` | `ported` | [`../src/legacy/commands/branches/update/update.command.ts`](../src/legacy/commands/branches/update/update.command.ts) | +| `branches pause` | `ported` | [`../src/legacy/commands/branches/pause/pause.command.ts`](../src/legacy/commands/branches/pause/pause.command.ts) | +| `branches unpause` | `ported` | [`../src/legacy/commands/branches/unpause/unpause.command.ts`](../src/legacy/commands/branches/unpause/unpause.command.ts) | +| `branches delete` | `ported` | [`../src/legacy/commands/branches/delete/delete.command.ts`](../src/legacy/commands/branches/delete/delete.command.ts) | +| `branches disable` | `ported` | [`../src/legacy/commands/branches/disable/disable.command.ts`](../src/legacy/commands/branches/disable/disable.command.ts) | +| `secrets list` | `ported` | [`../src/legacy/commands/secrets/list/list.command.ts`](../src/legacy/commands/secrets/list/list.command.ts) | +| `secrets set` | `ported` | [`../src/legacy/commands/secrets/set/set.command.ts`](../src/legacy/commands/secrets/set/set.command.ts) | +| `secrets unset` | `ported` | [`../src/legacy/commands/secrets/unset/unset.command.ts`](../src/legacy/commands/secrets/unset/unset.command.ts) | +| `config push` | `ported` | [`../src/legacy/commands/config/push/push.command.ts`](../src/legacy/commands/config/push/push.command.ts) | +| `backups list` | `ported` | [`../src/legacy/commands/backups/list/list.command.ts`](../src/legacy/commands/backups/list/list.command.ts) | +| `backups restore` | `ported` | [`../src/legacy/commands/backups/restore/restore.command.ts`](../src/legacy/commands/backups/restore/restore.command.ts) | +| `snippets list` | `ported` | [`../src/legacy/commands/snippets/list/list.command.ts`](../src/legacy/commands/snippets/list/list.command.ts) | +| `snippets download` | `ported` | [`../src/legacy/commands/snippets/download/download.command.ts`](../src/legacy/commands/snippets/download/download.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` | `ported` | [`../src/legacy/commands/domains/create/create.command.ts`](../src/legacy/commands/domains/create/create.command.ts) | +| `domains get` | `ported` | [`../src/legacy/commands/domains/get/get.command.ts`](../src/legacy/commands/domains/get/get.command.ts) | +| `domains reverify` | `ported` | [`../src/legacy/commands/domains/reverify/reverify.command.ts`](../src/legacy/commands/domains/reverify/reverify.command.ts) | +| `domains activate` | `ported` | [`../src/legacy/commands/domains/activate/activate.command.ts`](../src/legacy/commands/domains/activate/activate.command.ts) | +| `domains delete` | `ported` | [`../src/legacy/commands/domains/delete/delete.command.ts`](../src/legacy/commands/domains/delete/delete.command.ts) | +| `vanity-subdomains get` | `ported` | [`../src/legacy/commands/vanity-subdomains/get/get.command.ts`](../src/legacy/commands/vanity-subdomains/get/get.command.ts) | +| `vanity-subdomains check-availability` | `ported` | [`../src/legacy/commands/vanity-subdomains/check-availability/check-availability.command.ts`](../src/legacy/commands/vanity-subdomains/check-availability/check-availability.command.ts) | +| `vanity-subdomains activate` | `ported` | [`../src/legacy/commands/vanity-subdomains/activate/activate.command.ts`](../src/legacy/commands/vanity-subdomains/activate/activate.command.ts) | +| `vanity-subdomains delete` | `ported` | [`../src/legacy/commands/vanity-subdomains/delete/delete.command.ts`](../src/legacy/commands/vanity-subdomains/delete/delete.command.ts) | +| `network-bans get` | `ported` | [`../src/legacy/commands/network-bans/get/get.command.ts`](../src/legacy/commands/network-bans/get/get.command.ts) | +| `network-bans remove` | `ported` | [`../src/legacy/commands/network-bans/remove/remove.command.ts`](../src/legacy/commands/network-bans/remove/remove.command.ts) | +| `network-restrictions get` | `ported` | [`../src/legacy/commands/network-restrictions/get/get.command.ts`](../src/legacy/commands/network-restrictions/get/get.command.ts) | +| `network-restrictions update` | `ported` | [`../src/legacy/commands/network-restrictions/update/update.command.ts`](../src/legacy/commands/network-restrictions/update/update.command.ts) | +| `encryption get-root-key` | `ported` | [`../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` | `ported` | [`../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` | `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` | `ported` | [`../src/legacy/commands/postgres-config/get/get.command.ts`](../src/legacy/commands/postgres-config/get/get.command.ts) | +| `postgres-config update` | `ported` | [`../src/legacy/commands/postgres-config/update/update.command.ts`](../src/legacy/commands/postgres-config/update/update.command.ts) | +| `postgres-config delete` | `ported` | [`../src/legacy/commands/postgres-config/delete/delete.command.ts`](../src/legacy/commands/postgres-config/delete/delete.command.ts) | +| `login` | `ported` | [`../src/legacy/commands/login/login.command.ts`](../src/legacy/commands/login/login.command.ts) | +| `logout` | `ported` | [`../src/legacy/commands/logout/logout.command.ts`](../src/legacy/commands/logout/logout.command.ts) | +| `link` | `ported` | [`../src/legacy/commands/link/link.command.ts`](../src/legacy/commands/link/link.command.ts) | +| `unlink` | `ported` | [`../src/legacy/commands/unlink/unlink.command.ts`](../src/legacy/commands/unlink/unlink.command.ts) | +| `bootstrap` | `ported` | [`../src/legacy/commands/bootstrap/bootstrap.command.ts`](../src/legacy/commands/bootstrap/bootstrap.command.ts) (native; `db push` step delegated to the Go binary — interim) | +| `init` | `ported` | [`../src/legacy/commands/init/init.command.ts`](../src/legacy/commands/init/init.command.ts) | +| `services` | `ported` | [`../src/legacy/commands/services/services.command.ts`](../src/legacy/commands/services/services.command.ts) | +| `start` | `wrapped` | [`../src/legacy/commands/start/start.command.ts`](../src/legacy/commands/start/start.command.ts) | +| `stop` | `wrapped` | [`../src/legacy/commands/stop/stop.command.ts`](../src/legacy/commands/stop/stop.command.ts) | +| `status` | `wrapped` | [`../src/legacy/commands/status/status.command.ts`](../src/legacy/commands/status/status.command.ts) | +| `telemetry enable` | `ported` | [`../src/legacy/commands/telemetry/enable/enable.command.ts`](../src/legacy/commands/telemetry/enable/enable.command.ts) | +| `telemetry disable` | `ported` | [`../src/legacy/commands/telemetry/disable/disable.command.ts`](../src/legacy/commands/telemetry/disable/disable.command.ts) | +| `telemetry status` | `ported` | [`../src/legacy/commands/telemetry/status/status.command.ts`](../src/legacy/commands/telemetry/status/status.command.ts) | +| `migration list` | `ported` | [`../src/legacy/commands/migration/list/list.command.ts`](../src/legacy/commands/migration/list/list.command.ts) — native; merged Local/Remote/Time-UTC Glamour table | +| `migration new` | `ported` | [`../src/legacy/commands/migration/new/new.command.ts`](../src/legacy/commands/migration/new/new.command.ts) — native; writes `supabase/migrations/_.sql` from piped stdin | +| `migration repair` | `ported` | [`../src/legacy/commands/migration/repair/repair.command.ts`](../src/legacy/commands/migration/repair/repair.command.ts) — native; transactional TRUNCATE/UPSERT/DELETE, repair-all prompt | +| `migration squash` | `wrapped` | [`../src/legacy/commands/migration/squash/squash.command.ts`](../src/legacy/commands/migration/squash/squash.command.ts) | +| `migration up` | `ported` | [`../src/legacy/commands/migration/up/up.command.ts`](../src/legacy/commands/migration/up/up.command.ts) — native; pending compute + vault upsert + per-file apply | +| `migration down` | `ported` | [`../src/legacy/commands/migration/down/down.command.ts`](../src/legacy/commands/migration/down/down.command.ts) — native; drop + vault + migrate&seed to target version | +| `migration fetch` | `ported` | [`../src/legacy/commands/migration/fetch/fetch.command.ts`](../src/legacy/commands/migration/fetch/fetch.command.ts) — native; writes history rows to `supabase/migrations/` | +| `gen types` | `ported` | [`../src/legacy/commands/gen/types/types.command.ts`](../src/legacy/commands/gen/types/types.command.ts) | +| `gen signing-key` | `ported` | [`../src/legacy/commands/gen/signing-key/signing-key.command.ts`](../src/legacy/commands/gen/signing-key/signing-key.command.ts) | +| `gen bearer-jwt` | `wrapped` | [`../src/legacy/commands/gen/bearer-jwt/bearer-jwt.command.ts`](../src/legacy/commands/gen/bearer-jwt/bearer-jwt.command.ts) | +| `gen keys` | `wrapped` | [`../src/legacy/commands/gen/keys/keys.command.ts`](../src/legacy/commands/gen/keys/keys.command.ts) | +| `functions list` | `wrapped` | [`../src/legacy/commands/functions/list/list.command.ts`](../src/legacy/commands/functions/list/list.command.ts) | +| `functions delete` | `ported` | [`../src/legacy/commands/functions/delete/delete.command.ts`](../src/legacy/commands/functions/delete/delete.command.ts) | +| `functions download` | `ported` | [`../src/legacy/commands/functions/download/download.command.ts`](../src/legacy/commands/functions/download/download.command.ts) | +| `functions deploy` | `ported` | [`../src/legacy/commands/functions/deploy/deploy.command.ts`](../src/legacy/commands/functions/deploy/deploy.command.ts) | +| `functions new` | `ported` | [`../src/legacy/commands/functions/new/new.command.ts`](../src/legacy/commands/functions/new/new.command.ts) | +| `functions serve` | `ported` | [`../src/legacy/commands/functions/serve/serve.command.ts`](../src/legacy/commands/functions/serve/serve.command.ts) | +| `storage ls` | `ported` | [`../src/legacy/commands/storage/ls/ls.command.ts`](../src/legacy/commands/storage/ls/ls.command.ts) | +| `storage cp` | `ported` | [`../src/legacy/commands/storage/cp/cp.command.ts`](../src/legacy/commands/storage/cp/cp.command.ts) | +| `storage mv` | `ported` | [`../src/legacy/commands/storage/mv/mv.command.ts`](../src/legacy/commands/storage/mv/mv.command.ts) | +| `storage rm` | `ported` | [`../src/legacy/commands/storage/rm/rm.command.ts`](../src/legacy/commands/storage/rm/rm.command.ts) | +| `test db` | `ported` | [`../src/legacy/commands/test/db/db.command.ts`](../src/legacy/commands/test/db/db.command.ts) | +| `test new` | `ported` | [`../src/legacy/commands/test/new/new.command.ts`](../src/legacy/commands/test/new/new.command.ts) | +| `seed buckets` | `ported` | [`../src/legacy/commands/seed/buckets/buckets.command.ts`](../src/legacy/commands/seed/buckets/buckets.command.ts) | +| `db diff` | `ported` | [`../src/legacy/commands/db/diff/diff.command.ts`](../src/legacy/commands/db/diff/diff.command.ts) — native pg-delta / migra; `--use-pgadmin` / `--use-pg-schema` delegate to Go | +| `db dump` | `ported` | [`../src/legacy/commands/db/dump/dump.command.ts`](../src/legacy/commands/db/dump/dump.command.ts) | +| `db push` | `ported` | [`../src/legacy/commands/db/push/push.command.ts`](../src/legacy/commands/db/push/push.command.ts) | +| `db pull` | `ported` | [`../src/legacy/commands/db/pull/pull.command.ts`](../src/legacy/commands/db/pull/pull.command.ts) — native pg-delta / migra; `--declarative` (deprecated alias `--use-pg-delta`) + `--diff-engine` (migra\|pg-delta); initial-migra pull dumps the schema natively (`pg_dump`) + appends the diff; only `--experimental` structured dump still delegates to Go (needs a TS DDL parser for `WriteStructuredSchemas`) | +| `db reset` | `ported` | [`../src/legacy/commands/db/reset/reset.command.ts`](../src/legacy/commands/db/reset/reset.command.ts) — includes Go-parity `--sql-paths` override for `[db.seed].sql_paths` | +| `db lint` | `ported` | [`../src/legacy/commands/db/lint/lint.command.ts`](../src/legacy/commands/db/lint/lint.command.ts) | +| `db start` | `ported` | [`../src/legacy/commands/db/start/start.command.ts`](../src/legacy/commands/db/start/start.command.ts) | +| `db query` | `ported` | [`../src/legacy/commands/db/query/query.command.ts`](../src/legacy/commands/db/query/query.command.ts) | +| `db advisors` | `ported` | [`../src/legacy/commands/db/advisors/advisors.command.ts`](../src/legacy/commands/db/advisors/advisors.command.ts) | +| `db test` | `wrapped` | [`../src/legacy/commands/db/test/test.command.ts`](../src/legacy/commands/db/test/test.command.ts) | +| `db branch create` | `wrapped` | [`../src/legacy/commands/db/branch/create/create.command.ts`](../src/legacy/commands/db/branch/create/create.command.ts) | +| `db branch delete` | `wrapped` | [`../src/legacy/commands/db/branch/delete/delete.command.ts`](../src/legacy/commands/db/branch/delete/delete.command.ts) | +| `db branch list` | `wrapped` | [`../src/legacy/commands/db/branch/list/list.command.ts`](../src/legacy/commands/db/branch/list/list.command.ts) | +| `db branch switch` | `wrapped` | [`../src/legacy/commands/db/branch/switch/switch.command.ts`](../src/legacy/commands/db/branch/switch/switch.command.ts) | +| `db remote changes` | `wrapped` | [`../src/legacy/commands/db/remote/changes/changes.command.ts`](../src/legacy/commands/db/remote/changes/changes.command.ts) | +| `db remote commit` | `wrapped` | [`../src/legacy/commands/db/remote/commit/commit.command.ts`](../src/legacy/commands/db/remote/commit/commit.command.ts) | +| `db schema declarative sync` | `ported` | [`../src/legacy/commands/db/schema/declarative/sync/sync.command.ts`](../src/legacy/commands/db/schema/declarative/sync/sync.command.ts) | +| `db schema declarative generate` | `ported` | [`../src/legacy/commands/db/schema/declarative/generate/generate.command.ts`](../src/legacy/commands/db/schema/declarative/generate/generate.command.ts) | Flag divergences from the Go reference: diff --git a/apps/cli/src/legacy/auth/legacy-platform-api.layer.unit.test.ts b/apps/cli/src/legacy/auth/legacy-platform-api.layer.unit.test.ts index df39fcba27..8e69052093 100644 --- a/apps/cli/src/legacy/auth/legacy-platform-api.layer.unit.test.ts +++ b/apps/cli/src/legacy/auth/legacy-platform-api.layer.unit.test.ts @@ -37,6 +37,7 @@ function mockCliConfig(opts: { apiUrl: opts.apiUrl ?? "https://api.supabase.com", projectHost: opts.projectHost ?? "supabase.co", poolerHost: "supabase.com", + dashboardUrl: "https://supabase.com/dashboard", accessToken: opts.accessToken === undefined ? Option.none() : Option.some(Redacted.make(opts.accessToken)), projectId: Option.none(), diff --git a/apps/cli/src/legacy/commands/db/push/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/db/push/SIDE_EFFECTS.md index 11337a8955..5216459f76 100644 --- a/apps/cli/src/legacy/commands/db/push/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/db/push/SIDE_EFFECTS.md @@ -1,59 +1,104 @@ # `supabase db push` +Native TypeScript port of `apps/cli-go/internal/db/push/push.go`. Applies pending +local migrations (and optionally seed data and custom roles) to the local or +linked/remote Postgres database. + ## Files Read -| Path | Format | When | -| -------------------------------- | ---------- | ------------------------------------------------- | -| `~/.supabase/access-token` | plain text | when `SUPABASE_ACCESS_TOKEN` unset and `--linked` | -| `/supabase/migrations/` | directory | always, to list migration files to push | -| `/supabase/roles.sql` | SQL | when `--include-roles` is set | -| seed files from config | SQL | when `--include-seed` is set | +| Path | Format | When | +| ------------------------------------- | ---------- | ----------------------------------------------------------------------- | +| `/supabase/config.toml` | TOML | always (embedded defaults used when absent) | +| `~/.supabase//project-ref` | plain text | on the `--linked` path (and the default target), to resolve the ref | +| `~/.supabase/access-token` | plain text | when `SUPABASE_ACCESS_TOKEN` unset and a linked temp-role is minted | +| `/supabase/migrations/` | directory | when `[db.migrations].enabled` (default true), to list local files | +| `/supabase/migrations/*.sql` | SQL | for each pending migration, when applied (and not `--dry-run`) | +| seed files from `[db.seed].sql_paths` | SQL | when `--include-seed` and `[db.seed].enabled` (paths under `supabase/`) | +| `/supabase/roles.sql` | SQL | when `--include-roles` (existence check + apply) | ## Files Written -| Path | Format | When | -| ---- | ------ | ---- | -| — | — | — | +| Path | Format | When | +| ------------------------------------------------ | ------ | ------------------------------------------------------------------------- | +| `~/.supabase//linked-project.json` | JSON | on the `--linked` path (post-run cache, Go's `ensureProjectGroupsCached`) | +| `~/.supabase/telemetry.json` | JSON | always (post-run telemetry flush) | + +No project files are written. All other effects are database mutations (below). + +## Database Mutations + +| Statement | When | +| ---------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | +| `RESET ALL` + `BEGIN` … migration statements … `INSERT INTO supabase_migrations.schema_migrations(version, name, statements)` … `COMMIT` | per pending migration (after confirmation) | +| `CREATE SCHEMA/TABLE … supabase_migrations.schema_migrations`, `ALTER TABLE … ADD COLUMN …` | once before applying migrations (idempotent) | +| `RESET ALL` + `BEGIN` … roles.sql statements … `COMMIT` (no history row) | per `--include-roles` globals file (after confirmation) | +| `SELECT id, name FROM vault.secrets …`, `SELECT vault.update_secret(...)`, `SELECT vault.create_secret(...)` | when `[db.vault]` has syncable secrets and migrations are applied | +| `CREATE TABLE … supabase_migrations.seed_files`, seed statements, `INSERT … seed_files(path, hash) … ON CONFLICT …` | per pending seed file with `--include-seed` (after confirmation); a dirty seed only refreshes the hash | ## API Routes -| Method | Path | Auth | Request body | Response (used fields) | -| ------ | ---- | ---- | ------------ | ---------------------- | -| — | — | — | — | — | +| Method | Path | Auth | Request body | Response (used fields) | +| ------ | ---- | ---- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| — | — | — | — | The native handler connects to Postgres directly. On the `--linked` path the db-config resolver may call the Management API to mint a temporary login role (inherited from the shared resolver). | ## Environment Variables -| Variable | Purpose | Required? | -| ----------------------- | --------------------------------------- | ------------------------------------------------------- | -| `SUPABASE_ACCESS_TOKEN` | auth token for `--linked` mode | no (falls back to keyring → `~/.supabase/access-token`) | -| `DB_PASSWORD` | password for direct database connection | no | +| Variable | Purpose | Required? | +| ----------------------- | ------------------------------------------- | ------------------------------------------------------- | +| `SUPABASE_ACCESS_TOKEN` | auth token for the `--linked` resolver path | no (falls back to keyring → `~/.supabase/access-token`) | +| `SUPABASE_DB_PASSWORD` | password for the linked/remote connection | no (`--password`/`-p` takes precedence) | +| `SUPABASE_YES` | auto-confirm prompts (Go's `viper YES`) | no (also `--yes`) | ## Exit Codes -| Code | Condition | -| ---- | --------------------------- | -| `0` | success | -| `1` | database connection failure | -| `1` | migration apply error | +| Code | Condition | +| ---- | ------------------------------------------------------------------------- | +| `0` | success (including "up to date") | +| `1` | mutually exclusive target flags (`[db-url linked local]`) | +| `1` | `ErrMissingLocal` — remote versions absent locally (suggests repair/pull) | +| `1` | `ErrMissingRemote` without `--include-all` (suggests `--include-all`) | +| `1` | user declined a confirmation prompt (`context canceled`) | +| `1` | `config.toml` parse failure | +| `1` | database connection / migration / seed / roles / vault apply failure | ## Output -### `--output-format text` (Go CLI compatible) +Diagnostics ("Connecting to…", "Applying migration…", "Seeding…", "Updating vault +secrets…", skip/up-to-date notices, dry-run plan, prompts) go to **stderr**. The +two summary lines Go prints to **stdout** — ` is up to date.` and +`Finished supabase db push.` (the command name in Aqua) — go to stdout in text +mode; in machine modes they are suppressed and a structured result is emitted. -Prints applied migration versions to stderr. With `--dry-run`, prints the migrations that would be applied. +### `--output-format text` (Go CLI compatible) -### `--output-format json` +Byte-matches Go: connection status, per-item progress, prompts, and the stdout +summary line, including ANSI color (Aqua command name, Bold file paths). -Not applicable. +### `--output-format json` / `stream-json` -### `--output-format stream-json` +stdout is payload-only. A single `result` object is emitted: -Not applicable. +```json +{ + "upToDate": false, + "dryRun": false, + "migrations": [".sql"], + "seeds": ["supabase/seed.sql"], + "roles": ["supabase/roles.sql"] +} +``` ## Notes -- `--dry-run` prints the migrations that would be applied without applying them. -- `--include-all` includes all migrations not found in remote history table. -- `--include-roles` includes custom roles from the roles file. -- `--include-seed` includes seed data from config. -- `--db-url`, `--linked` (default true), and `--local` are mutually exclusive. +- **Targets**: `--db-url`, `--linked` (default), and `--local` are mutually + exclusive; with no flag the target defaults to linked, matching Go. +- **Prompt order**: custom roles → migrations → seeds; each defaults to "yes" and + declining returns `context canceled`. +- **`--dry-run`** prints the plan (roles / migrations / seeds) and applies nothing. +- **`[db.migrations].enabled = false`** / **`[db.seed].enabled = false`** print a + skip notice naming the project ref (empty for local/db-url). +- **Vault**: only non-empty, non-`env()` `[db.vault]` literals are synced (Go syncs + secrets with a non-empty SHA256). **Known gap vs Go**: `encrypted:`-prefixed + vault secrets are currently skipped — dotenvx/ECIES decryption is not yet ported. +- **Migrations catalog cache** (Go's best-effort `pgcache.TryCacheMigrationsCatalog`, + warning-only) is not ported; it produces no output, so parity is preserved. diff --git a/apps/cli/src/legacy/commands/db/push/push.command.ts b/apps/cli/src/legacy/commands/db/push/push.command.ts index 2d6d8d17e9..8c936315ca 100644 --- a/apps/cli/src/legacy/commands/db/push/push.command.ts +++ b/apps/cli/src/legacy/commands/db/push/push.command.ts @@ -1,6 +1,10 @@ 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 { withLegacyCommandInstrumentation } from "../../../telemetry/legacy-command-instrumentation.ts"; import { legacyDbPush } from "./push.handler.ts"; +import { legacyDbPushRuntimeLayer } from "./push.layers.ts"; const config = { includeAll: Flag.boolean("include-all").pipe( @@ -37,5 +41,24 @@ export type LegacyDbPushFlags = CliCommand.Command.Config.Infer; export const legacyDbPushCommand = Command.make("push", config).pipe( Command.withDescription("Push new migrations to the remote database."), Command.withShortDescription("Push new migrations to the remote database"), - Command.withHandler((flags) => legacyDbPush(flags)), + Command.withHandler((flags) => + legacyDbPush(flags).pipe( + withLegacyCommandInstrumentation({ + flags: { + "include-all": flags.includeAll, + "include-roles": flags.includeRoles, + "include-seed": flags.includeSeed, + "dry-run": flags.dryRun, + "db-url": flags.dbUrl, + linked: flags.linked, + local: flags.local, + // `password` is a credential — always reaches telemetry as ``. + password: flags.password, + }, + aliases: { p: "password" }, + }), + withJsonErrorHandling, + ), + ), + Command.provide(legacyDbPushRuntimeLayer), ); diff --git a/apps/cli/src/legacy/commands/db/push/push.errors.ts b/apps/cli/src/legacy/commands/db/push/push.errors.ts new file mode 100644 index 0000000000..3849d55add --- /dev/null +++ b/apps/cli/src/legacy/commands/db/push/push.errors.ts @@ -0,0 +1,56 @@ +import { Data } from "effect"; + +/** + * Conflicting database-target flags. Reproduces cobra's + * `MarkFlagsMutuallyExclusive("db-url", "linked", "local")` error byte-for-byte + * (`apps/cli-go/cmd/db.go:526`). + */ +export class LegacyDbPushTargetFlagsError extends Data.TaggedError("LegacyDbPushTargetFlagsError")<{ + readonly message: string; +}> {} + +/** + * Remote migration versions are missing from the local directory. Byte-matches + * Go's `migration.ErrMissingLocal` (`pkg/migration/apply.go:16`); the + * `migration repair` / `db pull` suggestion is attached (Go's `CmdSuggestion`). + */ +export class LegacyDbPushMissingLocalError extends Data.TaggedError( + "LegacyDbPushMissingLocalError", +)<{ + readonly message: string; + readonly suggestion: string; +}> {} + +/** + * Local migration files are ordered before the remote head and `--include-all` + * was not passed. Byte-matches Go's `migration.ErrMissingRemote` + * (`pkg/migration/apply.go:15`); the `--include-all` suggestion is attached. + */ +export class LegacyDbPushMissingRemoteError extends Data.TaggedError( + "LegacyDbPushMissingRemoteError", +)<{ + readonly message: string; + readonly suggestion: string; +}> {} + +/** + * The user declined a confirmation prompt. Go returns `errors.New(context.Canceled)` + * (`internal/db/push/push.go:80,91,110`), rendered as `context canceled`. + */ +export class LegacyDbPushCancelledError extends Data.TaggedError("LegacyDbPushCancelledError")<{ + readonly message: string; +}> {} + +/** Locating `supabase/roles.sql` failed (Go's `failed to find custom roles: %w`). */ +export class LegacyDbPushRolesError extends Data.TaggedError("LegacyDbPushRolesError")<{ + readonly message: string; +}> {} + +/** + * A migration / seed / globals / vault statement failed while applying. Carries + * the underlying Postgres error (with Go's `At statement: ` context for + * migrations) so stderr matches Go's propagated error. + */ +export class LegacyDbPushApplyError extends Data.TaggedError("LegacyDbPushApplyError")<{ + readonly message: string; +}> {} diff --git a/apps/cli/src/legacy/commands/db/push/push.handler.ts b/apps/cli/src/legacy/commands/db/push/push.handler.ts index fddb270174..8bb4fdcd76 100644 --- a/apps/cli/src/legacy/commands/db/push/push.handler.ts +++ b/apps/cli/src/legacy/commands/db/push/push.handler.ts @@ -1,17 +1,340 @@ -import { Effect, Option } from "effect"; -import { LegacyGoProxy } from "../../../../shared/legacy/go-proxy.service.ts"; +import { Effect, FileSystem, Option, Path } from "effect"; + +import { CliArgs } from "../../../../shared/cli/cli-args.service.ts"; +import { LegacyDnsResolverFlag } from "../../../../shared/legacy/global-flags.ts"; +import { legacyResolveYesWithProjectEnv } from "../../../../shared/legacy/global-flags.ts"; +import { Output } from "../../../../shared/output/output.service.ts"; +import { LegacyCliConfig } from "../../../config/legacy-cli-config.service.ts"; +import { LegacyProjectRefResolver } from "../../../config/legacy-project-ref.service.ts"; +import { legacyAqua, legacyBold } from "../../../shared/legacy-colors.ts"; +import { LegacyDbConfigResolver } from "../../../shared/legacy-db-config.service.ts"; +import { + legacyCheckDbToml, + legacyLoadProjectEnv, +} from "../../../shared/legacy-db-config.toml-read.ts"; +import { LegacyDbConnection } from "../../../shared/legacy-db-connection.service.ts"; +import { + legacyApplyMigrations, + legacySeedGlobals, +} from "../../../shared/legacy-migration-apply.ts"; +import { legacyPromptYesNo } from "../../../shared/legacy-prompt-yes-no.ts"; +import { resolveLegacyDbTargetFlags } from "../../../shared/legacy-db-target-flags.ts"; +import { LegacyLinkedProjectCache } from "../../../telemetry/legacy-linked-project-cache.service.ts"; +import { LegacyTelemetryState } from "../../../telemetry/legacy-telemetry-state.service.ts"; +import { legacyListLocalMigrations } from "../shared/legacy-pgdelta.cache.ts"; +import { + LEGACY_ERR_MISSING_LOCAL, + LEGACY_ERR_MISSING_REMOTE, + legacyFindPendingMigrations, + legacyIncludeAllPending, + legacySuggestIgnoreFlag, + legacySuggestRevertHistory, +} from "../shared/legacy-migration-pending.ts"; +import { + type LegacySeedFile, + legacyGetPendingSeeds, + legacySeedData, +} from "../shared/legacy-seed-ops.ts"; +import { legacyUpsertVaultSecrets } from "../../../shared/legacy-vault.ts"; +// Listing the remote `schema_migrations` history (with the 42P01 → empty rule) +// lives in the shared migration-history module (Go's `migration.ListRemoteMigrations`). +import { legacyListRemoteMigrations } from "../../../shared/legacy-migration-history.ts"; import type { LegacyDbPushFlags } from "./push.command.ts"; +import { + LegacyDbPushApplyError, + LegacyDbPushCancelledError, + LegacyDbPushMissingLocalError, + LegacyDbPushMissingRemoteError, + LegacyDbPushRolesError, + LegacyDbPushTargetFlagsError, +} from "./push.errors.ts"; + +const CUSTOM_ROLES_PATH = "supabase/roles.sql"; + +const toSlash = (p: string): string => p.replaceAll("\\", "/"); + +/** Go's `confirmPushAll` (`internal/db/push/push.go:123-129`) — bold filenames. */ +const confirmPushAll = (filenames: ReadonlyArray): string => + filenames.map((name) => ` • ${legacyBold(name)}\n`).join(""); + +/** Go's `confirmSeedAll` (`internal/db/push/push.go:131-140`) — bold paths, hash notice. */ +const confirmSeedAll = (seeds: ReadonlyArray): string => + seeds + .map((seed) => ` • ${legacyBold(seed.dirty ? `${seed.path} (hash update)` : seed.path)}\n`) + .join(""); + +const applyError = (message: string) => new LegacyDbPushApplyError({ message }); +/** + * `supabase db push` — apply pending local migrations (and optionally seed data + * and custom roles) to the local or linked/remote database. + * + * Strict 1:1 port of `apps/cli-go/internal/db/push/push.go`. + */ export const legacyDbPush = Effect.fn("legacy.db.push")(function* (flags: LegacyDbPushFlags) { - const proxy = yield* LegacyGoProxy; - const args: string[] = ["db", "push"]; - if (flags.includeAll) args.push("--include-all"); - if (flags.includeRoles) args.push("--include-roles"); - if (flags.includeSeed) args.push("--include-seed"); - if (flags.dryRun) args.push("--dry-run"); - if (Option.isSome(flags.dbUrl)) args.push("--db-url", flags.dbUrl.value); - if (flags.linked) args.push("--linked"); - if (flags.local) args.push("--local"); - if (Option.isSome(flags.password)) args.push("--password", flags.password.value); - yield* proxy.exec(args); + const output = yield* Output; + const resolver = yield* LegacyDbConfigResolver; + const dbConn = yield* LegacyDbConnection; + const cliConfig = yield* LegacyCliConfig; + const telemetryState = yield* LegacyTelemetryState; + const linkedProjectCache = yield* LegacyLinkedProjectCache; + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const cliArgs = yield* CliArgs; + const dnsResolver = yield* LegacyDnsResolverFlag; + + const workdir = cliConfig.workdir; + // Go's `ParseDatabaseConfig` runs `loadNestedEnv` (which `os.Setenv`s each project-`.env` + // key) before `PromptYesNo` reads `viper.GetBool("YES")`, so a `SUPABASE_YES` set only in + // `supabase/.env` auto-confirms. Resolve `yes` with that project env, as `db pull` does. + const projectEnv = yield* legacyLoadProjectEnv(fs, path, workdir); + const yes = yield* legacyResolveYesWithProjectEnv(projectEnv); + let linkedRefForCache: string | undefined; + + const body = Effect.gen(function* () { + const target = resolveLegacyDbTargetFlags(cliArgs.args); + // cobra MarkFlagsMutuallyExclusive("db-url", "linked", "local"), keyed off the + // explicitly-set flags (cobra's `Changed`), not the `--linked` default value. + if (target.setFlags.length > 1) { + return yield* Effect.fail( + new LegacyDbPushTargetFlagsError({ + message: `if any flags in the group [db-url linked local] are set none of the others can be; [${target.setFlags.join(" ")}] were all set`, + }), + ); + } + // Go's push defaults `--linked` to true, so no target flag → linked. + const connType = target.connType ?? "linked"; + + // The linked path resolves the project ref before loading config so a matching + // `[remotes.]` block merges (Go's ParseDatabaseConfig → LoadConfig). For + // `--local` / `--db-url`, Go leaves `flags.ProjectRef` empty. + let projectRef = ""; + if (connType === "linked") { + const refResolver = yield* LegacyProjectRefResolver; + projectRef = yield* refResolver.loadProjectRef(Option.none()); + linkedRefForCache = projectRef; + } + + // Single Go-parity config load (`flags.LoadConfig` → `config.Load` + `Validate`): + // decodes the whole config with Go's env-expansion + `strconv.ParseBool` weak typing + // (so `enabled = "env(SEED_ENABLED)"` etc. load like Go), applies `SUPABASE_*` + // AutomaticEnv overrides, merges a matching `[remotes.]` block, and decrypts every + // `encrypted:` secret with the shell AND project-`.env` `DOTENV_PRIVATE_KEY*` keys — + // aborting here (before connecting or writing) on any undecryptable/invalid config. + const toml = yield* legacyCheckDbToml( + fs, + path, + workdir, + projectRef !== "" ? projectRef : undefined, + ); + if (toml.appliedRemote !== undefined) { + yield* output.raw(`Loading config override: [remotes.${toml.appliedRemote}]\n`, "stderr"); + } + const vaultSecrets = toml.vault; + + if (flags.dryRun) { + yield* output.raw("DRY RUN: migrations will *not* be pushed to the database.\n", "stderr"); + } + + const cfg = yield* resolver.resolve({ + dbUrl: flags.dbUrl, + connType, + dnsResolver, + password: flags.password, + }); + const databaseName = cfg.isLocal ? "local database" : "remote database"; + const statusTarget = cfg.isLocal ? "Local database" : "Remote database"; + + yield* Effect.scoped( + Effect.gen(function* () { + yield* output.raw( + `Connecting to ${cfg.isLocal ? "local" : "remote"} database...\n`, + "stderr", + ); + const session = yield* dbConn.connect(cfg.conn, { isLocal: cfg.isLocal, dnsResolver }); + + // --- Collect pending migrations --- + let pending: ReadonlyArray = []; + if (!toml.migrationsEnabled) { + yield* output.raw( + `Skipping migrations because it is disabled in config.toml for project: ${projectRef}\n`, + "stderr", + ); + } else { + const migrationsDir = path.join(workdir, "supabase", "migrations"); + const remote = yield* legacyListRemoteMigrations(session); + const local = yield* legacyListLocalMigrations(fs, path, migrationsDir); + const result = legacyFindPendingMigrations(local, remote); + if (result.kind === "missing-local") { + return yield* Effect.fail( + new LegacyDbPushMissingLocalError({ + message: LEGACY_ERR_MISSING_LOCAL, + suggestion: legacySuggestRevertHistory(result.versions), + }), + ); + } + if (result.kind === "missing-remote") { + if (!flags.includeAll) { + // Go's suggestIgnoreFlag lists the workdir-relative paths. + const relPaths = result.paths.map((p) => toSlash(path.relative(workdir, p))); + return yield* Effect.fail( + new LegacyDbPushMissingRemoteError({ + message: LEGACY_ERR_MISSING_REMOTE, + suggestion: legacySuggestIgnoreFlag(relPaths), + }), + ); + } + pending = legacyIncludeAllPending(local, remote.length, result.paths); + } else { + pending = result.pending; + } + } + + // --- Collect pending seeds --- + let seeds: ReadonlyArray = []; + if (flags.includeSeed) { + if (!toml.seed.enabled) { + yield* output.raw( + `Skipping seed because it is disabled in config.toml for project: ${projectRef}\n`, + "stderr", + ); + } else { + seeds = yield* legacyGetPendingSeeds(session, fs, path, toml.seed.sqlPaths, workdir); + } + } + + // --- Collect custom roles --- + const globals: Array = []; + if (flags.includeRoles) { + const exists = yield* fs.exists(path.join(workdir, CUSTOM_ROLES_PATH)).pipe( + Effect.mapError( + (cause) => + new LegacyDbPushRolesError({ + message: `failed to find custom roles: ${cause.message}`, + }), + ), + ); + if (exists) globals.push(CUSTOM_ROLES_PATH); + } + + // --- Nothing to push --- + if (pending.length === 0 && seeds.length === 0 && globals.length === 0) { + if (output.format === "text") { + yield* output.raw(`${statusTarget} is up to date.\n`); + } else { + yield* output.success(`${statusTarget} is up to date.`, { + upToDate: true, + dryRun: flags.dryRun, + migrations: [], + seeds: [], + roles: [], + }); + } + return; + } + + if (flags.dryRun) { + if (globals.length > 0) { + yield* output.raw( + `Would create custom roles ${legacyBold(globals[0]!)}...\n`, + "stderr", + ); + } + if (pending.length > 0) { + yield* output.raw("Would push these migrations:\n", "stderr"); + yield* output.raw(confirmPushAll(pending.map((p) => path.basename(p))), "stderr"); + } + if (seeds.length > 0) { + yield* output.raw("Would seed these files:\n", "stderr"); + yield* output.raw(confirmSeedAll(seeds), "stderr"); + } + } else { + // --- Custom roles --- + if (globals.length > 0) { + const ok = yield* legacyPromptYesNo( + output, + yes, + "Do you want to create custom roles in the database cluster?", + true, + ); + if (!ok) { + return yield* Effect.fail( + new LegacyDbPushCancelledError({ message: "context canceled" }), + ); + } + yield* legacySeedGlobals( + session, + fs, + path, + globals.map((g) => path.join(workdir, g)), + applyError, + ); + } + + // --- Migrations --- + if (pending.length > 0) { + const ok = yield* legacyPromptYesNo( + output, + yes, + `Do you want to push these migrations to the ${databaseName}?\n${confirmPushAll(pending.map((p) => path.basename(p)))}`, + true, + ); + if (!ok) { + return yield* Effect.fail( + new LegacyDbPushCancelledError({ message: "context canceled" }), + ); + } + yield* legacyUpsertVaultSecrets(session, vaultSecrets); + yield* legacyApplyMigrations(session, fs, path, pending, applyError); + // Go best-effort caches the migrations catalog for pg-delta; a failure + // only warns (`push.go:99-101`). The catalog cache is not yet ported, so + // there is nothing to warn about — parity is preserved (no extra output). + } else { + yield* output.raw("Schema migrations are up to date.\n", "stderr"); + } + + // --- Seeds --- + if (seeds.length > 0) { + const ok = yield* legacyPromptYesNo( + output, + yes, + `Do you want to seed the ${databaseName} with these files?\n${confirmSeedAll(seeds)}`, + true, + ); + if (!ok) { + return yield* Effect.fail( + new LegacyDbPushCancelledError({ message: "context canceled" }), + ); + } + yield* legacySeedData(session, fs, workdir, path, seeds, applyError); + } else if (flags.includeSeed) { + yield* output.raw("Seed files are up to date.\n", "stderr"); + } + } + + if (output.format === "text") { + yield* output.raw(`Finished ${legacyAqua("supabase db push")}.\n`); + } else { + yield* output.success("Finished supabase db push.", { + upToDate: false, + dryRun: flags.dryRun, + migrations: pending.map((p) => path.basename(p)), + seeds: seeds.map((s) => s.path), + roles: globals, + }); + } + }), + ); + }); + + yield* body.pipe( + Effect.ensuring( + Effect.suspend(() => + linkedRefForCache !== undefined && linkedRefForCache !== "" + ? linkedProjectCache.cache(linkedRefForCache) + : Effect.void, + ), + ), + Effect.ensuring(telemetryState.flush), + ); }); diff --git a/apps/cli/src/legacy/commands/db/push/push.integration.test.ts b/apps/cli/src/legacy/commands/db/push/push.integration.test.ts new file mode 100644 index 0000000000..9a4b21db68 --- /dev/null +++ b/apps/cli/src/legacy/commands/db/push/push.integration.test.ts @@ -0,0 +1,826 @@ +import { createHash } from "node:crypto"; +import { mkdirSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; + +import { BunServices } from "@effect/platform-bun"; +import { describe, expect, it } from "@effect/vitest"; +import { Effect, Exit, Layer, Option } from "effect"; + +import { mockOutput, mockStdin, mockTty } from "../../../../../tests/helpers/mocks.ts"; +import { + LEGACY_VALID_REF, + mockLegacyCliConfig, + mockLegacyLinkedProjectCacheTracked, + mockLegacyTelemetryStateTracked, + useLegacyTempWorkdir, +} from "../../../../../tests/helpers/legacy-mocks.ts"; +import { CliArgs } from "../../../../shared/cli/cli-args.service.ts"; +import { LegacyDnsResolverFlag, LegacyYesFlag } from "../../../../shared/legacy/global-flags.ts"; +import type { OutputFormat } from "../../../../shared/output/types.ts"; +import { LegacyProjectRefResolver } from "../../../config/legacy-project-ref.service.ts"; +import { LegacyProjectNotLinkedError } from "../../../config/legacy-project-ref.errors.ts"; +import { LegacyDbConfigResolver } from "../../../shared/legacy-db-config.service.ts"; +import type { + LegacyDbConfigFlags, + LegacyResolvedDbConfig, +} from "../../../shared/legacy-db-config.types.ts"; +import { LegacyDbExecError } from "../../../shared/legacy-db-connection.errors.ts"; +import { + LegacyDbConnection, + type LegacyPgConnInput, +} from "../../../shared/legacy-db-connection.service.ts"; +import { legacyDbPush } from "./push.handler.ts"; +import type { LegacyDbPushFlags } from "./push.command.ts"; + +const LIST_MIGRATIONS = + "SELECT version FROM supabase_migrations.schema_migrations ORDER BY version"; +const SELECT_SEEDS = "SELECT path, hash FROM supabase_migrations.seed_files"; +const READ_VAULT = "SELECT id, name FROM vault.secrets WHERE name = ANY($1)"; + +const LOCAL_CONN: LegacyPgConnInput = { + host: "127.0.0.1", + port: 54322, + user: "postgres", + password: "postgres", + database: "postgres", +}; + +const DEFAULT_FLAGS: LegacyDbPushFlags = { + includeAll: false, + includeRoles: false, + includeSeed: false, + dryRun: false, + dbUrl: Option.none(), + linked: false, + local: true, + password: Option.none(), +}; + +function mockResolver(opts: { isLocal?: boolean } = {}) { + return Layer.succeed(LegacyDbConfigResolver, { + resolve: (_flags: LegacyDbConfigFlags) => + Effect.succeed({ + conn: LOCAL_CONN, + isLocal: opts.isLocal ?? true, + } satisfies LegacyResolvedDbConfig), + resolvePoolerFallback: () => Effect.succeed(Option.none()), + }); +} + +function mockConnection(opts: { + remoteMigrations?: ReadonlyArray; + remoteSeeds?: Readonly>; + vaultRows?: ReadonlyArray<{ id: string; name: string }>; + noSeedTable?: boolean; + failExec?: string; +}) { + const execs: Array = []; + const queries: Array<{ sql: string; params?: ReadonlyArray }> = []; + const layer = Layer.succeed(LegacyDbConnection, { + connect: () => + Effect.succeed({ + extensionExists: () => Effect.succeed(false), + copyToCsv: () => Effect.succeed(new Uint8Array()), + queryRaw: () => Effect.succeed({ fields: [], rows: [], commandTag: "" }), + exec: (sql: string): Effect.Effect => + Effect.suspend((): Effect.Effect => { + execs.push(sql); + if (opts.failExec !== undefined && sql === opts.failExec) { + return Effect.fail( + new LegacyDbExecError({ message: "ERROR: boom (SQLSTATE 42601)" }), + ); + } + return Effect.void; + }), + query: ( + sql: string, + params?: ReadonlyArray, + ): Effect.Effect>, LegacyDbExecError> => + Effect.suspend( + (): Effect.Effect>, LegacyDbExecError> => { + queries.push({ sql, params }); + if (sql === LIST_MIGRATIONS) { + return Effect.succeed( + (opts.remoteMigrations ?? []).map((version) => ({ version })), + ); + } + if (sql === SELECT_SEEDS) { + if (opts.noSeedTable === true) { + return Effect.fail( + new LegacyDbExecError({ + message: 'relation "supabase_migrations.seed_files" does not exist', + code: "42P01", + }), + ); + } + return Effect.succeed( + Object.entries(opts.remoteSeeds ?? {}).map(([path, hash]) => ({ path, hash })), + ); + } + if (sql === READ_VAULT) { + return Effect.succeed(opts.vaultRows ?? []); + } + return Effect.succeed([]); + }, + ), + }), + }); + return { + layer, + get execs() { + return execs; + }, + get queries() { + return queries; + }, + }; +} + +function setup( + workdir: string, + opts: { + toml?: string; + files?: Readonly>; + format?: OutputFormat; + confirm?: ReadonlyArray; + args?: ReadonlyArray; + yes?: boolean; + isLocal?: boolean; + projectRef?: string; + linkedFails?: boolean; + remoteMigrations?: ReadonlyArray; + remoteSeeds?: Readonly>; + vaultRows?: ReadonlyArray<{ id: string; name: string }>; + noSeedTable?: boolean; + failExec?: string; + }, +) { + if (opts.toml !== undefined) { + mkdirSync(join(workdir, "supabase"), { recursive: true }); + writeFileSync(join(workdir, "supabase", "config.toml"), opts.toml); + } + for (const [rel, content] of Object.entries(opts.files ?? {})) { + const abs = join(workdir, rel); + mkdirSync(dirname(abs), { recursive: true }); + writeFileSync(abs, content); + } + + const out = mockOutput({ format: opts.format ?? "text", promptConfirmResponses: opts.confirm }); + const conn = mockConnection(opts); + const telemetry = mockLegacyTelemetryStateTracked(); + const linkedCache = mockLegacyLinkedProjectCacheTracked(); + const projectRefLayer = Layer.succeed(LegacyProjectRefResolver, { + resolve: () => Effect.succeed(opts.projectRef ?? LEGACY_VALID_REF), + resolveForLink: () => Effect.succeed(opts.projectRef ?? LEGACY_VALID_REF), + resolveOptional: () => Effect.succeed(Option.some(opts.projectRef ?? LEGACY_VALID_REF)), + loadProjectRef: () => + opts.linkedFails === true + ? Effect.fail( + new LegacyProjectNotLinkedError({ + message: "Cannot find project ref. Have you run supabase link?", + }), + ) + : Effect.succeed(opts.projectRef ?? LEGACY_VALID_REF), + promptProjectRef: () => Effect.succeed(opts.projectRef ?? LEGACY_VALID_REF), + }); + + const layer = Layer.mergeAll( + out.layer, + conn.layer, + mockResolver({ isLocal: opts.isLocal ?? true }), + mockLegacyCliConfig({ workdir }), + BunServices.layer, + // Prompts (migration/seed confirmation) are answered through mockOutput's + // `promptConfirmResponses` (the TTY/clack path), so mark stdin a TTY. Stdin is + // only referenced by legacyPromptYesNo's non-TTY branch (unreached here). + mockTty({ stdinIsTty: true }), + mockStdin(true), + Layer.succeed(CliArgs, { args: opts.args ?? ["db", "push", "--local"] }), + Layer.succeed(LegacyYesFlag, opts.yes ?? false), + Layer.succeed(LegacyDnsResolverFlag, "native"), + projectRefLayer, + telemetry.layer, + linkedCache.layer, + ); + return { layer, out, conn, telemetry, linkedCache }; +} + +const MIGRATION_DIR = "supabase/migrations"; +const migrationFile = (version: string, body = "create table t ();") => ({ + [`${MIGRATION_DIR}/${version}_test.sql`]: body, +}); + +describe("legacy db push", () => { + const tmp = useLegacyTempWorkdir("supabase-db-push-"); + + it.live("reports up to date when nothing is pending (text)", () => { + const { layer, out, conn } = setup(tmp.current, { toml: 'project_id = "test"\n' }); + return Effect.gen(function* () { + const exit = yield* legacyDbPush(DEFAULT_FLAGS).pipe(Effect.provide(layer), Effect.exit); + expect(Exit.isSuccess(exit)).toBe(true); + expect(out.stdoutText).toBe("Local database is up to date.\n"); + // No migration was applied. + expect(conn.execs).not.toContain("BEGIN"); + }); + }); + + it.live("emits a json result for an up-to-date run", () => { + const { layer, out } = setup(tmp.current, { toml: 'project_id = "test"\n', format: "json" }); + return Effect.gen(function* () { + yield* legacyDbPush(DEFAULT_FLAGS).pipe(Effect.provide(layer)); + const success = out.messages.find((m) => m.type === "success"); + expect(success?.data?.["upToDate"]).toBe(true); + expect(success?.data?.["migrations"]).toEqual([]); + }); + }); + + it.live("rejects mutually exclusive target flags", () => { + const { layer } = setup(tmp.current, { + toml: 'project_id = "test"\n', + args: ["db", "push", "--local", "--linked"], + }); + return Effect.gen(function* () { + const exit = yield* legacyDbPush(DEFAULT_FLAGS).pipe(Effect.provide(layer), Effect.exit); + expect(Exit.isFailure(exit)).toBe(true); + }); + }); + + it.live("applies a pending migration after confirmation", () => { + const { layer, out, conn } = setup(tmp.current, { + toml: 'project_id = "test"\n', + files: migrationFile("20240101000000"), + confirm: [true], + }); + return Effect.gen(function* () { + yield* legacyDbPush(DEFAULT_FLAGS).pipe(Effect.provide(layer)); + expect(out.stderrText).toContain("Applying migration 20240101000000_test.sql..."); + // "supabase db push" is wrapped in Aqua (cyan) on stdout, matching Go. + expect(out.stdoutText).toContain("Finished"); + expect(out.stdoutText).toContain("supabase db push"); + // The migration body + history insert ran inside a transaction. + expect(conn.execs).toContain("BEGIN"); + expect(conn.execs).toContain("COMMIT"); + expect(conn.queries.some((q) => q.sql.includes("INSERT INTO supabase_migrations"))).toBe( + true, + ); + }); + }); + + it.live("returns context canceled when the migration prompt is declined", () => { + const { layer, conn } = setup(tmp.current, { + toml: 'project_id = "test"\n', + files: migrationFile("20240101000000"), + confirm: [false], + }); + return Effect.gen(function* () { + const exit = yield* legacyDbPush(DEFAULT_FLAGS).pipe(Effect.provide(layer), Effect.exit); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(JSON.stringify(exit.cause)).toContain("context canceled"); + } + expect(conn.execs).not.toContain("BEGIN"); + }); + }); + + it.live("prints the plan without applying in dry-run mode", () => { + const { layer, out, conn } = setup(tmp.current, { + toml: 'project_id = "test"\n', + files: migrationFile("20240101000000"), + }); + return Effect.gen(function* () { + yield* legacyDbPush({ ...DEFAULT_FLAGS, dryRun: true }).pipe(Effect.provide(layer)); + expect(out.stderrText).toContain("DRY RUN: migrations will *not* be pushed to the database."); + expect(out.stderrText).toContain("Would push these migrations:"); + expect(out.stderrText).toContain("20240101000000_test.sql"); + expect(conn.execs).not.toContain("BEGIN"); + }); + }); + + it.live("fails with a repair suggestion when remote has versions missing locally", () => { + const { layer, out } = setup(tmp.current, { + toml: 'project_id = "test"\n', + remoteMigrations: ["20240101000000"], + }); + return Effect.gen(function* () { + const exit = yield* legacyDbPush(DEFAULT_FLAGS).pipe(Effect.provide(layer), Effect.exit); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(JSON.stringify(exit.cause)).toContain( + "Remote migration versions not found in local migrations directory.", + ); + expect(JSON.stringify(exit.cause)).toContain("migration repair --status reverted"); + } + expect(out).toBeDefined(); + }); + }); + + it.live("fails with an --include-all suggestion for out-of-order local migrations", () => { + const { layer } = setup(tmp.current, { + toml: 'project_id = "test"\n', + // 0101 is local-only and ordered before the already-applied remote 0202. + files: { ...migrationFile("20240101000000"), ...migrationFile("20240202000000") }, + remoteMigrations: ["20240202000000"], + }); + return Effect.gen(function* () { + const exit = yield* legacyDbPush(DEFAULT_FLAGS).pipe(Effect.provide(layer), Effect.exit); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(JSON.stringify(exit.cause)).toContain("--include-all"); + } + }); + }); + + it.live("pushes out-of-order migrations with --include-all", () => { + const { layer, out } = setup(tmp.current, { + toml: 'project_id = "test"\n', + files: { ...migrationFile("20240101000000"), ...migrationFile("20240202000000") }, + remoteMigrations: ["20240202000000"], + confirm: [true], + }); + return Effect.gen(function* () { + yield* legacyDbPush({ ...DEFAULT_FLAGS, includeAll: true }).pipe(Effect.provide(layer)); + expect(out.stderrText).toContain("Applying migration 20240101000000_test.sql..."); + }); + }); + + it.live("skips migrations when disabled in config and reports up to date", () => { + const { layer, out } = setup(tmp.current, { + toml: 'project_id = "test"\n\n[db.migrations]\nenabled = false\n', + files: migrationFile("20240101000000"), + }); + return Effect.gen(function* () { + yield* legacyDbPush(DEFAULT_FLAGS).pipe(Effect.provide(layer)); + expect(out.stderrText).toContain( + "Skipping migrations because it is disabled in config.toml for project:", + ); + expect(out.stdoutText).toBe("Local database is up to date.\n"); + }); + }); + + it.live("seeds a new file with --include-seed", () => { + const { layer, out, conn } = setup(tmp.current, { + toml: 'project_id = "test"\n', + files: { "supabase/seed.sql": "insert into t values (1);" }, + confirm: [true], + }); + return Effect.gen(function* () { + yield* legacyDbPush({ ...DEFAULT_FLAGS, includeSeed: true }).pipe(Effect.provide(layer)); + expect(out.stderrText).toContain("Seeding data from supabase/seed.sql..."); + expect( + conn.queries.some((q) => q.sql.includes("INSERT INTO supabase_migrations.seed_files")), + ).toBe(true); + }); + }); + + it.live("expands a directory in [db.seed].sql_paths to its sorted .sql children", () => { + // Go's `GetPendingSeeds` (`Glob.SQLFiles`) walks a matched directory and seeds its + // regular `.sql` files recursively; non-.sql files are skipped. Without dir expansion + // the directory path reached `readFileString()` and failed. + const { layer, out } = setup(tmp.current, { + toml: 'project_id = "test"\n\n[db.seed]\nsql_paths = ["seeds"]\n', + files: { + "supabase/seeds/a.sql": "insert into t values (1);", + "supabase/seeds/nested/b.sql": "insert into t values (2);", + "supabase/seeds/notes.txt": "not a seed", + }, + confirm: [true], + }); + return Effect.gen(function* () { + yield* legacyDbPush({ ...DEFAULT_FLAGS, includeSeed: true }).pipe(Effect.provide(layer)); + expect(out.stderrText).toContain("Seeding data from supabase/seeds/a.sql..."); + expect(out.stderrText).toContain("Seeding data from supabase/seeds/nested/b.sql..."); + expect(out.stderrText).not.toContain("notes.txt"); + }); + }); + + it.live("reports seed files up to date when hash matches remote", () => { + // sha256 of the seed body must match the remote hash to be skipped. + const body = "insert into t values (1);"; + const hash = createHash("sha256").update(body).digest("hex"); + const { layer, out } = setup(tmp.current, { + toml: 'project_id = "test"\n', + files: { "supabase/seed.sql": body }, + remoteSeeds: { "supabase/seed.sql": hash }, + }); + return Effect.gen(function* () { + yield* legacyDbPush({ ...DEFAULT_FLAGS, includeSeed: true }).pipe(Effect.provide(layer)); + expect(out.stdoutText).toBe("Local database is up to date.\n"); + }); + }); + + it.live("hashes a non-UTF-8 seed file by its raw bytes (Go's io.Copy parity)", () => { + // Go's `NewSeedFile` hashes the raw stream; a UTF-8 string decode would replace the + // invalid bytes and change the hash. Write invalid UTF-8 and pre-seed the remote with + // the RAW-byte sha256 — the push must treat it as already-applied (byte hash matches), + // not re-run it. A string-decoded hash here would differ and mark the seed dirty. + const raw = Buffer.from([0x2d, 0x2d, 0x20, 0xff, 0xfe, 0x00, 0x01, 0x0a]); + const rawHash = createHash("sha256").update(raw).digest("hex"); + const { layer, out } = setup(tmp.current, { + toml: 'project_id = "test"\n', + remoteSeeds: { "supabase/seed.sql": rawHash }, + }); + mkdirSync(join(tmp.current, "supabase"), { recursive: true }); + writeFileSync(join(tmp.current, "supabase", "seed.sql"), raw); + return Effect.gen(function* () { + yield* legacyDbPush({ ...DEFAULT_FLAGS, includeSeed: true }).pipe(Effect.provide(layer)); + expect(out.stdoutText).toBe("Local database is up to date.\n"); + }); + }); + + it.live("skips seeding when disabled in config", () => { + const { layer, out } = setup(tmp.current, { + toml: 'project_id = "test"\n\n[db.seed]\nenabled = false\n', + files: { "supabase/seed.sql": "insert into t values (1);" }, + }); + return Effect.gen(function* () { + yield* legacyDbPush({ ...DEFAULT_FLAGS, includeSeed: true }).pipe(Effect.provide(layer)); + expect(out.stderrText).toContain( + "Skipping seed because it is disabled in config.toml for project:", + ); + }); + }); + + it.live("creates custom roles with --include-roles", () => { + const { layer, out } = setup(tmp.current, { + toml: 'project_id = "test"\n', + files: { "supabase/roles.sql": "create role app;" }, + confirm: [true], + }); + return Effect.gen(function* () { + yield* legacyDbPush({ ...DEFAULT_FLAGS, includeRoles: true }).pipe(Effect.provide(layer)); + expect(out.stderrText).toContain("Seeding globals from roles.sql..."); + }); + }); + + it.live("--include-roles without a roles.sql pushes migrations and skips globals", () => { + // Go's push only globs supabase/roles.sql when it exists; an absent file is + // silently skipped (no error, no "Seeding globals" line) and the rest pushes. + const { layer, out } = setup(tmp.current, { + toml: 'project_id = "test"\n', + files: migrationFile("20240101000000"), + confirm: [true], + }); + return Effect.gen(function* () { + yield* legacyDbPush({ ...DEFAULT_FLAGS, includeRoles: true }).pipe(Effect.provide(layer)); + expect(out.stderrText).not.toContain("Seeding globals"); + expect(out.stderrText).toContain("Applying migration 20240101000000_test.sql..."); + }); + }); + + it.live("emits the seeded file paths in the json success payload", () => { + const { layer, out } = setup(tmp.current, { + toml: 'project_id = "test"\n', + files: { + ...migrationFile("20240101000000"), + "supabase/seed.sql": "insert into t values (1);", + }, + format: "json", + }); + return Effect.gen(function* () { + yield* legacyDbPush({ ...DEFAULT_FLAGS, includeSeed: true }).pipe(Effect.provide(layer)); + const success = out.messages.find((m) => m.type === "success"); + expect(success?.data?.["upToDate"]).toBe(false); + expect(success?.data?.["migrations"]).toEqual(["20240101000000_test.sql"]); + expect(success?.data?.["seeds"]).toEqual(["supabase/seed.sql"]); + }); + }); + + it.live("reports schema migrations up to date when only roles are pushed", () => { + const { layer, out } = setup(tmp.current, { + toml: 'project_id = "test"\n', + files: { "supabase/roles.sql": "create role app;" }, + confirm: [true], + }); + return Effect.gen(function* () { + yield* legacyDbPush({ ...DEFAULT_FLAGS, includeRoles: true }).pipe(Effect.provide(layer)); + expect(out.stderrText).toContain("Schema migrations are up to date."); + }); + }); + + it.live("returns context canceled when the roles prompt is declined", () => { + const { layer } = setup(tmp.current, { + toml: 'project_id = "test"\n', + files: { "supabase/roles.sql": "create role app;" }, + confirm: [false], + }); + return Effect.gen(function* () { + const exit = yield* legacyDbPush({ ...DEFAULT_FLAGS, includeRoles: true }).pipe( + Effect.provide(layer), + Effect.exit, + ); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) expect(JSON.stringify(exit.cause)).toContain("context canceled"); + }); + }); + + it.live("returns context canceled when the seed prompt is declined", () => { + const { layer } = setup(tmp.current, { + toml: 'project_id = "test"\n', + files: { "supabase/seed.sql": "insert into t values (1);" }, + confirm: [false], + }); + return Effect.gen(function* () { + const exit = yield* legacyDbPush({ ...DEFAULT_FLAGS, includeSeed: true }).pipe( + Effect.provide(layer), + Effect.exit, + ); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) expect(JSON.stringify(exit.cause)).toContain("context canceled"); + }); + }); + + it.live("re-hashes a dirty seed without re-running its statements", () => { + const { layer, out, conn } = setup(tmp.current, { + toml: 'project_id = "test"\n', + files: { "supabase/seed.sql": "insert into t values (1);" }, + // Remote hash differs → dirty. + remoteSeeds: { "supabase/seed.sql": "stalehash" }, + confirm: [true], + }); + return Effect.gen(function* () { + yield* legacyDbPush({ ...DEFAULT_FLAGS, includeSeed: true }).pipe(Effect.provide(layer)); + expect(out.stderrText).toContain("Updating seed hash to supabase/seed.sql..."); + // Dirty seed only upserts the hash; the body statement is not executed. + expect(conn.execs).not.toContain("insert into t values (1);"); + }); + }); + + it.live("treats every seed as pending when the seed_files table is absent", () => { + const { layer, out } = setup(tmp.current, { + toml: 'project_id = "test"\n', + files: { "supabase/seed.sql": "insert into t values (1);" }, + noSeedTable: true, + confirm: [true], + }); + return Effect.gen(function* () { + yield* legacyDbPush({ ...DEFAULT_FLAGS, includeSeed: true }).pipe(Effect.provide(layer)); + expect(out.stderrText).toContain("Seeding data from supabase/seed.sql..."); + }); + }); + + it.live("warns and reports up to date when no seed files match", () => { + const { layer, out } = setup(tmp.current, { + toml: 'project_id = "test"\n\n[db.seed]\nsql_paths = ["missing.sql"]\n', + }); + return Effect.gen(function* () { + yield* legacyDbPush({ ...DEFAULT_FLAGS, includeSeed: true }).pipe(Effect.provide(layer)); + expect(out.stderrText).toContain("WARN: no files matched pattern: supabase/missing.sql"); + expect(out.stdoutText).toBe("Local database is up to date.\n"); + }); + }); + + it.live("reports seed files up to date when migrations push but no seeds match", () => { + const { layer, out } = setup(tmp.current, { + toml: 'project_id = "test"\n\n[db.seed]\nsql_paths = ["missing.sql"]\n', + files: migrationFile("20240101000000"), + confirm: [true], + }); + return Effect.gen(function* () { + yield* legacyDbPush({ ...DEFAULT_FLAGS, includeSeed: true }).pipe(Effect.provide(layer)); + expect(out.stderrText).toContain("Seed files are up to date."); + }); + }); + + it.live("upserts vault secrets (update existing, create new) before migrating", () => { + const { layer, out, conn } = setup(tmp.current, { + toml: 'project_id = "test"\n\n[db.vault]\nexisting = "v1"\nfresh = "v2"\n', + files: migrationFile("20240101000000"), + // `existing` already present remotely → update; `fresh` → create. + vaultRows: [{ id: "id-1", name: "existing" }], + confirm: [true], + }); + return Effect.gen(function* () { + yield* legacyDbPush(DEFAULT_FLAGS).pipe(Effect.provide(layer)); + expect(out.stderrText).toContain("Updating vault secrets..."); + const sqls = conn.queries.map((q) => q.sql); + expect(sqls).toContain("SELECT vault.update_secret($1, $2)"); + expect(sqls).toContain("SELECT vault.create_secret($1, $2)"); + }); + }); + + it.live("decrypts an encrypted vault secret keyed by the project .env (not process.env)", () => { + // Regression: the old point-of-use vault decryption keyed only on `process.env`, so a + // `DOTENV_PRIVATE_KEY` present only in the project `.env` failed to decrypt. Go's config + // load merges the project `.env` into the key set (`legacyCheckDbToml`), so it resolves. + const PRIVATE_KEY = "7fd7210cef8f331ee8c55897996aaaafd853a2b20a4dc73d6d75759f65d2a7eb"; + const ENCRYPTED = + "encrypted:BKiXH15AyRzeohGyUrmB6cGjSklCrrBjdesQlX1VcXo/Xp20Bi2gGZ3AlIqxPQDmjVAALnhZamKnuY73l8Dz1P+BYiZUgxTSLzdCvdYUyVbNekj2UudbdUizBViERtZkuQwZHIv/"; + const { layer, out, conn } = setup(tmp.current, { + toml: `project_id = "test"\n\n[db.vault]\nmy_secret = "${ENCRYPTED}"\n`, + files: { + ...migrationFile("20240101000000"), + "supabase/.env": `DOTENV_PRIVATE_KEY=${PRIVATE_KEY}\n`, + }, + confirm: [true], + }); + return Effect.gen(function* () { + yield* legacyDbPush(DEFAULT_FLAGS).pipe(Effect.provide(layer)); + expect(out.stderrText).toContain("Updating vault secrets..."); + // The decrypted plaintext ("value") is written, proving the project-.env key was used. + const create = conn.queries.find((q) => q.sql === "SELECT vault.create_secret($1, $2)"); + expect(create?.params).toEqual(["value", "my_secret"]); + }); + }); + + it.live("defaults to the linked target when no target flag is set", () => { + const { layer, out } = setup(tmp.current, { + toml: 'project_id = "test"\n', + args: ["db", "push"], + isLocal: false, + projectRef: LEGACY_VALID_REF, + }); + return Effect.gen(function* () { + yield* legacyDbPush({ ...DEFAULT_FLAGS, local: false }).pipe(Effect.provide(layer)); + expect(out.stderrText).toContain("Connecting to remote database..."); + expect(out.stdoutText).toBe("Remote database is up to date.\n"); + }); + }); + + it.live("surfaces an apply error with statement context", () => { + const { layer } = setup(tmp.current, { + toml: 'project_id = "test"\n', + files: migrationFile("20240101000000", "BOOM;"), + failExec: "BOOM", + confirm: [true], + }); + return Effect.gen(function* () { + const exit = yield* legacyDbPush(DEFAULT_FLAGS).pipe(Effect.provide(layer), Effect.exit); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(JSON.stringify(exit.cause)).toContain("At statement: 0"); + } + }); + }); + + it.live("dry-run lists roles, migrations and seeds without applying", () => { + const { layer, out, conn } = setup(tmp.current, { + toml: 'project_id = "test"\n', + files: { + ...migrationFile("20240101000000"), + "supabase/roles.sql": "create role app;", + "supabase/seed.sql": "insert into t values (1);", + }, + }); + return Effect.gen(function* () { + yield* legacyDbPush({ + ...DEFAULT_FLAGS, + dryRun: true, + includeRoles: true, + includeSeed: true, + }).pipe(Effect.provide(layer)); + // The roles path is wrapped in Bold (ANSI), matching Go's utils.Bold. + expect(out.stderrText).toContain("Would create custom roles"); + expect(out.stderrText).toContain("roles.sql"); + expect(out.stderrText).toContain("Would push these migrations:"); + expect(out.stderrText).toContain("Would seed these files:"); + expect(conn.execs).not.toContain("BEGIN"); + }); + }); + + it.live("dry-run with only custom roles lists them without a migration section", () => { + const { layer, out } = setup(tmp.current, { + toml: 'project_id = "test"\n', + files: { "supabase/roles.sql": "create role app;" }, + }); + return Effect.gen(function* () { + yield* legacyDbPush({ ...DEFAULT_FLAGS, dryRun: true, includeRoles: true }).pipe( + Effect.provide(layer), + ); + expect(out.stderrText).toContain("Would create custom roles"); + expect(out.stderrText).not.toContain("Would push these migrations:"); + }); + }); + + it.live("uses embedded defaults when no config file is present", () => { + const { layer, out } = setup(tmp.current, { + files: migrationFile("20240101000000"), + confirm: [true], + }); + return Effect.gen(function* () { + // No config.toml written → loadProjectConfig returns null → default config + // (migrations enabled), and the vault document is absent. + yield* legacyDbPush(DEFAULT_FLAGS).pipe(Effect.provide(layer)); + expect(out.stderrText).toContain("Applying migration 20240101000000_test.sql..."); + }); + }); + + it.live("auto-confirms pending migrations via SUPABASE_YES set only in the project .env", () => { + // Go's loadNestedEnv sets project-.env keys before PromptYesNo reads viper YES, so a + // `SUPABASE_YES` in supabase/.env auto-confirms without any interactive answer. + const { layer, out } = setup(tmp.current, { + toml: 'project_id = "test"\n', + files: { ...migrationFile("20240101000000"), "supabase/.env": "SUPABASE_YES=true\n" }, + // Deliberately no `confirm` responses — the prompt must be auto-confirmed. + }); + return Effect.gen(function* () { + yield* legacyDbPush(DEFAULT_FLAGS).pipe(Effect.provide(layer)); + expect(out.stderrText).toContain("Applying migration 20240101000000_test.sql..."); + }); + }); + + it.live("fails when config.toml cannot be parsed", () => { + const { layer } = setup(tmp.current, { toml: "this is = = not [[[ valid toml" }); + return Effect.gen(function* () { + const exit = yield* legacyDbPush(DEFAULT_FLAGS).pipe(Effect.provide(layer), Effect.exit); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + // Config now loads through the Go-parity reader (`legacyCheckDbToml`), so a malformed + // config aborts with Go's `failed to load config` message (the reader path), same as + // the other db commands (diff/dump/pull/migration). + expect(JSON.stringify(exit.cause)).toContain("failed to load config"); + } + }); + }); + + it.live("loads a Go-style env() boolean in config (no ProjectConfigParseError)", () => { + // Regression for the strict @supabase/config loader rejecting `enabled = "env(VAR)"`: + // Go decodes it via env-expansion + strconv.ParseBool, so the config must load and the + // migration proceed. Previously native push aborted before the Go-compatible parse ran. + const previous = process.env["SEED_ENABLED"]; + process.env["SEED_ENABLED"] = "true"; + const { layer, out } = setup(tmp.current, { + toml: 'project_id = "test"\n\n[db.seed]\nenabled = "env(SEED_ENABLED)"\n', + files: migrationFile("20240101000000"), + confirm: [true], + }); + return Effect.gen(function* () { + yield* legacyDbPush(DEFAULT_FLAGS).pipe(Effect.provide(layer)); + expect(out.stderrText).toContain("Applying migration 20240101000000_test.sql..."); + }).pipe( + Effect.ensuring( + Effect.sync(() => { + if (previous === undefined) delete process.env["SEED_ENABLED"]; + else process.env["SEED_ENABLED"] = previous; + }), + ), + ); + }); + + it.live("a matched remote block's migrations.enabled beats the shell env override", () => { + // Go merges a matched [remotes.] block at viper's override tier (`v.Set`), which + // sits ABOVE AutomaticEnv — so `[remotes.preview.db.migrations] enabled = false` wins + // over `SUPABASE_DB_MIGRATIONS_ENABLED=true` and the push skips migrations. (Before the + // config-reader convergence, push resolved this gate env-first and wrongly applied.) + const previous = process.env["SUPABASE_DB_MIGRATIONS_ENABLED"]; + process.env["SUPABASE_DB_MIGRATIONS_ENABLED"] = "true"; + const { layer, out } = setup(tmp.current, { + toml: `project_id = "base"\n\n[remotes.preview]\nproject_id = "${LEGACY_VALID_REF}"\n\n[remotes.preview.db.migrations]\nenabled = false\n`, + files: migrationFile("20240101000000"), + args: ["db", "push", "--linked"], + isLocal: false, + projectRef: LEGACY_VALID_REF, + confirm: [true], + }); + return Effect.gen(function* () { + yield* legacyDbPush({ ...DEFAULT_FLAGS, local: false, linked: true }).pipe( + Effect.provide(layer), + ); + expect(out.stderrText).toContain("Skipping migrations because it is disabled"); + expect(out.stderrText).not.toContain("Applying migration 20240101000000"); + }).pipe( + Effect.ensuring( + Effect.sync(() => { + if (previous === undefined) delete process.env["SUPABASE_DB_MIGRATIONS_ENABLED"]; + else process.env["SUPABASE_DB_MIGRATIONS_ENABLED"] = previous; + }), + ), + ); + }); + + it.live("announces a matching [remotes.*] override on the linked path", () => { + const { layer, out } = setup(tmp.current, { + toml: `project_id = "base"\n\n[remotes.preview]\nproject_id = "${LEGACY_VALID_REF}"\n`, + args: ["db", "push", "--linked"], + isLocal: false, + projectRef: LEGACY_VALID_REF, + }); + return Effect.gen(function* () { + yield* legacyDbPush({ ...DEFAULT_FLAGS, local: false, linked: true }).pipe( + Effect.provide(layer), + ); + expect(out.stderrText).toContain("Loading config override: [remotes.preview]"); + }); + }); + + it.live("pushes to the linked project and caches the project ref (json)", () => { + const { layer, out, linkedCache } = setup(tmp.current, { + toml: 'project_id = "test"\n', + files: migrationFile("20240101000000"), + args: ["db", "push", "--linked"], + isLocal: false, + projectRef: LEGACY_VALID_REF, + format: "json", + confirm: [true], + }); + return Effect.gen(function* () { + yield* legacyDbPush({ ...DEFAULT_FLAGS, local: false, linked: true }).pipe( + Effect.provide(layer), + ); + expect(out.stderrText).toContain("Connecting to remote database..."); + expect(linkedCache.cached).toBe(true); + expect(linkedCache.cachedRef).toBe(LEGACY_VALID_REF); + const success = out.messages.find((m) => m.type === "success"); + expect(success?.data?.["migrations"]).toEqual(["20240101000000_test.sql"]); + }); + }); +}); diff --git a/apps/cli/src/legacy/commands/db/push/push.layers.ts b/apps/cli/src/legacy/commands/db/push/push.layers.ts new file mode 100644 index 0000000000..dbc5bd5250 --- /dev/null +++ b/apps/cli/src/legacy/commands/db/push/push.layers.ts @@ -0,0 +1,78 @@ +import { Layer } from "effect"; + +import { commandRuntimeLayer } from "../../../../shared/runtime/command-runtime.layer.ts"; +import { legacyCredentialsLayer } from "../../../auth/legacy-credentials.layer.ts"; +import { legacyHttpClientLayer } from "../../../auth/legacy-http-debug.layer.ts"; +import { legacyPlatformApiFactoryLayer } from "../../../auth/legacy-platform-api-factory.layer.ts"; +import { legacyCliConfigLayer } from "../../../config/legacy-cli-config.layer.ts"; +import { legacyProjectRefLayer } from "../../../config/legacy-project-ref.layer.ts"; +import { legacyDbConfigLayer } from "../../../shared/legacy-db-config.layer.ts"; +import { legacyDbConnectionLayer } from "../../../shared/legacy-db-connection.layer.ts"; +import { legacyDebugLoggerLayer } from "../../../shared/legacy-debug-logger.layer.ts"; +import { stdinLayer } from "../../../../shared/runtime/stdin.layer.ts"; +import { legacyIdentityStitchLayer } from "../../../shared/legacy-identity-stitch.ts"; +import { legacyLinkedProjectCacheLayer } from "../../../telemetry/legacy-linked-project-cache.layer.ts"; +import { legacyTelemetryStateLayer } from "../../../telemetry/legacy-telemetry-state.layer.ts"; + +/** + * Runtime layer for `supabase db push`. Same shape as `db lint`: it spans local + * (`--local` / `--db-url`) and linked DB access, so it composes the Postgres + * connection, the db-config resolver, project-ref resolution, and the + * linked-project cache (Go's PersistentPostRun `ensureProjectGroupsCached`). + * + * Like `db lint`, it deliberately uses the **lazy** `legacyPlatformApiFactoryLayer` + * (not the eager management-API runtime) so the auth-free `--local` path never + * resolves an access token at layer-build time. `legacyCliConfigLayer` is provided + * to each consumer that needs it (legacy CLAUDE.md item 5); the single + * `legacyIdentityStitchLayer` reference is shared so the factory, the cache, and + * the db-config resolver share one `stitchAttempted` guard. + */ +const cliConfig = legacyCliConfigLayer.pipe(Layer.provide(legacyDebugLoggerLayer)); +const httpClient = legacyHttpClientLayer.pipe(Layer.provide(legacyDebugLoggerLayer)); +const credentials = legacyCredentialsLayer.pipe( + Layer.provide(cliConfig), + Layer.provide(legacyDebugLoggerLayer), +); + +const platformApiFactory = legacyPlatformApiFactoryLayer.pipe( + Layer.provide(credentials), + Layer.provide(cliConfig), + Layer.provide(legacyDebugLoggerLayer), + Layer.provide(legacyIdentityStitchLayer), +); + +const projectRef = legacyProjectRefLayer.pipe( + Layer.provide(platformApiFactory), + Layer.provide(cliConfig), +); + +const linkedProjectCache = legacyLinkedProjectCacheLayer.pipe( + Layer.provide(credentials), + Layer.provide(cliConfig), + Layer.provide(httpClient), + Layer.provide(legacyIdentityStitchLayer), +); + +const dbConfig = legacyDbConfigLayer.pipe( + Layer.provide(cliConfig), + Layer.provide(legacyDbConnectionLayer), + Layer.provide(legacyDebugLoggerLayer), + Layer.provide(legacyIdentityStitchLayer), +); + +export const legacyDbPushRuntimeLayer = Layer.mergeAll( + dbConfig, + legacyDbConnectionLayer, + cliConfig, + httpClient, + credentials, + projectRef, + linkedProjectCache, + legacyIdentityStitchLayer, + legacyTelemetryStateLayer, + // `legacyPromptYesNo`'s non-TTY branch reads the piped answer via `Stdin` (Go's + // `console.ReadLine`); without it a CI/piped `db push` that reaches a confirmation + // prompt fails with a missing-service defect instead of honoring `y`/`n` or the default. + stdinLayer, + commandRuntimeLayer(["db", "push"]), +); diff --git a/apps/cli/src/legacy/commands/db/reset/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/db/reset/SIDE_EFFECTS.md index d1291e27a4..6089303d32 100644 --- a/apps/cli/src/legacy/commands/db/reset/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/db/reset/SIDE_EFFECTS.md @@ -1,60 +1,143 @@ # `supabase db reset` +Native TypeScript port of `apps/cli-go/internal/db/reset/reset.go`. Reinitialises a +database from local migrations (plus seed). The **remote** path (`--linked`, or a +remote `--db-url`) is native: drop all user schemas, upsert vault secrets, then +re-apply migrations and seed. The **local** path (`--local`/default, or a `--db-url` +pointing at the local stack) is also native: TS orchestrates the running check, +messages, bucket seeding, and git-branch line, while the container-recreate +primitives run behind the hidden Go `db __db-bootstrap` seam. Only the niche +**`--experimental`** remote schema-files path still delegates to the Go binary. + ## Files Read -| Path | Format | When | -| --------------------------------------- | ---------- | ------------------------------------------------- | -| `~/.supabase/access-token` | plain text | when `SUPABASE_ACCESS_TOKEN` unset and `--linked` | -| `/supabase/migrations/` | directory | always, to load migration files | -| seed files from config or `--sql-paths` | SQL | unless `--no-seed` is set | +| Path | Format | When | +| ------------------------------------------------------ | ---------- | ------------------------------------------------------------------------- | +| `/supabase/migrations/` | directory | to validate `--version` / resolve `--last`, and to load migrations | +| `/supabase/config.toml` | TOML | remote path + local bucket seeding (embedded defaults when absent) | +| `/.git/HEAD` (walked upward) | plain text | local path, for the `Finished … on branch .` line | +| `~/.supabase//project-ref` | plain text | `--linked`, to resolve the ref | +| `~/.supabase/access-token` | plain text | `--linked`, when `SUPABASE_ACCESS_TOKEN` unset and a temp role is minted | +| seed files from `--sql-paths` or `[db.seed].sql_paths` | SQL | when seeding is enabled (not `--no-seed`); `--sql-paths` overrides config | +| `/supabase/buckets/` | files | local path, when storage is up and `[storage.buckets]` configure objects | ## Files Written -| Path | Format | When | -| ---- | ------ | ---- | -| — | — | — | +| Path | Format | When | +| ------------------------------------------------ | ------ | --------------------------------- | +| `~/.supabase//linked-project.json` | JSON | `--linked` (post-run cache) | +| `~/.supabase/telemetry.json` | JSON | always (post-run telemetry flush) | + +On the local path the Go seam additionally recreates the `supabase_db_` +container/volume and applies the initial schema (`SetupLocalDatabase`); the +`--experimental` remote path produces whatever the delegated Go binary writes. + +## Subprocesses + +| Command | When | Purpose | +| --------------------------------------------------------------------------- | ----------------------------------- | ----------------------------------------------------------------------- | +| `docker container inspect supabase_db_` | local path | `AssertSupabaseDbIsRunning` probe (Podman fallback) | +| `supabase-go db __db-bootstrap --mode recreate [--version ] [--no-seed]` | local path | recreate container + init schema + migrate + seed + restart services | +| `supabase-go db __db-bootstrap --mode await-storage` | local path | storage health gate before bucket seeding (`ready` / `absent`) | +| `supabase-go db reset --linked\|--db-url … [--no-seed]` | `--experimental` remote, no version | the un-ported experimental schema-files apply path (telemetry disabled) | + +The seam subprocesses run with `SUPABASE_TELEMETRY_DISABLED=1`, stderr inherited; +`--network-id` / a flag-selected `--profile` are forwarded. + +## Database Mutations + +### Remote path (native, in TS) + +| Statement | When | +| ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | +| `drop.sql` `DO` block (drops user schemas/extensions/public objects, truncates auth/migrations) | always, first | +| `SELECT vault.update_secret(...)` / `vault.create_secret(...)` | when `[db.vault]` has syncable secrets | +| migration statements + `schema_migrations` history insert (per file, transactional) | when `[db.migrations].enabled`, for migrations `≤ --version` | +| seed statements + `seed_files` hash upsert | when `[db.seed].enabled` and not `--no-seed` | + +### Local path (inside the Go seam) + +The recreate seam drops & recreates the `postgres`/`_supabase` databases (PG≤14) or +removes & recreates the db container/volume (PG15), applies the initial schema + +roles, then runs `MigrateAndSeed` (migrations `≤ --version`, seed unless `--no-seed`) +and restarts the storage/auth/realtime/pooler containers. Bucket objects are then +seeded over the Storage gateway (reusing the `seed buckets` local path). ## API Routes -| Method | Path | Auth | Request body | Response (used fields) | -| ------ | ---- | ---- | ------------ | ---------------------- | -| — | — | — | — | — | +| Method | Path | Auth | Request body | Response (used fields) | +| ------ | ---- | ---- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| — | — | — | — | Connects to Postgres directly. The `--linked` resolver may call the Management API to mint a temporary login role; local bucket seeding calls the Storage gateway. | ## Environment Variables -| Variable | Purpose | Required? | -| ----------------------- | --------------------------------------- | ------------------------------------------------------- | -| `SUPABASE_ACCESS_TOKEN` | auth token for `--linked` mode | no (falls back to keyring → `~/.supabase/access-token`) | -| `DB_PASSWORD` | password for direct database connection | no | +| Variable | Purpose | Required? | +| ----------------------- | ----------------------------------------------- | ------------------------------------------------------- | +| `SUPABASE_ACCESS_TOKEN` | auth token for the `--linked` resolver path | no (falls back to keyring → `~/.supabase/access-token`) | +| `SUPABASE_DB_PASSWORD` | password for the linked/remote connection | no | +| `SUPABASE_YES` | auto-confirm the reset prompt | no (also `--yes`) | +| `SUPABASE_EXPERIMENTAL` | routes the experimental schema-files path to Go | no (also `--experimental`) | +| `SUPABASE_PROJECT_ID` | overrides the local container id (`utils.DbId`) | no | ## Exit Codes -| Code | Condition | -| ---- | --------------------------- | -| `0` | success | -| `1` | database connection failure | -| `1` | migration apply error | +| Code | Condition | +| ---- | ---------------------------------------------------------------- | +| `0` | success | +| `1` | mutually exclusive target flags (`[db-url linked local]`) | +| `1` | `--version` + `--last` together (`[last version]`) | +| `1` | `--version` not an integer (`invalid version number`) | +| `1` | `--version` has no matching migration file | +| `1` | local: database not running (`supabase start is not running.`) | +| `1` | user declined the reset confirmation (`context canceled`) | +| `1` | `config.toml` parse failure | +| `1` | drop / migrate / seed / vault apply failure, or connection error | +| `1` | local: container recreate / storage health-gate failure (seam) | ## Output +The remote path prints `Resetting remote database…` to **stderr**, then the +drop/migrate/seed progress (`Applying migration …`, `Seeding data from …`). Go +connects with `io.Discard`, so there is **no** `Connecting to … database…` line and +**no** `Finished …` line on the remote path. + +The local path prints `Resetting local database…` to **stderr**, then the seam's +`Recreating database...` / `Restarting containers...` progress, and finally +`Finished supabase db reset on branch .` (`supabase db reset` and `` +in Aqua). + ### `--output-format text` (Go CLI compatible) -Prints progress to stderr as migrations are applied. +Byte-matches Go's stderr progress for both the remote and local paths. The +`--experimental` remote path passes the delegated Go binary's output through +unchanged. -### `--output-format json` +### `--output-format json` / `stream-json` -Not applicable. +stdout is payload-only; a `result` object is emitted: -### `--output-format stream-json` +```json +{ "target": "remote" | "local", "version": "" } +``` -Not applicable. +In machine modes the remote confirmation prompt is non-interactive and takes its +default (`false`), so a remote reset is declined unless `--yes` is set. The local +path has no confirmation prompt. ## Notes -- `--no-seed` skips running the seed script after reset. -- `--sql-paths` overrides `[db.seed].sql_paths` for one reset; repeat it to seed multiple files or glob patterns. -- `--sql-paths` force-enables seeding for that reset even when `[db.seed].enabled = false`. -- With `--linked` or `--db-url`, `--sql-paths` seeds the selected remote database after migrations. -- `--version` resets up to the specified migration version. -- `--last` resets up to the last n migration versions; mutually exclusive with `--version`. +- **Target/local split** follows Go's `IsLocalDatabase(resolved config)`, not the + flag name: a `--db-url` pointing at the local stack is treated as a local reset. +- `--no-seed` forces seeding off (Go sets `Config.Db.Seed.Enabled = false`); on the + local path it is forwarded to the recreate seam so `MigrateAndSeed` skips the seed. +- `--sql-paths` overrides `[db.seed].sql_paths` for one reset and force-enables seeding + even when `[db.seed].enabled = false`; repeat it to seed multiple files or glob + patterns (supabase-relative). Mutually exclusive with `--no-seed`. On the local path + it is forwarded to the recreate seam; on the remote path it seeds the selected + database after migrations (Go warns when paired with `--linked` / `--db-url`). +- `--last n` reverts the most recent `n` migrations; if `n ≥ total`, the reset target + version becomes `-` (revert everything). Mutually exclusive with `--version`. - `--db-url`, `--linked`, and `--local` (default true) are mutually exclusive. +- **Known interim**: only `--experimental` remote resets run via the Go binary; the + best-effort pg-delta catalog cache (inside the seam) is not surfaced (no output + impact). `encrypted:` vault secrets are skipped on the remote path. diff --git a/apps/cli/src/legacy/commands/db/reset/reset.command.ts b/apps/cli/src/legacy/commands/db/reset/reset.command.ts index 15a61c8d88..979d088f84 100644 --- a/apps/cli/src/legacy/commands/db/reset/reset.command.ts +++ b/apps/cli/src/legacy/commands/db/reset/reset.command.ts @@ -1,6 +1,10 @@ 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 { withLegacyCommandInstrumentation } from "../../../telemetry/legacy-command-instrumentation.ts"; import { legacyDbReset } from "./reset.handler.ts"; +import { legacyDbResetRuntimeLayer } from "./reset.layers.ts"; const noSqlPaths: ReadonlyArray = []; @@ -42,5 +46,24 @@ export type LegacyDbResetFlags = CliCommand.Command.Config.Infer; export const legacyDbResetCommand = Command.make("reset", config).pipe( Command.withDescription("Resets the local database to current migrations."), Command.withShortDescription("Resets the local database to current migrations"), - Command.withHandler((flags) => legacyDbReset(flags)), + Command.withHandler((flags) => + legacyDbReset(flags).pipe( + withLegacyCommandInstrumentation({ + flags: { + "db-url": flags.dbUrl, + linked: flags.linked, + local: flags.local, + "no-seed": flags.noSeed, + "sql-paths": flags.sqlPaths, + version: flags.version, + last: flags.last, + }, + // NO safeFlags: `markFlagTelemetrySafe` is per flag INSTANCE, and Go only + // marks migration squash's `--version` (cmd/migration.go:134). db reset's + // `--version` (cmd/db.go) is unmarked, so Go redacts it — match that. + }), + withJsonErrorHandling, + ), + ), + Command.provide(legacyDbResetRuntimeLayer), ); diff --git a/apps/cli/src/legacy/commands/db/reset/reset.errors.ts b/apps/cli/src/legacy/commands/db/reset/reset.errors.ts new file mode 100644 index 0000000000..bfb4c3e539 --- /dev/null +++ b/apps/cli/src/legacy/commands/db/reset/reset.errors.ts @@ -0,0 +1,88 @@ +import { Data } from "effect"; + +/** + * Conflicting database-target flags. Reproduces cobra's + * `MarkFlagsMutuallyExclusive("db-url", "linked", "local")` (`cmd/db.go:573`). + */ +export class LegacyDbResetTargetFlagsError extends Data.TaggedError( + "LegacyDbResetTargetFlagsError", +)<{ + readonly message: string; +}> {} + +/** + * `--version` and `--last` together. Reproduces cobra's + * `MarkFlagsMutuallyExclusive("version", "last")` (`cmd/db.go:576`). + */ +export class LegacyDbResetVersionFlagsError extends Data.TaggedError( + "LegacyDbResetVersionFlagsError", +)<{ + readonly message: string; +}> {} + +/** + * `--version` is not a valid integer. Byte-matches Go's + * `failed to parse : invalid version number` (`repair.go:24-29`). + */ +export class LegacyDbResetInvalidVersionError extends Data.TaggedError( + "LegacyDbResetInvalidVersionError", +)<{ + readonly message: string; +}> {} + +/** + * No migration file matches `--version`. Byte-matches Go's + * `glob supabase/migrations/_*.sql: file does not exist` + * (`repair.GetMigrationFile`). + */ +export class LegacyDbResetMigrationFileError extends Data.TaggedError( + "LegacyDbResetMigrationFileError", +)<{ + readonly message: string; +}> {} + +/** + * The user declined the reset confirmation. Go returns + * `errors.New(context.Canceled)` (`internal/db/reset/reset.go:248`). + */ +export class LegacyDbResetCancelledError extends Data.TaggedError("LegacyDbResetCancelledError")<{ + readonly message: string; +}> {} + +/** A drop / migrate / seed / vault statement failed during the remote reset. */ +export class LegacyDbResetApplyError extends Data.TaggedError("LegacyDbResetApplyError")<{ + readonly message: string; +}> {} + +/** + * The local database container is not running. Byte-matches Go's + * `utils.ErrNotRunning` (`internal/utils/misc.go:116`), `"supabase start + * is not running."`, returned by `AssertSupabaseDbIsRunning` before the local + * reset (`internal/db/reset/reset.go:57`). + */ +export class LegacyDbResetNotRunningError extends Data.TaggedError("LegacyDbResetNotRunningError")<{ + readonly message: string; +}> {} + +/** + * `--last` was given a negative value. Go declares `--last` as an unsigned flag + * (`UintVar`, `cmd/db.go`), so cobra rejects a negative at parse time. Byte-matches + * cobra's parse error for `strconv.ParseUint`. + */ +export class LegacyDbResetLastFlagError extends Data.TaggedError("LegacyDbResetLastFlagError")<{ + readonly message: string; +}> {} + +/** + * Invalid `--sql-paths` usage. Byte-matches Go's `validateDbResetSeedFlags` + * (`cmd/db.go`): `"--no-seed cannot be used with --sql-paths"` and + * `"--sql-paths requires a non-empty path or glob pattern"`. + */ +export class LegacyDbResetSeedFlagsError extends Data.TaggedError("LegacyDbResetSeedFlagsError")<{ + readonly message: string; + /** + * Actionable hint rendered as a `Suggestion:` line, mirroring Go's + * `validateDbResetSeedFlags` `utils.CmdSuggestion` (`cmd/db.go`). + */ + readonly suggestion?: string; +}> {} diff --git a/apps/cli/src/legacy/commands/db/reset/reset.handler.ts b/apps/cli/src/legacy/commands/db/reset/reset.handler.ts index de35923ac2..640392a7c0 100644 --- a/apps/cli/src/legacy/commands/db/reset/reset.handler.ts +++ b/apps/cli/src/legacy/commands/db/reset/reset.handler.ts @@ -1,16 +1,426 @@ -import { Effect, Option } from "effect"; +import { Effect, FileSystem, Option, Path } from "effect"; + +import { CliArgs } from "../../../../shared/cli/cli-args.service.ts"; +import { detectGitBranch } from "../../../../shared/git/git-branch.ts"; +import { LegacyDnsResolverFlag } from "../../../../shared/legacy/global-flags.ts"; +import { + legacyResolveExperimentalWithProjectEnv, + legacyResolveYesWithProjectEnv, +} from "../../../../shared/legacy/global-flags.ts"; import { LegacyGoProxy } from "../../../../shared/legacy/go-proxy.service.ts"; +import { Output } from "../../../../shared/output/output.service.ts"; +import { legacyAqua, legacyYellow } from "../../../shared/legacy-colors.ts"; +import { LegacyCliConfig } from "../../../config/legacy-cli-config.service.ts"; +import { LegacyProjectRefResolver } from "../../../config/legacy-project-ref.service.ts"; +import { LegacyDbConfigResolver } from "../../../shared/legacy-db-config.service.ts"; +import { + legacyCheckDbToml, + legacyLoadProjectEnv, + legacyResolveSeedSqlPath, +} from "../../../shared/legacy-db-config.toml-read.ts"; +import { LegacyDbConnection } from "../../../shared/legacy-db-connection.service.ts"; +import { legacyApplyMigrations } from "../../../shared/legacy-migration-apply.ts"; +import { legacyPromptYesNo } from "../../../shared/legacy-prompt-yes-no.ts"; +import { + type LegacyDbConnType, + resolveLegacyDbTargetFlags, +} from "../../../shared/legacy-db-target-flags.ts"; +import { LegacyLinkedProjectCache } from "../../../telemetry/legacy-linked-project-cache.service.ts"; +import { LegacyTelemetryState } from "../../../telemetry/legacy-telemetry-state.service.ts"; +import { legacyDropUserSchemas } from "../shared/legacy-drop-schemas.ts"; +import { LegacyDbBootstrapSeam } from "../shared/legacy-db-bootstrap.seam.service.ts"; +import { legacyListLocalMigrations } from "../shared/legacy-pgdelta.cache.ts"; +import { + legacyGetPendingSeeds, + legacyMatchPattern, + legacySeedData, +} from "../shared/legacy-seed-ops.ts"; +import { legacyUpsertVaultSecrets } from "../../../shared/legacy-vault.ts"; +import { legacySeedBucketsRun } from "../../../shared/legacy-seed-buckets.ts"; import type { LegacyDbResetFlags } from "./reset.command.ts"; +import { + LegacyDbResetApplyError, + LegacyDbResetCancelledError, + LegacyDbResetInvalidVersionError, + LegacyDbResetLastFlagError, + LegacyDbResetMigrationFileError, + LegacyDbResetNotRunningError, + LegacyDbResetSeedFlagsError, + LegacyDbResetTargetFlagsError, + LegacyDbResetVersionFlagsError, +} from "./reset.errors.ts"; -export const legacyDbReset = Effect.fn("legacy.db.reset")(function* (flags: LegacyDbResetFlags) { - const proxy = yield* LegacyGoProxy; - const args: string[] = ["db", "reset"]; +const INTEGER_PATTERN = /^[+-]?\d+$/u; +const MIGRATE_FILE_PATTERN = /^([0-9]+)_(.*)\.sql$/u; + +const applyError = (message: string) => new LegacyDbResetApplyError({ message }); + +/** Go's `toLogMessage` (`internal/db/reset/reset.go:88-91`). */ +const toLogMessage = (version: string): string => + version.length > 0 ? ` to version: ${version}` : "..."; + +/** + * Rebuilds the `db reset` argv for the remaining Go-delegated path: a remote + * `--experimental` reset with no resolved version. Only the flags reachable on + * that path are forwarded — `--local` always takes the native path, and a set + * `--version`/`--last` resolves a non-empty version which disables the experimental + * delegation (a degenerate `--last 0` resolves to "" and is behaviourally identical + * whether or not it is forwarded, so it is omitted). + * + * The target selector is forwarded from the RESOLVED `connType`, not the raw `--linked` + * boolean: the parent's `resolveLegacyDbTargetFlags` follows Cobra's `Changed` semantics, so + * `--linked=false` selects the linked/remote target (this path is remote-only). Forwarding + * only when `flags.linked === true` would drop the selector for `--linked=false` and let the + * Go child fall back to its local default — resetting the wrong database. + */ +const buildResetArgs = ( + flags: LegacyDbResetFlags, + connType: LegacyDbConnType, + yes: boolean, +): Array => { + const args = ["db", "reset"]; if (Option.isSome(flags.dbUrl)) args.push("--db-url", flags.dbUrl.value); - if (flags.linked) args.push("--linked"); - if (flags.local) args.push("--local"); + else if (connType === "linked") args.push("--linked"); if (flags.noSeed) args.push("--no-seed"); - for (const path of flags.sqlPaths) args.push("--sql-paths", path); - if (Option.isSome(flags.version)) args.push("--version", flags.version.value); - if (Option.isSome(flags.last)) args.push("--last", String(flags.last.value)); - yield* proxy.exec(args); + for (const p of flags.sqlPaths) args.push("--sql-paths", p); + // Forward the parent's RESOLVED `yes` as a bound flag. Go's `--yes` beats `AutomaticEnv`, + // so `--yes=false` overrides an inherited `SUPABASE_YES=true` (the child no longer + // auto-confirms a reset the user protected with `--yes=false`), while `--yes=true` honors + // an explicit `--yes` / env even in machine mode where the child's stdin is ignored. + // `--yes=false` still prompts on a TTY (Go's PromptYesNo only short-circuits on true), so + // this matches the default behavior when neither flag nor env is set. + args.push(`--yes=${yes}`); + return args; +}; + +/** + * `supabase db reset` — reinitialise a database from local migrations (+ seed). + * + * Strict 1:1 port of `apps/cli-go/internal/db/reset/reset.go`. The remote path + * (`--linked` / a remote `--db-url`) is native. The local path (and the niche + * `--experimental` schema-files path) delegate to the Go binary as a documented + * interim until the container-bootstrap seam is ported (CLI-1325 Stage 3). + */ +export const legacyDbReset = Effect.fn("legacy.db.reset")(function* (flags: LegacyDbResetFlags) { + const output = yield* Output; + const resolver = yield* LegacyDbConfigResolver; + const dbConn = yield* LegacyDbConnection; + const proxy = yield* LegacyGoProxy; + const seam = yield* LegacyDbBootstrapSeam; + const cliConfig = yield* LegacyCliConfig; + const telemetryState = yield* LegacyTelemetryState; + const linkedProjectCache = yield* LegacyLinkedProjectCache; + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const cliArgs = yield* CliArgs; + const dnsResolver = yield* LegacyDnsResolverFlag; + + const workdir = cliConfig.workdir; + const migrationsDir = path.join(workdir, "supabase", "migrations"); + // Go's `ParseDatabaseConfig` runs `loadNestedEnv` (which `os.Setenv`s each project-.env key) + // before `reset.Run` reads `viper.GetBool("YES")` / `viper.GetBool("EXPERIMENTAL")`, so a + // `SUPABASE_YES` / `SUPABASE_EXPERIMENTAL` set only in `supabase/.env` is honored. Load the + // project env first and resolve both gates against it, as `db pull` does for `yes`. + const projectEnv = yield* legacyLoadProjectEnv(fs, path, workdir); + const yes = yield* legacyResolveYesWithProjectEnv(projectEnv); + const experimental = yield* legacyResolveExperimentalWithProjectEnv(projectEnv); + let linkedRefForCache: string | undefined; + + const body = Effect.gen(function* () { + const target = resolveLegacyDbTargetFlags(cliArgs.args); + // cobra MarkFlagsMutuallyExclusive("db-url", "linked", "local"). + if (target.setFlags.length > 1) { + return yield* Effect.fail( + new LegacyDbResetTargetFlagsError({ + message: `if any flags in the group [db-url linked local] are set none of the others can be; [${target.setFlags.join(" ")}] were all set`, + }), + ); + } + // Go declares `--last` as `UintVar`, so cobra rejects a negative at parse time + // (`Flag.integer` here accepts it). Reject it the same way rather than silently + // treating it as "no --last" and resetting the full history. + if (Option.isSome(flags.last) && flags.last.value < 0) { + return yield* Effect.fail( + new LegacyDbResetLastFlagError({ + message: `invalid argument "${flags.last.value}" for "--last" flag: strconv.ParseUint: parsing "${flags.last.value}": invalid syntax`, + }), + ); + } + // cobra MarkFlagsMutuallyExclusive("version", "last") — alphabetical group. + if (Option.isSome(flags.version) && Option.isSome(flags.last)) { + return yield* Effect.fail( + new LegacyDbResetVersionFlagsError({ + message: + "if any flags in the group [last version] are set none of the others can be; [last version] were all set", + }), + ); + } + + // Go's validateDbResetSeedFlags (PreRunE): `--no-seed` conflicts with + // `--sql-paths`, and each `--sql-paths` value must be non-empty. + if (flags.noSeed && flags.sqlPaths.length > 0) { + return yield* Effect.fail( + new LegacyDbResetSeedFlagsError({ + message: "--no-seed cannot be used with --sql-paths", + suggestion: `Use either ${legacyAqua("--no-seed")} to skip seeding or ${legacyAqua( + "--sql-paths", + )} to override seed files, not both.`, + }), + ); + } + if (flags.sqlPaths.some((p) => p.length === 0)) { + return yield* Effect.fail( + new LegacyDbResetSeedFlagsError({ + message: "--sql-paths requires a non-empty path or glob pattern", + suggestion: `Pass a non-empty file path or glob pattern to ${legacyAqua("--sql-paths")}.`, + }), + ); + } + // Go's warnRemoteResetSeedOverride (PreRunE): a remote target flag + --sql-paths. + if ( + flags.sqlPaths.length > 0 && + (target.setFlags.includes("linked") || target.setFlags.includes("db-url")) + ) { + yield* output.raw( + `${legacyYellow("WARNING:")} --sql-paths overrides [db.seed].sql_paths and seeds the remote database selected by --linked or --db-url.\n`, + "stderr", + ); + } + + // Version / last resolution (Go's reset.Run lines 34-52), filesystem only. + let resolvedVersion = ""; + if (Option.isSome(flags.version)) { + const v = flags.version.value; + if (!INTEGER_PATTERN.test(v)) { + return yield* Effect.fail( + new LegacyDbResetInvalidVersionError({ + message: `failed to parse ${v}: invalid version number`, + }), + ); + } + // Go validates the version with `repair.GetMigrationFile` (repair.go:90-100), + // which globs `supabase/migrations/_*.sql` DIRECTLY with no filtering — + // so a deprecated first migration (e.g. `20200101000000_init.sql`) that + // `legacyListLocalMigrations` excludes is still accepted. Mirror that with a raw + // directory read + Go-glob match instead of the filtered migration listing. + const entries = yield* fs + .readDirectory(migrationsDir) + .pipe(Effect.orElseSucceed(() => [] as ReadonlyArray)); + const found = entries.some((name) => legacyMatchPattern(`${v}_*.sql`, path.basename(name))); + if (!found) { + return yield* Effect.fail( + new LegacyDbResetMigrationFileError({ + message: `glob supabase/migrations/${v}_*.sql: file does not exist`, + }), + ); + } + resolvedVersion = v; + } else if (Option.isSome(flags.last) && flags.last.value > 0) { + const locals = yield* legacyListLocalMigrations(fs, path, migrationsDir); + const versions = locals.flatMap((p) => { + const m = MIGRATE_FILE_PATTERN.exec(path.basename(p)); + return m?.[1] !== undefined ? [m[1]] : []; + }); + const total = versions.length; + const last = flags.last.value; + resolvedVersion = last < total ? versions[total - last - 1]! : "-"; + } + + const connType = target.connType ?? "local"; + // Go's ParseDatabaseConfig runs LoadProjectRef BEFORE the fallible linked + // resolution (db_url.go:87-95), and Execute() writes the linked-project cache + // even when a later step errors (root.go:171-181). Pre-load the ref so the + // post-run cache finalizer still fires when resolve fails mid-way (merged + // config, temp-role mint, connection) — mirrors push.handler. + if (connType === "linked") { + const refResolver = yield* LegacyProjectRefResolver; + linkedRefForCache = yield* refResolver.loadProjectRef(Option.none()); + } + const cfg = yield* resolver.resolve({ dbUrl: flags.dbUrl, connType, dnsResolver }); + + // Local target → native local reset. The container-recreate primitives live + // behind the hidden Go `db __db-bootstrap` seam; TS orchestrates the rest + // (running check, messages, bucket seeding, git-branch line, output shaping). + // Mirrors `internal/db/reset/reset.go:57-77`. + if (cfg.isLocal) { + // AssertSupabaseDbIsRunning — error if the local db container is down. + const running = yield* seam.isDbRunning(); + if (!running) { + return yield* Effect.fail( + new LegacyDbResetNotRunningError({ + message: `${legacyAqua("supabase start")} is not running.`, + }), + ); + } + // resetDatabase: "Resetting local database…" then recreate + migrate + seed. + yield* output.raw(`Resetting local database${toLogMessage(resolvedVersion)}\n`, "stderr"); + yield* seam.recreateDatabase({ + version: resolvedVersion, + noSeed: flags.noSeed, + sqlPaths: flags.sqlPaths, + }); + + // Seed objects from supabase/buckets when storage is up (Go gates buckets on + // an existing, healthy storage container). Reuses the ported seed-buckets + // local path; its summary is suppressed (reset emits its own result). + const storageReady = yield* seam.awaitStorageReady(); + if (storageReady) { + // Go's `buckets.Run(ctx, "", false, fsys)` — non-interactive: overwrite/prune + // confirmations take their defaults instead of blocking on input. + // + // Bucket seeding re-loads config.toml through the strict `@supabase/config` + // loader, which (unlike the Go-parity reader used elsewhere in reset) rejects some + // Go-valid configs — e.g. `[db.seed] enabled = "env(SEED_ENABLED)"`. The seam's Go + // `recreate` has already run Go's full `LoadConfig`+`Validate` on this same config, + // so a parse failure HERE is that loader-strictness gap, not a genuinely invalid + // config. Recreate already dropped/rebuilt the DB, so aborting now would leave the + // reset half-done; warn and skip buckets so `db reset` finishes like Go instead. + yield* legacySeedBucketsRun({ + projectRef: "", + emitSummary: false, + interactive: false, + // Go loads nested env before `buckets.Run`, so `SUPABASE_YES` in `supabase/.env` + // auto-confirms bucket/vector/analytics prune prompts. Pass the project-env-resolved + // `yes` (the shared runner's own `legacyResolveYes` only sees the shell env). + yes, + }).pipe( + Effect.catchTag("LegacySeedConfigLoadError", (error) => + output.raw( + `${legacyYellow("WARNING:")} skipped seeding storage buckets: ${error.message}\n`, + "stderr", + ), + ), + ); + } + + // "Finished supabase db reset on branch ." (both Aqua). + const branch = Option.getOrElse(yield* detectGitBranch(workdir), () => "main"); + yield* output.raw( + `Finished ${legacyAqua("supabase db reset")} on branch ${legacyAqua(branch)}.\n`, + "stderr", + ); + if (output.format !== "text") { + yield* output.success("Reset local database.", { + target: "local", + version: resolvedVersion, + }); + } + return; + } + + // Resolve the linked ref before any return so the post-run cache (Go's + // `PersistentPostRun` `ensureProjectGroupsCached`) is written even on the + // delegated `--experimental` path below — the Go child runs with telemetry + // disabled and skips that cache, so the TS finalizer must own it. + const linkedRef = Option.getOrUndefined(cfg.ref ?? Option.none()); + if (connType === "linked" && linkedRef !== undefined) linkedRefForCache = linkedRef; + + // Remote path. The niche `--experimental` schema-files apply path + // (`apply.MigrateAndSeed`) is not ported; delegate it to the Go child. In text + // mode inherit its stdio. Under a machine-output mode (`--output-format + // json|stream-json`) the Go child emits no TS envelope, so suppress its stdout + // (capture + discard) and emit the same structured success the native local and + // remote paths do, keeping the JSON contract consistent across all reset paths. + if (experimental && resolvedVersion === "") { + const env = { SUPABASE_TELEMETRY_DISABLED: "1" }; + if (output.format === "text") { + yield* proxy.exec(buildResetArgs(flags, connType, yes), { env }); + } else { + // Machine-output mode is non-interactive: give the Go child a non-TTY stdin + // (`stdin: "ignore"`) so it can't block on (or be answered at) Go's + // destructive reset prompt — it takes the default `false`, matching the + // native reset path which suppresses prompts under json/stream-json. + yield* proxy.execCapture(buildResetArgs(flags, connType, yes), { env, stdin: "ignore" }); + yield* output.success("Reset remote database.", { + target: "remote", + version: resolvedVersion, + }); + } + return; + } + + // Single Go-parity config load (`flags.LoadConfig` → `config.Load` + `Validate`): + // decodes the whole config with Go's env-expansion + `strconv.ParseBool` weak typing + // (so `enabled = "env(SEED_ENABLED)"` etc. load like Go), applies `SUPABASE_*` + // AutomaticEnv overrides, merges a matching `[remotes.]` block, and decrypts every + // `encrypted:` secret with the shell AND project-`.env` `DOTENV_PRIVATE_KEY*` keys — + // aborting here (before the destructive prompt / `legacyDropUserSchemas`) on any + // undecryptable/invalid config, exactly like Go's `LoadConfig` before ResetAll. + const configRef = connType === "linked" && linkedRef !== undefined ? linkedRef : undefined; + const toml = yield* legacyCheckDbToml(fs, path, workdir, configRef); + if (toml.appliedRemote !== undefined) { + yield* output.raw(`Loading config override: [remotes.${toml.appliedRemote}]\n`, "stderr"); + } + const vaultSecrets = toml.vault; + + // Go's resetRemote: prompt (default false) → cancel, then ResetAll. + const shouldReset = yield* legacyPromptYesNo( + output, + yes, + "Do you want to reset the remote database?", + false, + ); + if (!shouldReset) { + return yield* Effect.fail(new LegacyDbResetCancelledError({ message: "context canceled" })); + } + yield* output.raw(`Resetting remote database${toLogMessage(resolvedVersion)}\n`, "stderr"); + + // Go connects with io.Discard, so NO "Connecting to ... database..." line. + yield* Effect.scoped( + Effect.gen(function* () { + const session = yield* dbConn.connect(cfg.conn, { isLocal: false, dnsResolver }); + // ResetAll: drop user schemas → upsert vault → migrate + seed. + yield* legacyDropUserSchemas(session, applyError); + yield* legacyUpsertVaultSecrets(session, vaultSecrets); + + if (toml.migrationsEnabled) { + const locals = yield* legacyListLocalMigrations(fs, path, migrationsDir); + // LoadPartialMigrations filter: version === "" || v <= version. + const pending = locals.filter((p) => { + if (resolvedVersion === "") return true; + const m = MIGRATE_FILE_PATTERN.exec(path.basename(p)); + return m?.[1] !== undefined && m[1] <= resolvedVersion; + }); + yield* legacyApplyMigrations(session, fs, path, pending, applyError); + } + + // `--no-seed` disables seeding; `--sql-paths` overrides [db.seed].sql_paths + // and force-enables it (Go's applyDbResetSeedFlags). The two are mutually + // exclusive (validated above). + const overrideSeed = flags.sqlPaths.length > 0; + // `--sql-paths` force-enables seeding (Go's applyDbResetSeedFlags); otherwise + // honor `db.seed.enabled` (already `SUPABASE_DB_SEED_ENABLED`-resolved by the reader). + const seedEnabled = overrideSeed || (toml.seed.enabled && !flags.noSeed); + if (seedEnabled) { + // `[db.seed].sql_paths` is already Go-config-resolved (supabase/-joined) by the + // reader; the `--sql-paths` override is resolved here the same way Go's + // `resolveSeedSqlPaths` does, so both feed the glob identical paths. + const seedPaths = overrideSeed + ? flags.sqlPaths.map((p) => legacyResolveSeedSqlPath(path, p)) + : toml.seed.sqlPaths; + const seeds = yield* legacyGetPendingSeeds(session, fs, path, seedPaths, workdir); + yield* legacySeedData(session, fs, workdir, path, seeds, applyError); + } + // Go's best-effort pgcache catalog warning is not ported (no output impact). + }), + ); + + if (output.format !== "text") { + yield* output.success("Reset remote database.", { + target: "remote", + version: resolvedVersion, + }); + } + }); + + yield* body.pipe( + Effect.ensuring( + Effect.suspend(() => + linkedRefForCache !== undefined && linkedRefForCache !== "" + ? linkedProjectCache.cache(linkedRefForCache) + : Effect.void, + ), + ), + Effect.ensuring(telemetryState.flush), + ); }); diff --git a/apps/cli/src/legacy/commands/db/reset/reset.integration.test.ts b/apps/cli/src/legacy/commands/db/reset/reset.integration.test.ts index bd533d2adc..1ddec7f3b3 100644 --- a/apps/cli/src/legacy/commands/db/reset/reset.integration.test.ts +++ b/apps/cli/src/legacy/commands/db/reset/reset.integration.test.ts @@ -1,26 +1,65 @@ +import { mkdirSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; + +import { BunServices } from "@effect/platform-bun"; import { describe, expect, it } from "@effect/vitest"; -import { Effect, Layer, Option } from "effect"; -import { CliOutput, Command } from "effect/unstable/cli"; -import { mockOutput } from "../../../../../tests/helpers/mocks.ts"; +import { Effect, Exit, Layer, Option } from "effect"; +import * as HttpClient from "effect/unstable/http/HttpClient"; +import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse"; + +import { + mockOutput, + mockRuntimeInfo, + mockStdin, + mockTty, +} from "../../../../../tests/helpers/mocks.ts"; +import { + LEGACY_VALID_REF, + mockLegacyCliConfig, + mockLegacyLinkedProjectCacheTracked, + mockLegacyPlatformApiService, + mockLegacyTelemetryStateTracked, + useLegacyTempWorkdir, +} from "../../../../../tests/helpers/legacy-mocks.ts"; +import { LegacyPlatformApi } from "../../../auth/legacy-platform-api.service.ts"; +import { LegacyPlatformApiFactory } from "../../../auth/legacy-platform-api-factory.service.ts"; +import { LegacyProjectRefResolver } from "../../../config/legacy-project-ref.service.ts"; +import { CliArgs } from "../../../../shared/cli/cli-args.service.ts"; +import { + LegacyDnsResolverFlag, + LegacyExperimentalFlag, + LegacyYesFlag, +} from "../../../../shared/legacy/global-flags.ts"; import { LegacyGoProxy } from "../../../../shared/legacy/go-proxy.service.ts"; -import { textCliOutputFormatter } from "../../../../shared/output/text-formatter.ts"; -import { legacyDbResetCommand } from "./reset.command.ts"; +import type { OutputFormat } from "../../../../shared/output/types.ts"; +import { LegacyDbConfigResolver } from "../../../shared/legacy-db-config.service.ts"; +import type { + LegacyDbConfigFlags, + LegacyResolvedDbConfig, +} from "../../../shared/legacy-db-config.types.ts"; +import { LegacyDbConfigConnectTempRoleError } from "../../../shared/legacy-db-config.errors.ts"; +import { LegacyDbExecError } from "../../../shared/legacy-db-connection.errors.ts"; +import { + LegacyDbConnection, + type LegacyPgConnInput, +} from "../../../shared/legacy-db-connection.service.ts"; +import { LegacyDbBootstrapSeam } from "../shared/legacy-db-bootstrap.seam.service.ts"; import { legacyDbReset } from "./reset.handler.ts"; import type { LegacyDbResetFlags } from "./reset.command.ts"; -function setupLegacyDbReset() { - const calls: Array> = []; - const layer = Layer.succeed(LegacyGoProxy, { - exec: (args) => - Effect.sync(() => { - calls.push(args); - }), - execCapture: () => Effect.succeed(""), - }); - return { layer, calls }; -} +const LIST_MIGRATIONS = + "SELECT version FROM supabase_migrations.schema_migrations ORDER BY version"; +const SELECT_SEEDS = "SELECT path, hash FROM supabase_migrations.seed_files"; + +const CONN: LegacyPgConnInput = { + host: "db.example.supabase.co", + port: 5432, + user: "postgres", + password: "secret", + database: "postgres", +}; -const baseFlags: LegacyDbResetFlags = { +const DEFAULT_FLAGS: LegacyDbResetFlags = { dbUrl: Option.none(), linked: false, local: false, @@ -30,118 +69,1026 @@ const baseFlags: LegacyDbResetFlags = { last: Option.none(), }; +function mockResolver(opts: { + isLocal: boolean; + ref?: string; + omitRef?: boolean; + resolveFails?: boolean; +}) { + return Layer.succeed(LegacyDbConfigResolver, { + resolve: (_flags: LegacyDbConfigFlags) => + opts.resolveFails === true + ? Effect.fail( + new LegacyDbConfigConnectTempRoleError({ + message: "failed to create login role: network error", + }), + ) + : Effect.succeed( + (opts.omitRef === true + ? { conn: CONN, isLocal: opts.isLocal } + : { + conn: CONN, + isLocal: opts.isLocal, + ref: opts.ref !== undefined ? Option.some(opts.ref) : Option.none(), + }) satisfies LegacyResolvedDbConfig, + ), + resolvePoolerFallback: () => Effect.succeed(Option.none()), + }); +} + +function mockConnection(opts: { remoteSeeds?: Readonly> }) { + const execs: Array = []; + const queries: Array<{ sql: string; params?: ReadonlyArray }> = []; + const layer = Layer.succeed(LegacyDbConnection, { + connect: () => + Effect.succeed({ + extensionExists: () => Effect.succeed(false), + copyToCsv: () => Effect.succeed(new Uint8Array()), + queryRaw: () => Effect.succeed({ fields: [], rows: [], commandTag: "" }), + exec: (sql: string): Effect.Effect => + Effect.sync(() => { + execs.push(sql); + }), + query: ( + sql: string, + params?: ReadonlyArray, + ): Effect.Effect>, LegacyDbExecError> => + Effect.suspend( + (): Effect.Effect>, LegacyDbExecError> => { + queries.push({ sql, params }); + if (sql === SELECT_SEEDS) { + return Effect.succeed( + Object.entries(opts.remoteSeeds ?? {}).map(([path, hash]) => ({ path, hash })), + ); + } + if (sql === LIST_MIGRATIONS) return Effect.succeed([]); + return Effect.succeed([]); + }, + ), + }), + }); + return { + layer, + get execs() { + return execs; + }, + get queries() { + return queries; + }, + }; +} + +/** + * Stateful mock of the container-bootstrap seam. `running` drives + * `AssertSupabaseDbIsRunning`; `storageReady` drives the bucket-seed gate. Records + * the recreate args so tests can assert version / `--no-seed` propagation. + */ +function mockBootstrapSeam(opts: { running?: boolean; storageReady?: boolean }) { + const recreateCalls: Array<{ + version: string; + noSeed: boolean; + sqlPaths: ReadonlyArray; + }> = []; + let storageChecked = false; + const layer = Layer.succeed(LegacyDbBootstrapSeam, { + isDbRunning: () => Effect.succeed(opts.running ?? true), + startDatabase: () => Effect.void, + recreateDatabase: (args: { + version: string; + noSeed: boolean; + sqlPaths: ReadonlyArray; + }) => + Effect.sync(() => { + recreateCalls.push(args); + }), + awaitStorageReady: () => + Effect.sync(() => { + storageChecked = true; + return opts.storageReady ?? false; + }), + }); + return { + layer, + get recreateCalls() { + return recreateCalls; + }, + get storageChecked() { + return storageChecked; + }, + }; +} + +// Dummy HTTP client; the local-reset bucket-seed core only reaches it when storage +// is ready AND buckets are configured (no reset test configures buckets, so the +// gateway is never actually called). Present to satisfy the handler's R. +const mockStorageHttp = Layer.succeed( + HttpClient.HttpClient, + HttpClient.make((request) => + Effect.succeed(HttpClientResponse.fromWeb(request, new Response("{}", { status: 404 }))), + ), +); + +function mockProxy() { + const calls: Array<{ args: ReadonlyArray; env?: Record }> = []; + const layer = Layer.succeed(LegacyGoProxy, { + exec: (args, opts) => + Effect.sync(() => { + calls.push({ args, env: opts?.env }); + }), + execCapture: () => Effect.succeed(""), + }); + return { + layer, + get calls() { + return calls; + }, + }; +} + +function setup( + workdir: string, + opts: { + toml?: string; + files?: Readonly>; + format?: OutputFormat; + confirm?: ReadonlyArray; + args?: ReadonlyArray; + isLocal?: boolean; + ref?: string; + experimental?: boolean; + remoteSeeds?: Readonly>; + yes?: boolean; + omitRef?: boolean; + resolveFails?: boolean; + running?: boolean; + storageReady?: boolean; + }, +) { + if (opts.toml !== undefined) { + mkdirSync(join(workdir, "supabase"), { recursive: true }); + writeFileSync(join(workdir, "supabase", "config.toml"), opts.toml); + } + for (const [rel, content] of Object.entries(opts.files ?? {})) { + const abs = join(workdir, rel); + mkdirSync(dirname(abs), { recursive: true }); + writeFileSync(abs, content); + } + + const out = mockOutput({ format: opts.format ?? "text", promptConfirmResponses: opts.confirm }); + const conn = mockConnection(opts); + const proxy = mockProxy(); + const seam = mockBootstrapSeam({ running: opts.running, storageReady: opts.storageReady }); + const telemetry = mockLegacyTelemetryStateTracked(); + const linkedCache = mockLegacyLinkedProjectCacheTracked(); + // The local-reset bucket-seed core statically requires the (lazy) Management-API + // factory; never invoked on `--local` (projectRef === ""). + const platformApi = mockLegacyPlatformApiService({}); + + const layer = Layer.mergeAll( + out.layer, + conn.layer, + proxy.layer, + seam.layer, + mockResolver({ + isLocal: opts.isLocal ?? false, + ref: opts.ref ?? LEGACY_VALID_REF, + omitRef: opts.omitRef, + resolveFails: opts.resolveFails, + }), + mockLegacyCliConfig({ workdir }), + BunServices.layer, + mockRuntimeInfo(), + // The remote-reset confirmation is answered through mockOutput's + // `promptConfirmResponses` (the TTY/clack path), so mark stdin a TTY. Stdin is + // only referenced by legacyPromptYesNo's non-TTY branch (unreached here) but must + // be present to satisfy the effect's requirements. + mockTty({ stdinIsTty: true }), + mockStdin(true), + // The linked ref is pre-loaded (for the post-run cache) before resolve, + // mirroring Go's LoadProjectRef-before-NewDbConfigWithPassword order. + Layer.succeed(LegacyProjectRefResolver, { + resolve: () => Effect.succeed(opts.ref ?? LEGACY_VALID_REF), + resolveForLink: () => Effect.succeed(opts.ref ?? LEGACY_VALID_REF), + resolveOptional: () => Effect.succeed(Option.some(opts.ref ?? LEGACY_VALID_REF)), + loadProjectRef: () => Effect.succeed(opts.ref ?? LEGACY_VALID_REF), + promptProjectRef: () => Effect.succeed(opts.ref ?? LEGACY_VALID_REF), + }), + mockStorageHttp, + Layer.succeed(LegacyPlatformApiFactory, { + make: LegacyPlatformApi.pipe(Effect.provide(platformApi.layer)), + }), + Layer.succeed(CliArgs, { args: opts.args ?? ["db", "reset", "--linked"] }), + Layer.succeed(LegacyYesFlag, opts.yes ?? false), + Layer.succeed(LegacyDnsResolverFlag, "native"), + Layer.succeed(LegacyExperimentalFlag, opts.experimental ?? false), + telemetry.layer, + linkedCache.layer, + ); + return { layer, out, conn, proxy, seam, telemetry, linkedCache }; +} + +const migrationFile = (version: string, body = "create table t ();") => ({ + [`supabase/migrations/${version}_test.sql`]: body, +}); + describe("legacy db reset", () => { - it.live("forwards the empty-array baseline without seed override flags", () => { - const { layer, calls } = setupLegacyDbReset(); + const tmp = useLegacyTempWorkdir("supabase-db-reset-"); + + it.live("resets the local database via the bootstrap seam", () => { + const { layer, out, seam, proxy } = setup(tmp.current, { + toml: 'project_id = "test"\n', + args: ["db", "reset"], + isLocal: true, + running: true, + }); + return Effect.gen(function* () { + yield* legacyDbReset(DEFAULT_FLAGS).pipe(Effect.provide(layer)); + // Native path — no Go delegation. + expect(proxy.calls).toHaveLength(0); + expect(out.stderrText).toContain("Resetting local database..."); + expect(seam.recreateCalls).toEqual([{ version: "", noSeed: false, sqlPaths: [] }]); + // Storage gate checked; with no buckets configured nothing is seeded. + expect(seam.storageChecked).toBe(true); + expect(out.stderrText).toContain("Finished "); + expect(out.stderrText).toContain("on branch "); + }); + }); + + it.live("fails a local reset when the database is not running", () => { + const { layer, seam } = setup(tmp.current, { + toml: 'project_id = "test"\n', + args: ["db", "reset"], + isLocal: true, + running: false, + }); + return Effect.gen(function* () { + const exit = yield* legacyDbReset(DEFAULT_FLAGS).pipe(Effect.provide(layer), Effect.exit); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) expect(JSON.stringify(exit.cause)).toContain("is not running."); + expect(seam.recreateCalls).toHaveLength(0); + }); + }); + + it.live("seeds buckets after a local reset when storage is ready", () => { + const { layer, seam } = setup(tmp.current, { + toml: 'project_id = "test"\n', + args: ["db", "reset"], + isLocal: true, + running: true, + storageReady: true, + }); + return Effect.gen(function* () { + // No buckets configured → the seed-buckets core short-circuits, but the + // storage gate is still consulted (Go inspects storage before buckets.Run). + yield* legacyDbReset(DEFAULT_FLAGS).pipe(Effect.provide(layer)); + expect(seam.storageChecked).toBe(true); + expect(seam.recreateCalls).toHaveLength(1); + }); + }); + + it.live("finishes a local reset when bucket seeding hits a strict-loader-rejected config", () => { + // The bucket-seeding core re-loads config via the strict `@supabase/config` loader, + // which rejects some Go-valid configs (e.g. `[db.seed] enabled = "env(SEED_ENABLED)"`). + // The seam's Go recreate already validated + rebuilt the DB, so aborting here would + // leave the reset half-done — warn and skip buckets so reset finishes like Go. + const previous = process.env["SEED_ENABLED"]; + process.env["SEED_ENABLED"] = "1"; + const { layer, out, seam } = setup(tmp.current, { + toml: 'project_id = "test"\n\n[db.seed]\nenabled = "env(SEED_ENABLED)"\n', + args: ["db", "reset"], + isLocal: true, + running: true, + storageReady: true, + }); + return Effect.gen(function* () { + yield* legacyDbReset(DEFAULT_FLAGS).pipe(Effect.provide(layer)); + expect(out.stderrText).toContain("skipped seeding storage buckets"); + expect(out.stderrText).toContain("Finished "); + expect(seam.recreateCalls).toHaveLength(1); + }).pipe( + Effect.ensuring( + Effect.sync(() => { + if (previous === undefined) delete process.env["SEED_ENABLED"]; + else process.env["SEED_ENABLED"] = previous; + }), + ), + ); + }); + + it.live("uses the detected git branch in the Finished line", () => { + const { layer, out } = setup(tmp.current, { + toml: 'project_id = "test"\n', + args: ["db", "reset"], + isLocal: true, + running: true, + }); + // `detectGitBranch` checks `$GITHUB_HEAD_REF` first (matching Go's + // `GetGitBranchOrDefault`). Set it explicitly so the test is deterministic in + // both a plain checkout and a GitHub Actions PR run (where it is preset to the + // PR branch); restore it afterwards. + const previous = process.env["GITHUB_HEAD_REF"]; + process.env["GITHUB_HEAD_REF"] = "feature-x"; + return Effect.gen(function* () { + yield* legacyDbReset(DEFAULT_FLAGS).pipe(Effect.provide(layer)); + // The branch name is wrapped in ANSI (legacyAqua), so assert on the token. + expect(out.stderrText).toContain("on branch "); + expect(out.stderrText).toContain("feature-x"); + }).pipe( + Effect.ensuring( + Effect.sync(() => { + if (previous === undefined) delete process.env["GITHUB_HEAD_REF"]; + else process.env["GITHUB_HEAD_REF"] = previous; + }), + ), + ); + }); + + it.live("fails a remote reset on a malformed config.toml", () => { + const { layer } = setup(tmp.current, { toml: 'project_id = "unterminated\n' }); + return Effect.gen(function* () { + const exit = yield* legacyDbReset({ ...DEFAULT_FLAGS, linked: true }).pipe( + Effect.provide(layer), + Effect.exit, + ); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + // Config now loads through the Go-parity reader (`legacyCheckDbToml`), so a malformed + // config aborts with Go's `failed to load config` message, same as the other db + // commands (diff/dump/pull/migration). + expect(JSON.stringify(exit.cause)).toContain("failed to load config"); + } + }); + }); + + it.live("loads a Go-style env() boolean in config for a remote reset", () => { + // Regression: `enabled = "env(VAR)"` must load via Go's env-expansion + ParseBool + // (`legacyCheckDbToml`) instead of the strict @supabase/config loader rejecting it. + const previous = process.env["MIGRATIONS_ENABLED"]; + process.env["MIGRATIONS_ENABLED"] = "true"; + const { layer, out } = setup(tmp.current, { + toml: 'project_id = "test"\n\n[db.migrations]\nenabled = "env(MIGRATIONS_ENABLED)"\n', + files: migrationFile("20240101000000"), + confirm: [true], + }); + return Effect.gen(function* () { + yield* legacyDbReset({ ...DEFAULT_FLAGS, linked: true }).pipe(Effect.provide(layer)); + expect(out.stderrText).toContain("Applying migration 20240101000000_test.sql..."); + }).pipe( + Effect.ensuring( + Effect.sync(() => { + if (previous === undefined) delete process.env["MIGRATIONS_ENABLED"]; + else process.env["MIGRATIONS_ENABLED"] = previous; + }), + ), + ); + }); + + it.live("emits a json result for a local reset", () => { + const { layer, out } = setup(tmp.current, { + toml: 'project_id = "test"\n', + args: ["db", "reset"], + isLocal: true, + running: true, + format: "json", + }); + return Effect.gen(function* () { + yield* legacyDbReset(DEFAULT_FLAGS).pipe(Effect.provide(layer)); + const success = out.messages.find((m) => m.type === "success"); + expect(success?.data?.["target"]).toBe("local"); + }); + }); + + it.live("rejects mutually exclusive target flags", () => { + const { layer } = setup(tmp.current, { + toml: 'project_id = "test"\n', + args: ["db", "reset", "--linked", "--local"], + }); + return Effect.gen(function* () { + const exit = yield* legacyDbReset(DEFAULT_FLAGS).pipe(Effect.provide(layer), Effect.exit); + expect(Exit.isFailure(exit)).toBe(true); + }); + }); + + it.live("rejects --version together with --last", () => { + const { layer } = setup(tmp.current, { toml: 'project_id = "test"\n' }); + return Effect.gen(function* () { + const exit = yield* legacyDbReset({ + ...DEFAULT_FLAGS, + linked: true, + version: Option.some("20240101000000"), + last: Option.some(1), + }).pipe(Effect.provide(layer), Effect.exit); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) expect(JSON.stringify(exit.cause)).toContain("[last version]"); + }); + }); + + it.live("rejects a non-integer --version", () => { + const { layer } = setup(tmp.current, { toml: 'project_id = "test"\n' }); + return Effect.gen(function* () { + const exit = yield* legacyDbReset({ + ...DEFAULT_FLAGS, + linked: true, + version: Option.some("not-a-number"), + }).pipe(Effect.provide(layer), Effect.exit); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) + expect(JSON.stringify(exit.cause)).toContain("invalid version number"); + }); + }); + + it.live("fails when --version has no matching migration file", () => { + const { layer } = setup(tmp.current, { toml: 'project_id = "test"\n' }); + return Effect.gen(function* () { + const exit = yield* legacyDbReset({ + ...DEFAULT_FLAGS, + linked: true, + version: Option.some("20240101000000"), + }).pipe(Effect.provide(layer), Effect.exit); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(JSON.stringify(exit.cause)).toContain( + "glob supabase/migrations/20240101000000_*.sql: file does not exist", + ); + } + }); + }); + + it.live("returns context canceled when the reset prompt is declined", () => { + const { layer, conn } = setup(tmp.current, { + toml: 'project_id = "test"\n', + confirm: [false], + }); + return Effect.gen(function* () { + const exit = yield* legacyDbReset({ ...DEFAULT_FLAGS, linked: true }).pipe( + Effect.provide(layer), + Effect.exit, + ); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) expect(JSON.stringify(exit.cause)).toContain("context canceled"); + expect(conn.execs).toHaveLength(0); + }); + }); + + it.live("drops schemas and applies migrations + seed on a confirmed remote reset", () => { + const { layer, out, conn, linkedCache } = setup(tmp.current, { + toml: 'project_id = "test"\n', + files: { + ...migrationFile("20240101000000"), + "supabase/seed.sql": "insert into t values (1);", + }, + confirm: [true], + }); + return Effect.gen(function* () { + yield* legacyDbReset({ ...DEFAULT_FLAGS, linked: true }).pipe(Effect.provide(layer)); + expect(out.stderrText).toContain("Resetting remote database..."); + // No "Connecting to ... database..." line (Go uses io.Discard). + expect(out.stderrText).not.toContain("Connecting to"); + // Drop block ran, then the migration applied. + expect(conn.execs.some((s) => s.includes("drop schema if exists"))).toBe(true); + expect(out.stderrText).toContain("Applying migration 20240101000000_test.sql..."); + expect(out.stderrText).toContain("Seeding data from supabase/seed.sql..."); + expect(linkedCache.cached).toBe(true); + }); + }); + + it.live("fails a remote reset before dropping schemas on an undecryptable secret", () => { + // Regression: the old point-of-use vault decryption ran AFTER `legacyDropUserSchemas`, + // so an undecryptable `encrypted:` secret dropped the schemas before failing. Go runs + // `flags.LoadConfig` (which decrypts every secret) before ResetAll, so the reset must + // abort before any destructive work — matched here by `legacyCheckDbToml` at load time. + const { layer, conn } = setup(tmp.current, { + toml: 'project_id = "test"\n\n[db.vault]\nmy_secret = "encrypted:anything"\n', + confirm: [true], + }); + return Effect.gen(function* () { + const exit = yield* legacyDbReset({ ...DEFAULT_FLAGS, linked: true }).pipe( + Effect.provide(layer), + Effect.exit, + ); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(JSON.stringify(exit.cause)).toContain("failed to parse config: missing private key"); + } + // Config load failed before ResetAll → schemas were never dropped. + expect(conn.execs.some((s) => s.includes("drop schema if exists"))).toBe(false); + }); + }); + + it.live("fails a remote reset before dropping schemas on an empty project_id", () => { + // Go's config.Validate rejects an explicit `project_id = ""` before the reset prompt, so + // the native remote reset must abort before `legacyDropUserSchemas`. + const { layer, conn } = setup(tmp.current, { + toml: 'project_id = ""\n', + confirm: [true], + }); + return Effect.gen(function* () { + const exit = yield* legacyDbReset({ ...DEFAULT_FLAGS, linked: true }).pipe( + Effect.provide(layer), + Effect.exit, + ); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(JSON.stringify(exit.cause)).toContain( + "Missing required field in config: project_id", + ); + } + expect(conn.execs.some((s) => s.includes("drop schema if exists"))).toBe(false); + }); + }); + + it.live("auto-confirms a remote reset via SUPABASE_YES set only in the project .env", () => { + // Go's loadNestedEnv sets project-.env keys before the reset prompt reads viper YES, so + // a `SUPABASE_YES` in supabase/.env auto-confirms the destructive prompt (default false). + const { layer, conn } = setup(tmp.current, { + toml: 'project_id = "test"\n', + files: { "supabase/.env": "SUPABASE_YES=true\n" }, + // Deliberately no `confirm` responses — the prompt must be auto-confirmed. + }); return Effect.gen(function* () { - yield* legacyDbReset(baseFlags); - expect(calls).toEqual([["db", "reset"]]); - }).pipe(Effect.provide(layer)); + yield* legacyDbReset({ ...DEFAULT_FLAGS, linked: true }).pipe(Effect.provide(layer)); + expect(conn.execs.some((s) => s.includes("drop schema if exists"))).toBe(true); + }); }); - it.live("forwards --no-seed alone", () => { - const { layer, calls } = setupLegacyDbReset(); + it.live("still caches the linked ref when DB-config resolution fails", () => { + // Go's Execute() runs ensureProjectGroupsCached after ExecuteC returns even on + // error (root.go:171-181), and ParseDatabaseConfig sets ProjectRef via + // LoadProjectRef BEFORE the fallible temp-role/connection step — so a failed + // linked resolve must not skip the post-run linked-project cache write. + const { layer, linkedCache } = setup(tmp.current, { + toml: 'project_id = "test"\n', + resolveFails: true, + }); return Effect.gen(function* () { - yield* legacyDbReset({ ...baseFlags, noSeed: true }); - expect(calls).toEqual([["db", "reset", "--no-seed"]]); - }).pipe(Effect.provide(layer)); + const exit = yield* legacyDbReset({ ...DEFAULT_FLAGS, linked: true }).pipe( + Effect.provide(layer), + Effect.exit, + ); + expect(Exit.isFailure(exit)).toBe(true); + expect(linkedCache.cached).toBe(true); + expect(linkedCache.cachedRef).toBe(LEGACY_VALID_REF); + }); }); - it.live("forwards a single --sql-paths flag", () => { - const { layer, calls } = setupLegacyDbReset(); + it.live("resets to a specific version, applying only migrations up to it", () => { + const { layer, out, conn } = setup(tmp.current, { + toml: 'project_id = "test"\n', + files: { + ...migrationFile("20240101000000"), + ...migrationFile("20240202000000"), + }, + confirm: [true], + }); return Effect.gen(function* () { yield* legacyDbReset({ - ...baseFlags, - sqlPaths: ["./seeds/base.sql"], + ...DEFAULT_FLAGS, + linked: true, + version: Option.some("20240101000000"), + }).pipe(Effect.provide(layer)); + expect(out.stderrText).toContain("Resetting remote database to version: 20240101000000"); + expect(out.stderrText).toContain("Applying migration 20240101000000_test.sql..."); + expect(out.stderrText).not.toContain("Applying migration 20240202000000_test.sql..."); + expect(conn).toBeDefined(); + }); + }); + + it.live("resolves --last to a version prefix", () => { + const { layer, out } = setup(tmp.current, { + toml: 'project_id = "test"\n', + files: { + ...migrationFile("20240101000000"), + ...migrationFile("20240202000000"), + }, + confirm: [true], + }); + return Effect.gen(function* () { + // last=1 → revert the most recent → reset to version 20240101000000. + yield* legacyDbReset({ ...DEFAULT_FLAGS, linked: true, last: Option.some(1) }).pipe( + Effect.provide(layer), + ); + expect(out.stderrText).toContain("Resetting remote database to version: 20240101000000"); + }); + }); + + it.live("reverts all migrations when --last covers the full history", () => { + const { layer, out } = setup(tmp.current, { + toml: 'project_id = "test"\n', + files: { ...migrationFile("20240101000000"), ...migrationFile("20240202000000") }, + confirm: [true], + }); + return Effect.gen(function* () { + // last=2 with 2 local migrations → revert all → version "-". + yield* legacyDbReset({ ...DEFAULT_FLAGS, linked: true, last: Option.some(2) }).pipe( + Effect.provide(layer), + ); + expect(out.stderrText).toContain("Resetting remote database to version: -"); + }); + }); + + it.live("skips seeding with --no-seed", () => { + const { layer, out } = setup(tmp.current, { + toml: 'project_id = "test"\n', + files: { + ...migrationFile("20240101000000"), + "supabase/seed.sql": "insert into t values (1);", + }, + confirm: [true], + }); + return Effect.gen(function* () { + yield* legacyDbReset({ ...DEFAULT_FLAGS, linked: true, noSeed: true }).pipe( + Effect.provide(layer), + ); + expect(out.stderrText).not.toContain("Seeding data from"); + }); + }); + + it.live("delegates an experimental remote reset to the Go binary", () => { + const { layer, proxy } = setup(tmp.current, { + toml: 'project_id = "test"\n', + experimental: true, + }); + return Effect.gen(function* () { + yield* legacyDbReset({ ...DEFAULT_FLAGS, linked: true }).pipe(Effect.provide(layer)); + expect(proxy.calls).toHaveLength(1); + expect(proxy.calls[0]!.args).toEqual(["db", "reset", "--linked", "--yes=false"]); + expect(proxy.calls[0]!.env).toEqual({ SUPABASE_TELEMETRY_DISABLED: "1" }); + }); + }); + + it.live("forwards the linked selector to the delegate even for --linked=false", () => { + // Cobra `Changed` semantics: `--linked=false` still selects the linked/remote target in + // the parent, so the delegated argv must carry `--linked` — otherwise the Go child falls + // back to its local default and resets the wrong database. + const { layer, proxy } = setup(tmp.current, { + toml: 'project_id = "test"\n', + experimental: true, + args: ["db", "reset", "--linked=false"], + }); + return Effect.gen(function* () { + yield* legacyDbReset({ ...DEFAULT_FLAGS, linked: false }).pipe(Effect.provide(layer)); + expect(proxy.calls).toHaveLength(1); + expect(proxy.calls[0]!.args).toEqual(["db", "reset", "--linked", "--yes=false"]); + }); + }); + + it.live("forwards --yes=false to the delegate even when SUPABASE_YES is set", () => { + // Explicit `--yes=false` beats `AutomaticEnv` in Go; the delegated child must receive the + // bound false flag so an inherited `SUPABASE_YES=true` doesn't auto-confirm the reset and + // drop the remote schemas the user tried to protect. + const previous = process.env["SUPABASE_YES"]; + process.env["SUPABASE_YES"] = "true"; + const { layer, proxy } = setup(tmp.current, { + toml: 'project_id = "test"\n', + experimental: true, + args: ["db", "reset", "--linked", "--yes=false"], + }); + return Effect.gen(function* () { + yield* legacyDbReset({ ...DEFAULT_FLAGS, linked: true }).pipe(Effect.provide(layer)); + expect(proxy.calls[0]!.args).toContain("--yes=false"); + }).pipe( + Effect.ensuring( + Effect.sync(() => { + if (previous === undefined) delete process.env["SUPABASE_YES"]; + else process.env["SUPABASE_YES"] = previous; + }), + ), + ); + }); + + it.live("forwards --yes=true to the delegate when --yes is set", () => { + const { layer, proxy } = setup(tmp.current, { + toml: 'project_id = "test"\n', + experimental: true, + args: ["db", "reset", "--linked", "--yes"], + yes: true, + }); + return Effect.gen(function* () { + yield* legacyDbReset({ ...DEFAULT_FLAGS, linked: true }).pipe(Effect.provide(layer)); + expect(proxy.calls[0]!.args).toContain("--yes=true"); + }); + }); + + it.live( + "takes the experimental delegate path via SUPABASE_EXPERIMENTAL in the project .env", + () => { + // Go loads nested env before reset.Run reads viper EXPERIMENTAL, so the versionless remote + // reset delegates to the Go binary rather than replaying migrations natively. + const previous = process.env["SUPABASE_EXPERIMENTAL"]; + delete process.env["SUPABASE_EXPERIMENTAL"]; + const { layer, proxy, conn } = setup(tmp.current, { + toml: 'project_id = "test"\n', + files: { "supabase/.env": "SUPABASE_EXPERIMENTAL=true\n" }, + // No experimental flag / shell env — only the project .env sets it. }); - expect(calls).toEqual([["db", "reset", "--sql-paths", "./seeds/base.sql"]]); - }).pipe(Effect.provide(layer)); + return Effect.gen(function* () { + yield* legacyDbReset({ ...DEFAULT_FLAGS, linked: true }).pipe(Effect.provide(layer)); + expect(proxy.calls).toHaveLength(1); + // Delegated, so the native remote path never dropped schemas. + expect(conn.execs.some((s) => s.includes("drop schema if exists"))).toBe(false); + }).pipe( + Effect.ensuring( + Effect.sync(() => { + if (previous === undefined) delete process.env["SUPABASE_EXPERIMENTAL"]; + else process.env["SUPABASE_EXPERIMENTAL"] = previous; + }), + ), + ); + }, + ); + + it.live("attaches the Go seed-flag conflict suggestion to --no-seed + --sql-paths", () => { + const { layer } = setup(tmp.current, { toml: 'project_id = "test"\n' }); + return Effect.gen(function* () { + const exit = yield* legacyDbReset({ + ...DEFAULT_FLAGS, + noSeed: true, + sqlPaths: ["seed.sql"], + }).pipe(Effect.provide(layer), Effect.exit); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(JSON.stringify(exit.cause)).toContain("--no-seed cannot be used with --sql-paths"); + // Go's validateDbResetSeedFlags CmdSuggestion, rendered as a Suggestion: line. + expect(JSON.stringify(exit.cause)).toContain("Use either"); + } + }); }); - it.live("forwards repeated --sql-paths flags in order", () => { - const { layer, calls } = setupLegacyDbReset(); + it.live("forwards --db-url and --no-seed on an experimental remote db-url reset", () => { + const { layer, proxy } = setup(tmp.current, { + toml: 'project_id = "test"\n', + experimental: true, + args: ["db", "reset", "--db-url", "postgresql://db.example.com:5432/postgres"], + }); return Effect.gen(function* () { yield* legacyDbReset({ - ...baseFlags, - sqlPaths: ["./seeds/base.sql", "./seeds/demo/*.sql"], - }); - expect(calls).toEqual([ - ["db", "reset", "--sql-paths", "./seeds/base.sql", "--sql-paths", "./seeds/demo/*.sql"], + ...DEFAULT_FLAGS, + dbUrl: Option.some("postgresql://db.example.com:5432/postgres"), + noSeed: true, + }).pipe(Effect.provide(layer)); + expect(proxy.calls[0]!.args).toEqual([ + "db", + "reset", + "--db-url", + "postgresql://db.example.com:5432/postgres", + "--no-seed", + "--yes=false", ]); - }).pipe(Effect.provide(layer)); + }); }); - it.live("forwards --no-seed with --sql-paths so Go owns the diagnostic", () => { - const { layer, calls } = setupLegacyDbReset(); + it.live("passes --no-seed and the resolved --last version to the recreate seam", () => { + const { layer, seam } = setup(tmp.current, { + toml: 'project_id = "test"\n', + files: { ...migrationFile("20240101000000"), ...migrationFile("20240202000000") }, + args: ["db", "reset", "--local"], + isLocal: true, + running: true, + }); return Effect.gen(function* () { + // last=1 with 2 local migrations → recreate up to version 20240101000000. yield* legacyDbReset({ - ...baseFlags, + ...DEFAULT_FLAGS, + local: true, noSeed: true, - sqlPaths: ["./seeds/base.sql"], - }); - expect(calls).toEqual([["db", "reset", "--no-seed", "--sql-paths", "./seeds/base.sql"]]); - }).pipe(Effect.provide(layer)); + last: Option.some(1), + }).pipe(Effect.provide(layer)); + expect(seam.recreateCalls).toEqual([ + { version: "20240101000000", noSeed: true, sqlPaths: [] }, + ]); + }); }); - it.live("forwards an empty --sql-paths value so Go owns the diagnostic", () => { - const { layer, calls } = setupLegacyDbReset(); + it.live("recreates to a specific --version on a local db-url reset", () => { + const { layer, out, seam } = setup(tmp.current, { + toml: 'project_id = "test"\n', + files: migrationFile("20240101000000"), + args: ["db", "reset", "--db-url", "postgresql://localhost:54322/postgres"], + isLocal: true, + running: true, + }); return Effect.gen(function* () { yield* legacyDbReset({ - ...baseFlags, + ...DEFAULT_FLAGS, + dbUrl: Option.some("postgresql://localhost:54322/postgres"), + version: Option.some("20240101000000"), + }).pipe(Effect.provide(layer)); + expect(out.stderrText).toContain("Resetting local database to version: 20240101000000"); + expect(seam.recreateCalls).toEqual([ + { version: "20240101000000", noSeed: false, sqlPaths: [] }, + ]); + }); + }); + + it.live("resets a remote --db-url target without loading a remote config override", () => { + const { layer, out, conn } = setup(tmp.current, { + // No config file → embedded defaults (migrations + seed enabled). + files: migrationFile("20240101000000"), + args: ["db", "reset", "--db-url", "postgresql://db.example.com:5432/postgres"], + isLocal: false, + omitRef: true, + confirm: [true], + }); + return Effect.gen(function* () { + yield* legacyDbReset({ + ...DEFAULT_FLAGS, + dbUrl: Option.some("postgresql://db.example.com:5432/postgres"), + }).pipe(Effect.provide(layer)); + expect(out.stderrText).toContain("Resetting remote database..."); + expect(conn.execs.some((s) => s.includes("drop schema if exists"))).toBe(true); + }); + }); + + it.live("announces a matching [remotes.*] override", () => { + const { layer, out } = setup(tmp.current, { + toml: `project_id = "base"\n\n[remotes.preview]\nproject_id = "${LEGACY_VALID_REF}"\n`, + confirm: [true], + ref: LEGACY_VALID_REF, + }); + return Effect.gen(function* () { + yield* legacyDbReset({ ...DEFAULT_FLAGS, linked: true }).pipe(Effect.provide(layer)); + expect(out.stderrText).toContain("Loading config override: [remotes.preview]"); + }); + }); + + it.live("skips migrations and seed when both are disabled in config", () => { + const { layer, out, conn } = setup(tmp.current, { + toml: 'project_id = "test"\n\n[db.migrations]\nenabled = false\n\n[db.seed]\nenabled = false\n', + files: { + ...migrationFile("20240101000000"), + "supabase/seed.sql": "insert into t values (1);", + }, + confirm: [true], + }); + return Effect.gen(function* () { + yield* legacyDbReset({ ...DEFAULT_FLAGS, linked: true }).pipe(Effect.provide(layer)); + // Schemas are still dropped, but nothing is applied or seeded. + expect(conn.execs.some((s) => s.includes("drop schema if exists"))).toBe(true); + expect(out.stderrText).not.toContain("Applying migration"); + expect(out.stderrText).not.toContain("Seeding data from"); + }); + }); + + it.live("emits a json result for a confirmed remote reset (--yes)", () => { + const { layer, out } = setup(tmp.current, { + toml: 'project_id = "test"\n', + files: migrationFile("20240101000000"), + format: "json", + yes: true, + }); + return Effect.gen(function* () { + yield* legacyDbReset({ ...DEFAULT_FLAGS, linked: true }).pipe(Effect.provide(layer)); + const success = out.messages.find((m) => m.type === "success"); + expect(success?.data?.["target"]).toBe("remote"); + }); + }); + + it.live("emits a json result for a confirmed remote reset", () => { + const { layer, out } = setup(tmp.current, { + toml: 'project_id = "test"\n', + files: migrationFile("20240101000000"), + format: "json", + }); + return Effect.gen(function* () { + // json mode is non-interactive → prompt takes the default (false) → cancel. + const exit = yield* legacyDbReset({ ...DEFAULT_FLAGS, linked: true }).pipe( + Effect.provide(layer), + Effect.exit, + ); + // default-false prompt in non-text mode declines → context canceled. + expect(Exit.isFailure(exit)).toBe(true); + expect(out).toBeDefined(); + }); + }); + + it.live("rejects --no-seed together with --sql-paths", () => { + const { layer } = setup(tmp.current, { toml: 'project_id = "test"\n' }); + return Effect.gen(function* () { + const exit = yield* legacyDbReset({ + ...DEFAULT_FLAGS, + linked: true, + noSeed: true, + sqlPaths: ["seed.sql"], + }).pipe(Effect.provide(layer), Effect.exit); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(JSON.stringify(exit.cause)).toContain("--no-seed cannot be used with --sql-paths"); + } + }); + }); + + it.live("rejects an empty --sql-paths value", () => { + const { layer } = setup(tmp.current, { toml: 'project_id = "test"\n' }); + return Effect.gen(function* () { + const exit = yield* legacyDbReset({ + ...DEFAULT_FLAGS, + linked: true, sqlPaths: [""], - }); - expect(calls).toEqual([["db", "reset", "--sql-paths", ""]]); - }).pipe(Effect.provide(layer)); - }); - - it("parses repeated --sql-paths flags from the command surface", async () => { - const { layer, calls } = setupLegacyDbReset(); - await Effect.runPromise( - Effect.scoped( - Effect.gen(function* () { - yield* Command.runWith(legacyDbResetCommand, { version: "0.0.0-test" })([ - "--sql-paths", - "./seeds/base.sql", - "--sql-paths", - "./seeds/demo/*.sql", - ]); - expect(calls).toEqual([ - ["db", "reset", "--sql-paths", "./seeds/base.sql", "--sql-paths", "./seeds/demo/*.sql"], - ]); - }), - ).pipe( - Effect.provide( - Layer.mergeAll( - layer, - mockOutput({ format: "text" }).layer, - CliOutput.layer(textCliOutputFormatter()), - ), - ), - ) as Effect.Effect, - ); + }).pipe(Effect.provide(layer), Effect.exit); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(JSON.stringify(exit.cause)).toContain( + "--sql-paths requires a non-empty path or glob pattern", + ); + } + }); }); - it("forwards mutually exclusive seed flags from the command surface", async () => { - const { layer, calls } = setupLegacyDbReset(); - await Effect.runPromise( - Effect.scoped( - Effect.gen(function* () { - yield* Command.runWith(legacyDbResetCommand, { version: "0.0.0-test" })([ - "--no-seed", - "--sql-paths", - "./seeds/base.sql", - ]); - expect(calls).toEqual([["db", "reset", "--no-seed", "--sql-paths", "./seeds/base.sql"]]); - }), - ).pipe( - Effect.provide( - Layer.mergeAll( - layer, - mockOutput({ format: "text" }).layer, - CliOutput.layer(textCliOutputFormatter()), - ), - ), - ) as Effect.Effect, - ); + it.live("rejects a negative --last value", () => { + const { layer } = setup(tmp.current, { toml: 'project_id = "test"\n' }); + return Effect.gen(function* () { + const exit = yield* legacyDbReset({ + ...DEFAULT_FLAGS, + linked: true, + last: Option.some(-1), + }).pipe(Effect.provide(layer), Effect.exit); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + const cause = JSON.stringify(exit.cause); + expect(cause).toContain("invalid argument"); + expect(cause).toContain("strconv.ParseUint"); + } + }); + }); + + it.live("seeds an absolute --sql-paths file on a remote reset", () => { + const absSeed = join(tmp.current, "external-seed.sql"); + writeFileSync(absSeed, "insert into t values (3);"); + const { layer, out } = setup(tmp.current, { + toml: 'project_id = "test"\n', + files: migrationFile("20240101000000"), + confirm: [true], + }); + return Effect.gen(function* () { + yield* legacyDbReset({ + ...DEFAULT_FLAGS, + linked: true, + sqlPaths: [absSeed], + }).pipe(Effect.provide(layer)); + // Absolute paths are preserved (not prefixed with supabase/) and seeded. + expect(out.stderrText).toContain(`Seeding data from ${absSeed}...`); + }); + }); + + it.live("warns and seeds from --sql-paths overriding config on a remote reset", () => { + const { layer, out } = setup(tmp.current, { + // Seed disabled in config — --sql-paths must force-enable it. + toml: 'project_id = "test"\n\n[db.seed]\nenabled = false\n', + files: { + ...migrationFile("20240101000000"), + "supabase/custom-seed.sql": "insert into t values (2);", + }, + confirm: [true], + }); + return Effect.gen(function* () { + yield* legacyDbReset({ + ...DEFAULT_FLAGS, + linked: true, + sqlPaths: ["custom-seed.sql"], + }).pipe(Effect.provide(layer)); + expect(out.stderrText).toContain("--sql-paths overrides [db.seed].sql_paths"); + expect(out.stderrText).toContain("Seeding data from supabase/custom-seed.sql..."); + }); + }); + + it.live("forwards --sql-paths to the recreate seam on a local reset", () => { + const { layer, seam } = setup(tmp.current, { + toml: 'project_id = "test"\n', + args: ["db", "reset", "--local"], + isLocal: true, + running: true, + }); + return Effect.gen(function* () { + yield* legacyDbReset({ + ...DEFAULT_FLAGS, + local: true, + sqlPaths: ["custom-seed.sql", "demo/*.sql"], + }).pipe(Effect.provide(layer)); + expect(seam.recreateCalls).toEqual([ + { version: "", noSeed: false, sqlPaths: ["custom-seed.sql", "demo/*.sql"] }, + ]); + }); + }); + + it.live("forwards --sql-paths to the Go binary on an experimental remote reset", () => { + const { layer, proxy } = setup(tmp.current, { + toml: 'project_id = "test"\n', + experimental: true, + }); + return Effect.gen(function* () { + yield* legacyDbReset({ + ...DEFAULT_FLAGS, + linked: true, + sqlPaths: ["custom-seed.sql"], + }).pipe(Effect.provide(layer)); + expect(proxy.calls[0]!.args).toEqual([ + "db", + "reset", + "--linked", + "--sql-paths", + "custom-seed.sql", + "--yes=false", + ]); + }); }); }); diff --git a/apps/cli/src/legacy/commands/db/reset/reset.layers.ts b/apps/cli/src/legacy/commands/db/reset/reset.layers.ts new file mode 100644 index 0000000000..7cf648f3fe --- /dev/null +++ b/apps/cli/src/legacy/commands/db/reset/reset.layers.ts @@ -0,0 +1,80 @@ +import { Layer } from "effect"; + +import { commandRuntimeLayer } from "../../../../shared/runtime/command-runtime.layer.ts"; +import { legacyCredentialsLayer } from "../../../auth/legacy-credentials.layer.ts"; +import { legacyHttpClientLayer } from "../../../auth/legacy-http-debug.layer.ts"; +import { legacyPlatformApiFactoryLayer } from "../../../auth/legacy-platform-api-factory.layer.ts"; +import { legacyCliConfigLayer } from "../../../config/legacy-cli-config.layer.ts"; +import { legacyProjectRefLayer } from "../../../config/legacy-project-ref.layer.ts"; +import { legacyDbConfigLayer } from "../../../shared/legacy-db-config.layer.ts"; +import { legacyDbConnectionLayer } from "../../../shared/legacy-db-connection.layer.ts"; +import { legacyDebugLoggerLayer } from "../../../shared/legacy-debug-logger.layer.ts"; +import { stdinLayer } from "../../../../shared/runtime/stdin.layer.ts"; +import { legacyIdentityStitchLayer } from "../../../shared/legacy-identity-stitch.ts"; +import { legacyLinkedProjectCacheLayer } from "../../../telemetry/legacy-linked-project-cache.layer.ts"; +import { legacyTelemetryStateLayer } from "../../../telemetry/legacy-telemetry-state.layer.ts"; +import { legacyDbBootstrapSeamLayer } from "../shared/legacy-db-bootstrap.seam.layer.ts"; + +/** + * Runtime layer for `supabase db reset`. Same composition as `db push` / `db lint`: + * the Postgres connection, the db-config resolver, project-ref resolution, and the + * linked-project cache, all over the lazy management-API factory so the local / + * `--db-url` paths never resolve an access token at layer-build time. `LegacyGoProxy` + * (used to delegate the local / experimental reset paths) is ambient from the root. + */ +const cliConfig = legacyCliConfigLayer.pipe(Layer.provide(legacyDebugLoggerLayer)); +const httpClient = legacyHttpClientLayer.pipe(Layer.provide(legacyDebugLoggerLayer)); +const credentials = legacyCredentialsLayer.pipe( + Layer.provide(cliConfig), + Layer.provide(legacyDebugLoggerLayer), +); + +const platformApiFactory = legacyPlatformApiFactoryLayer.pipe( + Layer.provide(credentials), + Layer.provide(cliConfig), + Layer.provide(legacyDebugLoggerLayer), + Layer.provide(legacyIdentityStitchLayer), +); + +const projectRef = legacyProjectRefLayer.pipe( + Layer.provide(platformApiFactory), + Layer.provide(cliConfig), +); + +const linkedProjectCache = legacyLinkedProjectCacheLayer.pipe( + Layer.provide(credentials), + Layer.provide(cliConfig), + Layer.provide(httpClient), + Layer.provide(legacyIdentityStitchLayer), +); + +const dbConfig = legacyDbConfigLayer.pipe( + Layer.provide(cliConfig), + Layer.provide(legacyDbConnectionLayer), + Layer.provide(legacyDebugLoggerLayer), + Layer.provide(legacyIdentityStitchLayer), +); + +export const legacyDbResetRuntimeLayer = Layer.mergeAll( + dbConfig, + legacyDbConnectionLayer, + cliConfig, + httpClient, + credentials, + projectRef, + // Exposed (not just provided to `projectRef`) because the local reset path reuses + // the seed-buckets core, whose `legacyResolveStorageCredentials` requires the + // (lazy) Management-API factory for the linked branch — never hit on `--local`, + // but a static service requirement of the shared core. + platformApiFactory, + linkedProjectCache, + legacyIdentityStitchLayer, + legacyTelemetryStateLayer, + // `legacyPromptYesNo`'s non-TTY branch reads the piped answer via `Stdin` (Go's + // `console.ReadLine`); without it a CI/piped remote `db reset` that reaches the + // confirmation prompt fails with a missing-service defect instead of the default. + stdinLayer, + // Container-recreate / storage-health primitives for the native local reset. + legacyDbBootstrapSeamLayer.pipe(Layer.provide(cliConfig)), + commandRuntimeLayer(["db", "reset"]), +); diff --git a/apps/cli/src/legacy/commands/db/shared/legacy-db-bootstrap.errors.ts b/apps/cli/src/legacy/commands/db/shared/legacy-db-bootstrap.errors.ts new file mode 100644 index 0000000000..673e78a466 --- /dev/null +++ b/apps/cli/src/legacy/commands/db/shared/legacy-db-bootstrap.errors.ts @@ -0,0 +1,20 @@ +import { Data } from "effect"; + +/** + * Driving the bundled Go binary's hidden `db __db-bootstrap` seam failed — the + * container-lifecycle primitives that back native `db start` / `db reset --local` + * (create/recreate the local Postgres container, apply the initial schema, the + * storage health gate) are not yet ported to TypeScript. Wraps a failed inspect, + * a missing `supabase-go` binary, or a non-zero seam exit. The seam tees its own + * progress to stderr, so this message is the fallback shown when the subprocess + * dies without surfacing a more specific Go error. + */ +export class LegacyDbBootstrapError extends Data.TaggedError("LegacyDbBootstrapError")<{ + readonly message: string; + /** + * Optional actionable hint rendered as a separate "Suggestion:" line, mirroring + * Go's `utils.CmdSuggestion` — set to the Docker-install hint when the container + * runtime's daemon is unreachable (`AssertServiceIsRunning`, `misc.go:148-154`). + */ + readonly suggestion?: string; +}> {} diff --git a/apps/cli/src/legacy/commands/db/shared/legacy-db-bootstrap.seam.layer.ts b/apps/cli/src/legacy/commands/db/shared/legacy-db-bootstrap.seam.layer.ts new file mode 100644 index 0000000000..bcf5735978 --- /dev/null +++ b/apps/cli/src/legacy/commands/db/shared/legacy-db-bootstrap.seam.layer.ts @@ -0,0 +1,241 @@ +import { Effect, FileSystem, Layer, Option, Path, Stream } from "effect"; +import * as ChildProcess from "effect/unstable/process/ChildProcess"; +import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner"; + +import { + LegacyNetworkIdFlag, + LegacyProfileFlag, + legacyResolveExperimental, +} from "../../../../shared/legacy/global-flags.ts"; +import { resolveBinary } from "../../../../shared/legacy/go-proxy.layer.ts"; +import { ProcessControl } from "../../../../shared/runtime/process-control.service.ts"; +import { LegacyCliConfig } from "../../../config/legacy-cli-config.service.ts"; +import { spawnContainerCli } from "../../../shared/legacy-container-cli.ts"; +import { legacyReadDbToml } from "../../../shared/legacy-db-config.toml-read.ts"; +import { + legacyResolveLocalProjectId, + localDbContainerId, +} from "../../../shared/legacy-docker-ids.ts"; +import { + LEGACY_SUGGEST_DOCKER_INSTALL, + legacyIsDockerDaemonUnreachable, +} from "../../../shared/legacy-docker-suggest.ts"; +import { LegacyDbBootstrapError } from "./legacy-db-bootstrap.errors.ts"; +import { LegacyDbBootstrapSeam } from "./legacy-db-bootstrap.seam.service.ts"; + +const seamFailure = (message: string) => new LegacyDbBootstrapError({ message }); + +const decodeChunks = (chunks: ReadonlyArray): string => { + const total = chunks.reduce((size, chunk) => size + chunk.length, 0); + const bytes = new Uint8Array(total); + let offset = 0; + for (const chunk of chunks) { + bytes.set(chunk, offset); + offset += chunk.length; + } + return new TextDecoder().decode(bytes); +}; + +/** + * Real {@link LegacyDbBootstrapSeam}: drives the bundled `supabase-go`'s hidden + * `db __db-bootstrap --mode ` command. The binary is resolved exactly like + * `LegacyGoProxy` (`resolveBinary`); the child's telemetry is disabled and its + * progress teed to stderr, matching the `db __shadow` seam. `--network-id` and a + * flag-selected `--profile` are forwarded so the spawned containers land on the + * same network and the child re-runs Go's identical config resolution. + */ +export const legacyDbBootstrapSeamLayer = Layer.effect( + LegacyDbBootstrapSeam, + Effect.gen(function* () { + const cliConfig = yield* LegacyCliConfig; + const networkId = yield* LegacyNetworkIdFlag; + const profile = yield* LegacyProfileFlag; + const profileArgs = profile !== "supabase" ? ["--profile", profile] : []; + const networkArgs = Option.isSome(networkId) ? ["--network-id", networkId.value] : []; + // Forward `--experimental` (env-aware) so the seam's `SetupLocalDatabase` / + // `apply.MigrateAndSeed` takes Go's experimental schema-file path on a + // versionless reset/start, matching `viper.GetBool("EXPERIMENTAL")`. + const experimental = yield* legacyResolveExperimental; + const experimentalArgs = experimental ? ["--experimental"] : []; + const spawner = yield* ChildProcessSpawner; + const processControl = yield* ProcessControl; + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const resolved = resolveBinary(); + + /** + * Run `db __db-bootstrap` with the given mode args. `captureStdout` pipes + * stdout (for the `await-storage` marker); otherwise stdout is inherited. + * Returns the captured stdout (empty when inherited). + */ + const runBootstrap = (modeArgs: ReadonlyArray, captureStdout: boolean) => + Effect.scoped( + Effect.gen(function* () { + if (!("found" in resolved)) { + return yield* Effect.fail( + seamFailure( + "Could not find the supabase-go binary required to bootstrap the local database.", + ), + ); + } + // `runCli` treats `db start`/`db reset` as self-managed and installs no + // global signal handler, and this direct child spawn (unlike + // `LegacyGoProxy.exec`) inherits the foreground process group. Hold + // SIGINT/SIGTERM/SIGHUP with no-op listeners so an interactive Ctrl-C + // during container startup/restore does not default-terminate the TS + // parent out from under the Go child's docker-cleanup path — the parent + // stays blocked on the child's exit and propagates its real status. + // Scoped, so the listeners are removed on completion/failure/interrupt. + yield* processControl.holdSignals(["SIGINT", "SIGTERM", "SIGHUP"]); + const args = [ + "db", + "__db-bootstrap", + ...modeArgs, + ...networkArgs, + ...profileArgs, + ...experimentalArgs, + ]; + const command = ChildProcess.make(resolved.found, args, { + cwd: cliConfig.workdir, + stdin: "inherit", + stdout: captureStdout ? "pipe" : "inherit", + stderr: "inherit", + extendEnv: true, + // Disable the child's telemetry so the hidden seam never records its + // own `cli_command_executed` on top of the user's TS command, matching + // the `db __shadow` seam and the explicit LegacyGoProxy delegates. + env: { SUPABASE_TELEMETRY_DISABLED: "1" }, + detached: false, + }); + if (!captureStdout) { + const exitCode = yield* spawner + .exitCode(command) + .pipe(Effect.mapError(() => seamFailure("failed to run supabase-go."))); + if (exitCode !== 0) { + // Fail (rather than `processControl.exit`) so the handler's finalizers — + // `Effect.ensuring(telemetryState.flush)` + the legacy command + // instrumentation — still run; an immediate `process.exit` here would + // skip them. Go likewise exits non-zero on a bootstrap error only after + // its `PersistentPostRun`. The child's detailed failure is already on the + // inherited stderr. (Preserving the child's *exact* exit code while still + // running finalizers would require a shared `runCli` change — deferred.) + return yield* Effect.fail( + seamFailure(`failed to bootstrap the local database: exit ${exitCode}`), + ); + } + return ""; + } + const handle = yield* spawner + .spawn(command) + .pipe(Effect.mapError(() => seamFailure("failed to run supabase-go."))); + const chunks: Array = []; + yield* Stream.runForEach(handle.stdout, (chunk) => + Effect.sync(() => { + chunks.push(chunk); + }), + ).pipe(Effect.mapError(() => seamFailure("failed to bootstrap the local database."))); + const exitCode = yield* handle.exitCode.pipe( + Effect.mapError(() => seamFailure("failed to bootstrap the local database.")), + ); + if (exitCode !== 0) { + return yield* Effect.fail( + seamFailure(`failed to bootstrap the local database: exit ${exitCode}`), + ); + } + return decodeChunks(chunks); + }), + ); + + return LegacyDbBootstrapSeam.of({ + isDbRunning: () => + Effect.scoped( + Effect.gen(function* () { + // Resolve `utils.DbId` exactly as Go does (env → config.toml → workdir + // basename); the config.toml read is best-effort (`validate: false`) since + // the handler has already run Go's `LoadConfig` validation — an invalid + // config would have failed there, so here we only want the `projectId` and + // tolerate a fallback to the workdir basename rather than re-throwing. + const tomlProjectId = yield* legacyReadDbToml(fs, path, cliConfig.workdir, undefined, { + validate: false, + }).pipe( + Effect.map((toml) => toml.projectId), + // The lenient read still surfaces a genuinely unreadable/malformed project + // `.env`; fall back to the workdir basename in that case rather than failing + // the running-check (the handler has already validated config). + Effect.orElseSucceed(() => Option.none()), + ); + const projectId = legacyResolveLocalProjectId( + Option.getOrUndefined(cliConfig.projectId), + Option.getOrUndefined(tomlProjectId), + cliConfig.workdir, + ); + const containerId = localDbContainerId(projectId); + // Go's AssertSupabaseDbIsRunning = ContainerInspect → NotFound ⇒ not + // running. Discard stdout (the inspect JSON) so the unconsumed pipe can + // never deadlock; only the exit code + stderr matter. + const child = yield* spawnContainerCli(spawner, ["container", "inspect", containerId], { + stdin: "ignore", + stdout: "ignore", + stderr: "pipe", + extendEnv: true, + }).pipe(Effect.mapError(() => seamFailure("failed to inspect service"))); + const stderrChunks: Array = []; + yield* Stream.runForEach(child.stderr, (chunk) => + Effect.sync(() => { + stderrChunks.push(chunk); + }), + ).pipe(Effect.mapError(() => seamFailure("failed to inspect service"))); + const inspectExit = yield* child.exitCode.pipe( + Effect.map(Number), + Effect.mapError(() => seamFailure("failed to inspect service")), + ); + if (inspectExit === 0) return true; // container exists ⇒ running + + const stderr = decodeChunks(stderrChunks).trim(); + // Only a missing container means "not running". Docker reports this as + // either "No such container" or "No such object" depending on daemon + // version/CLI path (the same pair handled in `shared/functions/serve.ts`). + // Any other inspect failure (e.g. the Docker daemon is down) propagates, + // matching Go's `AssertSupabaseDbIsRunning`. + if (!stderr.includes("No such container") && !stderr.includes("No such object")) { + // Go's `AssertServiceIsRunning` sets `CmdSuggestion = suggestDockerInstall` + // on a daemon-connection failure (`misc.go:148-154`), so a down daemon + // still surfaces the actionable Docker Desktop hint, not just raw stderr. + return yield* Effect.fail( + new LegacyDbBootstrapError({ + message: + stderr.length > 0 + ? `failed to inspect service: ${stderr}` + : "failed to inspect service", + ...(legacyIsDockerDaemonUnreachable(stderr) + ? { suggestion: LEGACY_SUGGEST_DOCKER_INSTALL } + : {}), + }), + ); + } + return false; + }), + ), + startDatabase: ({ fromBackup }) => + runBootstrap( + ["--mode", "start", ...(fromBackup !== undefined ? ["--from-backup", fromBackup] : [])], + false, + ).pipe(Effect.asVoid), + recreateDatabase: ({ version, noSeed, sqlPaths }) => + runBootstrap( + [ + "--mode", + "recreate", + ...(version !== "" ? ["--version", version] : []), + ...(noSeed ? ["--no-seed"] : []), + ...sqlPaths.flatMap((p) => ["--sql-paths", p]), + ], + false, + ).pipe(Effect.asVoid), + awaitStorageReady: () => + runBootstrap(["--mode", "await-storage"], true).pipe( + Effect.map((stdout) => stdout.trim() === "ready"), + ), + }); + }), +); diff --git a/apps/cli/src/legacy/commands/db/shared/legacy-db-bootstrap.seam.service.ts b/apps/cli/src/legacy/commands/db/shared/legacy-db-bootstrap.seam.service.ts new file mode 100644 index 0000000000..4c29ddb67a --- /dev/null +++ b/apps/cli/src/legacy/commands/db/shared/legacy-db-bootstrap.seam.service.ts @@ -0,0 +1,68 @@ +import { Context, type Effect } from "effect"; + +import type { LegacyDbBootstrapError } from "./legacy-db-bootstrap.errors.ts"; + +/** + * Seam over the bundled Go binary's hidden `db __db-bootstrap` command, exposing + * the container-bootstrap primitives that native `db start` / `db reset --local` + * still need but that are not ported to TypeScript: the local-stack "is running?" + * probe, the database container create/recreate flows, and the storage health gate + * before bucket seeding. The TS handlers orchestrate everything else (user-facing + * messages, version resolution, bucket seeding, the git-branch line, telemetry, + * and `--output-format` shaping); only the Docker lifecycle lives behind here. + * + * Mirrors {@link LegacyDeclarativeSeam} (`db __shadow`): each method shells out to + * the same resolved `supabase-go`, with the child's telemetry disabled so the + * hidden seam never double-counts the user's command, and its progress teed to + * stderr. + */ +interface LegacyDbBootstrapSeamShape { + /** + * Go's `utils.AssertSupabaseDbIsRunning` (`internal/utils/misc.go:144`): inspect + * the local Postgres container. `true` when it exists (the stack is up), `false` + * when Docker reports "No such container" (Go's `ErrNotRunning`). Any other + * inspect failure (e.g. the Docker daemon is unreachable) fails with + * {@link LegacyDbBootstrapError}, matching Go, which returns the wrapped inspect + * error rather than treating the database as stopped. + */ + readonly isDbRunning: () => Effect.Effect; + /** + * `db start`'s container bootstrap — `start.StartDatabase(fromBackup)` plus Go's + * `DockerRemoveAll` cleanup on failure (`internal/db/start/start.go:54-60`): + * create the Postgres container, wait for health, apply the initial schema + + * roles + migrations + seed on a fresh volume, and write `_current_branch`. + * Progress (`Starting database...`, `Initialising schema...`) is teed to stderr. + */ + readonly startDatabase: (opts: { + readonly fromBackup?: string; + }) => Effect.Effect; + /** + * The PG14/PG15 container-recreate half of local `db reset` + * (`reset.RecreateLocalDatabase`): recreate the db container/volume, init schema, + * migrate + seed up to `version`, and restart the satellite containers. The + * caller has already printed `Resetting local database…`; the seam tees the + * remaining progress (`Recreating database...`, `Restarting containers...`) to + * stderr. `version` is the resolved migration version ("" for all migrations); + * `noSeed` disables the seed and `sqlPaths` overrides `[db.seed].sql_paths` + * inside the recreate's MigrateAndSeed, mirroring the `db reset` + * `--no-seed` / `--sql-paths` handling (`cmd/db.go` `dbResetCmd`). + */ + readonly recreateDatabase: (opts: { + readonly version: string; + readonly noSeed: boolean; + readonly sqlPaths: ReadonlyArray; + }) => Effect.Effect; + /** + * The storage health gate local `db reset` runs before seeding buckets + * (`reset.AwaitStorageReady`): if the storage container exists but is unhealthy, + * wait up to 30s for it. Resolves `true` when the storage container exists (so + * the caller should run the ported bucket seeding) and `false` when it does not + * — matching Go, which silently skips buckets when storage is absent. + */ + readonly awaitStorageReady: () => Effect.Effect; +} + +export class LegacyDbBootstrapSeam extends Context.Service< + LegacyDbBootstrapSeam, + LegacyDbBootstrapSeamShape +>()("supabase/legacy/DbBootstrapSeam") {} diff --git a/apps/cli/src/legacy/commands/db/shared/legacy-drop-schemas.ts b/apps/cli/src/legacy/commands/db/shared/legacy-drop-schemas.ts new file mode 100644 index 0000000000..88d394498c --- /dev/null +++ b/apps/cli/src/legacy/commands/db/shared/legacy-drop-schemas.ts @@ -0,0 +1,169 @@ +import { Effect } from "effect"; + +import type { LegacyDbExecError } from "../../../shared/legacy-db-connection.errors.ts"; +import type { LegacyDbSession } from "../../../shared/legacy-db-connection.service.ts"; + +/** + * Verbatim port of Go's embedded `pkg/migration/queries/drop.sql` + * (`DropUserSchemas`). A single PL/pgSQL `DO` block that drops user schemas, + * extensions, public-schema objects, and non-managed publications, then + * truncates the auth / supabase_functions / supabase_migrations tables. Run as a + * single simple-query statement, matching Go's one-statement `ExecBatch`. + */ +const DROP_OBJECTS = `do $$ declare + rec record; +begin + -- schemas + for rec in + select pn.* + from pg_namespace pn + left join pg_depend pd on pd.objid = pn.oid + where pd.deptype is null + and not pn.nspname like any(array['information\\_schema', 'pg\\_%', '\\_analytics', '\\_realtime', '\\_supavisor', 'pgbouncer', 'pgmq', 'pgsodium', 'pgtle', 'supabase\\_migrations', 'vault', 'extensions', 'public']) + and pn.nspowner::regrole::text != 'supabase_admin' + loop + -- If an extension uses a schema it doesn't create, dropping the schema will cascade to also + -- drop the extension. But if an extension creates its own schema, dropping the schema will + -- throw an error. Hence, we drop schemas first while excluding those created by extensions. + raise notice 'dropping schema: %', rec.nspname; + execute format('drop schema if exists %I cascade', rec.nspname); + end loop; + + -- extensions + for rec in + select * + from pg_extension p + where p.extname not in ('pg_graphql', 'pg_net', 'pg_stat_statements', 'pgcrypto', 'pgjwt', 'pgsodium', 'plpgsql', 'supabase_vault', 'uuid-ossp') + loop + raise notice 'dropping extension: %', rec.extname; + execute format('drop extension if exists %I cascade', rec.extname); + end loop; + + -- functions + for rec in + select * + from pg_proc p + where p.pronamespace::regnamespace::name = 'public' + loop + -- supports aggregate, function, and procedure + raise notice 'dropping function: %.%', rec.pronamespace::regnamespace::name, rec.proname; + execute format('drop routine if exists %I.%I(%s) cascade', rec.pronamespace::regnamespace::name, rec.proname, pg_catalog.pg_get_function_identity_arguments(rec.oid)); + end loop; + + -- views (necessary for views referencing objects in Supabase-managed schemas) + for rec in + select * + from pg_class c + where + c.relnamespace::regnamespace::name = 'public' + and c.relkind = 'v' + loop + raise notice 'dropping view: %.%', rec.relnamespace::regnamespace::name, rec.relname; + execute format('drop view if exists %I.%I cascade', rec.relnamespace::regnamespace::name, rec.relname); + end loop; + + -- materialized views (necessary for materialized views referencing objects in Supabase-managed schemas) + for rec in + select * + from pg_class c + where + c.relnamespace::regnamespace::name = 'public' + and c.relkind = 'm' + loop + raise notice 'dropping materialized view: %.%', rec.relnamespace::regnamespace::name, rec.relname; + execute format('drop materialized view if exists %I.%I cascade', rec.relnamespace::regnamespace::name, rec.relname); + end loop; + + -- tables (cascade to dependent objects) + for rec in + select * + from pg_class c + where + c.relnamespace::regnamespace::name = 'public' + and c.relkind not in ('c', 'S', 'v', 'm') + order by c.relkind desc + loop + -- supports all table like relations, except views, complex types, and sequences + raise notice 'dropping table: %.%', rec.relnamespace::regnamespace::name, rec.relname; + execute format('drop table if exists %I.%I cascade', rec.relnamespace::regnamespace::name, rec.relname); + end loop; + + -- truncate tables in auth, webhooks, and migrations schema + for rec in + select * + from pg_class c + where + (c.relnamespace::regnamespace::name = 'auth' and c.relname != 'schema_migrations' + or c.relnamespace::regnamespace::name = 'supabase_functions' and c.relname != 'migrations' + or c.relnamespace::regnamespace::name = 'supabase_migrations') + and c.relkind = 'r' + loop + raise notice 'truncating table: %.%', rec.relnamespace::regnamespace::name, rec.relname; + execute format('truncate %I.%I cascade', rec.relnamespace::regnamespace::name, rec.relname); + end loop; + + -- sequences + for rec in + select * + from pg_class c + where + c.relnamespace::regnamespace::name = 'public' + and c.relkind = 's' + loop + raise notice 'dropping sequence: %.%', rec.relnamespace::regnamespace::name, rec.relname; + execute format('drop sequence if exists %I.%I cascade', rec.relnamespace::regnamespace::name, rec.relname); + end loop; + + -- types + for rec in + select * + from pg_type t + where + t.typnamespace::regnamespace::name = 'public' + and typtype != 'b' + loop + raise notice 'dropping type: %.%', rec.typnamespace::regnamespace::name, rec.typname; + execute format('drop type if exists %I.%I cascade', rec.typnamespace::regnamespace::name, rec.typname); + end loop; + + -- policies + for rec in + select * + from pg_policies p + loop + raise notice 'dropping policy: %', rec.policyname; + execute format('drop policy if exists %I on %I.%I cascade', rec.policyname, rec.schemaname, rec.tablename); + end loop; + + -- publications + for rec in + select * + from pg_publication p + where + not p.pubname like any(array['supabase\\_realtime%', 'realtime\\_messages%']) + loop + raise notice 'dropping publication: %', rec.pubname; + execute format('drop publication if exists %I', rec.pubname); + end loop; +end $$;`; + +/** + * Drops all user-created database objects, mirroring Go's + * `migration.DropUserSchemas` (`pkg/migration/drop.go:34-38`): the `drop.sql` `DO` + * block runs as a single transactional statement (no migration-history row). + */ +export const legacyDropUserSchemas = ( + session: LegacyDbSession, + mapError: (message: string) => E, +): Effect.Effect => + Effect.gen(function* () { + // Go's `DropUserSchemas` runs only `drop.sql` via `ExecBatch` (drop.go:34-38) — + // no `RESET ALL`. Resetting here would clear caller-supplied DB URL runtime + // params (e.g. `options=-c statement_timeout=…`) before the destructive drop, so + // the remote `db reset --db-url` path must NOT reset (matches Go's ExecBatch). + yield* session.exec("BEGIN"); + yield* session + .exec(DROP_OBJECTS) + .pipe(Effect.tapError(() => session.exec("ROLLBACK").pipe(Effect.ignore))); + yield* session.exec("COMMIT"); + }).pipe(Effect.mapError((error: LegacyDbExecError) => mapError(error.message))); diff --git a/apps/cli/src/legacy/commands/db/shared/legacy-migration-pending.ts b/apps/cli/src/legacy/commands/db/shared/legacy-migration-pending.ts new file mode 100644 index 0000000000..579a0b675a --- /dev/null +++ b/apps/cli/src/legacy/commands/db/shared/legacy-migration-pending.ts @@ -0,0 +1,123 @@ +import { legacyBold } from "../../../shared/legacy-colors.ts"; + +/** + * `pkg/migration/file.go` — local migration filenames are `_.sql`. + * `ListLocalMigrations` guarantees every path in `localMigrations` matches, so the + * version capture group is always present. + */ +const MIGRATE_FILE_PATTERN = /^([0-9]+)_(.*)\.sql$/u; + +/** Last path segment, mirroring Go's `filepath.Base`. */ +const baseName = (path: string): string => { + const normalized = path.replace(/[/\\]+$/u, ""); + const slash = Math.max(normalized.lastIndexOf("/"), normalized.lastIndexOf("\\")); + return slash === -1 ? normalized : normalized.slice(slash + 1); +}; + +/** + * `pkg/migration/apply.go:14-16` — the exact error strings Go raises so the legacy + * handler can byte-match them on stderr. + */ +export const LEGACY_ERR_MISSING_REMOTE = + "Found local migration files to be inserted before the last migration on remote database."; +export const LEGACY_ERR_MISSING_LOCAL = + "Remote migration versions not found in local migrations directory."; + +/** + * The outcome of comparing local migration files against the remote + * `schema_migrations` history. Pure 1:1 port of Go's `FindPendingMigrations` + * (`pkg/migration/apply.go:21-54`). + * + * - `ok` — `pending` are the local migration paths to apply (those + * beyond the remote history, in order). + * - `missing-local` — remote has versions with no local file (`ErrMissingLocal`). + * `versions` are the offending remote versions. + * - `missing-remote`— local has files ordered before the remote head + * (`ErrMissingRemote`). `paths` are the out-of-order local + * migration paths. + */ +export type LegacyPendingMigrations = + | { readonly kind: "ok"; readonly pending: ReadonlyArray } + | { readonly kind: "missing-local"; readonly versions: ReadonlyArray } + | { readonly kind: "missing-remote"; readonly paths: ReadonlyArray }; + +/** + * Two-pointer reconciliation of local migration paths vs remote applied versions. + * Mirrors Go's `FindPendingMigrations` exactly, including its **string** + * comparison of versions (`remote == local` / `remote < local`) — version + * prefixes are fixed-width timestamps, so lexical order equals chronological + * order, matching Go. + */ +export function legacyFindPendingMigrations( + localMigrations: ReadonlyArray, + remoteMigrations: ReadonlyArray, +): LegacyPendingMigrations { + const unapplied: Array = []; + const missing: Array = []; + let i = 0; + let j = 0; + while (i < remoteMigrations.length && j < localMigrations.length) { + const remote = remoteMigrations[i]!; + const filename = baseName(localMigrations[j]!); + // ListLocalMigrations guarantees a match, so the capture group is present. + const local = MIGRATE_FILE_PATTERN.exec(filename)![1]!; + if (remote === local) { + i++; + j++; + } else if (remote < local) { + missing.push(remote); + i++; + } else { + // Include out-of-order local migrations. + unapplied.push(localMigrations[j]!); + j++; + } + } + // Ensure all remote versions exist on local. + if (j === localMigrations.length) { + missing.push(...remoteMigrations.slice(i)); + } + if (missing.length > 0) { + return { kind: "missing-local", versions: missing }; + } + // Enforce migrations are applied in chronological order by default. + if (unapplied.length > 0) { + return { kind: "missing-remote", paths: unapplied }; + } + return { kind: "ok", pending: localMigrations.slice(remoteMigrations.length) }; +} + +/** + * Computes the `--include-all` pending set when reconciliation reports + * `missing-remote`. Mirrors Go's `GetPendingMigrations` includeAll branch + * (`internal/migration/up/up.go:46-48`): the out-of-order paths first, then the + * local migrations beyond `len(remote)+len(diff)`. + */ +export function legacyIncludeAllPending( + localMigrations: ReadonlyArray, + remoteCount: number, + diff: ReadonlyArray, +): ReadonlyArray { + return [...diff, ...localMigrations.slice(remoteCount + diff.length)]; +} + +/** + * Go's `suggestRevertHistory` (`internal/migration/up/up.go:55-61`). `fmt.Sprintln` + * appends a trailing newline to each line, so the suggestion ends with `\n`. + */ +export function legacySuggestRevertHistory(versions: ReadonlyArray): string { + return ( + "\nMake sure your local git repo is up-to-date. If the error persists, try repairing the migration history table:\n" + + `${legacyBold(`supabase migration repair --status reverted ${versions.join(" ")}`)}\n` + + "\nAnd update local migrations to match remote database:\n" + + `${legacyBold("supabase db pull")}\n` + ); +} + +/** Go's `suggestIgnoreFlag` (`internal/migration/up/up.go:63-67`). */ +export function legacySuggestIgnoreFlag(paths: ReadonlyArray): string { + return ( + "\nRerun the command with --include-all flag to apply these migrations:\n" + + `${legacyBold(paths.join("\n"))}\n` + ); +} diff --git a/apps/cli/src/legacy/commands/db/shared/legacy-migration-pending.unit.test.ts b/apps/cli/src/legacy/commands/db/shared/legacy-migration-pending.unit.test.ts new file mode 100644 index 0000000000..eaf5400d60 --- /dev/null +++ b/apps/cli/src/legacy/commands/db/shared/legacy-migration-pending.unit.test.ts @@ -0,0 +1,83 @@ +import { describe, expect, it } from "vitest"; + +import { + legacyFindPendingMigrations, + legacyIncludeAllPending, + legacySuggestIgnoreFlag, + legacySuggestRevertHistory, +} from "./legacy-migration-pending.ts"; + +const local = (...versions: ReadonlyArray) => + versions.map((v) => `supabase/migrations/${v}_name.sql`); + +describe("legacyFindPendingMigrations", () => { + it("returns the local migrations beyond the remote history when in sync", () => { + const result = legacyFindPendingMigrations(local("0001", "0002", "0003"), ["0001"]); + expect(result).toEqual({ + kind: "ok", + pending: ["supabase/migrations/0002_name.sql", "supabase/migrations/0003_name.sql"], + }); + }); + + it("is up to date when local and remote match exactly", () => { + const result = legacyFindPendingMigrations(local("0001", "0002"), ["0001", "0002"]); + expect(result).toEqual({ kind: "ok", pending: [] }); + }); + + it("reports missing-local when remote has a version with no local file", () => { + const result = legacyFindPendingMigrations(local("0001", "0003"), ["0001", "0002", "0003"]); + expect(result).toEqual({ kind: "missing-local", versions: ["0002"] }); + }); + + it("reports missing-local for trailing remote versions absent locally", () => { + const result = legacyFindPendingMigrations(local("0001"), ["0001", "0002"]); + expect(result).toEqual({ kind: "missing-local", versions: ["0002"] }); + }); + + it("reports missing-remote for an out-of-order local migration", () => { + const result = legacyFindPendingMigrations(local("0001", "0002"), ["0002"]); + expect(result).toEqual({ + kind: "missing-remote", + paths: ["supabase/migrations/0001_name.sql"], + }); + }); + + it("treats an empty remote history as all-local pending", () => { + const result = legacyFindPendingMigrations(local("0001", "0002"), []); + expect(result).toEqual({ + kind: "ok", + pending: ["supabase/migrations/0001_name.sql", "supabase/migrations/0002_name.sql"], + }); + }); +}); + +describe("legacyIncludeAllPending", () => { + it("prepends the out-of-order diff then the migrations beyond remote+diff", () => { + const locals = local("0001", "0002", "0003"); + const diff = ["supabase/migrations/0001_name.sql"]; + // remoteCount 1, diff length 1 → slice from index 2. + expect(legacyIncludeAllPending(locals, 1, diff)).toEqual([ + "supabase/migrations/0001_name.sql", + "supabase/migrations/0003_name.sql", + ]); + }); +}); + +describe("suggestion strings", () => { + it("builds the revert-history suggestion with a trailing newline per line", () => { + expect(legacySuggestRevertHistory(["0002", "0003"])).toContain( + "supabase migration repair --status reverted 0002 0003", + ); + expect(legacySuggestRevertHistory(["0002"])).toMatch(/\n$/u); + expect(legacySuggestRevertHistory(["0002"])).toContain("supabase db pull"); + }); + + it("builds the include-all suggestion listing each path on its own line", () => { + const suggestion = legacySuggestIgnoreFlag([ + "supabase/migrations/0001_a.sql", + "supabase/migrations/0002_b.sql", + ]); + expect(suggestion).toContain("--include-all"); + expect(suggestion).toContain("supabase/migrations/0001_a.sql\nsupabase/migrations/0002_b.sql"); + }); +}); diff --git a/apps/cli/src/legacy/commands/db/shared/legacy-pgdelta.seam.layer.ts b/apps/cli/src/legacy/commands/db/shared/legacy-pgdelta.seam.layer.ts index 634c4c7b56..29469069f8 100644 --- a/apps/cli/src/legacy/commands/db/shared/legacy-pgdelta.seam.layer.ts +++ b/apps/cli/src/legacy/commands/db/shared/legacy-pgdelta.seam.layer.ts @@ -209,9 +209,11 @@ export const legacyDeclarativeSeamLayer = Layer.effect( })(), ) .trim(); - // Only a missing container means "not running" → start it. Any other - // inspect failure (e.g. Docker daemon down) propagates, matching Go. - if (!stderr.includes("No such container")) { + // Only a missing container means "not running" → start it. Docker reports + // this as either "No such container" or "No such object" (the same pair + // handled in `shared/functions/serve.ts`). Any other inspect failure (e.g. + // Docker daemon down) propagates, matching Go. + if (!stderr.includes("No such container") && !stderr.includes("No such object")) { return yield* Effect.fail( new LegacyDeclarativeShadowDbError({ message: diff --git a/apps/cli/src/legacy/commands/db/shared/legacy-seed-ops.ts b/apps/cli/src/legacy/commands/db/shared/legacy-seed-ops.ts new file mode 100644 index 0000000000..5caba63a87 --- /dev/null +++ b/apps/cli/src/legacy/commands/db/shared/legacy-seed-ops.ts @@ -0,0 +1,372 @@ +import { createHash } from "node:crypto"; +import { Effect, type FileSystem, Option, type Path } from "effect"; + +import { Output } from "../../../../shared/output/output.service.ts"; +import type { LegacyDbExecError } from "../../../shared/legacy-db-connection.errors.ts"; +import type { LegacyDbSession } from "../../../shared/legacy-db-connection.service.ts"; +import { legacyCreateSeedTable } from "../../../shared/legacy-migration-history.ts"; +import { legacySplitAndTrim } from "../../../shared/legacy-sql-split.ts"; + +/** + * Seed-history DML, verbatim from Go's `pkg/migration/history.go`. The schema/table + * DDL (with a transaction-scoped lock timeout) lives in `legacyCreateSeedTable`. + */ +const UPSERT_SEED_FILE = + "INSERT INTO supabase_migrations.seed_files(path, hash) VALUES($1, $2) ON CONFLICT (path) DO UPDATE SET hash = EXCLUDED.hash"; +const SELECT_SEED_TABLE = "SELECT path, hash FROM supabase_migrations.seed_files"; + +/** A local seed file resolved from `[db.seed].sql_paths`, with its content hash. */ +export interface LegacySeedFile { + /** Workdir-relative, forward-slashed path (Go's `filepath.ToSlash`). */ + readonly path: string; + /** Lowercase hex SHA-256 of the file content (Go's `NewSeedFile`). */ + readonly hash: string; + /** True when the remote `seed_files` row has a different hash (re-hash only). */ + readonly dirty: boolean; +} + +const META_CHARS = /[*?[\\]/u; + +/** + * Go's `path.Match` for a single filename (no `/`). Supports `*` (any run of + * non-separator chars), `?` (one char), `[...]` classes with ranges and a + * leading `^`/`!` negation, and `\` escapes. Filenames never contain `/`, so the + * separator subtlety in Go's matcher does not apply here. + */ +export function legacyMatchPattern(pattern: string, name: string): boolean { + const matchClass = (cls: string, ch: string): boolean => { + let negated = false; + let body = cls; + if (body.startsWith("^") || body.startsWith("!")) { + negated = true; + body = body.slice(1); + } + let matched = false; + for (let k = 0; k < body.length; k++) { + if (body[k + 1] === "-" && k + 2 < body.length) { + if (ch >= body[k]! && ch <= body[k + 2]!) matched = true; + k += 2; + } else if (body[k] === ch) { + matched = true; + } + } + return matched !== negated; + }; + + const match = (p: number, n: number): boolean => { + while (p < pattern.length) { + const pc = pattern[p]!; + if (pc === "*") { + // Collapse consecutive stars, then try to match the rest at every offset. + while (pattern[p] === "*") p++; + if (p === pattern.length) return true; + for (let k = n; k <= name.length; k++) { + if (match(p, k)) return true; + } + return false; + } + if (n >= name.length) return false; + if (pc === "?") { + p++; + n++; + continue; + } + if (pc === "[") { + const end = pattern.indexOf("]", p + 1); + if (end === -1) return false; + if (!matchClass(pattern.slice(p + 1, end), name[n]!)) return false; + p = end + 1; + n++; + continue; + } + if (pc === "\\" && p + 1 < pattern.length) { + if (pattern[p + 1] !== name[n]) return false; + p += 2; + n++; + continue; + } + if (pc !== name[n]) return false; + p++; + n++; + } + return n === name.length; + }; + + return match(0, 0); +} + +/** Result of resolving `[db.seed].sql_paths` against the workspace. */ +interface LegacyGlobResult { + /** Workdir-relative, forward-slashed matches, deduplicated in pattern order. */ + readonly files: ReadonlyArray; + /** Per-pattern warnings (`no files matched pattern: …`), joined by Go's `errors.Join`. */ + readonly warning: Option.Option; +} + +/** + * Resolves seed glob patterns to existing files, porting Go's `config.Glob.Files` + * over `fs.Glob` (`pkg/config/config.go:102-124`). Each pattern is first joined + * under the `supabase/` directory (Go resolves `sql_paths` at config load, + * `config.go:884`). Matches per pattern are sorted; the overall result preserves + * first-seen order across patterns. A pattern that matches nothing contributes a + * `no files matched pattern: ` warning but is not fatal. + */ +const legacyGlobSeedFiles = Effect.fnUntraced(function* ( + fs: FileSystem.FileSystem, + path: Path.Path, + patterns: ReadonlyArray, + workdir: string, +) { + const seen = new Set(); + const files: Array = []; + const errors: Array = []; + + for (const rawPattern of patterns) { + // Patterns arrive already resolved to Go's config-load form (relative entries + // supabase/-joined, absolute preserved) via `legacyResolveSeedSqlPath` — the reader + // for `[db.seed].sql_paths`, the caller for `--sql-paths`. Go's `config.Glob.Files` + // globs those resolved paths without re-prefixing (`config.go:102-124`), so only + // normalize separators here; re-joining `supabase/` would double-prefix. + const pattern = toSlash(rawPattern); + const matches = yield* globOne(fs, path, workdir, pattern); + if (matches.length === 0) { + errors.push(`no files matched pattern: ${pattern}`); + continue; + } + for (const match of [...matches].sort()) { + const fp = toSlash(match); + // Go's `GetPendingSeeds` globs via `Glob.SQLFiles`, which `Stat`s each match: a + // directory is expanded to its regular `.sql` files recursively (`walkMatchedDir`, + // sorted) while a file match is kept verbatim (`config.go:157-183`). Without this a + // directory `sql_paths` entry (e.g. `["seeds"]`) would flow into + // `readFileString()` and fail — Go's `db push --include-seed` / remote reset + // seed the directory's SQL children instead. + const matchType = yield* fs.stat(path.isAbsolute(fp) ? fp : path.join(workdir, fp)).pipe( + Effect.map((info) => info.type), + Effect.orElseSucceed(() => "File" as const), + ); + if (matchType === "Directory") { + for (const file of yield* legacyWalkSeedSqlFiles(fs, path, workdir, fp)) { + if (!seen.has(file)) { + seen.add(file); + files.push(file); + } + } + continue; + } + if (!seen.has(fp)) { + seen.add(fp); + files.push(fp); + } + } + } + + return { + files, + warning: errors.length > 0 ? Option.some(errors.join("\n")) : Option.none(), + } satisfies LegacyGlobResult; +}); + +const toSlash = (p: string): string => p.replaceAll("\\", "/"); + +/** Splits a forward-slashed path into its directory prefix and final element. */ +const splitPath = (p: string): { readonly dir: string; readonly file: string } => { + const slash = p.lastIndexOf("/"); + return slash === -1 ? { dir: "", file: p } : { dir: p.slice(0, slash), file: p.slice(slash + 1) }; +}; + +/** Faithful port of Go's `fs.Glob` for one pattern, rooted at `workdir`. */ +const globOne = ( + fs: FileSystem.FileSystem, + path: Path.Path, + workdir: string, + pattern: string, +): Effect.Effect, never> => + Effect.gen(function* () { + // Absolute patterns resolve against the filesystem root (Go preserves absolute + // seed paths); relative ones are rooted at the workdir. + const resolve = (p: string): string => (path.isAbsolute(p) ? p : path.join(workdir, p)); + // No metacharacters: a direct existence check (Go's `fs.Glob` fast path). + if (!META_CHARS.test(pattern)) { + const exists = yield* fs.exists(resolve(pattern)).pipe(Effect.orElseSucceed(() => false)); + return exists ? [pattern] : []; + } + const { dir, file } = splitPath(pattern); + // Resolve the directory level first (recursively if it, too, is a glob). + const dirs = + dir === "" || !META_CHARS.test(dir) ? [dir] : yield* globOne(fs, path, workdir, dir); + const result: Array = []; + for (const d of dirs) { + const absDir = d === "" ? workdir : resolve(d); + const names = yield* fs + .readDirectory(absDir) + .pipe(Effect.orElseSucceed((): ReadonlyArray => [])); + for (const name of names) { + if (legacyMatchPattern(file, name)) { + result.push(d === "" ? name : `${d}/${name}`); + } + } + } + return result; + }); + +/** + * Recursively collects the regular `.sql` files under a matched seed directory, porting + * Go's `walkMatchedDir` with the `SQLFiles` include filter (`entry.Type().IsRegular() && + * filepath.Ext(path) == ".sql"`, `config.go:126-131,194-211`). Paths are workdir-relative + * (matching the glob output), forward-slashed, and sorted for deterministic application. + */ +const legacyWalkSeedSqlFiles = ( + fs: FileSystem.FileSystem, + path: Path.Path, + workdir: string, + dir: string, +): Effect.Effect, never> => + Effect.gen(function* () { + const collected: Array = []; + const walk = (rel: string): Effect.Effect => + Effect.gen(function* () { + const absDir = path.isAbsolute(rel) ? rel : path.join(workdir, rel); + const names = yield* fs + .readDirectory(absDir) + .pipe(Effect.orElseSucceed((): ReadonlyArray => [])); + for (const name of names) { + const childRel = `${rel}/${name}`; + const childType = yield* fs + .stat(path.isAbsolute(childRel) ? childRel : path.join(workdir, childRel)) + .pipe( + Effect.map((info) => info.type), + Effect.orElseSucceed(() => "Unknown" as const), + ); + if (childType === "Directory") { + yield* walk(childRel); + } else if (childType === "File" && childRel.endsWith(".sql")) { + collected.push(toSlash(childRel)); + } + } + }); + yield* walk(dir); + return collected.sort(); + }); + +/** `SELECT path, hash FROM supabase_migrations.seed_files`, `42P01` → empty map. */ +const readRemoteSeeds = (session: LegacyDbSession) => + session.query(SELECT_SEED_TABLE).pipe( + Effect.map((rows) => { + const applied = new Map(); + for (const row of rows) applied.set(String(row["path"]), String(row["hash"])); + return applied; + }), + Effect.catch((error: LegacyDbExecError) => + isUndefinedTable(error) ? Effect.succeed(new Map()) : Effect.fail(error), + ), + ); + +const isUndefinedTable = (error: LegacyDbExecError): boolean => + error.code !== undefined + ? error.code === "42P01" + : /relation .* does not exist/iu.test(error.message) && + !/column .* does not exist/iu.test(error.message); + +/** + * Resolves the pending seed files for `db push --include-seed`. Mirrors Go's + * `GetPendingSeeds` (`pkg/migration/seed.go:34-63`): glob the configured paths + * (warn, don't fail, on empty patterns), read the remote `seed_files` hashes, + * and emit each local file that is new (`dirty=false`) or hash-changed + * (`dirty=true`); files whose hash already matches are skipped. + */ +export const legacyGetPendingSeeds = Effect.fnUntraced(function* ( + session: LegacyDbSession, + fs: FileSystem.FileSystem, + path: Path.Path, + patterns: ReadonlyArray, + workdir: string, +) { + const output = yield* Output; + const { files, warning } = yield* legacyGlobSeedFiles(fs, path, patterns, workdir); + if (Option.isSome(warning)) { + yield* output.raw(`WARN: ${warning.value}\n`, "stderr"); + } + const pending: Array = []; + if (files.length === 0) return pending; + + const applied = yield* readRemoteSeeds(session); + for (const file of files) { + // Go's `NewSeedFile` hashes the raw file stream (`io.Copy`, `pkg/migration/file.go:184`), + // so hash the bytes — not a UTF-8-decoded string, which replaces invalid sequences and + // would drift from the Go-recorded `seed_files` hash for a non-UTF-8 seed (SQL_ASCII dump + // / binary COPY payload), spuriously re-running it across a Go ↔ native switch. + const content = yield* fs.readFile(path.isAbsolute(file) ? file : path.join(workdir, file)); + const hash = createHash("sha256").update(content).digest("hex"); + const appliedHash = applied.get(file); + if (appliedHash !== undefined) { + if (appliedHash === hash) continue; // Already applied, unchanged. + pending.push({ path: file, hash, dirty: true }); + continue; + } + pending.push({ path: file, hash, dirty: false }); + } + return pending; +}); + +/** + * Applies pending seed files. Mirrors Go's `SeedData` + `ExecBatchWithCache` + * (`pkg/migration/seed.go:65-83`, `file.go:198-217`): create the `seed_files` + * table, then per file emit the dirty/clean status line and, in one transaction, + * run the file's statements (skipped when dirty — only the hash is refreshed) + * followed by the `seed_files` hash upsert. + */ +export const legacySeedData = ( + session: LegacyDbSession, + fs: FileSystem.FileSystem, + workdir: string, + path: Path.Path, + seeds: ReadonlyArray, + mapError: (message: string) => E, +): Effect.Effect => + Effect.gen(function* () { + const output = yield* Output; + if (seeds.length === 0) return; + // Go's `CreateSeedTable` (history.go:54-64) runs `SET lock_timeout = '4s'` + + // schema/table DDL in one implicit transaction, so a conflicting schema/table lock + // fails promptly but the timeout reverts on COMMIT and never leaks into the seed + // SQL run below. `legacyCreateSeedTable` reproduces that with BEGIN + SET LOCAL + + // DDL + COMMIT (creating the schema first so a seed-only run doesn't fail). + yield* legacyCreateSeedTable(session); + for (const seed of seeds) { + yield* output.raw( + seed.dirty + ? `Updating seed hash to ${seed.path}...\n` + : `Seeding data from ${seed.path}...\n`, + "stderr", + ); + // Go's `ExecBatchWithCache` parses the file (read + `SplitAndTrim`) + // UNCONDITIONALLY before the dirty check (`file.go:198-211`), so a dirty seed + // that is unreadable or contains malformed SQL still fails and leaves the + // previous hash — only the queueing of statements is gated on `Dirty`. + const lines = legacySplitAndTrim( + yield* fs.readFileString( + path.isAbsolute(seed.path) ? seed.path : path.join(workdir, seed.path), + ), + ); + const statements = seed.dirty ? [] : lines; + yield* session.exec("BEGIN"); + const body = Effect.gen(function* () { + for (const statement of statements) yield* session.exec(statement); + yield* session.query(UPSERT_SEED_FILE, [seed.path, seed.hash]); + yield* session.exec("COMMIT"); + }); + yield* body.pipe(Effect.tapError(() => session.exec("ROLLBACK").pipe(Effect.ignore))); + } + }).pipe( + Effect.mapError((error) => + mapError( + typeof error === "object" && + error !== null && + "message" in error && + typeof error.message === "string" + ? error.message + : String(error), + ), + ), + ); diff --git a/apps/cli/src/legacy/commands/db/shared/legacy-seed-ops.unit.test.ts b/apps/cli/src/legacy/commands/db/shared/legacy-seed-ops.unit.test.ts new file mode 100644 index 0000000000..3c50f914c8 --- /dev/null +++ b/apps/cli/src/legacy/commands/db/shared/legacy-seed-ops.unit.test.ts @@ -0,0 +1,126 @@ +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { BunServices } from "@effect/platform-bun"; +import { describe, expect, it } from "@effect/vitest"; +import { Data, Effect, Exit, FileSystem, Path } from "effect"; + +import { mockOutput } from "../../../../../tests/helpers/mocks.ts"; +import type { LegacyDbSession } from "../../../shared/legacy-db-connection.service.ts"; +import { legacyMatchPattern, legacySeedData } from "./legacy-seed-ops.ts"; + +class TestError extends Data.TaggedError("TestError")<{ readonly message: string }> {} + +function fakeSeedSession() { + const calls: Array<{ kind: "exec" | "query"; sql: string }> = []; + const session: LegacyDbSession = { + exec: (sql) => { + calls.push({ kind: "exec", sql }); + return Effect.void; + }, + query: (sql) => { + calls.push({ kind: "query", sql }); + return Effect.succeed([]); + }, + extensionExists: () => Effect.succeed(false), + copyToCsv: () => Effect.succeed(new Uint8Array()), + queryRaw: () => Effect.succeed({ fields: [], rows: [], commandTag: "" }), + }; + return { session, calls }; +} + +describe("legacyMatchPattern", () => { + it("matches a literal filename", () => { + expect(legacyMatchPattern("seed.sql", "seed.sql")).toBe(true); + expect(legacyMatchPattern("seed.sql", "other.sql")).toBe(false); + }); + + it("matches `*` against any run of characters", () => { + expect(legacyMatchPattern("*.sql", "seed.sql")).toBe(true); + expect(legacyMatchPattern("*.sql", "0001_init.sql")).toBe(true); + expect(legacyMatchPattern("*.sql", "seed.txt")).toBe(false); + expect(legacyMatchPattern("seed.*", "seed.sql")).toBe(true); + }); + + it("matches `?` against exactly one character", () => { + expect(legacyMatchPattern("seed?.sql", "seed1.sql")).toBe(true); + expect(legacyMatchPattern("seed?.sql", "seed12.sql")).toBe(false); + expect(legacyMatchPattern("seed?.sql", "seed.sql")).toBe(false); + }); + + it("matches character classes with ranges and negation", () => { + expect(legacyMatchPattern("seed[0-9].sql", "seed5.sql")).toBe(true); + expect(legacyMatchPattern("seed[0-9].sql", "seedx.sql")).toBe(false); + expect(legacyMatchPattern("seed[!0-9].sql", "seedx.sql")).toBe(true); + expect(legacyMatchPattern("seed[!0-9].sql", "seed5.sql")).toBe(false); + }); + + it("honors backslash escapes", () => { + expect(legacyMatchPattern("seed\\*.sql", "seed*.sql")).toBe(true); + expect(legacyMatchPattern("seed\\*.sql", "seedx.sql")).toBe(false); + }); + + it("collapses consecutive stars", () => { + expect(legacyMatchPattern("**.sql", "seed.sql")).toBe(true); + }); +}); + +const runSeed = ( + session: LegacyDbSession, + workdir: string, + seeds: ReadonlyArray<{ readonly path: string; readonly hash: string; readonly dirty: boolean }>, +) => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + return yield* legacySeedData( + session, + fs, + workdir, + path, + seeds, + (message) => new TestError({ message }), + ); + }).pipe(Effect.provide(mockOutput({ format: "text" }).layer), Effect.provide(BunServices.layer)); + +describe("legacySeedData (dirty parse)", () => { + it.effect("fails on an unreadable dirty seed instead of refreshing its hash", () => { + // Go's `ExecBatchWithCache` reads + parses the file UNCONDITIONALLY before the + // dirty check, so a dirty seed pointing at a missing file must fail (and leave + // the previous hash) rather than silently upserting the new hash. + const dir = mkdtempSync(join(tmpdir(), "legacy-seed-")); + const { session, calls } = fakeSeedSession(); + return runSeed(session, dir, [{ path: "missing.sql", hash: "newhash", dirty: true }]).pipe( + Effect.exit, + Effect.tap((exit) => + Effect.sync(() => { + expect(Exit.isFailure(exit)).toBe(true); + // The hash upsert is a `query`; the only execs that ran are the + // schema/table creation (whose DDL also mentions `seed_files`), so assert + // no `query` ran rather than substring-matching the table name. + expect(calls.some((c) => c.kind === "query")).toBe(false); + rmSync(dir, { recursive: true, force: true }); + }), + ), + ); + }); + + it.effect("refreshes the hash for a dirty seed that parses, without running statements", () => { + const dir = mkdtempSync(join(tmpdir(), "legacy-seed-")); + writeFileSync(join(dir, "data.sql"), "insert into t values (1);"); + const { session, calls } = fakeSeedSession(); + return runSeed(session, dir, [{ path: "data.sql", hash: "newhash", dirty: true }]).pipe( + Effect.tap(() => + Effect.sync(() => { + // Go's CreateSeedTable scopes the lock timeout to the DDL transaction + // (BEGIN + SET LOCAL + COMMIT) so it never leaks into the seed SQL below. + expect(calls.some((c) => c.sql === "SET LOCAL lock_timeout = '4s'")).toBe(true); + // Statements are NOT executed for a dirty seed, but the hash IS upserted. + expect(calls.some((c) => c.sql.includes("insert into t"))).toBe(false); + expect(calls.some((c) => c.kind === "query" && c.sql.includes("seed_files"))).toBe(true); + rmSync(dir, { recursive: true, force: true }); + }), + ), + ); + }); +}); diff --git a/apps/cli/src/legacy/commands/db/start/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/db/start/SIDE_EFFECTS.md index 0c980a749c..dcf8466552 100644 --- a/apps/cli/src/legacy/commands/db/start/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/db/start/SIDE_EFFECTS.md @@ -1,17 +1,35 @@ # `supabase db start` +Native TS port of `apps/cli-go/internal/db/start/start.go` `Run`. The handler +validates config, checks whether the local Postgres container is already running, +and otherwise delegates the container bootstrap to the bundled Go binary's hidden +`db __db-bootstrap --mode start` seam (the container-lifecycle primitives are not +ported). This is `db start`, **not** the top-level `supabase start`: no status +table, no `cli_stack_started` event, no `Finished` line. + ## Files Read -| Path | Format | When | -| -------------------------------- | ------ | ---------------------------------- | -| `/supabase/config.toml` | TOML | always, to resolve local DB config | -| `` (from `--from-backup`) | binary | when `--from-backup` flag is set | +| Path | Format | When | +| -------------------------------- | ------ | --------------------------------------------------------------- | +| `/supabase/config.toml` | TOML | always — parsed up front; a malformed config aborts before work | +| `` (from `--from-backup`) | binary | when `--from-backup` is set (read by the Go seam on start) | ## Files Written -| Path | Format | When | -| ---- | ------ | ---- | -| — | — | — | +| Path | Format | When | +| ---------------------------------------------- | ------ | --------------------------------------------------------------------------- | +| `/supabase/.branches/_current_branch` | text | by the Go seam (`initCurrentBranch`) when starting; writes `main` if absent | +| local Docker volume `supabase_db_` | — | by the Go seam — the Postgres data volume created on first start | +| `~/.supabase/telemetry.json` | JSON | always (telemetry flush, success and failure) | + +## Subprocesses + +| Command | When | Purpose | +| ---------------------------------------------------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `docker container inspect supabase_db_` | always | `AssertSupabaseDbIsRunning` probe (Podman fallback) | +| `supabase-go db __db-bootstrap --mode start [--from-backup

]` | when the database is not running | create container + health check + initial schema/roles/migrations/seed + `_current_branch`; telemetry disabled (`SUPABASE_TELEMETRY_DISABLED=1`), progress teed to stderr | + +`--network-id` and a flag-selected `--profile` are forwarded to the seam. ## API Routes @@ -19,34 +37,45 @@ | ------ | ---- | ---- | ------------ | ---------------------- | | — | — | — | — | — | +(The Go seam may call Auth's JWKS endpoint while applying service migrations on a +fresh PG15 volume; that is internal to the seam, not the TS handler.) + ## Environment Variables -| Variable | Purpose | Required? | -| -------- | ------- | --------- | -| — | — | — | +| Variable | Purpose | Required? | +| ----------------------------- | ---------------------------------------------------- | ---------- | +| `SUPABASE_PROJECT_ID` | overrides the local container id (`utils.DbId`) | no | +| `SUPABASE_TELEMETRY_DISABLED` | set on the seam subprocess so it never double-counts | (internal) | ## Exit Codes -| Code | Condition | -| ---- | ------------------------------ | -| `0` | success | -| `1` | Docker not running | -| `1` | database container start error | +| Code | Condition | +| ---- | --------------------------------------------------------------------- | +| `0` | success — database started, or already running | +| `1` | malformed `supabase/config.toml` | +| `1` | Docker daemon unreachable / inspect failure | +| `1` | container bootstrap failed (the seam cleans up via `DockerRemoveAll`) | ## Output ### `--output-format text` (Go CLI compatible) -Prints progress to stderr as the local Postgres container starts. +- Already running → `Postgres database is already running.` on **stderr**, exit 0. +- Starting → the Go seam tees `Starting database...` / `Initialising schema...` to + **stderr**. No stdout output, no `Finished` line. ### `--output-format json` -Not applicable. +Emits a single result object to stdout: `{ status: "already-running" }` or +`{ status: "started" }`. Progress stays on stderr. ### `--output-format stream-json` -Not applicable. +Same result object as the terminal `result` event; progress on stderr. ## Notes -- `--from-backup` restores the database from a logical backup file on start. +- `--from-backup` restores the database from a logical backup file on start; the + health check is skipped for backups (a large restore can exceed the timeout). +- No `cli_stack_started` telemetry — that event belongs to `supabase start`, not + `db start`. The only event is the standard `cli_command_executed`. diff --git a/apps/cli/src/legacy/commands/db/start/start.command.ts b/apps/cli/src/legacy/commands/db/start/start.command.ts index cc4081ae84..c9457733ae 100644 --- a/apps/cli/src/legacy/commands/db/start/start.command.ts +++ b/apps/cli/src/legacy/commands/db/start/start.command.ts @@ -1,6 +1,10 @@ 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 { withLegacyCommandInstrumentation } from "../../../telemetry/legacy-command-instrumentation.ts"; import { legacyDbStart } from "./start.handler.ts"; +import { legacyDbStartRuntimeLayer } from "./start.layers.ts"; const config = { fromBackup: Flag.string("from-backup").pipe( @@ -14,5 +18,15 @@ export type LegacyDbStartFlags = CliCommand.Command.Config.Infer; export const legacyDbStartCommand = Command.make("start", config).pipe( Command.withDescription("Starts local Postgres database."), Command.withShortDescription("Starts local Postgres database"), - Command.withHandler((flags) => legacyDbStart(flags)), + Command.withHandler((flags) => + legacyDbStart(flags).pipe( + withLegacyCommandInstrumentation({ + // `--from-backup` is not telemetry-safe in Go (no markFlagTelemetrySafe), + // so a set value reaches telemetry as ``. + flags: { "from-backup": flags.fromBackup }, + }), + withJsonErrorHandling, + ), + ), + Command.provide(legacyDbStartRuntimeLayer), ); diff --git a/apps/cli/src/legacy/commands/db/start/start.handler.ts b/apps/cli/src/legacy/commands/db/start/start.handler.ts index a1aa3e586a..7428173f1a 100644 --- a/apps/cli/src/legacy/commands/db/start/start.handler.ts +++ b/apps/cli/src/legacy/commands/db/start/start.handler.ts @@ -1,10 +1,85 @@ -import { Effect, Option } from "effect"; -import { LegacyGoProxy } from "../../../../shared/legacy/go-proxy.service.ts"; +import { Effect, FileSystem, Option, Path } from "effect"; + +import { Output } from "../../../../shared/output/output.service.ts"; +import { RuntimeInfo } from "../../../../shared/runtime/runtime-info.service.ts"; +import { LegacyCliConfig } from "../../../config/legacy-cli-config.service.ts"; +import { LegacyTelemetryState } from "../../../telemetry/legacy-telemetry-state.service.ts"; +import { legacyCheckDbToml } from "../../../shared/legacy-db-config.toml-read.ts"; +import { LegacyDbBootstrapSeam } from "../shared/legacy-db-bootstrap.seam.service.ts"; import type { LegacyDbStartFlags } from "./start.command.ts"; +/** + * `supabase db start` — start the local Postgres database. + * + * Strict 1:1 port of `apps/cli-go/internal/db/start/start.go` `Run`. Native TS + * orchestrates: it validates config, checks whether the database is already + * running (printing Go's "already running" line), and otherwise delegates the + * container bootstrap to the hidden Go `__db-bootstrap` seam (create container + + * health + initial schema + `_current_branch`), whose progress is teed to stderr. + * + * Parity notes: this is `db start`, NOT the top-level `supabase start`. It does + * NOT print a status table and does NOT fire `cli_stack_started` — those belong to + * `internal/start/start.go`. There is no `Finished` line. + */ export const legacyDbStart = Effect.fn("legacy.db.start")(function* (flags: LegacyDbStartFlags) { - const proxy = yield* LegacyGoProxy; - const args: string[] = ["db", "start"]; - if (Option.isSome(flags.fromBackup)) args.push("--from-backup", flags.fromBackup.value); - yield* proxy.exec(args); + const output = yield* Output; + const cliConfig = yield* LegacyCliConfig; + const seam = yield* LegacyDbBootstrapSeam; + const telemetryState = yield* LegacyTelemetryState; + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const runtimeInfo = yield* RuntimeInfo; + + const body = Effect.gen(function* () { + // Go's `flags.LoadConfig(fsys)` runs first thing in `start.Run` + // (`internal/db/start/start.go:45`): a missing config is tolerated (defaults), but + // a present config that is malformed, references an undecryptable `encrypted:` + // secret, or fails Validate aborts before any container work. `legacyCheckDbToml` + // is that exact load+validate — call it here (not via the seam's best-effort read, + // which swallows config errors) so `db start` fails fast on a broken config. + yield* legacyCheckDbToml(fs, path, cliConfig.workdir); + + // Go's AssertSupabaseDbIsRunning: if the db container is already up, print to + // stderr and return nil (exit 0). + const running = yield* seam.isDbRunning(); + if (running) { + if (output.format === "text") { + yield* output.raw("Postgres database is already running.\n", "stderr"); + } else { + yield* output.success("Postgres database is already running.", { + status: "already-running", + }); + } + return; + } + + // Not running → bootstrap the container (StartDatabase + DockerRemoveAll on + // failure). The seam tees "Starting database...", "Initialising schema...", + // etc. to stderr. + // + // Resolve a relative `--from-backup` against the CALLER's cwd, mirroring Go's + // `StartDatabase` (`filepath.Join(utils.CurrentDirAbs, fromBackup)`, start.go:160-161) + // where `CurrentDirAbs` is captured before `ChangeWorkDir`. The seam spawns the Go child + // with cwd = the project workdir, so passing a relative path would resolve it against the + // project root (wrong file / not found) when `db start` runs from a subdirectory or with + // `--workdir`. Passing an absolute path makes the child's resolution a no-op. + const fromBackupFlag = Option.getOrUndefined(flags.fromBackup); + // An empty `--from-backup ""` is a normal no-backup start in Go (`len(fromBackup) == 0`), + // so treat it as absent rather than joining it to a directory path. + const fromBackup = + fromBackupFlag === undefined || fromBackupFlag === "" + ? undefined + : path.isAbsolute(fromBackupFlag) + ? fromBackupFlag + : path.join(runtimeInfo.cwd, fromBackupFlag); + yield* seam.startDatabase({ fromBackup }); + + if (output.format !== "text") { + yield* output.success("Started local database.", { status: "started" }); + } + }); + + // db start is local-only — no project ref, so no linked-project cache write. + // Telemetry still flushes on success and failure (Go's PersistentPostRun). + yield* body.pipe(Effect.ensuring(telemetryState.flush)); }); diff --git a/apps/cli/src/legacy/commands/db/start/start.integration.test.ts b/apps/cli/src/legacy/commands/db/start/start.integration.test.ts new file mode 100644 index 0000000000..bc560dd44e --- /dev/null +++ b/apps/cli/src/legacy/commands/db/start/start.integration.test.ts @@ -0,0 +1,240 @@ +import { mkdirSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; + +import { BunServices } from "@effect/platform-bun"; +import { describe, expect, it } from "@effect/vitest"; +import { Effect, Exit, Layer, Option } from "effect"; + +import { mockOutput, mockRuntimeInfo } from "../../../../../tests/helpers/mocks.ts"; +import { + mockLegacyCliConfig, + mockLegacyTelemetryStateTracked, + useLegacyTempWorkdir, +} from "../../../../../tests/helpers/legacy-mocks.ts"; +import type { OutputFormat } from "../../../../shared/output/types.ts"; +import { LegacyDbBootstrapError } from "../shared/legacy-db-bootstrap.errors.ts"; +import { LegacyDbBootstrapSeam } from "../shared/legacy-db-bootstrap.seam.service.ts"; +import { legacyDbStart } from "./start.handler.ts"; +import type { LegacyDbStartFlags } from "./start.command.ts"; + +const DEFAULT_FLAGS: LegacyDbStartFlags = { fromBackup: Option.none() }; + +/** + * Stateful mock of the container-bootstrap seam. `running` drives + * `AssertSupabaseDbIsRunning`; `runningFails` / `startFails` make the respective + * call fail (Docker daemon down / StartDatabase error). Records the args passed to + * `startDatabase`. + */ +function mockSeam(opts: { running?: boolean; runningFails?: boolean; startFails?: boolean } = {}) { + const startCalls: Array<{ fromBackup?: string }> = []; + const layer = Layer.succeed(LegacyDbBootstrapSeam, { + isDbRunning: () => + opts.runningFails === true + ? Effect.fail(new LegacyDbBootstrapError({ message: "failed to inspect service" })) + : Effect.succeed(opts.running ?? false), + startDatabase: (args: { fromBackup?: string }) => + opts.startFails === true + ? Effect.fail(new LegacyDbBootstrapError({ message: "failed to bootstrap" })) + : Effect.sync(() => { + startCalls.push(args); + }), + recreateDatabase: () => Effect.void, + awaitStorageReady: () => Effect.succeed(false), + }); + return { + layer, + get startCalls() { + return startCalls; + }, + }; +} + +function setup( + workdir: string, + opts: { + toml?: string; + format?: OutputFormat; + running?: boolean; + runningFails?: boolean; + startFails?: boolean; + /** Caller cwd (Go's `CurrentDirAbs`) for relative `--from-backup` resolution. */ + cwd?: string; + }, +) { + if (opts.toml !== undefined) { + mkdirSync(join(workdir, "supabase"), { recursive: true }); + writeFileSync(join(workdir, "supabase", "config.toml"), opts.toml); + } + const out = mockOutput({ format: opts.format ?? "text" }); + const seam = mockSeam(opts); + const telemetry = mockLegacyTelemetryStateTracked(); + const layer = Layer.mergeAll( + out.layer, + seam.layer, + mockLegacyCliConfig({ workdir }), + telemetry.layer, + mockRuntimeInfo({ cwd: opts.cwd ?? workdir }), + BunServices.layer, + ); + return { layer, out, seam, telemetry }; +} + +describe("legacy db start", () => { + const tmp = useLegacyTempWorkdir("supabase-db-start-"); + + it.live("reports an already-running database without starting a container", () => { + const { layer, out, seam, telemetry } = setup(tmp.current, { + toml: 'project_id = "test"\n', + running: true, + }); + return Effect.gen(function* () { + yield* legacyDbStart(DEFAULT_FLAGS).pipe(Effect.provide(layer)); + expect(out.stderrText).toContain("Postgres database is already running."); + expect(seam.startCalls).toHaveLength(0); + expect(telemetry.flushed).toBe(true); + }); + }); + + it.live("starts the database when it is not running", () => { + const { layer, out, seam } = setup(tmp.current, { + toml: 'project_id = "test"\n', + running: false, + }); + return Effect.gen(function* () { + yield* legacyDbStart(DEFAULT_FLAGS).pipe(Effect.provide(layer)); + expect(seam.startCalls).toEqual([{ fromBackup: undefined }]); + // db start prints no "Finished" line and no status table. + expect(out.stderrText).not.toContain("Finished"); + }); + }); + + it.live("forwards an absolute --from-backup to the bootstrap seam unchanged", () => { + const { layer, seam } = setup(tmp.current, { toml: 'project_id = "test"\n' }); + return Effect.gen(function* () { + yield* legacyDbStart({ fromBackup: Option.some("/tmp/dump.sql") }).pipe( + Effect.provide(layer), + ); + expect(seam.startCalls).toEqual([{ fromBackup: "/tmp/dump.sql" }]); + }); + }); + + it.live("resolves a relative --from-backup against the caller cwd, not the workdir", () => { + // Go resolves a relative fromBackup against `CurrentDirAbs` (the caller cwd, captured + // before ChangeWorkDir), so the seam must receive the caller-relative absolute path even + // though its Go child runs with cwd = the project workdir. + const { layer, seam } = setup(tmp.current, { + toml: 'project_id = "test"\n', + cwd: "/caller/here", + }); + return Effect.gen(function* () { + yield* legacyDbStart({ fromBackup: Option.some("dump.sql") }).pipe(Effect.provide(layer)); + expect(seam.startCalls).toEqual([{ fromBackup: "/caller/here/dump.sql" }]); + }); + }); + + it.live("treats an empty --from-backup as a normal no-backup start", () => { + // Go's StartDatabase sees `len(fromBackup) == 0` and starts without a backup; an empty + // string must not be joined to the caller cwd and passed as a directory path. + const { layer, seam } = setup(tmp.current, { + toml: 'project_id = "test"\n', + cwd: "/caller/here", + }); + return Effect.gen(function* () { + yield* legacyDbStart({ fromBackup: Option.some("") }).pipe(Effect.provide(layer)); + expect(seam.startCalls).toEqual([{ fromBackup: undefined }]); + }); + }); + + it.live("proceeds with no config file (missing config is tolerated)", () => { + const { layer, seam } = setup(tmp.current, { running: false }); + return Effect.gen(function* () { + yield* legacyDbStart(DEFAULT_FLAGS).pipe(Effect.provide(layer)); + expect(seam.startCalls).toHaveLength(1); + }); + }); + + it.live("fails fast on a malformed config.toml", () => { + const { layer, seam, telemetry } = setup(tmp.current, { + toml: 'project_id = "unterminated\n', + }); + return Effect.gen(function* () { + const exit = yield* legacyDbStart(DEFAULT_FLAGS).pipe(Effect.provide(layer), Effect.exit); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(JSON.stringify(exit.cause)).toContain("failed to load config"); + } + // No container work attempted; telemetry still flushes on failure. + expect(seam.startCalls).toHaveLength(0); + expect(telemetry.flushed).toBe(true); + }); + }); + + it.live("fails fast on an undecryptable secret even when the db is already running", () => { + // Regression for the seam swallowing config-load errors: Go runs `flags.LoadConfig` + // (which decrypts every secret) BEFORE `AssertSupabaseDbIsRunning`, so a broken + // config aborts `db start` regardless of container state. Previously the handler's + // only config read was the seam's best-effort one, so an undecryptable secret with + // the container already up printed "already running" and exited 0. + const { layer, out } = setup(tmp.current, { + toml: '[db]\nroot_key = "encrypted:anything"\n', + running: true, + }); + return Effect.gen(function* () { + const exit = yield* legacyDbStart(DEFAULT_FLAGS).pipe(Effect.provide(layer), Effect.exit); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(JSON.stringify(exit.cause)).toContain("failed to parse config: missing private key"); + } + expect(out.stderrText).not.toContain("already running"); + }); + }); + + it.live("propagates a Docker inspect failure", () => { + const { layer } = setup(tmp.current, { toml: 'project_id = "test"\n', runningFails: true }); + return Effect.gen(function* () { + const exit = yield* legacyDbStart(DEFAULT_FLAGS).pipe(Effect.provide(layer), Effect.exit); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(JSON.stringify(exit.cause)).toContain("failed to inspect service"); + } + }); + }); + + it.live("propagates a StartDatabase failure", () => { + const { layer } = setup(tmp.current, { toml: 'project_id = "test"\n', startFails: true }); + return Effect.gen(function* () { + const exit = yield* legacyDbStart(DEFAULT_FLAGS).pipe(Effect.provide(layer), Effect.exit); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(JSON.stringify(exit.cause)).toContain("failed to bootstrap"); + } + }); + }); + + it.live("emits a json result when the database is already running", () => { + const { layer, out } = setup(tmp.current, { + toml: 'project_id = "test"\n', + running: true, + format: "json", + }); + return Effect.gen(function* () { + yield* legacyDbStart(DEFAULT_FLAGS).pipe(Effect.provide(layer)); + const success = out.messages.find((m) => m.type === "success"); + expect(success?.data?.["status"]).toBe("already-running"); + }); + }); + + it.live("emits a json result after starting the database", () => { + const { layer, out, seam } = setup(tmp.current, { + toml: 'project_id = "test"\n', + running: false, + format: "json", + }); + return Effect.gen(function* () { + yield* legacyDbStart(DEFAULT_FLAGS).pipe(Effect.provide(layer)); + expect(seam.startCalls).toHaveLength(1); + const success = out.messages.find((m) => m.type === "success"); + expect(success?.data?.["status"]).toBe("started"); + }); + }); +}); diff --git a/apps/cli/src/legacy/commands/db/start/start.layers.ts b/apps/cli/src/legacy/commands/db/start/start.layers.ts new file mode 100644 index 0000000000..71603292ee --- /dev/null +++ b/apps/cli/src/legacy/commands/db/start/start.layers.ts @@ -0,0 +1,27 @@ +import { Layer } from "effect"; + +import { commandRuntimeLayer } from "../../../../shared/runtime/command-runtime.layer.ts"; +import { legacyCliConfigLayer } from "../../../config/legacy-cli-config.layer.ts"; +import { legacyDebugLoggerLayer } from "../../../shared/legacy-debug-logger.layer.ts"; +import { legacyTelemetryStateLayer } from "../../../telemetry/legacy-telemetry-state.layer.ts"; +import { legacyDbBootstrapSeamLayer } from "../shared/legacy-db-bootstrap.seam.layer.ts"; + +/** + * Runtime layer for `supabase db start`. The command is local-only, so it needs + * far less than the remote-capable db commands: just the container-bootstrap seam + * (`db __db-bootstrap`), the CLI config (workdir + project id), and the telemetry + * flush. The seam's other dependencies (`LegacyNetworkIdFlag`, `LegacyProfileFlag`, + * `ChildProcessSpawner`, `FileSystem`, `Path`) are ambient from the root runtime, + * matching how `db diff` composes the `db __shadow` seam. `LegacyCliConfig` is + * provided to the seam explicitly (legacy CLAUDE.md rule 5). + */ +const cliConfig = legacyCliConfigLayer.pipe(Layer.provide(legacyDebugLoggerLayer)); + +const seam = legacyDbBootstrapSeamLayer.pipe(Layer.provide(cliConfig)); + +export const legacyDbStartRuntimeLayer = Layer.mergeAll( + seam, + cliConfig, + legacyTelemetryStateLayer, + commandRuntimeLayer(["db", "start"]), +); diff --git a/apps/cli/src/legacy/commands/seed/buckets/buckets.handler.ts b/apps/cli/src/legacy/commands/seed/buckets/buckets.handler.ts index 9ddb8fdb86..24b2543055 100644 --- a/apps/cli/src/legacy/commands/seed/buckets/buckets.handler.ts +++ b/apps/cli/src/legacy/commands/seed/buckets/buckets.handler.ts @@ -1,138 +1,31 @@ -import { - loadProjectConfig, - type LoadProjectConfigOptions, - ProjectConfigSchema, -} from "@supabase/config"; -import { Effect, FileSystem, Option, Path, Schema } from "effect"; -import { FetchHttpClient } from "effect/unstable/http"; -import type { PlatformError } from "effect/PlatformError"; +import { Effect, Option } from "effect"; import { CliArgs } from "../../../../shared/cli/cli-args.service.ts"; -import { legacyResolveYes } from "../../../../shared/legacy/global-flags.ts"; -import { LegacyCliConfig } from "../../../config/legacy-cli-config.service.ts"; import { LegacyProjectRefResolver } from "../../../config/legacy-project-ref.service.ts"; -import { legacySeedChangedTargetFlags } from "./buckets.flags.ts"; -import { legacyBold, legacyYellow } from "../../../shared/legacy-colors.ts"; -import { - legacyResolveStorageCredentials, - legacyStorageGatewayFetch, -} from "../../../shared/legacy-storage-credentials.ts"; -import { - legacyParseFileSizeLimit, - legacyResolveBucketProps, -} from "../../../shared/legacy-storage-bucket-config.ts"; -import { - type LegacyStorageGateway, - type LegacyUpsertBucketProps, - legacyMakeStorageGateway, -} from "../../../shared/legacy-storage-gateway.ts"; -import type { LegacyStorageGatewayError } from "../../../shared/legacy-storage-gateway.errors.ts"; -import { Output } from "../../../../shared/output/output.service.ts"; -import { - legacyIsLocalVectorBucketsUnavailable, - legacyIsVectorBucketsFeatureNotEnabled, -} from "./buckets.classify.ts"; -import { LegacySeedConfigLoadError } from "./buckets.errors.ts"; -import { legacyBucketObjectKey } from "./buckets.upload.ts"; -import { legacyPromptYesNo } from "../../../shared/legacy-prompt-yes-no.ts"; -import { - legacyContentTypeForUpload, - legacyReadSniffBytes, -} from "../../../shared/legacy-storage-content-type.ts"; +import { legacySeedBucketsRun } from "../../../shared/legacy-seed-buckets.ts"; import { LegacyLinkedProjectCache } from "../../../telemetry/legacy-linked-project-cache.service.ts"; import { LegacyTelemetryState } from "../../../telemetry/legacy-telemetry-state.service.ts"; +import { legacySeedChangedTargetFlags } from "./buckets.flags.ts"; import type { LegacyBucketsFlags } from "./buckets.command.ts"; -const CONFIG_PATH = "supabase/config.toml"; -const UPLOAD_CONCURRENCY = 5; - -/** - * Mirrors Go's `ValidateBucketName` regex (`apps/cli-go/pkg/config/config.go:1382`). - * Used to validate `[storage.buckets]` names before any Storage API call, matching - * Go's config-load-time check (`config.go:899-903`). Vector and analytics names are - * NOT validated here — Go only validates `[storage.buckets]`. - */ -const LEGACY_BUCKET_NAME_PATTERN = /^(?:[0-9A-Za-z_]|!|-|\.|\*|'|\(|\)| |&|\$|@|=|;|:|\+|,|\?)*$/; - -/** - * Verbatim Go regex literal (`config.go:1382`) — used in the error message so it - * is byte-identical to Go's output. Do NOT derive from `LEGACY_BUCKET_NAME_PATTERN.source`. - */ -const LEGACY_BUCKET_NAME_PATTERN_SOURCE = - "^(\\w|!|-|\\.|\\*|'|\\(|\\)| |&|\\$|@|=|;|:|\\+|,|\\?)*$"; - -const legacyValidateBucketName = Effect.fnUntraced(function* (name: string) { - if (!LEGACY_BUCKET_NAME_PATTERN.test(name)) { - return yield* new LegacySeedConfigLoadError({ - message: `Invalid Bucket name: ${name}. Only lowercase letters, numbers, dots, hyphens, and spaces are allowed. (${LEGACY_BUCKET_NAME_PATTERN_SOURCE})`, - }); - } -}); - -interface CollectedFile { - readonly absPath: string; - readonly displayPath: string; -} - -/** Mutable run summary, emitted as the structured result in json/stream-json mode. */ -interface SeedSummary { - readonly buckets_created: Array; - readonly buckets_updated: Array; - readonly buckets_skipped: Array; - readonly vector_created: Array; - readonly vector_pruned: Array; - vector_skipped: boolean; - readonly objects_uploaded: Array; - readonly analytics_created: Array; - readonly analytics_pruned: Array; -} - -function emptySummary(): SeedSummary { - return { - buckets_created: [], - buckets_updated: [], - buckets_skipped: [], - vector_created: [], - vector_pruned: [], - vector_skipped: false, - objects_uploaded: [], - analytics_created: [], - analytics_pruned: [], - }; -} - -/** - * Embedded-default project config, decoded from an empty object — the same - * `decodeUnknownSync(ProjectConfigSchema)({})` the loader uses internally - * (`packages/config/src/io.ts:54-56`). Go's `seed buckets` never aborts on a - * missing `config.toml`: it reads the package-global `utils.Config`, initialized - * to embedded defaults, and `config.Load` no-ops on a missing file. So "no - * config file" behaves like the embedded-default config. - */ -const legacyDecodeDefaultProjectConfig = Schema.decodeUnknownSync(ProjectConfigSchema); - /** * `supabase seed buckets` — seeds Storage buckets from * `[storage.buckets]` / `[storage.vector]` in `supabase/config.toml`. * * Port of `apps/cli-go/internal/seed/buckets/buckets.go`. When `--linked` is * passed, the remote Storage gateway is used with the project's service-role key; - * otherwise the local stack is used. + * otherwise the local stack is used. The seeding work lives in the hoisted + * `legacySeedBucketsRun` (shared with `db reset --local`); this handler owns the + * target-flag resolution and the post-run cache + telemetry side effects. */ export const legacySeedBuckets = Effect.fn("legacy.seed.buckets")(function* ( // Target is selected from the changed-flag set (Go's flag.Changed), not the // parsed value, so the flags arg itself is unused here. _flags: LegacyBucketsFlags, ) { - const output = yield* Output; - const cliConfig = yield* LegacyCliConfig; const telemetryState = yield* LegacyTelemetryState; const linkedProjectCache = yield* LegacyLinkedProjectCache; - const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; const cliArgs = yield* CliArgs; - // `--yes` OR `SUPABASE_YES` (Go's viper AutomaticEnv, root.go:318-320). - const yes = yield* legacyResolveYes; // Set once --linked resolves a ref; drives the post-run linked-project cache // write + org/project group identify, mirroring Go's `ensureProjectGroupsCached` @@ -141,135 +34,18 @@ export const legacySeedBuckets = Effect.fn("legacy.seed.buckets")(function* ( let linkedRef = ""; yield* Effect.gen(function* () { - // 1. Resolve the project ref for --linked BEFORE loading config, so that - // the matching `[remotes.]` override (whose `project_id == ref`) is - // merged over the base config by `loadProjectConfig`. Go selects the target - // from `flag.Changed`, not the flag value: `--linked` is the linked path - // whenever it's *set* (even `--linked=false`). + // Resolve the project ref for --linked BEFORE loading config, so that the + // matching `[remotes.]` override (whose `project_id == ref`) is merged + // over the base config by `loadProjectConfig`. Go selects the target from + // `flag.Changed`, not the flag value: `--linked` is the linked path whenever + // it's *set* (even `--linked=false`). const setFlags = legacySeedChangedTargetFlags(cliArgs.args); const projectRefResolver = yield* LegacyProjectRefResolver; const projectRef = setFlags.includes("linked") ? yield* projectRefResolver.loadProjectRef(Option.none()) : ""; linkedRef = projectRef; - - // 2. Load config.toml, passing projectRef so `[remotes.*]` overrides are - // merged for --linked. A parse failure aborts before any network call. - const loadOptions: LoadProjectConfigOptions | undefined = - projectRef !== "" ? { projectRef } : undefined; - const loaded = yield* loadProjectConfig(cliConfig.workdir, loadOptions).pipe( - Effect.catchTag( - "ProjectConfigParseError", - (cause) => - new LegacySeedConfigLoadError({ - message: `failed to parse supabase/config.toml: ${String(cause.cause)}`, - }), - ), - ); - // A missing config file is NOT an early exit: Go uses embedded defaults and - // still gates the no-op on `len(projectRef) == 0`. So local + no-config falls - // into the no-op short-circuit; `--linked` + no-config falls through to the - // remote path so auth/project/API failures surface. - const config = loaded === null ? legacyDecodeDefaultProjectConfig({}) : loaded.config; - const document = loaded === null ? undefined : loaded.document; - - // Go prints this from inside config load (`config.go:513`) whenever a - // `[remotes.*]` block matched the linked ref. stderr in all output modes. - if (loaded !== null && loaded.appliedRemote !== undefined) { - yield* output.raw(`Loading config override: [remotes.${loaded.appliedRemote}]\n`, "stderr"); - } - const bucketsConfig = config.storage.buckets ?? {}; - const bucketNames = Object.keys(bucketsConfig); - const vectorEnabled = config.storage.vector.enabled; - const vectorBucketNames = Object.keys(config.storage.vector.buckets); - const hasVectorBuckets = vectorBucketNames.length > 0; - - // 3. Config-load-time validations run BEFORE the no-op short-circuit: Go - // decodes the whole config (storage.FileSizeLimit, bucket sizes) and runs - // ValidateBucketName during config.Load — before `buckets.Run` can take its - // no-op path — so an invalid value fails even when there's nothing to seed. - // - // 3a. Bucket names (Go ValidateBucketName, config.go:899-903). - for (const name of bucketNames) { - yield* legacyValidateBucketName(name); - } - - // 3b. Storage-level file_size_limit, parsed unconditionally. - const storageFileSizeLimitBytes = yield* parseFileSizeLimitOrFail( - config.storage.file_size_limit, - ); - - // 3c. Per-bucket props (sizes parsed before any Storage call). - const bucketPropsByName = new Map(); - for (const [name, bucket] of Object.entries(bucketsConfig)) { - bucketPropsByName.set( - name, - yield* computeBucketProps(document, name, bucket, storageFileSizeLimitBytes), - ); - } - - // 3d. Short-circuit: nothing to seed (ref present → never short-circuits). - if (projectRef === "" && bucketNames.length === 0 && !hasVectorBuckets) { - if (output.format !== "text") { - yield* output.success("", { ...emptySummary() }); - } - return; - } - - // 4. Build the Storage service-gateway client (local or remote). - const credentials = yield* legacyResolveStorageCredentials({ projectRef, config }); - - // All gateway operations run with an explicit non-DoH fetch (CA-trusting for - // local + https, plain `globalThis.fetch` otherwise). The api-keys lookup - // inside `legacyResolveStorageCredentials` runs BEFORE this scope, so it - // still honors `--dns-resolver https`, matching Go's `tenant.GetApiKeys`. - const gatewayOps = Effect.gen(function* () { - const gateway = yield* legacyMakeStorageGateway({ - baseUrl: credentials.baseUrl, - apiKey: credentials.apiKey, - userAgent: cliConfig.userAgent, - }); - - const summary = emptySummary(); - - // 5. Upsert configured buckets. - yield* upsertBuckets(output, yes, gateway, bucketPropsByName, summary); - - // 6. Upsert analytics buckets (remote --linked only). - if (config.storage.analytics.enabled && projectRef !== "") { - yield* output.raw("Updating analytics buckets...\n", "stderr"); - yield* upsertAnalyticsBuckets( - output, - yes, - gateway, - Object.keys(config.storage.analytics.buckets), - summary, - ); - } - - // 7. Upsert vector buckets (local), with graceful skip on unavailability. - if (vectorEnabled && hasVectorBuckets) { - yield* output.raw("Updating vector buckets...\n", "stderr"); - yield* upsertVectorBuckets(output, yes, gateway, vectorBucketNames, summary).pipe( - Effect.catch((error) => handleVectorError(output, error, summary)), - ); - } - - // 8. Upload objects for each bucket with a configured objects_path. - yield* uploadObjects(fs, path, output, gateway, cliConfig.workdir, bucketsConfig, summary); - - // 9. Machine-readable summary (Go has none; text mode emits nothing extra). - if (output.format !== "text") { - yield* output.success("", { ...summary }); - } - }); - - yield* gatewayOps.pipe( - Effect.provideService( - FetchHttpClient.Fetch, - legacyStorageGatewayFetch(credentials.localKongCa), - ), - ); + yield* legacySeedBucketsRun({ projectRef, emitSummary: true }); }).pipe( // Go's root `Execute` caches the linked project + fires org/project group // identify whenever `flags.ProjectRef` is set — only on the --linked path. @@ -279,315 +55,3 @@ export const legacySeedBuckets = Effect.fn("legacy.seed.buckets")(function* ( Effect.ensuring(telemetryState.flush), ); }); - -type BucketsConfig = Readonly< - Record< - string, - { - readonly public: boolean; - readonly file_size_limit: string; - readonly allowed_mime_types: ReadonlyArray; - readonly objects_path: string; - } - > ->; - -// Parse a `file_size_limit` string to bytes, mapping a parse failure to a -// config-load error (Go rejects an invalid `sizeInBytes` during `config.Load`, -// before NewStorageAPI). -const parseFileSizeLimitOrFail = (value: string) => - Effect.try({ - try: () => legacyParseFileSizeLimit(value), - catch: (cause) => - new LegacySeedConfigLoadError({ - message: cause instanceof Error ? cause.message : String(cause), - }), - }); - -const computeBucketProps = ( - document: Record | undefined, - name: string, - bucket: BucketsConfig[string], - storageFileSizeLimitBytes: number, -) => - Effect.try({ - try: () => legacyResolveBucketProps({ document, name, bucket, storageFileSizeLimitBytes }), - catch: (cause) => - new LegacySeedConfigLoadError({ - message: cause instanceof Error ? cause.message : String(cause), - }), - }); - -// Port of `pkg/storage/batch.go:UpsertBuckets`. `propsByName` is precomputed and -// size-validated before this runs (Go parses sizes at config-load, before any -// Storage call). -const upsertBuckets = Effect.fnUntraced(function* ( - output: typeof Output.Service, - yes: boolean, - gateway: LegacyStorageGateway, - propsByName: ReadonlyMap, - summary: SeedSummary, -) { - const existing = yield* gateway.listBuckets(); - const byName = new Map(existing.map((b) => [b.name, b.id])); - - for (const [name, props] of propsByName) { - const bucketId = byName.get(name); - if (bucketId !== undefined) { - const overwrite = yield* legacyPromptYesNo( - output, - yes, - `Bucket ${legacyBold(bucketId)} already exists. Do you want to overwrite its properties?`, - true, - ); - if (!overwrite) { - summary.buckets_skipped.push(bucketId); - continue; - } - yield* output.raw(`Updating Storage bucket: ${bucketId}\n`, "stderr"); - yield* gateway.updateBucket(bucketId, props); - summary.buckets_updated.push(bucketId); - } else { - yield* output.raw(`Creating Storage bucket: ${name}\n`, "stderr"); - yield* gateway.createBucket(name, props); - summary.buckets_created.push(name); - } - } -}); - -// Port of `pkg/storage/vector.go:UpsertVectorBuckets`. -const upsertVectorBuckets = Effect.fnUntraced(function* ( - output: typeof Output.Service, - yes: boolean, - gateway: LegacyStorageGateway, - configuredNames: ReadonlyArray, - summary: SeedSummary, -) { - const existing = yield* gateway.listVectorBuckets(); - const existingSet = new Set(existing); - const configuredSet = new Set(configuredNames); - const toDelete = existing.filter((name) => !configuredSet.has(name)); - - for (const name of configuredNames) { - if (existingSet.has(name)) { - yield* output.raw(`Bucket already exists: ${name}\n`, "stderr"); - continue; - } - yield* output.raw(`Creating vector bucket: ${name}\n`, "stderr"); - yield* gateway.createVectorBucket(name); - summary.vector_created.push(name); - } - - for (const name of toDelete) { - const prune = yield* legacyPromptYesNo( - output, - yes, - `Bucket ${legacyBold(name)} not found in ${legacyBold(CONFIG_PATH)}. Do you want to prune it?`, - false, - ); - if (!prune) { - continue; - } - yield* output.raw(`Pruning vector bucket: ${name}\n`, "stderr"); - yield* gateway.deleteVectorBucket(name); - summary.vector_pruned.push(name); - } -}); - -// Port of `pkg/storage/analytics.go:UpsertAnalyticsBuckets`. -const upsertAnalyticsBuckets = Effect.fnUntraced(function* ( - output: typeof Output.Service, - yes: boolean, - gateway: LegacyStorageGateway, - configuredNames: ReadonlyArray, - summary: SeedSummary, -) { - const existing = yield* gateway.listAnalyticsBuckets(); - const existingSet = new Set(existing); - const configuredSet = new Set(configuredNames); - const toDelete = existing.filter((name) => !configuredSet.has(name)); - - for (const name of configuredNames) { - if (existingSet.has(name)) { - yield* output.raw(`Bucket already exists: ${name}\n`, "stderr"); - continue; - } - yield* output.raw(`Creating analytics bucket: ${name}\n`, "stderr"); - yield* gateway.createAnalyticsBucket(name); - summary.analytics_created.push(name); - } - - for (const name of toDelete) { - const prune = yield* legacyPromptYesNo( - output, - yes, - `Bucket ${legacyBold(name)} not found in ${legacyBold(CONFIG_PATH)}. Do you want to prune it?`, - false, - ); - if (!prune) { - continue; - } - yield* output.raw(`Pruning analytics bucket: ${name}\n`, "stderr"); - yield* gateway.deleteAnalyticsBucket(name); - summary.analytics_pruned.push(name); - } -}); - -/** - * Vector graceful-skip (`buckets.go:57-66`): on `FeatureNotEnabled` / - * local-unavailable errors, print the matching WARNING and continue (object - * upload still runs). Any other error propagates. - */ -const handleVectorError = Effect.fnUntraced(function* ( - output: typeof Output.Service, - error: LegacyStorageGatewayError, - summary: SeedSummary, -) { - if (legacyIsVectorBucketsFeatureNotEnabled(error.message)) { - yield* output.raw( - `${legacyYellow("WARNING:")} Vector buckets are not available in this project's region yet. Skipping vector bucket seeding.\n`, - "stderr", - ); - summary.vector_skipped = true; - return; - } - if (legacyIsLocalVectorBucketsUnavailable(error.message)) { - yield* output.raw( - `${legacyYellow("WARNING:")} Vector buckets are not available in the local storage service. If this project is linked, run \`supabase link\` to update service versions, then restart the local stack. Skipping vector bucket seeding.\n`, - "stderr", - ); - summary.vector_skipped = true; - return; - } - return yield* Effect.fail(error); -}); - -// Port of `pkg/storage/batch.go:UpsertObjects` (+ object walk in objects.go). -const uploadObjects = Effect.fnUntraced(function* ( - fs: FileSystem.FileSystem, - path: Path.Path, - output: typeof Output.Service, - gateway: LegacyStorageGateway, - workdir: string, - bucketsConfig: BucketsConfig, - summary: SeedSummary, -) { - for (const [name, bucket] of Object.entries(bucketsConfig)) { - const objectsPath = bucket.objects_path; - if (objectsPath.length === 0) { - continue; - } - // Go resolves a relative bucket objects_path against SupabaseDirPath at - // config-resolve time (`pkg/config/config.go:757-759`); absolute paths are - // left untouched. `displayRoot` (workdir-relative) drives the `Uploading:` - // stderr and the destination key so both stay byte-identical to Go. - const displayRoot = path.isAbsolute(objectsPath) - ? objectsPath - : path.join("supabase", objectsPath); - const absRoot = path.isAbsolute(objectsPath) - ? objectsPath - : path.join(workdir, "supabase", objectsPath); - const files = yield* collectFiles(fs, path, output, absRoot, displayRoot); - yield* Effect.forEach( - files, - (file) => - Effect.gen(function* () { - const dstPath = legacyBucketObjectKey(name, displayRoot, file.displayPath); - yield* output.raw(`Uploading: ${file.displayPath} => ${dstPath}\n`, "stderr"); - // Content-type is byte-driven: Go sniffs the first 512 bytes with - // http.DetectContentType, refining only a generic text/plain by - // extension (`pkg/storage/objects.go:77-108`). - const sniff = yield* legacyReadSniffBytes(fs, file.absPath); - // Go's seed upload always sets Cache-Control max-age=3600 and x-upsert - // (Overwrite) true (`pkg/storage/batch.go`). - yield* gateway.uploadObject(dstPath, file.absPath, { - contentType: legacyContentTypeForUpload(sniff, file.absPath), - cacheControl: "max-age=3600", - overwrite: true, - }); - summary.objects_uploaded.push(dstPath); - }), - { concurrency: UPLOAD_CONCURRENCY }, - ); - } -}); - -/** - * Collect uploadable files under `absRoot`, lexically ordered, mirroring Go's - * `fs.WalkDir` + `isUploadableEntry` (`pkg/storage/batch.go:65-131`). - * - * Parity details: - * - The **root** is resolved with a following stat (Go's `fs.Stat`), so a - * symlinked `objects_path` is followed; a missing/dangling root fails. - * - **Nested** entries use no-follow detection: real directories are descended; - * symlinks are NOT descended — Go's `isUploadableEntry` OPENS the symlink - * target then stats the handle, uploading only a regular file and skipping - * dangling symlinks / symlinks-to-directories / unreadable targets. - */ -const collectFiles = ( - fs: FileSystem.FileSystem, - path: Path.Path, - output: typeof Output.Service, - absRoot: string, - displayRoot: string, -): Effect.Effect, PlatformError> => - Effect.gen(function* () { - const info = yield* fs.stat(absRoot); - if (info.type === "Directory") { - return yield* collectDir(fs, path, output, absRoot, displayRoot); - } - if (info.type === "File") { - return [{ absPath: absRoot, displayPath: displayRoot }]; - } - yield* output.raw(`Skipping non-regular file: ${displayRoot}\n`, "stderr"); - return []; - }); - -const collectDir = ( - fs: FileSystem.FileSystem, - path: Path.Path, - output: typeof Output.Service, - absDir: string, - displayDir: string, -): Effect.Effect, PlatformError> => - Effect.gen(function* () { - const names = [...(yield* fs.readDirectory(absDir))].sort(); - const collected: Array = []; - for (const name of names) { - const absChild = path.join(absDir, name); - const displayChild = path.join(displayDir, name); - // `readLink` succeeds only on a symlink — our no-follow detector (Effect's - // `stat` follows symlinks and has no `lstat`). - const isSymlink = yield* fs.readLink(absChild).pipe( - Effect.as(true), - Effect.catch(() => Effect.succeed(false)), - ); - if (isSymlink) { - // Go `isUploadableEntry` (batch.go:73-84) OPENS the target then stats the - // handle; it uploads only a regular file. `stat` alone would queue an - // unreadable target and abort later at upload, so mirror Go: open + stat. - const targetType = yield* Effect.scoped( - Effect.gen(function* () { - const handle = yield* fs.open(absChild, { flag: "r" }); - const targetInfo = yield* handle.stat; - return targetInfo.type; - }), - ).pipe(Effect.catch(() => Effect.succeed("Unknown" as const))); - if (targetType === "File") { - collected.push({ absPath: absChild, displayPath: displayChild }); - } else { - yield* output.raw(`Skipping non-regular file: ${displayChild}\n`, "stderr"); - } - continue; - } - const childInfo = yield* fs.stat(absChild); - if (childInfo.type === "Directory") { - collected.push(...(yield* collectDir(fs, path, output, absChild, displayChild))); - } else if (childInfo.type === "File") { - collected.push({ absPath: absChild, displayPath: displayChild }); - } else { - yield* output.raw(`Skipping non-regular file: ${displayChild}\n`, "stderr"); - } - } - return collected; - }); diff --git a/apps/cli/src/legacy/commands/seed/buckets/buckets.integration.test.ts b/apps/cli/src/legacy/commands/seed/buckets/buckets.integration.test.ts index 986fcc4af6..c91d172f5e 100644 --- a/apps/cli/src/legacy/commands/seed/buckets/buckets.integration.test.ts +++ b/apps/cli/src/legacy/commands/seed/buckets/buckets.integration.test.ts @@ -25,6 +25,7 @@ import { LegacyYesFlag } from "../../../../shared/legacy/global-flags.ts"; import type { OutputFormat } from "../../../../shared/output/types.ts"; import { LegacyProjectRefResolver } from "../../../../legacy/config/legacy-project-ref.service.ts"; import { LegacyProjectNotLinkedError } from "../../../../legacy/config/legacy-project-ref.errors.ts"; +import { legacySeedBucketsRun } from "../../../shared/legacy-seed-buckets.ts"; import { legacySeedBuckets } from "./buckets.handler.ts"; import type { LegacyBucketsFlags } from "./buckets.command.ts"; import { LegacyPlatformApi } from "../../../../legacy/auth/legacy-platform-api.service.ts"; @@ -323,6 +324,36 @@ describe("legacy seed buckets", () => { }); }); + it.live( + "honors a piped decline for the overwrite prompt when non-interactive (db reset path)", + () => { + const { layer, out, requests } = setupLegacySeedBuckets(tmp.current, { + toml: "[storage.buckets.test]\npublic = true\n", + routes: [ + { method: "GET", match: "/storage/v1/bucket", body: [{ name: "test", id: "test" }] }, + ], + pipedAnswers: ["n"], + }); + return Effect.gen(function* () { + // db reset seeds buckets with interactive=false (Go's `buckets.Run(ctx, "", + // false, fsys)` forces console.IsTTY=false). Go does NOT silently take the + // default: the prompt still prints its label, scans one line, and honors a + // parsed answer — so the piped "n" must skip the overwrite (default is yes). + const exit = yield* legacySeedBucketsRun({ + projectRef: "", + emitSummary: false, + interactive: false, + }).pipe(Effect.provide(layer), Effect.exit); + expect(Exit.isSuccess(exit)).toBe(true); + expect(out.stderrText).toContain( + "already exists. Do you want to overwrite its properties?", + ); + expect(out.stderrText).not.toContain("Updating Storage bucket"); + expect(requests.some((r) => r.method === "PUT")).toBe(false); + }); + }, + ); + it.live("creates configured vector buckets and leaves stale ones (prune default no)", () => { const { layer, out, requests } = setupLegacySeedBuckets(tmp.current, { toml: "[storage.vector]\nenabled = true\n[storage.vector.buckets.documents-openai]\n[storage.vector.buckets.existing-vec]\n", @@ -397,6 +428,40 @@ describe("legacy seed buckets", () => { }); }); + it.live( + "prunes a stale vector bucket when the caller passes yes (db reset project-env path)", + () => { + // `db reset` resolves `yes` with the nested project `.env` and passes it into the runner + // with `interactive: false`; the pre-resolved `yes` must drive pruning even though no + // prompt is answered and the runner's own shell-only resolveYes would default to false. + const { layer, out, requests } = setupLegacySeedBuckets(tmp.current, { + toml: "[storage.vector]\nenabled = true\n[storage.vector.buckets.keep-vec]\n", + routes: [ + { method: "GET", match: "/storage/v1/bucket", body: [] }, + { + method: "POST", + match: VECTOR_LIST, + body: { + vectorBuckets: [{ vectorBucketName: "keep-vec" }, { vectorBucketName: "stale-vec" }], + }, + }, + { method: "POST", match: VECTOR_DELETE, body: {} }, + ], + // No `confirm` and no `--yes` flag — pruning is driven solely by the passed `yes`. + }); + return Effect.gen(function* () { + yield* legacySeedBucketsRun({ + projectRef: "", + emitSummary: false, + interactive: false, + yes: true, + }).pipe(Effect.provide(layer)); + expect(out.stderrText).toContain("Pruning vector bucket: stale-vec"); + expect(requests.some((r) => r.url.includes(VECTOR_DELETE))).toBe(true); + }); + }, + ); + it.live("warns and continues when vector buckets are unavailable in the region", () => { const { layer, out } = setupLegacySeedBuckets(tmp.current, { toml: "[storage.vector]\nenabled = true\n[storage.vector.buckets.documents-openai]\n", diff --git a/apps/cli/src/legacy/commands/services/services.integration.test.ts b/apps/cli/src/legacy/commands/services/services.integration.test.ts index 7e03294354..c2e9b2eca8 100644 --- a/apps/cli/src/legacy/commands/services/services.integration.test.ts +++ b/apps/cli/src/legacy/commands/services/services.integration.test.ts @@ -71,6 +71,7 @@ function setup( apiUrl: "https://api.supabase.com", projectHost: "supabase.co", poolerHost: "supabase.com", + dashboardUrl: "https://supabase.com/dashboard", accessToken: Option.none(), projectId: Option.none(), workdir: opts.workdir ?? process.cwd(), diff --git a/apps/cli/src/legacy/config/legacy-cli-config.layer.ts b/apps/cli/src/legacy/config/legacy-cli-config.layer.ts index 70ec567d30..9ced346d5f 100644 --- a/apps/cli/src/legacy/config/legacy-cli-config.layer.ts +++ b/apps/cli/src/legacy/config/legacy-cli-config.layer.ts @@ -2,7 +2,11 @@ import { Effect, FileSystem, Layer, Option, Path, Redacted } from "effect"; import { parse as parseYaml } from "yaml"; import { CLI_VERSION } from "../../shared/cli/version.ts"; import { LegacyProfileFlag, LegacyWorkdirFlag } from "../../shared/legacy/global-flags.ts"; -import { legacyPoolerHost, legacyProjectHost } from "../shared/legacy-profile.ts"; +import { + legacyDashboardUrl, + legacyPoolerHost, + legacyProjectHost, +} from "../shared/legacy-profile.ts"; import { LegacyDebugLogger, type LegacyDebugLoggerShape, @@ -16,6 +20,7 @@ interface ResolvedProfile { readonly apiUrl: string; readonly projectHost: string; readonly poolerHost: string; + readonly dashboardUrl: string; } const BUILTIN_PROFILE_API_URLS: Record = { @@ -38,6 +43,7 @@ function resolvedBuiltin(name: LegacyProfileName): ResolvedProfile { apiUrl: BUILTIN_PROFILE_API_URLS[name], projectHost: legacyProjectHost(name), poolerHost: legacyPoolerHost(name), + dashboardUrl: legacyDashboardUrl(name), }; } @@ -47,6 +53,7 @@ function safeParseYaml(text: string): api_url?: unknown; project_host?: unknown; pooler_host?: unknown; + dashboard_url?: unknown; } | undefined { try { @@ -57,6 +64,7 @@ function safeParseYaml(text: string): api_url?: unknown; project_host?: unknown; pooler_host?: unknown; + dashboard_url?: unknown; }) : undefined; } catch { @@ -147,6 +155,13 @@ function resolveProfile( // that omits `pooler_host:` yields an empty host, which disables the MITM // domain assertion — it must NOT fall back to the production `supabase.com`. poolerHost: typeof parsed.pooler_host === "string" ? parsed.pooler_host : "", + // Go's `Profile.DashboardURL` is `required` (`profile.go:20`); a YAML profile + // that omits it falls back to the built-in `supabase` dashboard here rather + // than erroring, since it only feeds the connect-failure suggestion text. + dashboardUrl: + typeof parsed.dashboard_url === "string" + ? parsed.dashboard_url + : legacyDashboardUrl("supabase"), }; }); } @@ -200,6 +215,7 @@ export const legacyCliConfigLayer = Layer.unwrap( apiUrl, projectHost, poolerHost, + dashboardUrl, } = yield* resolveProfile( profileFlag, env["SUPABASE_PROFILE"], @@ -236,6 +252,7 @@ export const legacyCliConfigLayer = Layer.unwrap( apiUrl, projectHost, poolerHost, + dashboardUrl, accessToken, projectId, workdir, diff --git a/apps/cli/src/legacy/config/legacy-cli-config.layer.unit.test.ts b/apps/cli/src/legacy/config/legacy-cli-config.layer.unit.test.ts index 541e2d4a9b..f6d8ca20f8 100644 --- a/apps/cli/src/legacy/config/legacy-cli-config.layer.unit.test.ts +++ b/apps/cli/src/legacy/config/legacy-cli-config.layer.unit.test.ts @@ -56,6 +56,7 @@ describe("legacyCliConfigLayer", () => { expect(config.apiUrl).toBe("https://api.supabase.com"); expect(config.projectHost).toBe("supabase.co"); expect(config.poolerHost).toBe("supabase.com"); + expect(config.dashboardUrl).toBe("https://supabase.com/dashboard"); }).pipe(Effect.provide(makeLayer({ cwd: tempRoot }))), ); @@ -154,7 +155,7 @@ describe("legacyCliConfigLayer", () => { ), ); - it.effect("loads api_url, name, and pooler_host from a YAML profile file", () => { + it.effect("loads api_url, name, pooler_host, and dashboard_url from a YAML profile file", () => { const profilePath = join(tempRoot, "profile.yaml"); writeFileSync( profilePath, @@ -163,6 +164,7 @@ describe("legacyCliConfigLayer", () => { 'api_url: "http://127.0.0.1:9999"', "project_host: localhost", "pooler_host: staging.example.com", + 'dashboard_url: "http://127.0.0.1:9999"', ].join("\n"), ); return Effect.gen(function* () { @@ -171,6 +173,9 @@ describe("legacyCliConfigLayer", () => { expect(config.apiUrl).toBe("http://127.0.0.1:9999"); expect(config.projectHost).toBe("localhost"); expect(config.poolerHost).toBe("staging.example.com"); + // Go reads `dashboard_url` from the profile (used by the connect-failure hint); + // the cli-e2e harness points it at the replay server for parity. + expect(config.dashboardUrl).toBe("http://127.0.0.1:9999"); }).pipe(Effect.provide(makeLayer({ env: { SUPABASE_PROFILE: profilePath }, cwd: tempRoot }))); }); @@ -183,6 +188,8 @@ describe("legacyCliConfigLayer", () => { // Go's Profile.PoolerHost is `omitempty`: an absent pooler_host disables the // MITM domain assertion rather than falling back to supabase.com. expect(config.poolerHost).toBe(""); + // An omitted dashboard_url falls back to the built-in supabase dashboard. + expect(config.dashboardUrl).toBe("https://supabase.com/dashboard"); }).pipe(Effect.provide(makeLayer({ env: { SUPABASE_PROFILE: profilePath }, cwd: tempRoot }))); }); diff --git a/apps/cli/src/legacy/config/legacy-cli-config.service.ts b/apps/cli/src/legacy/config/legacy-cli-config.service.ts index 3bef2e2a1c..432d3d267a 100644 --- a/apps/cli/src/legacy/config/legacy-cli-config.service.ts +++ b/apps/cli/src/legacy/config/legacy-cli-config.service.ts @@ -29,6 +29,14 @@ interface LegacyCliConfigShape { * db-config resolver's MITM domain check. */ readonly poolerHost: string; + /** + * Dashboard base URL for the active profile (Go's `Profile.DashboardURL`, + * `apps/cli-go/internal/utils/profile.go:20`). Sourced from the resolved profile — + * the built-in table for named profiles, or the `dashboard_url:` key of a YAML + * profile file — so staging/custom dashboards are honored. Used by the + * connect-failure suggestion (Go's `SetConnectSuggestion` network-restrictions hint). + */ + readonly dashboardUrl: string; readonly accessToken: Option.Option>; readonly projectId: Option.Option; readonly workdir: string; diff --git a/apps/cli/src/legacy/config/legacy-project-ref.layer.unit.test.ts b/apps/cli/src/legacy/config/legacy-project-ref.layer.unit.test.ts index 3990163113..04b64ff06b 100644 --- a/apps/cli/src/legacy/config/legacy-project-ref.layer.unit.test.ts +++ b/apps/cli/src/legacy/config/legacy-project-ref.layer.unit.test.ts @@ -24,6 +24,7 @@ function mockCliConfig(opts: { workdir: string; projectId?: string }) { apiUrl: "https://api.supabase.com", projectHost: "supabase.co", poolerHost: "supabase.com", + dashboardUrl: "https://supabase.com/dashboard", accessToken: Option.none(), projectId: opts.projectId === undefined ? Option.none() : Option.some(opts.projectId), workdir: opts.workdir, diff --git a/apps/cli/src/legacy/shared/legacy-connect-errors.ts b/apps/cli/src/legacy/shared/legacy-connect-errors.ts index 504deb471e..5a269abf87 100644 --- a/apps/cli/src/legacy/shared/legacy-connect-errors.ts +++ b/apps/cli/src/legacy/shared/legacy-connect-errors.ts @@ -51,6 +51,97 @@ export function legacyIsIPv6ConnectivityError(message: string): boolean { return false; } +/** + * Go's `utils.SuggestEnvVar` (`internal/utils/connect.go:191`): the hint shown when + * a connection fails on password authentication, pointing users at the + * `SUPABASE_DB_PASSWORD` env var. + */ +export const LEGACY_SUGGEST_ENV_VAR = + "Connect to your database by setting the env var correctly: SUPABASE_DB_PASSWORD"; + +/** Context the connect-suggestion needs but cannot derive from the error alone. */ +export interface LegacyConnectSuggestionContext { + /** Active profile's dashboard URL (Go's `CurrentProfile.DashboardURL`). */ + readonly dashboardUrl: string; + /** Active profile name (Go's `CurrentProfile.Name`). */ + readonly profileName: string; + /** Whether `--debug` is set (Go's `viper.GetBool("DEBUG")`). */ + readonly debug: boolean; +} + +/** + * Flatten an error's `cause` chain and any `AggregateError.errors` into a single + * searchable string of every nested `message` and `code`. The `@effect/sql` + * `SqlError` wraps the node-postgres / node `net` driver error on its `cause`; a + * multi-address dial wraps an `AggregateError` whose `errors[]` carry the per-IP + * `ECONNREFUSED` / `ENETUNREACH` system errors. Including the `code` strings lets + * the matcher key off node's `ECONNREFUSED` the way Go keys off pgconn's + * `connect: connection refused`. + */ +function legacyCollectConnectErrorText(error: unknown): string { + const parts: string[] = []; + const seen = new Set(); + const visit = (node: unknown, depth: number): void => { + if (depth > 8 || typeof node !== "object" || node === null || seen.has(node)) return; + seen.add(node); + const message = Reflect.get(node, "message"); + if (typeof message === "string") parts.push(message); + const code = Reflect.get(node, "code"); + if (typeof code === "string") parts.push(code); + visit(Reflect.get(node, "cause"), depth + 1); + const errors = Reflect.get(node, "errors"); + if (Array.isArray(errors)) for (const child of errors) visit(child, depth + 1); + }; + visit(error, 0); + return parts.join("\n"); +} + +/** + * Port of Go's `SetConnectSuggestion` (`internal/utils/connect.go:313-335`): map a + * Postgres connect failure to an actionable hint that replaces the generic + * "Try rerunning the command with --debug" suggestion. Go matches `pgconn`'s + * error text; this matches the equivalent node-postgres / node `net` driver text + * and codes (e.g. `ECONNREFUSED` for `connect: connection refused`) gathered from + * the `SqlError` cause/aggregate chain. The branch order mirrors Go's `if/else if`. + * Returns `undefined` when no specific suggestion applies (the caller then falls + * back to the generic suggestion, like Go leaving `CmdSuggestion` empty). + */ +export function legacyConnectSuggestion( + error: unknown, + ctx: LegacyConnectSuggestionContext, +): string | undefined { + const text = legacyCollectConnectErrorText(error); + // connect: connection refused / Address not in tenant allow_list → network restrictions. + if ( + text.includes("ECONNREFUSED") || + text.includes("connection refused") || + text.includes("Address not in tenant allow_list") + ) { + return `Make sure your local IP is allowed in Network Restrictions and Network Bans.\n${ctx.dashboardUrl}/project/_/database/settings`; + } + // SSL connection is required (only under --debug, which disables TLS). + if (text.includes("SSL connection is required") && ctx.debug) { + return "SSL connection is not supported with --debug flag"; + } + // Wrong password (Go: "SCRAM exchange: Wrong password" / "failed SASL auth"; + // node-postgres surfaces the server's `28P01` "password authentication failed"). + if ( + text.includes("SCRAM exchange: Wrong password") || + text.includes("failed SASL auth") || + text.includes("password authentication failed") + ) { + return LEGACY_SUGGEST_ENV_VAR; + } + if (legacyIsIPv6ConnectivityError(text)) { + return legacyIpv6Suggestion(); + } + // no route to host / Tenant or user not found → wrong profile. + if (text.includes("no route to host") || text.includes("Tenant or user not found")) { + return `Make sure your project exists on profile: ${ctx.profileName}`; + } + return undefined; +} + function hasStringCode(error: unknown): error is { readonly code: string; readonly address?: unknown; diff --git a/apps/cli/src/legacy/shared/legacy-connect-errors.unit.test.ts b/apps/cli/src/legacy/shared/legacy-connect-errors.unit.test.ts index 05ff60186c..844cda45e0 100644 --- a/apps/cli/src/legacy/shared/legacy-connect-errors.unit.test.ts +++ b/apps/cli/src/legacy/shared/legacy-connect-errors.unit.test.ts @@ -1,6 +1,9 @@ import { describe, expect, it } from "vitest"; import { + LEGACY_SUGGEST_ENV_VAR, + legacyConnectSuggestion, + legacyIpv6Suggestion, legacyIsIPv6ConnectivityError, legacyIsIPv6ConnectivityErrorCause, } from "./legacy-connect-errors.ts"; @@ -44,6 +47,77 @@ describe("legacyIsIPv6ConnectivityError", () => { }); }); +describe("legacyConnectSuggestion", () => { + const ctx = { + dashboardUrl: "https://supabase.com/dashboard", + profileName: "supabase", + debug: false, + } as const; + + // The @effect/sql SqlError wraps the node driver error on `.cause`; a multi-address + // dial wraps an AggregateError whose `.errors[]` carry the per-IP system errors. + const sqlError = (cause: unknown) => + Object.assign(new Error("PgClient: Failed to connect"), { cause }); + const systemError = (message: string, code: string) => + Object.assign(new Error(message), { code }); + + it("maps a refused connection (node ECONNREFUSED) to the network-restrictions hint", () => { + const err = sqlError(systemError("connect ECONNREFUSED 127.0.0.1:54322", "ECONNREFUSED")); + expect(legacyConnectSuggestion(err, ctx)).toBe( + "Make sure your local IP is allowed in Network Restrictions and Network Bans.\nhttps://supabase.com/dashboard/project/_/database/settings", + ); + }); + + it("maps an AggregateError of refused dials to the network-restrictions hint", () => { + const err = sqlError( + Object.assign(new AggregateError([], "all attempts failed"), { + errors: [systemError("connect ECONNREFUSED [::1]:54322", "ECONNREFUSED")], + }), + ); + expect(legacyConnectSuggestion(err, ctx)).toContain( + "Make sure your local IP is allowed in Network Restrictions and Network Bans.", + ); + }); + + it("maps the pooler allow_list rejection to the network-restrictions hint", () => { + const err = sqlError(new Error("Address not in tenant allow_list")); + expect(legacyConnectSuggestion(err, ctx)).toContain("Network Restrictions and Network Bans"); + }); + + it("maps a password-auth failure to the env-var suggestion", () => { + const err = sqlError( + Object.assign(new Error('password authentication failed for user "postgres"'), { + code: "28P01", + }), + ); + expect(legacyConnectSuggestion(err, ctx)).toBe(LEGACY_SUGGEST_ENV_VAR); + }); + + it("suggests the --debug SSL note only under --debug", () => { + const err = sqlError(new Error("SSL connection is required")); + expect(legacyConnectSuggestion(err, ctx)).toBeUndefined(); + expect(legacyConnectSuggestion(err, { ...ctx, debug: true })).toBe( + "SSL connection is not supported with --debug flag", + ); + }); + + it("maps an IPv6-only connectivity failure to the IPv6 pooler suggestion", () => { + const err = sqlError(new Error("dial tcp: network is unreachable")); + expect(legacyConnectSuggestion(err, ctx)).toBe(legacyIpv6Suggestion()); + }); + + it("maps a tenant-not-found error to the wrong-profile hint", () => { + const err = sqlError(new Error("Tenant or user not found")); + expect(legacyConnectSuggestion(err, ctx)).toBe( + "Make sure your project exists on profile: supabase", + ); + }); + + it("returns undefined for an unrecognized connect error", () => { + expect(legacyConnectSuggestion(sqlError(new Error("some other failure")), ctx)).toBeUndefined(); + }); +}); + describe("legacyIsIPv6ConnectivityErrorCause", () => { it("classifies Node getaddrinfo and network-unreachable errors", () => { expect( diff --git a/apps/cli/src/legacy/shared/legacy-db-config.integration.test.ts b/apps/cli/src/legacy/shared/legacy-db-config.integration.test.ts index 78549ac06f..c277d2930e 100644 --- a/apps/cli/src/legacy/shared/legacy-db-config.integration.test.ts +++ b/apps/cli/src/legacy/shared/legacy-db-config.integration.test.ts @@ -158,6 +158,13 @@ describe("legacyDbConfigResolver (local + db-url)", () => { user: "postgres", password: "hunter2", database: "postgres", + // The resolver attaches the connect-failure suggestion context (Go's + // ambient CurrentProfile) to every resolved connection. + suggestionContext: { + dashboardUrl: "https://supabase.com/dashboard", + profileName: "supabase", + debug: false, + }, }); expect(r.isLocal).toBe(true); rmSync(dir, { recursive: true, force: true }); @@ -206,6 +213,11 @@ describe("legacyDbConfigResolver (local + db-url)", () => { user: "alice", password: "p@ss", database: "appdb", + suggestionContext: { + dashboardUrl: "https://supabase.com/dashboard", + profileName: "supabase", + debug: false, + }, }); expect(r.isLocal).toBe(false); rmSync(dir, { recursive: true, force: true }); @@ -495,6 +507,11 @@ describe("legacyDbConfigResolver (linked config ordering)", () => { user: `cli_login_role.${adHocRef}`, password: "temporary-role-password", database: "postgres", + suggestionContext: { + dashboardUrl: "https://supabase.com/dashboard", + profileName: "supabase", + debug: false, + }, }); expect(r.ref).toEqual(Option.some(adHocRef)); expect(requests).toEqual([ @@ -648,6 +665,11 @@ describe("legacyDbConfigResolver (linked config ordering)", () => { user: `cli_login_role.${adHocRef}`, password: "temporary-role-password", database: "postgres", + suggestionContext: { + dashboardUrl: "https://supabase.com/dashboard", + profileName: "supabase", + debug: false, + }, }); } expect(requests).toEqual([ @@ -785,6 +807,11 @@ describe("legacyDbConfigResolver (linked config ordering)", () => { user: `postgres.${linkedRef}`, password: "linked-password", database: "postgres", + suggestionContext: { + dashboardUrl: "https://supabase.com/dashboard", + profileName: "supabase", + debug: false, + }, }); } expect(requests).toEqual([ diff --git a/apps/cli/src/legacy/shared/legacy-db-config.layer.ts b/apps/cli/src/legacy/shared/legacy-db-config.layer.ts index 9bc7e00f45..45714feb97 100644 --- a/apps/cli/src/legacy/shared/legacy-db-config.layer.ts +++ b/apps/cli/src/legacy/shared/legacy-db-config.layer.ts @@ -20,6 +20,10 @@ import { RuntimeInfo } from "../../shared/runtime/runtime-info.service.ts"; import { Tty } from "../../shared/runtime/tty.service.ts"; import { Analytics } from "../../shared/telemetry/analytics.service.ts"; import { TelemetryRuntime } from "../../shared/telemetry/runtime.service.ts"; +import { + type LegacyConnectSuggestionContext, + LEGACY_SUGGEST_ENV_VAR, +} from "./legacy-connect-errors.ts"; import { LegacyDbConnection, type LegacyPgConnInput } from "./legacy-db-connection.service.ts"; import { LegacyIdentityStitch } from "./legacy-identity-stitch.ts"; import { @@ -44,9 +48,6 @@ const TCP_PROBE_TIMEOUT = Duration.seconds(5); const MAX_RETRIES = 8; const BACKOFF_INITIAL = Duration.seconds(3); const BACKOFF_MAX = Duration.seconds(60); -// Go: utils.SuggestEnvVar (`apps/cli-go/internal/utils/connect.go:174`). -const SUGGEST_ENV_VAR = - "Connect to your database by setting the env var correctly: SUPABASE_DB_PASSWORD"; const loginRoleErrorMapper = mapLegacyHttpError({ networkError: Errors.LegacyDbConfigLoginRoleNetworkError, @@ -106,6 +107,16 @@ export const legacyDbConfigLayer = Layer.effect( const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; + // Profile context for the connect-failure suggestion (Go's `SetConnectSuggestion` + // reads the ambient `CurrentProfile` + `viper.GetBool("DEBUG")`). Snapshot it once + // and attach it to every resolved connection so the driver layer can render Go's + // hint on a refused/auth/IPv6 connect error. + const suggestionContext: LegacyConnectSuggestionContext = { + dashboardUrl: cliConfig.dashboardUrl, + profileName: cliConfig.profile, + debug: yield* LegacyDebugFlag, + }; + // Capture the ambient services the Management API stack needs, so the // lazily-built linked stack is fully self-provided and `resolve`'s R stays // `never` (handler tests can mock this resolver without wiring the whole @@ -203,7 +214,7 @@ export const legacyDbConfigLayer = Layer.effect( return Effect.fail( new Errors.LegacyDbConfigConnectTempRoleError({ message: `failed to connect as temp role: ${cause.message}`, - suggestion: SUGGEST_ENV_VAR, + suggestion: LEGACY_SUGGEST_ENV_VAR, }), ); } @@ -549,6 +560,18 @@ export const legacyDbConfigLayer = Layer.effect( ); }); - return LegacyDbConfigResolver.of({ resolve, resolvePoolerFallback }); + // Attach the connect-failure suggestion context to every resolved connection in + // one place (Go sets it ambiently via `CurrentProfile`), so each connecting + // command inherits Go's `SetConnectSuggestion` hint without per-call-site wiring. + const withSuggestion = (conn: LegacyPgConnInput): LegacyPgConnInput => ({ + ...conn, + suggestionContext, + }); + return LegacyDbConfigResolver.of({ + resolve: (flags) => + resolve(flags).pipe(Effect.map((r) => ({ ...r, conn: withSuggestion(r.conn) }))), + resolvePoolerFallback: (flags) => + resolvePoolerFallback(flags).pipe(Effect.map(Option.map(withSuggestion))), + }); }), ); diff --git a/apps/cli/src/legacy/shared/legacy-db-config.toml-read.ts b/apps/cli/src/legacy/shared/legacy-db-config.toml-read.ts index 0e4bc28028..71b989aaeb 100644 --- a/apps/cli/src/legacy/shared/legacy-db-config.toml-read.ts +++ b/apps/cli/src/legacy/shared/legacy-db-config.toml-read.ts @@ -77,6 +77,11 @@ interface LegacyDbTomlValues { readonly seed: LegacyDbSeedTomlConfig; /** `[db.vault]` secrets (name → resolved value) — upserted by `up`/`down`. */ readonly vault: ReadonlyArray; + /** + * The matched `[remotes.]` block name when a linked ref merged its override + * (Go's `Loading config override: [remotes.]` line), else `undefined`. + */ + readonly appliedRemote: string | undefined; } /** `[db.seed]` config surfaced for `migration down`'s seed step. */ @@ -180,6 +185,12 @@ function deepMergeDoc(base: RawDoc, override: RawDoc): RawDoc { */ interface LegacyRemoteOverride { readonly doc: RawDoc | undefined; + /** + * The name of the matched `[remotes.]` block whose `project_id` equals the + * resolved ref, or `undefined` when no block matched. Callers echo Go's + * `Loading config override: [remotes.]` stderr line from this. + */ + readonly appliedRemote?: string; /** * The config keys the matched remote block contributed at viper's OVERRIDE tier. Go's * `mergeRemoteConfig` applies every block key via `v.Set(...)` after `AutomaticEnv` @@ -302,10 +313,11 @@ function applyRemoteOverride( if (blockSeed?.["enabled"] === undefined) { return { doc: deepMergeDoc(merged, { db: { seed: { enabled: false } } }), + appliedRemote: name, remoteOverrideKeys, }; } - return { doc: merged, remoteOverrideKeys }; + return { doc: merged, appliedRemote: name, remoteOverrideKeys }; } } return { doc, remoteOverrideKeys: new Set() }; @@ -476,6 +488,18 @@ function legacyJoinSupabaseSeedPath(pattern: string): string { return out.length === 0 ? "." : out.join("/"); } +/** + * Resolves a single seed `sql_paths` entry to Go's config-load form: a relative + * pattern is joined under `supabase/` (Go's `path.Join`, `config.go:918-921`); an + * absolute (or empty) pattern is returned verbatim. Used by the reader for + * `[db.seed].sql_paths` and by `db reset` for its `--sql-paths` override (Go's + * `resolveSeedSqlPaths`, `cmd/db.go`) so both feed the glob the same resolved paths. + */ +export const legacyResolveSeedSqlPath = (pathSvc: Path.Path, pattern: string): string => + pattern.length === 0 || pathSvc.isAbsolute(pattern) + ? pattern + : legacyJoinSupabaseSeedPath(pattern); + /** `[db]` ports default through the development env unless `SUPABASE_ENV` overrides. */ const DEFAULT_SUPABASE_ENV = "development"; @@ -644,7 +668,12 @@ function resolveBool(value: unknown, fallback: boolean, lookup: EnvLookup): bool // width). A TOML number (`enabled = 0`) is therefore an explicit false, NOT absent — it // must not fall through to the schema default. if (typeof value === "number") return value !== 0; - return fallback; + // Absent → the schema default. A PRESENT non-scalar (array/inline table, e.g. + // `enabled = []`) is a decode error in Go's `UnmarshalExact` during `LoadConfig`, so it + // must NOT fall through to the default — otherwise `db reset` could accept the prompt and + // drop schemas on a config Go rejects up front. + if (value === undefined) return fallback; + return "invalid"; } /** @@ -717,45 +746,115 @@ const resolveOptionalBoolOrFail = Effect.fnUntraced(function* ( } return Option.some(parsed); } - return Option.none(); + // Absent → `None` (Go's `*bool` stays nil). A present non-scalar value fails Go's + // `UnmarshalExact`, so reject it here rather than silently treating it as absent. + if (value === undefined) return Option.none(); + return yield* Effect.fail( + new LegacyDbConfigLoadError({ message: `failed to parse config: invalid ${field}.` }), + ); }); /** - * Recursively asserts every `encrypted:` secret in the (merged) config can be decrypted, - * mirroring Go's global `DecryptSecretHookFunc` (`pkg/config/secret.go:77-109`, - * `config.go:730`), which decrypts every `config.Secret` field during `UnmarshalExact` and - * aborts the load with `failed to parse config: ` when one cannot be decrypted (e.g. - * no `DOTENV_PRIVATE_KEY`). The reader's `[db.vault]` walk only covered vault secrets, so - * non-vault `Secret` fields (`db.root_key`, `auth.external.

.secret`, smtp `pass`, …) were - * silently passed through. A recursive string scan tracks Go's "decode the entire config" - * behaviour and stays robust as new `Secret` fields are added. The unset-`env(...)` and - * plain-string forms are returned verbatim by Go's hook (no error), so they are no-ops here. - * Returns the failure (or `undefined`); the caller surfaces it via `Effect.fail`. + * Dotted paths of every `config.Secret`-typed field Go decrypts via its global + * `DecryptSecretHookFunc` (`pkg/config/secret.go`, `config.go:730`) — the hook only runs + * while decoding INTO a `config.Secret`, never over arbitrary strings. `*` matches any map + * key (`auth.external.`, `auth.hook.`). `[db.vault]` (a `map[string]Secret`) + * is intentionally omitted — the reader decrypts it directly in the body with the same + * fail-on-undecryptable behaviour. Derived from the Go structs (`auth.go`, `db.go`, + * `config.go`); update alongside any new `Secret` field. + */ +const LEGACY_SECRET_PATHS: ReadonlyArray> = [ + ["db", "root_key"], + ["auth", "publishable_key"], + ["auth", "secret_key"], + ["auth", "jwt_secret"], + ["auth", "anon_key"], + ["auth", "service_role_key"], + ["auth", "email", "smtp", "pass"], + ["auth", "external", "*", "secret"], + ["auth", "hook", "*", "secrets"], + ["auth", "sms", "twilio", "auth_token"], + ["auth", "sms", "twilio_verify", "auth_token"], + ["auth", "sms", "messagebird", "access_key"], + ["auth", "sms", "textlocal", "api_key"], + ["auth", "sms", "vonage", "api_secret"], + ["auth", "captcha", "secret"], + ["studio", "openai_api_key"], + // Go decodes `[edge_runtime.secrets]` as `SecretsConfig map[string]Secret` (config.go:283,287), + // so every value is decrypted by the hook — `*` spans the arbitrary secret names. + ["edge_runtime", "secrets", "*"], +]; + +/** Collects the string leaves reachable from `node` along `segs` (`*` spans map keys). */ +const legacyCollectSecretStrings = ( + node: unknown, + segs: ReadonlyArray, + index: number, + out: Array, +): void => { + if (index === segs.length) { + if (typeof node === "string") out.push(node); + return; + } + const record = asRecord(node); + if (record === undefined) return; + const seg = segs[index]!; + if (seg === "*") { + for (const key of Object.keys(record)) { + legacyCollectSecretStrings(record[key], segs, index + 1, out); + } + } else { + legacyCollectSecretStrings(record[seg], segs, index + 1, out); + } +}; + +/** Fails when a single `encrypted:` secret value cannot be decrypted (Go's hook error). */ +const legacyAssertSecretValue = ( + value: string, + lookup: EnvLookup, + dotenvPrivateKeys: ReadonlyArray, +): LegacyDbConfigLoadError | undefined => { + const expanded = legacyExpandEnv(value, lookup); + // Unset `env(...)` and plain strings are returned verbatim by Go's hook (no error). + if (ENV_PATTERN.test(expanded) || !legacyIsEncryptedSecret(expanded)) return undefined; + const decrypted = legacyDecryptSecret(expanded, dotenvPrivateKeys); + return decrypted.ok + ? undefined + : new LegacyDbConfigLoadError({ message: `failed to parse config: ${decrypted.error}` }); +}; + +/** + * Asserts every `config.Secret`-typed `encrypted:` value in the (merged) config can be + * decrypted, mirroring Go's global `DecryptSecretHookFunc`, which aborts the load with + * `failed to parse config: ` when a secret cannot be decrypted. Only the actual + * `Secret` field paths ({@link LEGACY_SECRET_PATHS}) are scanned — a non-secret string that + * merely starts with `encrypted:` (e.g. an auth email-template `subject`) stays plain text + * in Go and must not block the load. Go decodes every `[remotes.]` block into the same + * struct, so the same paths are checked under each remote too. Returns the failure (or + * `undefined`); the caller surfaces it via `Effect.fail`. */ const legacyAssertDecryptableSecrets = ( - value: unknown, + doc: unknown, lookup: EnvLookup, dotenvPrivateKeys: ReadonlyArray, ): LegacyDbConfigLoadError | undefined => { - if (typeof value === "string") { - const expanded = legacyExpandEnv(value, lookup); - if (ENV_PATTERN.test(expanded) || !legacyIsEncryptedSecret(expanded)) return undefined; - const decrypted = legacyDecryptSecret(expanded, dotenvPrivateKeys); - return decrypted.ok - ? undefined - : new LegacyDbConfigLoadError({ message: `failed to parse config: ${decrypted.error}` }); - } - if (Array.isArray(value)) { - for (const item of value) { - const error = legacyAssertDecryptableSecrets(item, lookup, dotenvPrivateKeys); - if (error !== undefined) return error; + const scan = (node: unknown): LegacyDbConfigLoadError | undefined => { + for (const segs of LEGACY_SECRET_PATHS) { + const values: Array = []; + legacyCollectSecretStrings(node, segs, 0, values); + for (const value of values) { + const error = legacyAssertSecretValue(value, lookup, dotenvPrivateKeys); + if (error !== undefined) return error; + } } return undefined; - } - const record = asRecord(value); - if (record !== undefined) { - for (const key of Object.keys(record)) { - const error = legacyAssertDecryptableSecrets(record[key], lookup, dotenvPrivateKeys); + }; + const topLevel = scan(doc); + if (topLevel !== undefined) return topLevel; + const remotes = asRecord(asRecord(doc)?.["remotes"]); + if (remotes !== undefined) { + for (const name of Object.keys(remotes)) { + const error = scan(remotes[name]); if (error !== undefined) return error; } } @@ -1173,7 +1272,7 @@ const legacyValidateAuthConfig = Effect.fnUntraced(function* ( * Fails with `LegacyDbConfigLoadError` only when the config file is present but * unparseable; an absent file (and an absent/empty pooler-url file) is not an error. */ -export const legacyReadDbToml = Effect.fnUntraced(function* ( +const readDbTomlCore = Effect.fnUntraced(function* ( fs: FileSystem.FileSystem, path: Path.Path, workdir: string, @@ -1183,6 +1282,12 @@ export const legacyReadDbToml = Effect.fnUntraced(function* ( // `--local` / `--db-url` / declarative pass nothing and read the unmerged config, // matching Go (those paths never resolve a ref before config load). ref?: string, + // Internal: when true the on-disk `config.toml` is treated as absent so the body + // resolves pure defaults (still honoring `SUPABASE_*` env overrides, which Go binds + // regardless of a config file). The lenient `legacyReadDbToml({ validate: false })` + // wrapper uses this as its fallback after a config-load failure, mirroring the + // best-effort behavior the container-id seam relied on before. + ignoreConfigFile = false, ) { const supabaseDir = path.join(workdir, "supabase"); const configPath = path.join(supabaseDir, "config.toml"); @@ -1192,18 +1297,20 @@ export const legacyReadDbToml = Effect.fnUntraced(function* ( // is swallowed, every other read error aborts rather than silently running against the // default local database. Effect surfaces "not found" as `PlatformError` with a // `SystemError` reason tagged `"NotFound"`. - const maybeContent = yield* fs.readFileString(configPath).pipe( - Effect.map(Option.some), - Effect.catchTag("PlatformError", (error) => - error.reason._tag === "NotFound" - ? Effect.succeed(Option.none()) - : Effect.fail( - new LegacyDbConfigLoadError({ - message: `failed to read file config: ${error.message}`, - }), - ), - ), - ); + const maybeContent = ignoreConfigFile + ? Option.none() + : yield* fs.readFileString(configPath).pipe( + Effect.map(Option.some), + Effect.catchTag("PlatformError", (error) => + error.reason._tag === "NotFound" + ? Effect.succeed(Option.none()) + : Effect.fail( + new LegacyDbConfigLoadError({ + message: `failed to read file config: ${error.message}`, + }), + ), + ), + ); // Resolve `env(VAR)` against the shell env first, then the project `.env` files // (Go's `loadNestedEnv` populates the process env before `LoadEnvHook`). Built @@ -1228,9 +1335,16 @@ export const legacyReadDbToml = Effect.fnUntraced(function* ( let functionsRaw: RawDoc | undefined; let analyticsRaw: RawDoc | undefined; let projectId = Option.none(); + // Whether `config.toml` set a top-level `project_id` string that env-expanded to empty + // (`project_id = ""`). Go keeps that empty override and `config.Validate` fails with + // `Missing required field in config: project_id` (config.go:991); tracked here so the + // check can run after the `SUPABASE_PROJECT_ID` env override below may still rescue it. + let projectIdExplicitEmpty = false; // Config keys a matched remote block contributed at viper's override tier (Go's // `v.Set`), so they must beat the matching `SUPABASE_*` env overrides below. let remoteOverrideKeys: ReadonlySet = new Set(); + // The matched `[remotes.]` block name, echoed as Go's config-override line. + let appliedRemote: string | undefined; if (Option.isSome(maybeContent)) { let doc: RawDoc | undefined; try { @@ -1271,6 +1385,7 @@ export const legacyReadDbToml = Effect.fnUntraced(function* ( : applyRemoteOverride(doc, ref, lookup); const effectiveDoc = remoteOverride.doc; remoteOverrideKeys = remoteOverride.remoteOverrideKeys; + appliedRemote = remoteOverride.appliedRemote; db = asRecord(effectiveDoc?.["db"]); experimentalRaw = asRecord(effectiveDoc?.["experimental"]); pgDeltaRaw = asRecord(experimentalRaw?.["pgdelta"]); @@ -1289,6 +1404,8 @@ export const legacyReadDbToml = Effect.fnUntraced(function* ( projectId = nonEmptyString( typeof rawProjectId === "string" ? legacyExpandEnv(rawProjectId, lookup) : rawProjectId, ); + // A present `project_id` string that resolves to empty is Go's "kept empty override". + projectIdExplicitEmpty = typeof rawProjectId === "string" && Option.isNone(projectId); // Go's `DecryptSecretHookFunc` is a global decode hook (config.go:730) that decrypts // EVERY `config.Secret` field during `UnmarshalExact`, so an `encrypted:` secret anywhere @@ -1344,6 +1461,16 @@ export const legacyReadDbToml = Effect.fnUntraced(function* ( projectId = nonEmptyString(legacyExpandEnv(projectIdEnv, lookup)); } + // Go's `config.Validate` rejects an empty top-level `project_id` (config.go:991). An + // absent field is tolerated here (deferred), but a present `project_id = ""` that the + // `SUPABASE_PROJECT_ID` override did not rescue is a load error, so a destructive command + // (e.g. remote `db reset`) fails fast rather than dropping schemas on a config Go rejects. + if (projectIdExplicitEmpty && Option.isNone(projectId)) { + return yield* Effect.fail( + new LegacyDbConfigLoadError({ message: "Missing required field in config: project_id" }), + ); + } + // A present-but-unmarshalable port aborts in Go rather than defaulting; mirror // that so `test db --local` never silently targets the default local database // while hiding a broken `[db]` config. @@ -1770,13 +1897,9 @@ export const legacyReadDbToml = Effect.fnUntraced(function* ( : typeof rawSqlPaths === "string" ? splitGoSeedPaths(rawSqlPaths) : ["seed.sql"]; - const seedSqlPaths = sqlPathPatterns.map((pattern) => { - // Patterns are already env-expanded above (Go's LoadEnvHook runs before the split), so - // an absolute path is used verbatim and a relative one is supabase-prefixed via Go's - // `path.Join("supabase", pattern)` → `path.Clean` (collapses `.`/`..`). - if (pattern.length === 0 || path.isAbsolute(pattern)) return pattern; - return legacyJoinSupabaseSeedPath(pattern); - }); + // Patterns are already env-expanded above (Go's LoadEnvHook runs before the split); + // resolve each to Go's config-load form (absolute verbatim, relative supabase-joined). + const seedSqlPaths = sqlPathPatterns.map((pattern) => legacyResolveSeedSqlPath(path, pattern)); // `[db.vault]` secrets: env-expand each value, then decrypt dotenvx `encrypted:` // ciphertext. `resolved` mirrors Go's `len(SHA256) > 0` gate (Go sets SHA256 only @@ -1860,10 +1983,53 @@ export const legacyReadDbToml = Effect.fnUntraced(function* ( migrationsEnabled, seed: { enabled: seedEnabled, sqlPaths: seedSqlPaths }, vault, + appliedRemote, }; return values; }); +/** + * Read + validate `config.toml` exactly like Go's `flags.LoadConfig` → `config.Load` + * → `Validate`: an absent file yields defaults, but a present config that is + * unreadable/malformed, references an undecryptable `encrypted:` secret, or fails any + * of Go's decode/Validate checks (remote refs, `db.port`, `db.major_version`, + * `edge_runtime.deno_version`, pgdelta gate, `format_options` JSON, bucket names, + * function slugs, auth, analytics) aborts with the matching Go error. Call this at the + * point Go fails fast — before asserting the stack is running, prompting, or any + * destructive work — so a broken config never runs against the default local database. + */ +export const legacyCheckDbToml = ( + fs: FileSystem.FileSystem, + path: Path.Path, + workdir: string, + ref?: string, +) => readDbTomlCore(fs, path, workdir, ref, false); + +/** + * Read `config.toml`. Defaults to Go's validating behavior (identical to + * {@link legacyCheckDbToml}); pass `{ validate: false }` for a best-effort read that + * never throws on an invalid config — a config-load failure falls back to pure + * defaults (env overrides still applied), matching the tolerant behavior the + * container-id seam needs when it only wants `projectId` and the handler has already + * validated the config. The throwing default keeps every existing caller at Go parity. + */ +export const legacyReadDbToml = ( + fs: FileSystem.FileSystem, + path: Path.Path, + workdir: string, + ref?: string, + opts?: { readonly validate?: boolean }, +) => + opts?.validate === false + ? readDbTomlCore(fs, path, workdir, ref, false).pipe( + // Fall back to the ignore-file defaults path (never re-reads the broken config) + // so a best-effort caller gets a well-formed defaults result instead of a throw. + Effect.catchTag("LegacyDbConfigLoadError", () => + readDbTomlCore(fs, path, workdir, ref, true), + ), + ) + : readDbTomlCore(fs, path, workdir, ref, false); + /** * The effective declarative schema directory: the configured * `declarative_schema_path` (already `supabase/`-prefixed when relative) or the diff --git a/apps/cli/src/legacy/shared/legacy-db-config.toml-read.unit.test.ts b/apps/cli/src/legacy/shared/legacy-db-config.toml-read.unit.test.ts index 986c8a6075..3fd179eaf8 100644 --- a/apps/cli/src/legacy/shared/legacy-db-config.toml-read.unit.test.ts +++ b/apps/cli/src/legacy/shared/legacy-db-config.toml-read.unit.test.ts @@ -7,6 +7,7 @@ import { Effect, Exit, FileSystem, Option, Path } from "effect"; import { legacyApplyProjectEnv, + legacyCheckDbToml, legacyLoadProjectEnv, legacyReadDbToml, legacyResolveDeclarativeDir, @@ -46,6 +47,69 @@ const loadEnv = (workdir: string) => return yield* legacyLoadProjectEnv(fs, path, workdir); }).pipe(Effect.provide(BunServices.layer)); +describe("read (lenient) vs check (throws) split", () => { + const withServices = ( + dir: string, + run: (fs: FileSystem.FileSystem, path: Path.Path) => Effect.Effect, + ) => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + return yield* run(fs, path); + }).pipe(Effect.provide(BunServices.layer)); + + it.effect("legacyCheckDbToml throws on an undecryptable secret", () => { + const dir = withConfig('[db]\nroot_key = "encrypted:anything"\n'); + return withServices(dir, (fs, path) => legacyCheckDbToml(fs, path, dir)).pipe( + Effect.exit, + Effect.tap((exit) => + Effect.sync(() => { + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(JSON.stringify(exit.cause)).toContain( + "failed to parse config: missing private key", + ); + } + rmSync(dir, { recursive: true, force: true }); + }), + ), + ); + }); + + it.effect( + "legacyReadDbToml({ validate: false }) tolerates the same secret, returning defaults", + () => { + const dir = withConfig('[db]\nroot_key = "encrypted:anything"\n'); + return withServices(dir, (fs, path) => + legacyReadDbToml(fs, path, dir, undefined, { validate: false }), + ).pipe( + Effect.tap((v) => + Effect.sync(() => { + // The broken config is swallowed → the ignore-file defaults path (no vault). + expect(v.vault).toEqual([]); + expect(v.port).toBeGreaterThan(0); + rmSync(dir, { recursive: true, force: true }); + }), + ), + ); + }, + ); + + it.effect("legacyReadDbToml({ validate: false }) still returns a valid config's values", () => { + const dir = withConfig('project_id = "lenientproj"\n'); + return withServices(dir, (fs, path) => + legacyReadDbToml(fs, path, dir, undefined, { validate: false }), + ).pipe( + Effect.tap((v) => + Effect.sync(() => { + expect(v.projectId).toEqual(Option.some("lenientproj")); + rmSync(dir, { recursive: true, force: true }); + }), + ), + ); + }); +}); + describe("legacyReadDbToml", () => { it.effect("returns defaults when config.toml is absent", () => { const dir = withConfig(undefined); @@ -2190,6 +2254,102 @@ describe("legacyReadDbToml encrypted secret decryption (Go DecryptSecretHookFunc it.effect("treats an unset env() secret as a no-op (verbatim, like Go's hook)", () => expectLoads(["[db]", 'root_key = "env(SOME_UNSET_ROOT_KEY)"']), ); + it.effect("does NOT decrypt a non-secret string that starts with encrypted:", () => + // Go's hook only runs while decoding into `config.Secret`; a non-secret field like an + // email-template subject stays plain text, so `db push`/`reset`/`start` must not abort. + expectLoads([ + "[auth.email.template.invite]", + 'subject = "encrypted: your invite"', + "[db]", + 'root_key = "env(SOME_UNSET_ROOT_KEY)"', + ]), + ); + it.effect("fails on an undecryptable auth.captcha.secret (Secret-typed field)", () => + expectFails( + [ + "[auth.captcha]", + "enabled = false", + 'provider = "hcaptcha"', + 'secret = "encrypted:anything"', + ], + "failed to parse config: missing private key", + ), + ); + it.effect("fails on an undecryptable [edge_runtime.secrets] value (map[string]Secret)", () => + expectFails( + ["[edge_runtime.secrets]", 'MY_SECRET = "encrypted:anything"'], + "failed to parse config: missing private key", + ), + ); + it.effect("fails on an undecryptable Secret inside a [remotes.*] block", () => + // Go decodes every remote block into the same struct, so an undecryptable secret in any + // remote (matched or not) aborts the load. + expectFails( + [ + "[remotes.preview]", + 'project_id = "abcdefghijklmnopqrst"', + "[remotes.preview.db]", + 'root_key = "encrypted:anything"', + ], + "failed to parse config: missing private key", + ), + ); +}); + +describe("legacyReadDbToml non-scalar config booleans (Go UnmarshalExact parity)", () => { + // Go's `UnmarshalExact` fails to decode a bool field given an array/inline-table value, + // aborting `LoadConfig` before any destructive work. A present non-scalar must not fall + // through to the schema default (which would let `db reset` prompt + drop schemas). + const failsInvalid = (lines: ReadonlyArray, field: string) => + Effect.gen(function* () { + const dir = withConfig(lines.join("\n")); + const exit = yield* read(dir).pipe(Effect.exit); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(JSON.stringify(exit.cause)).toContain(`failed to parse config: invalid ${field}.`); + } + rmSync(dir, { recursive: true, force: true }); + }); + it.effect("rejects an array value for [db.migrations] enabled", () => + failsInvalid(["[db.migrations]", "enabled = []"], "db.migrations.enabled"), + ); + it.effect("rejects an inline-table value for [db.seed] enabled", () => + failsInvalid(["[db.seed]", "enabled = {}"], "db.seed.enabled"), + ); +}); + +describe("legacyReadDbToml empty project_id (Go config.Validate parity)", () => { + it.effect("rejects a present-but-empty top-level project_id", () => { + // Go keeps the empty override and `config.Validate` fails "Missing required field in + // config: project_id" (config.go:991) before any destructive command runs. + const dir = withConfig('project_id = ""\n'); + return read(dir).pipe( + Effect.exit, + Effect.tap((exit) => + Effect.sync(() => { + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(JSON.stringify(exit.cause)).toContain( + "Missing required field in config: project_id", + ); + } + rmSync(dir, { recursive: true, force: true }); + }), + ), + ); + }); + + it.effect("still tolerates an absent project_id (deferred broader requirement)", () => { + const dir = withConfig("[db]\nmajor_version = 15\n"); + return read(dir).pipe( + Effect.tap((v) => + Effect.sync(() => { + expect(v.baseline).toBeDefined(); + rmSync(dir, { recursive: true, force: true }); + }), + ), + ); + }); }); describe("legacyReadDbToml [analytics] validation (Go config.Validate parity)", () => { diff --git a/apps/cli/src/legacy/shared/legacy-db-connection.service.ts b/apps/cli/src/legacy/shared/legacy-db-connection.service.ts index bbf1e81dbf..d2bfb2b261 100644 --- a/apps/cli/src/legacy/shared/legacy-db-connection.service.ts +++ b/apps/cli/src/legacy/shared/legacy-db-connection.service.ts @@ -1,4 +1,5 @@ import { Context, type Effect, type Scope } from "effect"; +import type { LegacyConnectSuggestionContext } from "./legacy-connect-errors.ts"; import type { LegacyDbConnectError, LegacyDbCopyError, @@ -71,6 +72,13 @@ export interface LegacyPgConnInput { * `ToPostgresURL`/`ConnectLocalPostgres`). */ readonly connectTimeoutSeconds?: number; + /** + * Profile context for the connect-failure suggestion (Go's `SetConnectSuggestion`, + * which reads the ambient `CurrentProfile` in `ConnectByUrl`). The resolver attaches + * it so the driver layer can map a refused/auth/IPv6 connect error to Go's actionable + * hint. Absent → the driver omits the suggestion (callers fall back to the generic one). + */ + readonly suggestionContext?: LegacyConnectSuggestionContext; } /** diff --git a/apps/cli/src/legacy/shared/legacy-db-connection.sql-pg.layer.ts b/apps/cli/src/legacy/shared/legacy-db-connection.sql-pg.layer.ts index fbd5bfca90..f291140c54 100644 --- a/apps/cli/src/legacy/shared/legacy-db-connection.sql-pg.layer.ts +++ b/apps/cli/src/legacy/shared/legacy-db-connection.sql-pg.layer.ts @@ -10,6 +10,7 @@ import * as Reactivity from "effect/unstable/reactivity/Reactivity"; // resolves, so the COPY path and the pooled path use the same driver. import * as Pg from "pg"; import { to as pgCopyTo } from "pg-copy-streams"; +import { legacyConnectSuggestion } from "./legacy-connect-errors.ts"; import { LegacyDbConnectError, LegacyDbCopyError, @@ -407,8 +408,20 @@ const connect = ( connectionTimeoutMillis: connectTimeoutSeconds * 1000, }); - const toConnectError = (error: unknown) => - new LegacyDbConnectError({ message: `failed to connect to postgres: ${error}` }); + // Go's `ConnectByUrl` calls `SetConnectSuggestion(err)` on every connect failure + // (`connect.go:187`), mapping the driver error to an actionable hint that replaces + // the generic "--debug" suggestion. The resolver attaches the profile context to + // `cfg.suggestionContext`; map it here so the suggestion travels on the error. + const toConnectError = (error: unknown) => { + const suggestion = + cfg.suggestionContext === undefined + ? undefined + : legacyConnectSuggestion(error, cfg.suggestionContext); + return new LegacyDbConnectError({ + message: `failed to connect to postgres: ${error}`, + ...(suggestion === undefined ? {} : { suggestion }), + }); + }; // Load the `sslrootcert` CA bundle (pgconn reads it into `RootCAs` at parse // time; a missing/unreadable file aborts). Skipped for local connections, which @@ -548,8 +561,7 @@ const connect = ( const fresh = new Pg.Client(winningRawConfig); yield* Effect.tryPromise({ try: () => fresh.connect(), - catch: (error) => - new LegacyDbConnectError({ message: `failed to connect to postgres: ${error}` }), + catch: toConnectError, }); if (!isLocal && needsRoleStepDown(cfg.user)) { yield* Effect.tryPromise({ diff --git a/apps/cli/src/legacy/shared/legacy-db-target-flags.ts b/apps/cli/src/legacy/shared/legacy-db-target-flags.ts index 2f47d3c84e..eb891c924a 100644 --- a/apps/cli/src/legacy/shared/legacy-db-target-flags.ts +++ b/apps/cli/src/legacy/shared/legacy-db-target-flags.ts @@ -49,6 +49,8 @@ export interface LegacyDbTargetSelection { export const VALUE_CONSUMING_LONG_FLAGS = new Set([ // db-family command flags "db-url", + "password", // db push/pull/dump/remote (StringVarP, short -p) + "sql-paths", "schema", "level", "fail-on", @@ -79,7 +81,7 @@ export const VALUE_CONSUMING_LONG_FLAGS = new Set([ export const VALUE_CONSUMING_SHORT_FLAGS = new Set([ "s", // --schema / -s "o", // --output / -o - "p", // --password / -p + "p", // --password / -p (migration list, db push/pull/dump/remote) "j", // --jobs / -j (storage cp) ]); diff --git a/apps/cli/src/legacy/shared/legacy-docker-run.layer.ts b/apps/cli/src/legacy/shared/legacy-docker-run.layer.ts index f9b301821e..915268b704 100644 --- a/apps/cli/src/legacy/shared/legacy-docker-run.layer.ts +++ b/apps/cli/src/legacy/shared/legacy-docker-run.layer.ts @@ -7,13 +7,10 @@ import { legacyApplyBitbucketDockerFilter, } from "./legacy-docker-run.args.ts"; import { LegacyDockerRunError } from "./legacy-docker-run.errors.ts"; +import { LEGACY_SUGGEST_DOCKER_INSTALL } from "./legacy-docker-suggest.ts"; import { legacyGetRegistryImageUrlCandidates } from "./legacy-docker-registry.ts"; import { LegacyDockerRun, type LegacyDockerRunOpts } from "./legacy-docker-run.service.ts"; -// Go's prerequisite hint (`apps/cli-go/internal/utils/docker.go:248`). -const SUGGEST_DOCKER_INSTALL = - "Docker Desktop is a prerequisite for local development. Follow the official docs to install: https://docs.docker.com/desktop"; - // Go's `DockerStart` checks `os.Getenv("BITBUCKET_CLONE_DIR") != ""` // (`apps/cli-go/internal/utils/docker.go:289`) to drop named volumes / security-opts. const legacyIsBitbucketPipeline = (): boolean => { @@ -49,7 +46,9 @@ export const legacyDockerRunLayer: Layer.Layer< // Never embed the spawn error verbatim: it can leak the full argv and // environment of the failed exec (CWE-214/209). Emit a fixed, // credential-free message that still points at the likely cause. - new LegacyDockerRunError({ message: `failed to run docker. ${SUGGEST_DOCKER_INSTALL}` }); + new LegacyDockerRunError({ + message: `failed to run docker. ${LEGACY_SUGGEST_DOCKER_INSTALL}`, + }); const concat = (chunks: ReadonlyArray): Uint8Array => { const total = chunks.reduce((size, chunk) => size + chunk.length, 0); @@ -309,7 +308,7 @@ export const legacyDockerRunLayer: Layer.Layer< Effect.mapError( () => new LegacyDockerRunError({ - message: `failed to run docker. ${SUGGEST_DOCKER_INSTALL}`, + message: `failed to run docker. ${LEGACY_SUGGEST_DOCKER_INSTALL}`, }), ), ); diff --git a/apps/cli/src/legacy/shared/legacy-docker-suggest.ts b/apps/cli/src/legacy/shared/legacy-docker-suggest.ts new file mode 100644 index 0000000000..76397c3ab1 --- /dev/null +++ b/apps/cli/src/legacy/shared/legacy-docker-suggest.ts @@ -0,0 +1,21 @@ +/** + * Go's Docker prerequisite hint (`apps/cli-go/internal/utils/docker.go:350`, + * `suggestDockerInstall`). Go sets it as `CmdSuggestion` — rendered as a separate + * "Suggestion:" line — whenever a container-runtime call fails because the daemon + * is unreachable (`client.IsErrConnectionFailed`, `misc.go:148-154`). + */ +export const LEGACY_SUGGEST_DOCKER_INSTALL = + "Docker Desktop is a prerequisite for local development. Follow the official docs to install: https://docs.docker.com/desktop"; + +/** + * Whether a container-CLI stderr indicates the daemon is unreachable — the + * subprocess-stderr equivalent of Go's `client.IsErrConnectionFailed` (which + * inspects the Docker API client error). The docker / podman CLIs print + * "Cannot connect to the Docker daemon …" / "Cannot connect to Podman …" (often + * followed by "Is the docker daemon running?") when the socket is down. + */ +export function legacyIsDockerDaemonUnreachable(stderr: string): boolean { + return /cannot connect to the docker daemon|cannot connect to podman|is the docker daemon running/iu.test( + stderr, + ); +} diff --git a/apps/cli/src/legacy/shared/legacy-docker-suggest.unit.test.ts b/apps/cli/src/legacy/shared/legacy-docker-suggest.unit.test.ts new file mode 100644 index 0000000000..0fcadd4f22 --- /dev/null +++ b/apps/cli/src/legacy/shared/legacy-docker-suggest.unit.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from "vitest"; + +import { + LEGACY_SUGGEST_DOCKER_INSTALL, + legacyIsDockerDaemonUnreachable, +} from "./legacy-docker-suggest.ts"; + +describe("legacyIsDockerDaemonUnreachable", () => { + it("detects the docker/podman daemon-down CLI messages (Go's IsErrConnectionFailed)", () => { + expect( + legacyIsDockerDaemonUnreachable( + "Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?", + ), + ).toBe(true); + // Case-insensitive + the podman phrasing. + expect(legacyIsDockerDaemonUnreachable("cannot connect to podman")).toBe(true); + expect(legacyIsDockerDaemonUnreachable("Is the docker daemon running?")).toBe(true); + }); + + it("does not flag an unrelated inspect failure (e.g. a permission error)", () => { + expect(legacyIsDockerDaemonUnreachable("permission denied while trying to connect")).toBe( + false, + ); + expect(legacyIsDockerDaemonUnreachable("Error: No such container: supabase_db_x")).toBe(false); + expect(legacyIsDockerDaemonUnreachable("")).toBe(false); + }); + + it("exposes Go's install hint verbatim", () => { + expect(LEGACY_SUGGEST_DOCKER_INSTALL).toContain("https://docs.docker.com/desktop"); + }); +}); diff --git a/apps/cli/src/legacy/shared/legacy-migration-apply.ts b/apps/cli/src/legacy/shared/legacy-migration-apply.ts index b9b376bea5..3ca0812bdd 100644 --- a/apps/cli/src/legacy/shared/legacy-migration-apply.ts +++ b/apps/cli/src/legacy/shared/legacy-migration-apply.ts @@ -1,5 +1,6 @@ import { Data, Effect, type FileSystem, type Path } from "effect"; +import { Output } from "../../shared/output/output.service.ts"; import type { LegacyDbSession } from "./legacy-db-connection.service.ts"; import { INSERT_MIGRATION_VERSION, @@ -60,7 +61,7 @@ const legacyTrimLeadingSqlComments = (sql: string): string => { * Whether a migration statement cannot run inside a transaction block — `CREATE * [UNIQUE] INDEX CONCURRENTLY`, `REINDEX … CONCURRENTLY`, `VACUUM`, `ALTER SYSTEM`, * `CLUSTER`. Such statements fail with SQLSTATE 25001 inside the `BEGIN`/`COMMIT` - * that wraps a migration, so `legacyApplyMigrationFile` runs them standalone. + * that wraps a migration, so `execMigrationBatch` runs them standalone. * Port of Go's `isPipelineIncompatible` (`pkg/migration/file.go`, supabase/cli#5156). */ export const legacyIsPipelineIncompatible = (sql: string): boolean => { @@ -79,55 +80,45 @@ type LegacyBatchItem = | { readonly kind: "exec"; readonly sql: string } | { readonly kind: "version" }; +const errMessage = (e: unknown): string => + typeof e === "object" && e !== null && "message" in e && typeof e.message === "string" + ? e.message + : String(e); + /** - * Applies a single migration file to the connected database and records it in - * `supabase_migrations.schema_migrations`. Mirrors Go's `migration.ApplyMigrations` - * for one file (`pkg/migration/apply.go` + `(*MigrationFile).ExecBatch`): `RESET ALL` - * first to clear any session state leaked by a prior file, then create the history - * table, then run the file's statements + the history insert. - * - * Statements run inside a `BEGIN`/`COMMIT` batch, except pipeline-incompatible ones + * Runs a single migration/seed file's statements (plus the optional history insert). + * Mirrors Go's `(*MigrationFile).ExecBatch` (`pkg/migration/file.go`): statements run + * inside a `BEGIN`/`COMMIT` batch, except pipeline-incompatible ones * (`legacyIsPipelineIncompatible` — `CREATE INDEX CONCURRENTLY`, `VACUUM`, …) which - * cannot run in a transaction block: the batch is flushed (committed), the statement - * runs standalone, then batching resumes — mirroring Go's `ExecBatch` flush logic - * (supabase/cli#5156). The history insert goes in the final batch, so the migration - * is recorded only after every statement succeeds. A file with no such statements is - * a single `BEGIN`/`COMMIT` around everything, identical to the pre-fix behaviour. + * cannot run in a transaction block: the open batch is flushed (committed), the + * statement runs standalone, then batching resumes (supabase/cli#5156). The history + * insert goes in the final batch, so the migration is recorded only after every + * statement succeeds. A file with no such statements is a single `BEGIN`/`COMMIT`. * - * `mapError` lets the caller tag the failure (e.g. `LegacyDeclarativeApplyError`). + * Does NOT create the history table and does NOT `RESET ALL` — Go's `ExecBatch` does + * neither; those are the migration-apply path's responsibility (`ApplyMigrations`, + * apply.go:65-69), so role/globals files (`legacySeedGlobals`) stay reset-free like Go. + * When `forceNoVersion` is set the history insert is skipped regardless of filename + * (Go's `SeedGlobals` clears `Version`). */ -export const legacyApplyMigrationFile = ( +const execMigrationBatch = ( session: LegacyDbSession, fs: FileSystem.FileSystem, path: Path.Path, migrationPath: string, mapError: (message: string) => E, + forceNoVersion: boolean, ): Effect.Effect => Effect.gen(function* () { const content = yield* fs.readFileString(migrationPath); const statements = legacySplitAndTrim(content); const filename = path.basename(migrationPath); const matches = MIGRATE_FILE_PATTERN.exec(filename); - const version = matches?.[1] ?? ""; + const version = forceNoVersion ? "" : (matches?.[1] ?? ""); const name = matches?.[2] ?? ""; - // `RESET ALL` runs FIRST, before the history-table DDL: an earlier migration applied - // on this same connection may have left a session default (e.g. - // `SET default_transaction_read_only = on`) that would otherwise make this DDL fail - // before it is cleared. Go resets connection state at the top of each file's apply, - // ahead of any work (`apps/cli-go/pkg/migration/apply.go:65-69`). - yield* session.exec("RESET ALL"); - yield* legacyCreateMigrationTable(session); - // Mirror Go's `MigrationFile.ExecBatch` error context (`pkg/migration/file.go`): - // on a failed statement, append `At statement: ` and the statement text so the - // error (and the debug bundle) point at the exact failing SQL. (Go also adds a caret / - // pgErr.Detail / extension-type hint, which need the driver SQLSTATE the session does - // not currently surface — the statement number + text is the always-present context.) - const errMessage = (e: unknown): string => - typeof e === "object" && e !== null && "message" in e && typeof e.message === "string" - ? e.message - : String(e); + // on a failed statement, append `At statement: ` and the statement text. const atStatement = (e: unknown, index: number, stat: string) => new Error(`${errMessage(e)}\nAt statement: ${index}\n${stat}`); @@ -183,10 +174,91 @@ export const legacyApplyMigrationFile = ( pending = [...pending, { kind: "version" }]; } yield* flushBatch; - }).pipe( - Effect.mapError((error) => - mapError( - "message" in error && typeof error.message === "string" ? error.message : String(error), - ), - ), - ); + }).pipe(Effect.mapError((error) => mapError(errMessage(error)))); + +/** + * Go's per-migration connection reset (`apply.go:65-69`): `RESET ALL` clears any + * connection settings a prior statement on the same session may have changed + * (e.g. `set_config('search_path', …)`), run before each migration's `ExecBatch`. + * Only the migration-apply path does this — `SeedGlobals` (role/globals files) + * must NOT, so this is a caller responsibility, never inside `execMigrationBatch`. + */ +const resetConnectionState = ( + session: LegacyDbSession, + mapError: (message: string) => E, +): Effect.Effect => + session.exec("RESET ALL").pipe(Effect.mapError((e) => mapError(errMessage(e)))); + +/** + * Applies a single migration file to the connected database and records it in + * `supabase_migrations.schema_migrations`. Mirrors Go's `migration.ApplyMigrations` + * for one file (`pkg/migration/apply.go` + `(*MigrationFile).ExecBatch`): `RESET ALL` + * first to clear any session state leaked by a prior file (e.g. + * `SET default_transaction_read_only = on`) before the history-table DDL, then create + * the history table, then run the file's statements + the history insert. + * + * `mapError` lets the caller tag the failure (e.g. `LegacyDeclarativeApplyError`). + */ +export const legacyApplyMigrationFile = ( + session: LegacyDbSession, + fs: FileSystem.FileSystem, + path: Path.Path, + migrationPath: string, + mapError: (message: string) => E, +): Effect.Effect => + Effect.gen(function* () { + yield* resetConnectionState(session, mapError); + yield* legacyCreateMigrationTable(session).pipe( + Effect.mapError((e) => mapError(errMessage(e))), + ); + yield* execMigrationBatch(session, fs, path, migrationPath, mapError, false); + }); + +/** + * Applies a list of pending migration files, mirroring Go's + * `migration.ApplyMigrations` (`pkg/migration/apply.go:56-77`): create the + * history table once when there is anything to apply, then for each file emit + * `Applying migration ...` to stderr, `RESET ALL`, and run it transactionally. + */ +export const legacyApplyMigrations = ( + session: LegacyDbSession, + fs: FileSystem.FileSystem, + path: Path.Path, + pending: ReadonlyArray, + mapError: (message: string) => E, +): Effect.Effect => + Effect.gen(function* () { + const output = yield* Output; + if (pending.length === 0) return; + yield* legacyCreateMigrationTable(session).pipe( + Effect.mapError((e) => mapError(errMessage(e))), + ); + for (const migrationPath of pending) { + yield* output.raw(`Applying migration ${path.basename(migrationPath)}...\n`, "stderr"); + // Go resets connection state per migration (apply.go:65-69) before ExecBatch. + yield* resetConnectionState(session, mapError); + yield* execMigrationBatch(session, fs, path, migrationPath, mapError, false); + } + }); + +/** + * Applies custom-role / globals files, mirroring Go's `migration.SeedGlobals` + * (`pkg/migration/seed.go:85-100`): for each file emit `Seeding globals from + * ...` to stderr and run it transactionally WITHOUT inserting a migration + * history row (Go clears `Version`), WITHOUT creating the history table, and WITHOUT + * `RESET ALL` (Go's `SeedGlobals` → `ExecBatch` never resets). + */ +export const legacySeedGlobals = ( + session: LegacyDbSession, + fs: FileSystem.FileSystem, + path: Path.Path, + globals: ReadonlyArray, + mapError: (message: string) => E, +): Effect.Effect => + Effect.gen(function* () { + const output = yield* Output; + for (const globalPath of globals) { + yield* output.raw(`Seeding globals from ${path.basename(globalPath)}...\n`, "stderr"); + yield* execMigrationBatch(session, fs, path, globalPath, mapError, true); + } + }); diff --git a/apps/cli/src/legacy/shared/legacy-migration-apply.unit.test.ts b/apps/cli/src/legacy/shared/legacy-migration-apply.unit.test.ts index 567be21e75..a5039843de 100644 --- a/apps/cli/src/legacy/shared/legacy-migration-apply.unit.test.ts +++ b/apps/cli/src/legacy/shared/legacy-migration-apply.unit.test.ts @@ -5,10 +5,12 @@ import { BunServices } from "@effect/platform-bun"; import { describe, expect, it } from "@effect/vitest"; import { Data, Effect, Exit, FileSystem, Path } from "effect"; +import { mockOutput } from "../../../tests/helpers/mocks.ts"; import type { LegacyDbSession } from "./legacy-db-connection.service.ts"; import { legacyApplyMigrationFile, legacyIsPipelineIncompatible, + legacySeedGlobals, } from "./legacy-migration-apply.ts"; class TestError extends Data.TaggedError("TestError")<{ readonly message: string }> {} @@ -230,3 +232,27 @@ describe("legacyIsPipelineIncompatible", () => { expect(legacyIsPipelineIncompatible(sql)).toBe(want); }); }); + +describe("legacySeedGlobals", () => { + it.effect("runs the globals file WITHOUT RESET ALL and without a history insert", () => { + const dir = mkdtempSync(join(tmpdir(), "legacy-globals-")); + const file = join(dir, "roles.sql"); + writeFileSync(file, "CREATE ROLE my_role;"); + const { session, calls } = fakeSession(); + return Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + yield* legacySeedGlobals(session, fs, path, [file], (message) => new TestError({ message })); + const execs = calls.filter((c) => c.kind === "exec").map((c) => c.sql); + // Go's SeedGlobals calls ExecBatch directly — no RESET ALL (that's only the + // migration-apply path) and no schema-migrations history insert. + expect(execs).not.toContain("RESET ALL"); + expect(execs).toContain("CREATE ROLE my_role"); + expect(calls.some((c) => c.kind === "query")).toBe(false); + rmSync(dir, { recursive: true, force: true }); + }).pipe( + Effect.provide(mockOutput({ format: "text" }).layer), + Effect.provide(BunServices.layer), + ); + }); +}); diff --git a/apps/cli/src/legacy/shared/legacy-prompt-yes-no.ts b/apps/cli/src/legacy/shared/legacy-prompt-yes-no.ts index 64ba6368a6..81499e2b72 100644 --- a/apps/cli/src/legacy/shared/legacy-prompt-yes-no.ts +++ b/apps/cli/src/legacy/shared/legacy-prompt-yes-no.ts @@ -29,6 +29,9 @@ export const legacyParseYesNo = (input: string): boolean | undefined => { * `db pull`, `seed buckets`, and `storage rm`: * - when `yes` is set, echoes `

Release notes

Sourced from google.golang.org/grpc's releases.

Release 1.82.0

Behavior Changes

  • server: Remove support for GRPC_GO_EXPERIMENTAL_DISABLE_STRICT_PATH_CHECKING environment varibale. Strict incoming RPC path validation (which has been the default since v1.79.3) can no longer be disabled. (#9112)
  • transport: Add environment variable to change the default max header list size from 16MB to 8KB. This may be enabled by setting GRPC_GO_EXPERIMENTAL_ENABLE_8KB_DEFAULT_HEADER_LIST_SIZE=true. This will be enabled by default in a subsequent release. (#9019)
  • balancer: Load Balancing policy registry is now case-sensitive. Set GRPC_GO_EXPERIMENTAL_CASE_SENSITIVE_BALANCER_REGISTRIES=false (and file an issue) to revert to case-insensitive behavior. (#9017)

New Features

  • experimental/stats: Expose a new API, NewContextWithLabelCallback, to register a callback that is invoked when telemetry labels are added. (#8877)
  • client: Return a portion of the response body in the error message, when the client receives an unexpected non-gRPC HTTP response, to make debugging easier. (#8929)
  • server: Add environment variable GRPC_GO_SERVER_GOROUTINE_LABELS that controls setting runtime/pprof.Labels on goroutines spawned by the server. Set GRPC_GO_SERVER_GOROUTINE_LABELS=grpc.method=true to add the grpc.method label on goroutines spawned to handle incoming requests. (#9082)

Bug Fixes

  • xds/server: Fix a memory leak of HTTP filter instances occurring when route configurations are updated in-place during a Route Discovery Service (RDS) update. (#9138)
  • grpc: In the deprecated gzip Compressor (used via the deprecated WithCompressor dial option), enforce the MaxRecvMsgSize limit on the decompressed message buffer, preventing excessive memory allocation from highly compressed payloads. (#9114)
  • stats/opentelemetry: Record retry attempts, grpc.previous-rpc-attempts, at the call level and not the attempt level. (#8923)
  • encoding: Ensure Close() is always called on readers returned from Compressor.Decompress if possible. (#9135)
  • channelz: Fix the LastMessageSentTimestamp and LastMessageReceivedTimestamp fields in SocketMetrics to ensure they contain correct timestamp values. (#9109)
Commits
  • bd23985 Change version to 1.82.0 (#9170)
  • 0f3086d Fix minor issues not covered by PR #9137 (#9147)
  • fef07fb internal: Split v3procservicepb import into pb and grpc for extproc (#9163)
  • 91dd64f transport: surface subsequent data when receiving non-gRPC header (#8929)
  • adc97de test/kokoro: add config for regional-td test (#9158)
  • 57c9ff1 xds: ensure full-string matching for RBAC Filter rules (#9148)
  • b58f32d server: Set a pprof label on new stream goroutines (#9082)
  • 6c98be3 refactor(transport): extract shared stream state handling logic in `loopyWrit...
  • bcaa6f4 rls: only reset backoff on recovery from TRANSIENT_FAILURE (#9137)
  • 429e6e0 balancer: expose endpoint weight and hostname as experimental APIs (#9074)
  • Additional commits viewable in compare view

Updates `google.golang.org/grpc` from 1.81.1 to 1.82.0
Release notes

Sourced from google.golang.org/grpc's releases.

Release 1.82.0

Behavior Changes

  • server: Remove support for GRPC_GO_EXPERIMENTAL_DISABLE_STRICT_PATH_CHECKING environment varibale. Strict incoming RPC path validation (which has been the default since v1.79.3) can no longer be disabled. (#9112)
  • transport: Add environment variable to change the default max header list size from 16MB to 8KB. This may be enabled by setting GRPC_GO_EXPERIMENTAL_ENABLE_8KB_DEFAULT_HEADER_LIST_SIZE=true. This will be enabled by default in a subsequent release. (#9019)
  • balancer: Load Balancing policy registry is now case-sensitive. Set GRPC_GO_EXPERIMENTAL_CASE_SENSITIVE_BALANCER_REGISTRIES=false (and file an issue) to revert to case-insensitive behavior. (#9017)

New Features

  • experimental/stats: Expose a new API, NewContextWithLabelCallback, to register a callback that is invoked when telemetry labels are added. (#8877)
  • client: Return a portion of the response body in the error message, when the client receives an unexpected non-gRPC HTTP response, to make debugging easier. (#8929)
  • server: Add environment variable GRPC_GO_SERVER_GOROUTINE_LABELS that controls setting runtime/pprof.Labels on goroutines spawned by the server. Set GRPC_GO_SERVER_GOROUTINE_LABELS=grpc.method=true to add the grpc.method label on goroutines spawned to handle incoming requests. (#9082)

Bug Fixes

  • xds/server: Fix a memory leak of HTTP filter instances occurring when route configurations are updated in-place during a Route Discovery Service (RDS) update. (#9138)
  • grpc: In the deprecated gzip Compressor (used via the deprecated WithCompressor dial option), enforce the MaxRecvMsgSize limit on the decompressed message buffer, preventing excessive memory allocation from highly compressed payloads. (#9114)
  • stats/opentelemetry: Record retry attempts, grpc.previous-rpc-attempts, at the call level and not the attempt level. (#8923)
  • encoding: Ensure Close() is always called on readers returned from Compressor.Decompress if possible. (#9135)
  • channelz: Fix the LastMessageSentTimestamp and LastMessageReceivedTimestamp fields in SocketMetrics to ensure they contain correct timestamp values. (#9109)
Commits
  • bd23985 Change version to 1.82.0 (#9170)
  • 0f3086d Fix minor issues not covered by PR #9137 (#9147)
  • fef07fb internal: Split v3procservicepb import into pb and grpc for extproc (#9163)
  • 91dd64f transport: surface subsequent data when receiving non-gRPC header (#8929)
  • adc97de test/kokoro: add config for regional-td test (#9158)
  • 57c9ff1 xds: ensure full-string matching for RBAC Filter rules (#9148)
  • b58f32d server: Set a pprof label on new stream goroutines (#9082)
  • 6c98be3 refactor(transport): extract shared stream state handling logic in `loopyWrit...
  • bcaa6f4 rls: only reset backoff on recovery from TRANSIENT_FAILURE (#9137)
  • 429e6e0 balancer: expose endpoint weight and hostname as experimental APIs (#9074)
  • Additional commits viewable in compare view

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/cli-go/go.mod | 2 +- apps/cli-go/go.sum | 4 ++-- apps/cli-go/pkg/go.mod | 8 ++++---- apps/cli-go/pkg/go.sum | 16 ++++++++-------- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/apps/cli-go/go.mod b/apps/cli-go/go.mod index 82b8ea4526..507f7f885f 100644 --- a/apps/cli-go/go.mod +++ b/apps/cli-go/go.mod @@ -58,7 +58,7 @@ require ( golang.org/x/net v0.56.0 golang.org/x/oauth2 v0.36.0 golang.org/x/term v0.44.0 - google.golang.org/grpc v1.81.1 + google.golang.org/grpc v1.82.0 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/apps/cli-go/go.sum b/apps/cli-go/go.sum index f929d36494..bdde20104c 100644 --- a/apps/cli-go/go.sum +++ b/apps/cli-go/go.sum @@ -1441,8 +1441,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go. google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk= google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.0.5/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= -google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= -google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= +google.golang.org/grpc v1.82.0 h1:vguDnZUPjE26w09A63VoxZPnvPjB5Riyc0mkXPFmAIU= +google.golang.org/grpc v1.82.0/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= diff --git a/apps/cli-go/pkg/go.mod b/apps/cli-go/pkg/go.mod index 3de3c61ae8..eca10c84ab 100644 --- a/apps/cli-go/pkg/go.mod +++ b/apps/cli-go/pkg/go.mod @@ -26,7 +26,7 @@ require ( github.com/stretchr/testify v1.11.1 github.com/tidwall/jsonc v0.3.3 golang.org/x/mod v0.37.0 - google.golang.org/grpc v1.81.1 + google.golang.org/grpc v1.82.0 ) require ( @@ -51,8 +51,8 @@ require ( github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.48.0 // indirect - golang.org/x/sys v0.42.0 // indirect - golang.org/x/text v0.34.0 // indirect + golang.org/x/crypto v0.50.0 // indirect + golang.org/x/sys v0.43.0 // indirect + golang.org/x/text v0.36.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/apps/cli-go/pkg/go.sum b/apps/cli-go/pkg/go.sum index 8394b09a98..c622bcdf1e 100644 --- a/apps/cli-go/pkg/go.sum +++ b/apps/cli-go/pkg/go.sum @@ -217,8 +217,8 @@ golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= -golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= -golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= @@ -255,8 +255,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= -golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -272,8 +272,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= @@ -290,8 +290,8 @@ golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= -google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= +google.golang.org/grpc v1.82.0 h1:vguDnZUPjE26w09A63VoxZPnvPjB5Riyc0mkXPFmAIU= +google.golang.org/grpc v1.82.0/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= From bfcb4c4537059970395127318013ff31e6001816 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 00:13:46 +0000 Subject: [PATCH 15/79] chore(ci): bump the actions-major group with 2 updates (#5824) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the actions-major group with 2 updates: [docker/build-push-action](https://github.com/docker/build-push-action) and [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action). Updates `docker/build-push-action` from 7.2.0 to 7.3.0
Release notes

Sourced from docker/build-push-action's releases.

v7.3.0

Full Changelog: https://github.com/docker/build-push-action/compare/v7.2.0...v7.3.0

Commits
  • 53b7df9 Merge pull request #1572 from docker/dependabot/npm_and_yarn/docker/actions-t...
  • 154298c [dependabot skip] chore: update generated content
  • cb1238b chore(deps): Bump @​docker/actions-toolkit from 0.91.0 to 0.92.0
  • 24f845d Merge pull request #1566 from docker/dependabot/npm_and_yarn/js-yaml-4.2.0
  • 9c69730 [dependabot skip] chore: update generated content
  • bc3a3a5 Merge pull request #1574 from docker/dependabot/github_actions/aws-actions/co...
  • a82c504 chore(deps): Bump js-yaml from 4.1.1 to 4.3.0
  • 0285a75 Merge pull request #1573 from docker/dependabot/github_actions/actions/cache-...
  • c6ad2a3 Merge pull request #1575 from docker/dependabot/github_actions/actions/checko...
  • d37484f Merge pull request #1564 from docker/dependabot/npm_and_yarn/undici-6.27.0
  • Additional commits viewable in compare view

Updates `docker/setup-qemu-action` from 4.1.0 to 4.2.0
Release notes

Sourced from docker/setup-qemu-action's releases.

v4.2.0

Full Changelog: https://github.com/docker/setup-qemu-action/compare/v4.1.0...v4.2.0

Commits
  • 96fe6ef Merge pull request #315 from docker/dependabot/npm_and_yarn/docker/actions-to...
  • 31f08d3 [dependabot skip] chore: update generated content
  • 4e7017a build(deps): bump @​docker/actions-toolkit from 0.91.0 to 0.92.0
  • 0eca235 Merge pull request #314 from crazy-max/fix-yarn-preapprove-actions-toolkit
  • ea66a41 chore: allow actions-toolkit to bypass yarn age gate
  • 451542b Merge pull request #308 from docker/dependabot/npm_and_yarn/undici-6.27.0
  • 532ae00 [dependabot skip] chore: update generated content
  • b6f5af6 build(deps): bump undici from 6.26.0 to 6.27.0
  • cf96b86 Merge pull request #304 from docker/dependabot/npm_and_yarn/tmp-0.2.7
  • f0ba643 [dependabot skip] chore: update generated content
  • Additional commits viewable in compare view

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/cli-go-pg-prove.yml | 4 ++-- .github/workflows/cli-go-publish-migra.yml | 4 ++-- .github/workflows/release-shared.yml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/cli-go-pg-prove.yml b/.github/workflows/cli-go-pg-prove.yml index 14202acc5c..4cd2b086e5 100644 --- a/.github/workflows/cli-go-pg-prove.yml +++ b/.github/workflows/cli-go-pg-prove.yml @@ -12,7 +12,7 @@ jobs: image_tag: supabase/pg_prove:${{ steps.version.outputs.pg_prove }} steps: - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 - - uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 + - uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 with: load: true context: https://github.com/horrendo/pg_prove.git @@ -50,7 +50,7 @@ jobs: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - id: build - uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 with: push: true context: https://github.com/horrendo/pg_prove.git diff --git a/.github/workflows/cli-go-publish-migra.yml b/.github/workflows/cli-go-publish-migra.yml index 5220a0562d..95449a8baa 100644 --- a/.github/workflows/cli-go-publish-migra.yml +++ b/.github/workflows/cli-go-publish-migra.yml @@ -12,7 +12,7 @@ jobs: image_tag: supabase/migra:${{ steps.version.outputs.migra }} steps: - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 - - uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 + - uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 with: load: true context: https://github.com/djrobstep/migra.git @@ -50,7 +50,7 @@ jobs: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - id: build - uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 with: push: true context: https://github.com/djrobstep/migra.git diff --git a/.github/workflows/release-shared.yml b/.github/workflows/release-shared.yml index 79a3175805..a2e30ccfe0 100644 --- a/.github/workflows/release-shared.yml +++ b/.github/workflows/release-shared.yml @@ -160,7 +160,7 @@ jobs: - name: Setup QEMU for cross-platform Docker if: runner.os == 'Linux' - uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0 + uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0 - name: Verify linux/arm64 emulation is registered if: runner.os == 'Linux' From 38a2a98e47bd4121e8517c148a66f625f4e0cc15 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 00:14:24 +0000 Subject: [PATCH 16/79] fix(deps): bump the npm-major group with 6 updates (#5823) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the npm-major group with 6 updates: | Package | From | To | | --- | --- | --- | | [@supabase/supabase-js](https://github.com/supabase/supabase-js/tree/HEAD/packages/core/supabase-js) | `2.108.2` | `2.110.0` | | [@anthropic-ai/claude-agent-sdk](https://github.com/anthropics/claude-agent-sdk-typescript) | `0.3.196` | `0.3.197` | | [@anthropic-ai/sdk](https://github.com/anthropics/anthropic-sdk-typescript) | `0.107.0` | `0.109.0` | | [posthog-node](https://github.com/PostHog/posthog-js/tree/HEAD/packages/node) | `5.38.8` | `5.39.1` | | [@typescript/native-preview](https://github.com/microsoft/typescript-go) | `7.0.0-dev.20260629.1` | `7.0.0-dev.20260630.1` | | [oxlint-tsgolint](https://github.com/oxc-project/tsgolint) | `0.23.0` | `0.24.0` | Updates `@supabase/supabase-js` from 2.108.2 to 2.110.0
Release notes

Sourced from @​supabase/supabase-js's releases.

v2.110.0

2.110.0 (2026-06-30)

🚀 Features

  • repo: drop Node.js 20 support (#2482)

❤️ Thank You

v2.110.0-canary.0

2.110.0-canary.0 (2026-06-30)

🚀 Features

  • repo: drop Node.js 20 support (#2482)

❤️ Thank You

v2.109.0

2.109.0 (2026-06-30)

🚀 Features

  • auth: add custom_claims_allowlist to custom providers admin API (#2473)
  • realtime: add postgres_changes filter builder, new operators and select (#2463)
  • storage: expose purgeCache for buckets and single objects (#2429)

🩹 Fixes

  • functions: honor a caller's Content-Type override regardless of casing (#2455)
  • realtime: pin @​supabase/phoenix and browser test CDN deps (#2457)
  • realtime: add replication connection system message option (#2470)
  • storage: keep sortBy defaults when list() is given a partial sortBy (#2454)

❤️ Thank You

v2.108.3-canary.2

2.108.3-canary.2 (2026-06-19)

... (truncated)

Changelog

Sourced from @​supabase/supabase-js's changelog.

2.110.0 (2026-06-30)

🚀 Features

  • repo: drop Node.js 20 support (#2482)

❤️ Thank You

2.109.0 (2026-06-30)

🩹 Fixes

  • realtime: pin @​supabase/phoenix and browser test CDN deps (#2457)

❤️ Thank You

Commits

Updates `@anthropic-ai/claude-agent-sdk` from 0.3.196 to 0.3.197
Release notes

Sourced from @​anthropic-ai/claude-agent-sdk's releases.

v0.3.197

What's changed

  • Updated to parity with Claude Code v2.1.197

Update

npm install @anthropic-ai/claude-agent-sdk@0.3.197
# or
yarn add @anthropic-ai/claude-agent-sdk@0.3.197
# or
pnpm add @anthropic-ai/claude-agent-sdk@0.3.197
# or
bun add @anthropic-ai/claude-agent-sdk@0.3.197
Changelog

Sourced from @​anthropic-ai/claude-agent-sdk's changelog.

0.3.197

  • Updated to parity with Claude Code v2.1.197
Commits

Updates `@anthropic-ai/sdk` from 0.107.0 to 0.109.0
Release notes

Sourced from @​anthropic-ai/sdk's releases.

sdk: v0.109.0

0.109.0 (2026-06-30)

Full Changelog: sdk-v0.108.0...sdk-v0.109.0

Features

  • api: add support for Managed Agents event delta streaming, agent overrides, reverse pagination, vault credential injection scoping, and agent and deployment webhook events (7f3211b)

sdk: v0.108.0

0.108.0 (2026-06-30)

Full Changelog: sdk-v0.107.0...sdk-v0.108.0

Features

  • api: add support for claude-sonnet-5 (4588db0)

Bug Fixes

  • agent-toolset: allow absolute paths that resolve inside workdir (#112) (e951fb2)

Chores

Changelog

Sourced from @​anthropic-ai/sdk's changelog.

0.109.0 (2026-06-30)

Full Changelog: sdk-v0.108.0...sdk-v0.109.0

Features

  • api: add support for Managed Agents event delta streaming, agent overrides, reverse pagination, vault credential injection scoping, and agent and deployment webhook events (7f3211b)

0.108.0 (2026-06-30)

Full Changelog: sdk-v0.107.0...sdk-v0.108.0

Features

  • api: add support for claude-sonnet-5 (4588db0)

Bug Fixes

  • agent-toolset: allow absolute paths that resolve inside workdir (#112) (e951fb2)

Chores

Commits
  • cb829c7 chore: release main
  • fd0341d feat(api): add support for Managed Agents event delta streaming, agent overri...
  • 90f767a codegen metadata
  • ed986cd chore: release main
  • d3ffad4 chore: format README.md (#176)
  • 6b46ad3 feat(api): add support for claude-sonnet-5
  • 8200ffa feat(vertex): bump google-auth-library to ^10.2.0 (SDK-91) (#30)
  • fa06d36 feat(bedrock): pass client logger to AWS credential provider chain (SDK-90) (...
  • 0fff7fa fix(agent-toolset): allow absolute paths that resolve inside workdir (#112)
  • See full diff in compare view

Updates `posthog-node` from 5.38.8 to 5.39.1
Release notes

Sourced from posthog-node's releases.

posthog-node@5.39.1

5.39.1

Patch Changes

  • #4029 b36b1cc Thanks @​marandaneto! - Call before_send for identify, group identify, and alias events. (2026-06-30)

  • #4027 ab118d2 Thanks @​marandaneto! - Safely serialize event batches with circular property references instead of crashing during flush. (2026-06-30)

  • Updated dependencies [ab118d2]:

    • @​posthog/core@​1.39.2

posthog-node@5.39.0

5.39.0

Minor Changes

  • #4006 0063128 Thanks @​github-actions! - Add groupIdentifyImmediate() to await the network request when identifying a group, mirroring captureImmediate/identifyImmediate/aliasImmediate. Useful in edge/serverless environments where the background queue may not flush. The Convex integration now uses it directly instead of routing $groupidentify through captureImmediate. (2026-06-30)

Patch Changes

  • Updated dependencies [0063128]:
    • @​posthog/core@​1.39.0
Changelog

Sourced from posthog-node's changelog.

5.39.1

Patch Changes

  • #4029 b36b1cc Thanks @​marandaneto! - Call before_send for identify, group identify, and alias events. (2026-06-30)

  • #4027 ab118d2 Thanks @​marandaneto! - Safely serialize event batches with circular property references instead of crashing during flush. (2026-06-30)

  • Updated dependencies [ab118d2]:

    • @​posthog/core@​1.39.2

5.39.0

Minor Changes

  • #4006 0063128 Thanks @​github-actions! - Add groupIdentifyImmediate() to await the network request when identifying a group, mirroring captureImmediate/identifyImmediate/aliasImmediate. Useful in edge/serverless environments where the background queue may not flush. The Convex integration now uses it directly instead of routing $groupidentify through captureImmediate. (2026-06-30)

Patch Changes

  • Updated dependencies [0063128]:
    • @​posthog/core@​1.39.0
Commits
  • a5181ba chore: update versions and lockfile [version bump]
  • b36b1cc fix(node): run before_send for all captured events (#4029)
  • ab118d2 fix(node): handle circular event properties during flush (#4027)
  • 0c95bce fix: satisfy SDK compliance harness 0.8.0 (#3998)
  • 254c5b1 chore: update versions and lockfile [version bump]
  • 0063128 feat: Add groupIdentifyImmediate() method to Node.js SDK (#4006)
  • See full diff in compare view

Updates `@typescript/native-preview` from 7.0.0-dev.20260629.1 to 7.0.0-dev.20260630.1
Commits

Updates `oxlint-tsgolint` from 0.23.0 to 0.24.0
Release notes

Sourced from oxlint-tsgolint's releases.

v0.24.0

What's Changed

... (truncated)

Commits
  • 5a37e89 fix(dot-notation): determine the relevant accessor (#1028)
  • 67a281f perf(consistent-return): defer per-function type resolution (#1031)
  • a5e2ff0 perf(no-unnecessary-qualifier): skip symbol resolution outside namespaces. (#...
  • a8fc668 perf(no-confusing-void-expression): check ancestor position before type query...
  • 03158cc perf(no-unnecessary-type-conversion): hoist constant builtin-name slices (#1040)
  • d9e645c perf(prefer-optional-chain): lazily allocate chain-processor caches (#1041)
  • 63f578a refactor(no-unnecessary-condition): remove dead containsUnguardedElementAcces...
  • f174876 chore(deps): update gomod (#1035)
  • 47de9cf chore(deps): update github actions (#1036)
  • e209b5b chore(deps): update actions/cache action to v6 (#1037)
  • Additional commits viewable in compare view

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/cli/package.json | 6 +- packages/stack/package.json | 2 +- pnpm-lock.yaml | 330 ++++++++++++++++++------------------ pnpm-workspace.yaml | 4 +- 4 files changed, 171 insertions(+), 171 deletions(-) diff --git a/apps/cli/package.json b/apps/cli/package.json index c184c44d66..345a397df7 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -43,8 +43,8 @@ "jose": "^6.2.3" }, "devDependencies": { - "@anthropic-ai/claude-agent-sdk": "^0.3.196", - "@anthropic-ai/sdk": "^0.107.0", + "@anthropic-ai/claude-agent-sdk": "^0.3.197", + "@anthropic-ai/sdk": "^0.109.0", "@clack/prompts": "^1.6.0", "@effect/atom-react": "catalog:", "@effect/platform-bun": "catalog:", @@ -76,7 +76,7 @@ "oxlint-tsgolint": "catalog:", "pg": "^8.22.0", "pg-copy-streams": "^7.0.0", - "posthog-node": "^5.38.8", + "posthog-node": "^5.39.1", "react": "^19.2.7", "react-devtools-core": "^7.0.1", "semantic-release": "^25.0.5", diff --git a/packages/stack/package.json b/packages/stack/package.json index 284f92a896..3b6b7c3e9e 100644 --- a/packages/stack/package.json +++ b/packages/stack/package.json @@ -28,7 +28,7 @@ }, "devDependencies": { "@effect/vitest": "catalog:", - "@supabase/supabase-js": "^2.108.2", + "@supabase/supabase-js": "^2.110.0", "@tsconfig/bun": "catalog:", "@types/bun": "catalog:", "@typescript/native-preview": "catalog:", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a8a9506881..32618873a5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -37,8 +37,8 @@ catalogs: specifier: ^1.3.14 version: 1.3.14 '@typescript/native-preview': - specifier: 7.0.0-dev.20260629.1 - version: 7.0.0-dev.20260629.1 + specifier: 7.0.0-dev.20260630.1 + version: 7.0.0-dev.20260630.1 '@vitest/coverage-istanbul': specifier: ^4.1.9 version: 4.1.9 @@ -58,8 +58,8 @@ catalogs: specifier: ^1.72.0 version: 1.73.0 oxlint-tsgolint: - specifier: ^0.23.0 - version: 0.23.0 + specifier: ^0.24.0 + version: 0.24.0 tldts: specifier: ^7.4.5 version: 7.4.5 @@ -97,11 +97,11 @@ importers: version: 6.2.3 devDependencies: '@anthropic-ai/claude-agent-sdk': - specifier: ^0.3.196 - version: 0.3.196(@anthropic-ai/sdk@0.107.0(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(zod@4.4.3) + specifier: ^0.3.197 + version: 0.3.197(@anthropic-ai/sdk@0.109.0(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(zod@4.4.3) '@anthropic-ai/sdk': - specifier: ^0.107.0 - version: 0.107.0(zod@4.4.3) + specifier: ^0.109.0 + version: 0.109.0(zod@4.4.3) '@clack/prompts': specifier: ^1.6.0 version: 1.6.0 @@ -155,7 +155,7 @@ importers: version: 19.2.17 '@typescript/native-preview': specifier: 'catalog:' - version: 7.0.0-dev.20260629.1 + version: 7.0.0-dev.20260630.1 '@vercel/detect-agent': specifier: ^1.2.3 version: 1.2.3 @@ -185,10 +185,10 @@ importers: version: 0.57.0 oxlint: specifier: 'catalog:' - version: 1.73.0(oxlint-tsgolint@0.23.0) + version: 1.73.0(oxlint-tsgolint@0.24.0) oxlint-tsgolint: specifier: 'catalog:' - version: 0.23.0 + version: 0.24.0 pg: specifier: ^8.22.0 version: 8.22.0 @@ -196,8 +196,8 @@ importers: specifier: ^7.0.0 version: 7.0.0 posthog-node: - specifier: ^5.38.8 - version: 5.38.8 + specifier: ^5.39.1 + version: 5.39.1 react: specifier: ^19.2.7 version: 19.2.7 @@ -259,7 +259,7 @@ importers: version: 1.3.14 '@typescript/native-preview': specifier: 'catalog:' - version: 7.0.0-dev.20260629.1 + version: 7.0.0-dev.20260630.1 '@vitest/coverage-istanbul': specifier: 'catalog:' version: 4.1.9(vitest@4.1.9) @@ -271,10 +271,10 @@ importers: version: 0.57.0 oxlint: specifier: 'catalog:' - version: 1.73.0(oxlint-tsgolint@0.23.0) + version: 1.73.0(oxlint-tsgolint@0.24.0) oxlint-tsgolint: specifier: 'catalog:' - version: 0.23.0 + version: 0.24.0 vitest: specifier: 'catalog:' version: 4.1.9(@types/node@26.0.1)(@vitest/coverage-istanbul@4.1.9)(vite@8.0.14(@types/node@26.0.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) @@ -339,7 +339,7 @@ importers: version: 1.3.14 '@typescript/native-preview': specifier: 'catalog:' - version: 7.0.0-dev.20260629.1 + version: 7.0.0-dev.20260630.1 '@vitest/coverage-istanbul': specifier: 'catalog:' version: 4.1.9(vitest@4.1.9) @@ -351,10 +351,10 @@ importers: version: 0.57.0 oxlint: specifier: 'catalog:' - version: 1.73.0(oxlint-tsgolint@0.23.0) + version: 1.73.0(oxlint-tsgolint@0.24.0) oxlint-tsgolint: specifier: 'catalog:' - version: 0.23.0 + version: 0.24.0 vitest: specifier: 'catalog:' version: 4.1.9(@types/node@26.0.1)(@vitest/coverage-istanbul@4.1.9)(vite@8.0.14(@types/node@26.0.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) @@ -381,7 +381,7 @@ importers: version: 1.3.14 '@typescript/native-preview': specifier: 'catalog:' - version: 7.0.0-dev.20260629.1 + version: 7.0.0-dev.20260630.1 '@vitest/coverage-istanbul': specifier: 'catalog:' version: 4.1.9(vitest@4.1.9) @@ -393,10 +393,10 @@ importers: version: 0.57.0 oxlint: specifier: 'catalog:' - version: 1.73.0(oxlint-tsgolint@0.23.0) + version: 1.73.0(oxlint-tsgolint@0.24.0) oxlint-tsgolint: specifier: 'catalog:' - version: 0.23.0 + version: 0.24.0 vitest: specifier: 'catalog:' version: 4.1.9(@types/node@26.0.1)(@vitest/coverage-istanbul@4.1.9)(vite@8.0.14(@types/node@26.0.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) @@ -431,7 +431,7 @@ importers: version: 1.3.14 '@typescript/native-preview': specifier: 'catalog:' - version: 7.0.0-dev.20260629.1 + version: 7.0.0-dev.20260630.1 '@vitest/coverage-istanbul': specifier: 'catalog:' version: 4.1.9(vitest@4.1.9) @@ -443,10 +443,10 @@ importers: version: 0.57.0 oxlint: specifier: 'catalog:' - version: 1.73.0(oxlint-tsgolint@0.23.0) + version: 1.73.0(oxlint-tsgolint@0.24.0) oxlint-tsgolint: specifier: 'catalog:' - version: 0.23.0 + version: 0.24.0 vitest: specifier: 'catalog:' version: 4.1.9(@types/node@26.0.1)(@vitest/coverage-istanbul@4.1.9)(vite@8.0.14(@types/node@26.0.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) @@ -471,7 +471,7 @@ importers: version: 1.3.14 '@typescript/native-preview': specifier: 'catalog:' - version: 7.0.0-dev.20260629.1 + version: 7.0.0-dev.20260630.1 '@vitest/coverage-istanbul': specifier: 'catalog:' version: 4.1.9(vitest@4.1.9) @@ -483,10 +483,10 @@ importers: version: 0.57.0 oxlint: specifier: 'catalog:' - version: 1.73.0(oxlint-tsgolint@0.23.0) + version: 1.73.0(oxlint-tsgolint@0.24.0) oxlint-tsgolint: specifier: 'catalog:' - version: 0.23.0 + version: 0.24.0 vitest: specifier: 'catalog:' version: 4.1.9(@types/node@26.0.1)(@vitest/coverage-istanbul@4.1.9)(vite@8.0.14(@types/node@26.0.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) @@ -513,8 +513,8 @@ importers: specifier: 'catalog:' version: 4.0.0-beta.93(effect@4.0.0-beta.93)(vitest@4.1.9) '@supabase/supabase-js': - specifier: ^2.108.2 - version: 2.108.2 + specifier: ^2.110.0 + version: 2.110.0 '@tsconfig/bun': specifier: 'catalog:' version: 1.0.10 @@ -523,7 +523,7 @@ importers: version: 1.3.14 '@typescript/native-preview': specifier: 'catalog:' - version: 7.0.0-dev.20260629.1 + version: 7.0.0-dev.20260630.1 '@vitest/coverage-istanbul': specifier: 'catalog:' version: 4.1.9(vitest@4.1.9) @@ -535,10 +535,10 @@ importers: version: 0.57.0 oxlint: specifier: 'catalog:' - version: 1.73.0(oxlint-tsgolint@0.23.0) + version: 1.73.0(oxlint-tsgolint@0.24.0) oxlint-tsgolint: specifier: 'catalog:' - version: 0.23.0 + version: 0.24.0 vitest: specifier: 'catalog:' version: 4.1.9(@types/node@26.0.1)(@vitest/coverage-istanbul@4.1.9)(vite@8.0.14(@types/node@26.0.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) @@ -570,60 +570,60 @@ packages: resolution: {integrity: sha512-p+CMKJ93HFmLkjXKlXiVGlMQEuRb6H0MokBSwUsX+S6BRX8eV5naFZpQJFfJHjRZY0Hmnqy1/r6UWl3x+19zYA==} engines: {node: '>=18'} - '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.196': - resolution: {integrity: sha512-k1MKRDhSiNKpkTwhtU8QKGzfuRfr3YXS6oqsTuldMROX56L5iMXjzC7AYU1/KmPTeMl3SCQBQeDu0i8ciA0hQA==} + '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.197': + resolution: {integrity: sha512-jC6WvH5Hr6APTfbMjo4nC6LlyMMqbpCMwiHXIw7/AsQXIHQhZ+cRRMesQlV6UFI1l3O53gLZHzsG9cXwfrPHKw==} cpu: [arm64] os: [darwin] - '@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.196': - resolution: {integrity: sha512-bBwx/7yKZMQ9NSUt4bg8P+zp6pgd/O/DTkzdqsRIivBvARmwo1QY/2qGrLO8RH0T8CG2lTjFNfDfOlJWbAkvAg==} + '@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.197': + resolution: {integrity: sha512-ZQNvGkMrTyatBlHTIQ4w2i2aLBuvq355UP/FDLnVXIH8l23RsL1x/0w9P+dqB7EmY9OZi/cPxSrpskpo+dZWLA==} cpu: [x64] os: [darwin] - '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.196': - resolution: {integrity: sha512-BhLxfx4j6mC3Uzmve1IbhFS1uvNlwATeo6uWyYDOMW4n3XKjiSgjD8bTfkamijrxCMvJo5swLju2y14ayDsokA==} + '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.197': + resolution: {integrity: sha512-VuIGXsLGK/aqSQ0tTBqqPVNzjefWS5SWnK8mlYyQitT4s5UDzHXJm0UZBTGxRtlcS0e2+QAHKwbGBCq1ZKSXjg==} cpu: [arm64] os: [linux] libc: [musl] - '@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.196': - resolution: {integrity: sha512-fR5fy+pSQSpKZK0zTtAl3LZEGQTuwVK7svutH1bZUS5RGT2HdUWc71oltSZgW4upGaslj+gyusHGJHA5eAc+dw==} + '@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.197': + resolution: {integrity: sha512-pWhQgCtAft4EGM4Zn24HRad1a/k2u6oA+2uM/KCdjehfKtooDiHfMNd1yzXY/n9AEBWP0RHB2Vz3mJ30X2pVAg==} cpu: [arm64] os: [linux] libc: [glibc] - '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.196': - resolution: {integrity: sha512-EOiNbxCXQLYzV7SQhMWkUC1ScWyTw/Qp+JyV2sEmIbzl/e7KMOxNE9J20k+lpPs6CXdxVzuMwH7yAGuDu5y+IQ==} + '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.197': + resolution: {integrity: sha512-3Tuy7XhD4UIKE4A4RPmKJcbL7Q/3dcB1hEWQt2lKP7c/DlixeEv+tRzvpnFZKhFX2hy0tkBk3QjkozSAacMC/w==} cpu: [x64] os: [linux] libc: [musl] - '@anthropic-ai/claude-agent-sdk-linux-x64@0.3.196': - resolution: {integrity: sha512-9spZON7/tn0q9J+jICrdfHi7o7Fmjs9pIohCCxL+Yv7HbBXWVtEYJbYipBGLTl8ICG3mgPeEVMNsvOuh/jDTuA==} + '@anthropic-ai/claude-agent-sdk-linux-x64@0.3.197': + resolution: {integrity: sha512-AUccrbdcv4Hy/GteP/gYLjG/zDP+fe2BFtDMctEfRFVz40DazYDcOyW1+nIgSTQtxf5jSTAVVf3cNuXB2CZwlw==} cpu: [x64] os: [linux] libc: [glibc] - '@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.196': - resolution: {integrity: sha512-0xXkAWlDof/qFi3k5KJZ5WYbgp1X8hZHjyasOWTZWAmldoYaENI0vkL+PVMarvoAFYRRqcWoS81FSgm0QgH2sA==} + '@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.197': + resolution: {integrity: sha512-Wx8uiAKBenDuL8lWQmrqnX5ppljaH5unQ9cKiCz2/9Kgf09dgnrwbX8n/FhndCZR8PmYw539eWwYVrSVc/bl6w==} cpu: [arm64] os: [win32] - '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.196': - resolution: {integrity: sha512-FxWLA3aOYgDf2J0o6Ov1/wgg9X6RkrgnX4ifUWO1i3+6mbc4efNLHkDZLxCHEnQgW8yBidX+iro19f9k64vV8A==} + '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.197': + resolution: {integrity: sha512-ZXJO/VvR3SI4G0gwthWeFXWdHB5RXPu3rtfGRcKZ/YgtDeW17rQ+LZIJTk2ywzbLb8EvlghR5JPgn293hC179Q==} cpu: [x64] os: [win32] - '@anthropic-ai/claude-agent-sdk@0.3.196': - resolution: {integrity: sha512-yqsp1/04T2/tJ54jz+7YLsTZOPeQ/myUCrv17/IVZ9dbl+izIMP9ULuLpPCH5pj5msXxR80es+hpVgHoFuNm0A==} + '@anthropic-ai/claude-agent-sdk@0.3.197': + resolution: {integrity: sha512-XNIi8W1tb+QfMkcK+5kepOC6BsxG8wtupd72H+pIPzIJypVQhHy7FoX+KBMtTRYwtl+5dsjKyABhjWXebeUilw==} engines: {node: '>=18.0.0'} peerDependencies: '@anthropic-ai/sdk': '>=0.93.0' '@modelcontextprotocol/sdk': ^1.29.0 zod: ^4.0.0 - '@anthropic-ai/sdk@0.107.0': - resolution: {integrity: sha512-RWDWyvIeZnatUTzyX8+ayFzAqqLyoDHKnDEODFyW8H89zH+qEsh5h6XAmnbHY5DCoa58o3rjuNe3F3Hg851ayA==} + '@anthropic-ai/sdk@0.109.0': + resolution: {integrity: sha512-y7P4eLyW5uNut4fXpOUEHqhJwx7dnxrWAfCQE4Lcgm0hSFQuIeHa7CWEKE5dFolEjQJE/RFKkppjri05r2OK/Q==} hasBin: true peerDependencies: zod: ^3.25.0 || ^4.0.0 @@ -1852,33 +1852,33 @@ packages: cpu: [x64] os: [win32] - '@oxlint-tsgolint/darwin-arm64@0.23.0': - resolution: {integrity: sha512-gOs9PVr2wEg4ox9z0aJo+RKhhImW86YL5N6yav8BK/rgPsIrwN/igSZ+pbRr723NFvUNKde9fgMhRA6JrXAOZw==} + '@oxlint-tsgolint/darwin-arm64@0.24.0': + resolution: {integrity: sha512-C2uMmwK5Bc4ri4ysZ6sA8Rcu+A5zBQTp6ml2u0CLLbRZp4kMFPV3yWk8B5DK9Aw7y9bbjogIm75tUwGLFzlsYQ==} cpu: [arm64] os: [darwin] - '@oxlint-tsgolint/darwin-x64@0.23.0': - resolution: {integrity: sha512-kjJ8B+7n4tB9VJdxS5A9GdJt6/bYpzbu4lXp2uO1S3sRmCB5gDEABlGoiePNApRWaW+xqL4b4xgiE727jSLhuA==} + '@oxlint-tsgolint/darwin-x64@0.24.0': + resolution: {integrity: sha512-Wgvt/1lRbDxmoNqWQKKcL+UIiqLmdJ+EWLpQa1qzoNVAfNB0PJpa82/8dH1twT/3rSs4zrP5TXPWl4juB71WuQ==} cpu: [x64] os: [darwin] - '@oxlint-tsgolint/linux-arm64@0.23.0': - resolution: {integrity: sha512-6dCZuKNu135seMXilkRk9SpCx6i1XgmiipYGalLij5WVRX6ZYS8c4xI7preN/zv9fCXhsQclTIMDu2Y/cytTjw==} + '@oxlint-tsgolint/linux-arm64@0.24.0': + resolution: {integrity: sha512-PB1rxII7KV83+ASY4sSkXtqvpij6ME66+QCRL49uksi/ofs2Rf/UVboYr095n0Rkbl2wgvlsHGl6DHC361jQUQ==} cpu: [arm64] os: [linux] - '@oxlint-tsgolint/linux-x64@0.23.0': - resolution: {integrity: sha512-3bdilnyA7kmSTjK27rvjIjSxL5SIg3wt7vwNiRkouWB83ytssyKnuGvxSYJxgMEmFpSutzaBzcCUM2jDtPGcgA==} + '@oxlint-tsgolint/linux-x64@0.24.0': + resolution: {integrity: sha512-xcz3CxKmjTQLREtE/UShh+ruWmm9nAb7UM9zKcD65BStiuYgOakAKkPHl4YS5DztpVcDrE0+HqbOolTlRKYWmw==} cpu: [x64] os: [linux] - '@oxlint-tsgolint/win32-arm64@0.23.0': - resolution: {integrity: sha512-j+OEp44SVYiQ+ZD+uttsX7u6L9SvmbbQ77SO1pSFCcJlsVMeCk8qZsjhKfGKuT/jIA+ipOJMVs/+pqUfObBWNw==} + '@oxlint-tsgolint/win32-arm64@0.24.0': + resolution: {integrity: sha512-A2i6ZGBec3i20S7RaxkgHc6r3HYtD5Mn7j/mb22NkTz14u0JuudvTu6JggAnbGMcv8+dBKQI//EasxSPJLD8pw==} cpu: [arm64] os: [win32] - '@oxlint-tsgolint/win32-x64@0.23.0': - resolution: {integrity: sha512-5MyjFuqf+g8OUPJBSGWHJtmoWnzFJYyOg4To9WMQshZYEWig/vtu7JtJ03VWnzHv9LJkAUeApY0gVCOywFR/iQ==} + '@oxlint-tsgolint/win32-x64@0.24.0': + resolution: {integrity: sha512-0ZbGd9qRB6zs82moekaKdEvncRANq49EAwfNX62JpTS46feXUhKAuoyVDvZMj6Rywejylrmmu79Wo6faYCo4Ew==} cpu: [x64] os: [win32] @@ -2647,32 +2647,32 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@supabase/auth-js@2.108.2': - resolution: {integrity: sha512-tNaQmBgodDZwgB40mRwVbxFy8IDYwjdpcZ0BYrWiwlULCSQoJj4QoG4zgJT7QRPXcqipefNOzvO/qAu4dF98ag==} - engines: {node: '>=20.0.0'} + '@supabase/auth-js@2.110.0': + resolution: {integrity: sha512-Mi288WCTp6wxMFCOu/UgzgHEXODjdl2uVTLqK11eanzGZaldU3RyP8Am+ZbNuVzFP+5+iOvppxzv7N5Ym84xTg==} + engines: {node: '>=22.0.0'} - '@supabase/functions-js@2.108.2': - resolution: {integrity: sha512-RNUX8EiBy3iLwAX19jtRzLyePnl11/fHcgwDHLnpKcDSXt/5qBnh3LUwAtIjT21Q66QsmNUR2esrHziLCpNubw==} - engines: {node: '>=20.0.0'} + '@supabase/functions-js@2.110.0': + resolution: {integrity: sha512-Fde5wlY8ZZy+9yqrWlQHo8MacSyUBArBEtN2boB4thJQigPnQD/cc61qZN0n3I1L0gwhWtHYwIMnOBKxSvF6Hw==} + engines: {node: '>=22.0.0'} '@supabase/phoenix@0.4.4': resolution: {integrity: sha512-Gt0pqoXuIqX/8dvG0OKp/wMCobXNH3klNbUPBNyOfN0YA1IswrM3HyWFMOPk1Jy+BRaIyDPcFx4jLBwHNmlyfQ==} - '@supabase/postgrest-js@2.108.2': - resolution: {integrity: sha512-GQ28/Y8hk3CFmkb3kXH1h/AQx6JIYSQfO0CJMRVBcEKZoNy6C45cXAZ4fcJvRC5Id0cs6xnkUV0+c0rIocigsw==} - engines: {node: '>=20.0.0'} + '@supabase/postgrest-js@2.110.0': + resolution: {integrity: sha512-ZbC1QZL3jcvBUfVKjJbgRM27G4Mg3Zzqdm44m5pJafe1e52Cli793EOnwQucomBAGEUDd03Nzaf7XV3ji/XexQ==} + engines: {node: '>=22.0.0'} - '@supabase/realtime-js@2.108.2': - resolution: {integrity: sha512-aAGxCSUemZvQIibnCdvNvgaKib28I4rfrNjKbQ9cG1uBLwUsI7hVpGXgEbypCCDhLjQlDTAiJlu7rgljYUT73g==} - engines: {node: '>=20.0.0'} + '@supabase/realtime-js@2.110.0': + resolution: {integrity: sha512-Wn2AWpneZuDFTkp/65tqctvoh+3JvyTjMam8sTMqVWy5BgkU8zAvFwilPYPPPhkINeKF8NAJKP7FclJ2iGCUMw==} + engines: {node: '>=22.0.0'} - '@supabase/storage-js@2.108.2': - resolution: {integrity: sha512-TVZPQxXGxY2+A6yTtm77zUHsh70lBhYUEaJL8RQC+BghcX/ygiMG/rmXrNVBce30/WAeNPa8FiG8HbqlGeV05g==} - engines: {node: '>=20.0.0'} + '@supabase/storage-js@2.110.0': + resolution: {integrity: sha512-71+gU3HrhiylAhftY6FmO5PPdcsScnVcS766CVD+vTYK9qTDLbrx8FhgBYbqGm3iV/wkTfzrNJfjGsMeFRkJRQ==} + engines: {node: '>=22.0.0'} - '@supabase/supabase-js@2.108.2': - resolution: {integrity: sha512-hFhnPveb5JQg4a0QYicM0swT253YHMdfeRAl2BKHOlI5VAzuHxUGSr8RbwNLYNPauWOgQMS1H8sz8bvYlgwUfQ==} - engines: {node: '>=20.0.0'} + '@supabase/supabase-js@2.110.0': + resolution: {integrity: sha512-8yI84VJiEVW4zxZpLUmxXmjzQ7O2St9X/ymzlBETDHTURPWG3LmvbSiibq+7dqAJmyoUfxZnSfXeM4HCM8s4XQ==} + engines: {node: '>=22.0.0'} '@swc-node/core@1.14.1': resolution: {integrity: sha512-jrt5GUaZUU6cmMS+WTJEvGvaB6j1YNKPHPzC2PUi2BjaFbtxURHj6641Az6xN7b665hNniAIdvjxWcRml5yCnw==} @@ -2864,50 +2864,50 @@ packages: '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} - '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260629.1': - resolution: {integrity: sha512-wXRExZJweYoTzE4atRR7T5HwKJYkl6/KHxON0eF0iy2fvgLXDlyq4AQqhmV8mMx10PQKc/4sNbfhD4kjWWvm8A==} + '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260630.1': + resolution: {integrity: sha512-zo1TYD32r+MKOTnzhB1YGr2Jrw14tzGR/rpfr6hRMDz0kV9k2CWVvtOYOmOtRdmuO4KWZcWtVX13QyaPGhA8Hw==} engines: {node: '>=16.20.0'} cpu: [arm64] os: [darwin] - '@typescript/native-preview-darwin-x64@7.0.0-dev.20260629.1': - resolution: {integrity: sha512-xkL/tETzUuDxfRkk2rD0/CjjjGLyOLHeE103T52rpgj0VNSXMnn7tTiw+TSdxnS61b8ImOrWgQJujhPFTp0jng==} + '@typescript/native-preview-darwin-x64@7.0.0-dev.20260630.1': + resolution: {integrity: sha512-57RYyZQQ6+drfu+CagVdqUvQwNesvuu/rJb/au66jv+figfpDOugD0S6ruQl1SxpFFfr0lCny3KEplsBdqFDEg==} engines: {node: '>=16.20.0'} cpu: [x64] os: [darwin] - '@typescript/native-preview-linux-arm64@7.0.0-dev.20260629.1': - resolution: {integrity: sha512-y4ey82krq4iLNHML4G1dUObBWY6gNAjVqaUHFDv7+LBu5bfAJy8VClBaLtAJce3siQQeB0/4SkN4XsAa0oZktQ==} + '@typescript/native-preview-linux-arm64@7.0.0-dev.20260630.1': + resolution: {integrity: sha512-QQxNyZ9rVbE6lUqctrrRiTtA5Z0w2FFBy8SkiP/eDkuRy2WXrVpMQYntNn6d4nO1nWTmWJR1z/CS+H2rsrl8gg==} engines: {node: '>=16.20.0'} cpu: [arm64] os: [linux] - '@typescript/native-preview-linux-arm@7.0.0-dev.20260629.1': - resolution: {integrity: sha512-QlTxO+O6yELc0NYZZYIbncZhkhw+K/vBoVg+mKipbEIK5b1E6cwW7ivWIrDp1FfssQ0Wu7zxincPappoci7KNw==} + '@typescript/native-preview-linux-arm@7.0.0-dev.20260630.1': + resolution: {integrity: sha512-FLJSSt3bUmRpzWlr5cDE+td40FoDy/MT+VDYk4JGouisJo7g7GPjuF+fYOfsKI/RbD9f6gDFTyFPAoTLVVR/9g==} engines: {node: '>=16.20.0'} cpu: [arm] os: [linux] - '@typescript/native-preview-linux-x64@7.0.0-dev.20260629.1': - resolution: {integrity: sha512-ckb4HyugC5beDpOMPOVpEx1H3l2Z2RgsGPxFOLGMU6LLIHQwlCaSdRjSfH8Hq8yffPgKuotTQLdA89cP1IC8xQ==} + '@typescript/native-preview-linux-x64@7.0.0-dev.20260630.1': + resolution: {integrity: sha512-WqNPvUBngGfnkunqn3IuIkkF6PA/HPSSbVucolO7stc9DxWZqfRals6/C8RTvuQaif9qt+bcY9U6lLXuDFyClA==} engines: {node: '>=16.20.0'} cpu: [x64] os: [linux] - '@typescript/native-preview-win32-arm64@7.0.0-dev.20260629.1': - resolution: {integrity: sha512-Ktgepu9p0blqrbiryzYrw11ddnbESXPdeGKURDG/wXESoHQETsMNxKWhqAo587ItNnWjKOBFxoEM22eP9OzKnw==} + '@typescript/native-preview-win32-arm64@7.0.0-dev.20260630.1': + resolution: {integrity: sha512-awATkrGGm2L1IraQgaw21VXWtAqv79DJ57No/J65Y+bL8InOVR2zu+zCH+5E44m8bh9IXwnShI7cAdvp02WNEw==} engines: {node: '>=16.20.0'} cpu: [arm64] os: [win32] - '@typescript/native-preview-win32-x64@7.0.0-dev.20260629.1': - resolution: {integrity: sha512-b2wmeIpFJs/peej3NG26320ya+iYQz21JzFYDrnvtsEeR/g66rB9uvp4Owo4J1AG/BcmRWC/nuwrBy3Jdb+UDA==} + '@typescript/native-preview-win32-x64@7.0.0-dev.20260630.1': + resolution: {integrity: sha512-M/+P66Gsss/CANV+MkKVFeNi9jljYYR3N2COf/WrVYcDwrHyiYHZYExfTw2cEbbkH8LcawXAekp9d23bevBR4Q==} engines: {node: '>=16.20.0'} cpu: [x64] os: [win32] - '@typescript/native-preview@7.0.0-dev.20260629.1': - resolution: {integrity: sha512-KImWFkxGUa/V78bUEAgzeJQT+6wKQXDDYHHrOavof8wnbMCpj86KuPNyBIPQMtoHiz2If8aNTums2m50oE3oqw==} + '@typescript/native-preview@7.0.0-dev.20260630.1': + resolution: {integrity: sha512-5OTvy2YpoDsOwAPqj4LkI4mrlAtc3mRzNdHAPp/1JWUvsKSRTzqAB2JPVD4qHUkAd9qY89yb758kc7fGyn3gbw==} engines: {node: '>=16.20.0'} hasBin: true @@ -5386,8 +5386,8 @@ packages: vite-plus: optional: true - oxlint-tsgolint@0.23.0: - resolution: {integrity: sha512-3mBv3CoPbh8dFbzfDGIWa2ytZjn2v+3EX4aKRXjIhsoGFzG8GCjfRirz3rwZf1wYbZzsNLTSgpw8VjQuWdp/jA==} + oxlint-tsgolint@0.24.0: + resolution: {integrity: sha512-giCk5sEvG02d5tzPmFMX3hem8ndzEEu1xvGYS5OwNfO2WGl6ZVxt5LjE0yiMDoz94INI7XkXwgFAQiydPvVHDw==} hasBin: true oxlint@1.73.0: @@ -5667,8 +5667,8 @@ packages: postgres-range@1.1.4: resolution: {integrity: sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==} - posthog-node@5.38.8: - resolution: {integrity: sha512-AWsp9Tigf4iZepabPErAt/2sLTGwJ7w6631JP+3ifDqWzaBPyzcukD7cTPeoNDKGwJreQ69Ju4l53n9g2+X6nQ==} + posthog-node@5.39.1: + resolution: {integrity: sha512-ZvpNfbTg6mhuPScGXE+Yqh2mJpELHShHT98qjKD7cBdXVtIDOZ8Xoh7m3JkK9H1r0GiqEztnoBNe9aL1qOZCeg==} engines: {node: ^20.20.0 || >=22.22.0} peerDependencies: rxjs: ^7.0.0 @@ -6854,46 +6854,46 @@ snapshots: ansi-styles: 6.2.3 is-fullwidth-code-point: 5.1.0 - '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.196': + '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.197': optional: true - '@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.196': + '@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.197': optional: true - '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.196': + '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.197': optional: true - '@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.196': + '@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.197': optional: true - '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.196': + '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.197': optional: true - '@anthropic-ai/claude-agent-sdk-linux-x64@0.3.196': + '@anthropic-ai/claude-agent-sdk-linux-x64@0.3.197': optional: true - '@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.196': + '@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.197': optional: true - '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.196': + '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.197': optional: true - '@anthropic-ai/claude-agent-sdk@0.3.196(@anthropic-ai/sdk@0.107.0(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(zod@4.4.3)': + '@anthropic-ai/claude-agent-sdk@0.3.197(@anthropic-ai/sdk@0.109.0(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(zod@4.4.3)': dependencies: - '@anthropic-ai/sdk': 0.107.0(zod@4.4.3) + '@anthropic-ai/sdk': 0.109.0(zod@4.4.3) '@modelcontextprotocol/sdk': 1.29.0(zod@4.4.3) zod: 4.4.3 optionalDependencies: - '@anthropic-ai/claude-agent-sdk-darwin-arm64': 0.3.196 - '@anthropic-ai/claude-agent-sdk-darwin-x64': 0.3.196 - '@anthropic-ai/claude-agent-sdk-linux-arm64': 0.3.196 - '@anthropic-ai/claude-agent-sdk-linux-arm64-musl': 0.3.196 - '@anthropic-ai/claude-agent-sdk-linux-x64': 0.3.196 - '@anthropic-ai/claude-agent-sdk-linux-x64-musl': 0.3.196 - '@anthropic-ai/claude-agent-sdk-win32-arm64': 0.3.196 - '@anthropic-ai/claude-agent-sdk-win32-x64': 0.3.196 + '@anthropic-ai/claude-agent-sdk-darwin-arm64': 0.3.197 + '@anthropic-ai/claude-agent-sdk-darwin-x64': 0.3.197 + '@anthropic-ai/claude-agent-sdk-linux-arm64': 0.3.197 + '@anthropic-ai/claude-agent-sdk-linux-arm64-musl': 0.3.197 + '@anthropic-ai/claude-agent-sdk-linux-x64': 0.3.197 + '@anthropic-ai/claude-agent-sdk-linux-x64-musl': 0.3.197 + '@anthropic-ai/claude-agent-sdk-win32-arm64': 0.3.197 + '@anthropic-ai/claude-agent-sdk-win32-x64': 0.3.197 - '@anthropic-ai/sdk@0.107.0(zod@4.4.3)': + '@anthropic-ai/sdk@0.109.0(zod@4.4.3)': dependencies: json-schema-to-ts: 3.1.1 standardwebhooks: 1.0.0 @@ -7858,22 +7858,22 @@ snapshots: '@oxfmt/binding-win32-x64-msvc@0.57.0': optional: true - '@oxlint-tsgolint/darwin-arm64@0.23.0': + '@oxlint-tsgolint/darwin-arm64@0.24.0': optional: true - '@oxlint-tsgolint/darwin-x64@0.23.0': + '@oxlint-tsgolint/darwin-x64@0.24.0': optional: true - '@oxlint-tsgolint/linux-arm64@0.23.0': + '@oxlint-tsgolint/linux-arm64@0.24.0': optional: true - '@oxlint-tsgolint/linux-x64@0.23.0': + '@oxlint-tsgolint/linux-x64@0.24.0': optional: true - '@oxlint-tsgolint/win32-arm64@0.23.0': + '@oxlint-tsgolint/win32-arm64@0.24.0': optional: true - '@oxlint-tsgolint/win32-x64@0.23.0': + '@oxlint-tsgolint/win32-x64@0.24.0': optional: true '@oxlint/binding-android-arm-eabi@1.73.0': @@ -8538,37 +8538,37 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@supabase/auth-js@2.108.2': + '@supabase/auth-js@2.110.0': dependencies: tslib: 2.8.1 - '@supabase/functions-js@2.108.2': + '@supabase/functions-js@2.110.0': dependencies: tslib: 2.8.1 '@supabase/phoenix@0.4.4': {} - '@supabase/postgrest-js@2.108.2': + '@supabase/postgrest-js@2.110.0': dependencies: tslib: 2.8.1 - '@supabase/realtime-js@2.108.2': + '@supabase/realtime-js@2.110.0': dependencies: '@supabase/phoenix': 0.4.4 tslib: 2.8.1 - '@supabase/storage-js@2.108.2': + '@supabase/storage-js@2.110.0': dependencies: iceberg-js: 0.8.1 tslib: 2.8.1 - '@supabase/supabase-js@2.108.2': + '@supabase/supabase-js@2.110.0': dependencies: - '@supabase/auth-js': 2.108.2 - '@supabase/functions-js': 2.108.2 - '@supabase/postgrest-js': 2.108.2 - '@supabase/realtime-js': 2.108.2 - '@supabase/storage-js': 2.108.2 + '@supabase/auth-js': 2.110.0 + '@supabase/functions-js': 2.110.0 + '@supabase/postgrest-js': 2.110.0 + '@supabase/realtime-js': 2.110.0 + '@supabase/storage-js': 2.110.0 '@swc-node/core@1.14.1(@swc/core@1.15.43)(@swc/types@0.1.27)': dependencies: @@ -8748,36 +8748,36 @@ snapshots: dependencies: '@types/node': 26.0.1 - '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260629.1': + '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260630.1': optional: true - '@typescript/native-preview-darwin-x64@7.0.0-dev.20260629.1': + '@typescript/native-preview-darwin-x64@7.0.0-dev.20260630.1': optional: true - '@typescript/native-preview-linux-arm64@7.0.0-dev.20260629.1': + '@typescript/native-preview-linux-arm64@7.0.0-dev.20260630.1': optional: true - '@typescript/native-preview-linux-arm@7.0.0-dev.20260629.1': + '@typescript/native-preview-linux-arm@7.0.0-dev.20260630.1': optional: true - '@typescript/native-preview-linux-x64@7.0.0-dev.20260629.1': + '@typescript/native-preview-linux-x64@7.0.0-dev.20260630.1': optional: true - '@typescript/native-preview-win32-arm64@7.0.0-dev.20260629.1': + '@typescript/native-preview-win32-arm64@7.0.0-dev.20260630.1': optional: true - '@typescript/native-preview-win32-x64@7.0.0-dev.20260629.1': + '@typescript/native-preview-win32-x64@7.0.0-dev.20260630.1': optional: true - '@typescript/native-preview@7.0.0-dev.20260629.1': + '@typescript/native-preview@7.0.0-dev.20260630.1': optionalDependencies: - '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260629.1 - '@typescript/native-preview-darwin-x64': 7.0.0-dev.20260629.1 - '@typescript/native-preview-linux-arm': 7.0.0-dev.20260629.1 - '@typescript/native-preview-linux-arm64': 7.0.0-dev.20260629.1 - '@typescript/native-preview-linux-x64': 7.0.0-dev.20260629.1 - '@typescript/native-preview-win32-arm64': 7.0.0-dev.20260629.1 - '@typescript/native-preview-win32-x64': 7.0.0-dev.20260629.1 + '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260630.1 + '@typescript/native-preview-darwin-x64': 7.0.0-dev.20260630.1 + '@typescript/native-preview-linux-arm': 7.0.0-dev.20260630.1 + '@typescript/native-preview-linux-arm64': 7.0.0-dev.20260630.1 + '@typescript/native-preview-linux-x64': 7.0.0-dev.20260630.1 + '@typescript/native-preview-win32-arm64': 7.0.0-dev.20260630.1 + '@typescript/native-preview-win32-x64': 7.0.0-dev.20260630.1 '@ungap/structured-clone@1.3.2': {} @@ -11799,16 +11799,16 @@ snapshots: '@oxfmt/binding-win32-ia32-msvc': 0.57.0 '@oxfmt/binding-win32-x64-msvc': 0.57.0 - oxlint-tsgolint@0.23.0: + oxlint-tsgolint@0.24.0: optionalDependencies: - '@oxlint-tsgolint/darwin-arm64': 0.23.0 - '@oxlint-tsgolint/darwin-x64': 0.23.0 - '@oxlint-tsgolint/linux-arm64': 0.23.0 - '@oxlint-tsgolint/linux-x64': 0.23.0 - '@oxlint-tsgolint/win32-arm64': 0.23.0 - '@oxlint-tsgolint/win32-x64': 0.23.0 - - oxlint@1.73.0(oxlint-tsgolint@0.23.0): + '@oxlint-tsgolint/darwin-arm64': 0.24.0 + '@oxlint-tsgolint/darwin-x64': 0.24.0 + '@oxlint-tsgolint/linux-arm64': 0.24.0 + '@oxlint-tsgolint/linux-x64': 0.24.0 + '@oxlint-tsgolint/win32-arm64': 0.24.0 + '@oxlint-tsgolint/win32-x64': 0.24.0 + + oxlint@1.73.0(oxlint-tsgolint@0.24.0): optionalDependencies: '@oxlint/binding-android-arm-eabi': 1.73.0 '@oxlint/binding-android-arm64': 1.73.0 @@ -11829,7 +11829,7 @@ snapshots: '@oxlint/binding-win32-arm64-msvc': 1.73.0 '@oxlint/binding-win32-ia32-msvc': 1.73.0 '@oxlint/binding-win32-x64-msvc': 1.73.0 - oxlint-tsgolint: 0.23.0 + oxlint-tsgolint: 0.24.0 p-cancelable@2.1.1: {} @@ -12070,7 +12070,7 @@ snapshots: postgres-range@1.1.4: {} - posthog-node@5.38.8: + posthog-node@5.39.1: dependencies: '@posthog/core': 1.39.6 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index d42a59a931..eb8f8de15a 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -22,14 +22,14 @@ catalog: "@swc/core": "^1.15.43" "@tsconfig/bun": "^1.0.10" "@types/bun": "^1.3.14" - "@typescript/native-preview": "7.0.0-dev.20260629.1" + "@typescript/native-preview": "7.0.0-dev.20260630.1" "@vitest/coverage-istanbul": "^4.1.9" "effect": "4.0.0-beta.93" "knip": "^6.17.1" "nx": "^23.0.0" "oxfmt": "^0.57.0" "oxlint": "^1.72.0" - "oxlint-tsgolint": "^0.23.0" + "oxlint-tsgolint": "^0.24.0" "tldts": "^7.4.5" "vitest": "^4.1.9" From e7b208458101209132ce64033bfe3c32bcff55cb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 00:24:37 +0000 Subject: [PATCH 17/79] chore(deps): bump golang.org/x/crypto from 0.50.0 to 0.52.0 in /apps/cli-go/pkg (#5825) Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.50.0 to 0.52.0.
Commits
  • a1c0d99 go.mod: update golang.org/x dependencies
  • 3c7c869 ssh: fix deadlock on unexpected channel responses
  • 533fb3f ssh: fix source-address critical option bypass
  • abbc44d ssh: fix incorrect operator order
  • e052873 ssh: fix infinite loop on large channel writes due to integer overflow
  • b61cf85 ssh: enforce user presence verification for security keys
  • 9c2cd33 ssh: enforce strict limits on DSA key parameters
  • 8907318 ssh: reject RSA keys with excessively large moduli
  • ffd87b4 ssh: fix panic when authority callbacks are nil
  • 4e7a738 ssh: fix deadlock on unexpected global responses
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=golang.org/x/crypto&package-manager=go_modules&previous-version=0.50.0&new-version=0.52.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/supabase/cli/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/cli-go/pkg/go.mod | 6 +++--- apps/cli-go/pkg/go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/apps/cli-go/pkg/go.mod b/apps/cli-go/pkg/go.mod index eca10c84ab..8afe2e0de5 100644 --- a/apps/cli-go/pkg/go.mod +++ b/apps/cli-go/pkg/go.mod @@ -51,8 +51,8 @@ require ( github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.50.0 // indirect - golang.org/x/sys v0.43.0 // indirect - golang.org/x/text v0.36.0 // indirect + golang.org/x/crypto v0.52.0 // indirect + golang.org/x/sys v0.45.0 // indirect + golang.org/x/text v0.37.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/apps/cli-go/pkg/go.sum b/apps/cli-go/pkg/go.sum index c622bcdf1e..08ff799d66 100644 --- a/apps/cli-go/pkg/go.sum +++ b/apps/cli-go/pkg/go.sum @@ -217,8 +217,8 @@ golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= -golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= -golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= +golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= +golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= @@ -255,8 +255,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= -golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -272,8 +272,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= -golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= From 815b0a9f202db70285e8bfec0c38f20ea6476791 Mon Sep 17 00:00:00 2001 From: Vaibhav <117663341+7ttp@users.noreply.github.com> Date: Wed, 8 Jul 2026 13:59:09 +0530 Subject: [PATCH 18/79] fix(cli): colima socket (#5820) ## TL;DR adds Colima rootless docker socket fallback to vector start ## Ref - closes https://github.com/supabase/cli/issues/5073 --- apps/cli-go/internal/start/start.go | 10 ++++++++-- apps/cli-go/internal/start/start_test.go | 14 ++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/apps/cli-go/internal/start/start.go b/apps/cli-go/internal/start/start.go index 23aa631e3b..6264016e79 100644 --- a/apps/cli-go/internal/start/start.go +++ b/apps/cli-go/internal/start/start.go @@ -128,6 +128,13 @@ type vectorConfig struct { DbId string } +func shouldMountRootDockerSocket(host string) bool { + return strings.HasSuffix(host, "/.docker/run/docker.sock") || + strings.HasSuffix(host, "/.docker/desktop/docker.sock") || + (strings.Contains(host, "/.colima/") && strings.HasSuffix(host, "/docker.sock")) || + strings.HasSuffix(host, "/.colima/docker.sock") +} + var ( //go:embed templates/vector.yaml vectorConfigEmbed string @@ -424,8 +431,7 @@ EOF case "unix": if dindHost, err = client.ParseHostURL(client.DefaultDockerHost); err != nil { return errors.Errorf("failed to parse default host: %w", err) - } else if strings.HasSuffix(parsed.Host, "/.docker/run/docker.sock") || - strings.HasSuffix(parsed.Host, "/.docker/desktop/docker.sock") { + } else if shouldMountRootDockerSocket(parsed.Host) { // Docker will not mount rootless socket directly; // instead, specify root socket to have it handled under the hood binds = append(binds, fmt.Sprintf("%[1]s:%[1]s:ro", dindHost.Host)) diff --git a/apps/cli-go/internal/start/start_test.go b/apps/cli-go/internal/start/start_test.go index 2977c5ed79..a69f7bd8bc 100644 --- a/apps/cli-go/internal/start/start_test.go +++ b/apps/cli-go/internal/start/start_test.go @@ -162,6 +162,20 @@ func TestStartCommand(t *testing.T) { }) } +func TestShouldMountRootDockerSocket(t *testing.T) { + t.Run("returns true for Docker Desktop and Colima sockets", func(t *testing.T) { + assert.True(t, shouldMountRootDockerSocket("/Users/test/.docker/run/docker.sock")) + assert.True(t, shouldMountRootDockerSocket("/Users/test/.docker/desktop/docker.sock")) + assert.True(t, shouldMountRootDockerSocket("/Users/test/.colima/default/docker.sock")) + assert.True(t, shouldMountRootDockerSocket("/Users/test/.colima/local/docker.sock")) + assert.True(t, shouldMountRootDockerSocket("/Users/test/.colima/docker.sock")) + }) + + t.Run("returns false for directly mountable sockets", func(t *testing.T) { + assert.False(t, shouldMountRootDockerSocket("/Users/test/.orbstack/run/docker.sock")) + }) +} + func TestDatabaseStart(t *testing.T) { t.Run("starts database locally", func(t *testing.T) { // Setup in-memory fs From 5c8c144458adb0b707ab37101894116b6f4db768 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 10:29:40 +0200 Subject: [PATCH 19/79] fix(docker): bump the docker-minor group in /apps/cli-go/pkg/config/templates with 3 updates (#5822) Bumps the docker-minor group in /apps/cli-go/pkg/config/templates with 3 updates: supabase/studio, supabase/realtime and supabase/storage-api. Updates `supabase/studio` from 2026.07.06-sha-66cf431 to 2026.07.07-sha-a6a04f2 Updates `supabase/realtime` from v2.112.6 to v2.112.9 Updates `supabase/storage-api` from v1.62.5 to v1.63.1 Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
--------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- apps/cli-go/pkg/config/templates/Dockerfile | 6 +++--- packages/stack/src/versions.ts | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/cli-go/pkg/config/templates/Dockerfile b/apps/cli-go/pkg/config/templates/Dockerfile index ec7e643391..b2703b51fc 100644 --- a/apps/cli-go/pkg/config/templates/Dockerfile +++ b/apps/cli-go/pkg/config/templates/Dockerfile @@ -5,14 +5,14 @@ FROM library/kong:2.8.1 AS kong FROM axllent/mailpit:v1.30.2 AS mailpit FROM postgrest/postgrest:v14.14 AS postgrest FROM supabase/postgres-meta:v0.96.6 AS pgmeta -FROM supabase/studio:2026.07.06-sha-66cf431 AS studio +FROM supabase/studio:2026.07.07-sha-a6a04f2 AS studio FROM darthsim/imgproxy:v3.8.0 AS imgproxy FROM supabase/edge-runtime:v1.74.2 AS edgeruntime FROM timberio/vector:0.53.0-alpine AS vector FROM supabase/supavisor:2.9.7 AS supavisor FROM supabase/gotrue:v2.192.0 AS gotrue -FROM supabase/realtime:v2.112.6 AS realtime -FROM supabase/storage-api:v1.62.5 AS storage +FROM supabase/realtime:v2.112.9 AS realtime +FROM supabase/storage-api:v1.63.1 AS storage FROM supabase/logflare:1.46.0 AS logflare # Append to JobImages when adding new dependencies below FROM supabase/pgadmin-schema-diff:cli-0.0.5 AS differ diff --git a/packages/stack/src/versions.ts b/packages/stack/src/versions.ts index ce7b086789..bd675abf2d 100644 --- a/packages/stack/src/versions.ts +++ b/packages/stack/src/versions.ts @@ -50,12 +50,12 @@ export const DEFAULT_VERSIONS: VersionManifest = { postgrest: "14.14", auth: "2.192.0", "edge-runtime": "1.74.2", - realtime: "2.112.6", - storage: "1.62.5", + realtime: "2.112.9", + storage: "1.63.1", imgproxy: "v3.8.0", mailpit: "v1.30.2", pgmeta: "0.96.6", - studio: "2026.07.06-sha-66cf431", + studio: "2026.07.07-sha-a6a04f2", analytics: "1.46.0", vector: "0.53.0-alpine", pooler: "2.9.7", From 970873372b1c65abaae38f32e83c3c77eefdd2b6 Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Wed, 8 Jul 2026 11:08:33 +0100 Subject: [PATCH 20/79] fix(cli): remove "nano" from projects/branches create --size enum (#5799) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What kind of change does this PR introduce? Bug fix (Go parity). ## What is the current behavior? Go's `--size` flag on `projects create` (`apps/cli-go/cmd/projects.go:34-55`) is a single shared 18-value `EnumFlag` reused by `branches create` (`apps/cli-go/cmd/branches.go:212`), and does not include `"nano"` (or `"pico"`). The TS legacy port's `INSTANCE_SIZES` (`projects/create/create.command.ts`) and `BRANCH_SIZES` (`branches/create/create.command.ts`) both included `"nano"` as a valid `Flag.choice` value, so `--size nano` silently succeeded in TS while Go rejects it. Verified directly against the real, compiled Go binary (`apps/cli-go`) rather than just reading source — `projects create --size nano` genuinely errors with `invalid argument "nano" for "--size" flag: must be one of [...]`, confirming this is current, shipped Go behavior and not a stale enum. Note: the live Management API contract (`packages/api/src/generated/contracts.ts`) does list `"nano"`/`"pico"` as accepted `desired_instance_size` values, and `next/`'s independently-authored `branches create` already exposes both. Neither the Go CLI nor this legacy port surfaces them as CLI options, though — that's a separate product question (filed as CLI-1897) about whether these tiers should eventually be CLI-selectable, not a reason to keep an option the legacy shell's own Go-parity contract says should be rejected. ## What is the new behavior? `--size nano` is now rejected identically on both `projects create` and `branches create`, matching Go's 18-value enum exactly. Fixes CLI-1869. See also CLI-1897 (product decision on nano/pico) and CLI-1898 (unrelated pre-existing bug in `effect`'s `Flag.choice` error-message formatting, found during this PR's review). --- .../branches/create/create.command.ts | 1 - .../create/create.integration.test.ts | 46 ++++++++++++++- .../projects/create/create.command.ts | 15 +++-- .../create/create.integration.test.ts | 56 ++++++++++++++++++- 4 files changed, 105 insertions(+), 13 deletions(-) diff --git a/apps/cli/src/legacy/commands/branches/create/create.command.ts b/apps/cli/src/legacy/commands/branches/create/create.command.ts index 192a9a3530..1cc85ecbce 100644 --- a/apps/cli/src/legacy/commands/branches/create/create.command.ts +++ b/apps/cli/src/legacy/commands/branches/create/create.command.ts @@ -44,7 +44,6 @@ const BRANCH_SIZES = [ "48xlarge_optimized_memory", "4xlarge", "8xlarge", - "nano", "small", "xlarge", ] as const; diff --git a/apps/cli/src/legacy/commands/branches/create/create.integration.test.ts b/apps/cli/src/legacy/commands/branches/create/create.integration.test.ts index fcbb44aa0f..8fb2a9e5d5 100644 --- a/apps/cli/src/legacy/commands/branches/create/create.integration.test.ts +++ b/apps/cli/src/legacy/commands/branches/create/create.integration.test.ts @@ -1,8 +1,10 @@ import type { V1CreateABranchOutput } from "@supabase/api/effect"; import { describe, expect, it } from "@effect/vitest"; -import { Effect, Exit, Option } from "effect"; +import { Cause, Effect, Exit, Option } from "effect"; +import { Command } from "effect/unstable/cli"; import { mockAnalytics, mockOutput } from "../../../../../tests/helpers/mocks.ts"; +import { LEGACY_GLOBAL_FLAGS } from "../../../../shared/legacy/global-flags.ts"; import { LEGACY_VALID_REF, buildLegacyTestRuntime, @@ -13,7 +15,7 @@ import { mockLegacyTelemetryStateTracked, useLegacyTempWorkdir, } from "../../../../../tests/helpers/legacy-mocks.ts"; -import type { LegacyBranchesCreateFlags } from "./create.command.ts"; +import { legacyBranchesCreateCommand, type LegacyBranchesCreateFlags } from "./create.command.ts"; import { legacyBranchesCreate } from "./create.handler.ts"; type CreatedBranch = typeof V1CreateABranchOutput.Type; @@ -282,4 +284,44 @@ describe("legacy branches create integration", () => { expect(cache.cached).toBe(true); }).pipe(Effect.provide(layer)); }); + + // Go parity (`apps/cli-go/cmd/projects.go:34-55`, reused by `branches create` + // at `apps/cli-go/cmd/branches.go:212`): Go's --size EnumFlag is an 18-value + // list that does not include "nano" (or "pico") and rejects any other value + // at flag-parse time. TS previously listed "nano" as a valid choice, silently + // succeeding where Go errors. + it.live("rejects --size nano at flag-parse time, matching Go's 18-value enum", () => { + const root = Command.make("supabase").pipe( + Command.withGlobalFlags(LEGACY_GLOBAL_FLAGS), + Command.withSubcommands([legacyBranchesCreateCommand]), + ); + + return Effect.gen(function* () { + const exit = yield* Effect.exit( + Command.runWith(root, { version: "0.0.0-test" })(["create", "--size", "nano"]), + ); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(rejectsInvalidSizeChoice(Cause.squash(exit.cause))).toBe(true); + } + }) as Effect.Effect; + }); }); + +// Distinguishes "the --size flag itself was rejected at parse time" from any +// other failure (e.g. a missing runtime service in this minimal test setup), +// so the regression test above can't pass for the wrong reason. +function rejectsInvalidSizeChoice(error: unknown): boolean { + if (typeof error !== "object" || error === null || !("errors" in error)) return false; + const { errors } = error; + if (!Array.isArray(errors)) return false; + return errors.some( + (candidate: unknown) => + typeof candidate === "object" && + candidate !== null && + "_tag" in candidate && + candidate._tag === "InvalidValue" && + "option" in candidate && + candidate.option === "size", + ); +} diff --git a/apps/cli/src/legacy/commands/projects/create/create.command.ts b/apps/cli/src/legacy/commands/projects/create/create.command.ts index 238f33c508..d1be7dbb51 100644 --- a/apps/cli/src/legacy/commands/projects/create/create.command.ts +++ b/apps/cli/src/legacy/commands/projects/create/create.command.ts @@ -27,25 +27,24 @@ const AWS_REGIONS = [ ] as const; const INSTANCE_SIZES = [ - "nano", - "micro", - "small", - "medium", "large", - "xlarge", - "2xlarge", - "4xlarge", - "8xlarge", + "medium", + "micro", "12xlarge", "16xlarge", "24xlarge", "24xlarge_high_memory", "24xlarge_optimized_cpu", "24xlarge_optimized_memory", + "2xlarge", "48xlarge", "48xlarge_high_memory", "48xlarge_optimized_cpu", "48xlarge_optimized_memory", + "4xlarge", + "8xlarge", + "small", + "xlarge", ] as const; const config = { diff --git a/apps/cli/src/legacy/commands/projects/create/create.integration.test.ts b/apps/cli/src/legacy/commands/projects/create/create.integration.test.ts index f21138df6c..d38c35c021 100644 --- a/apps/cli/src/legacy/commands/projects/create/create.integration.test.ts +++ b/apps/cli/src/legacy/commands/projects/create/create.integration.test.ts @@ -1,8 +1,10 @@ import type { OrganizationResponseV1, V1CreateAProjectOutput } from "@supabase/api/effect"; import { describe, expect, it } from "@effect/vitest"; -import { Effect, Exit, Option } from "effect"; +import { Cause, Effect, Exit, Option } from "effect"; +import { Command } from "effect/unstable/cli"; import { mockOutput, mockTty } from "../../../../../tests/helpers/mocks.ts"; +import { LEGACY_GLOBAL_FLAGS } from "../../../../shared/legacy/global-flags.ts"; import { type LegacyApiResponse, type LegacyHttpMethod, @@ -13,7 +15,7 @@ import { mockLegacyTelemetryStateTracked, useLegacyTempWorkdir, } from "../../../../../tests/helpers/legacy-mocks.ts"; -import type { LegacyProjectsCreateFlags } from "./create.command.ts"; +import { legacyProjectsCreateCommand, type LegacyProjectsCreateFlags } from "./create.command.ts"; import { legacyProjectsCreate } from "./create.handler.ts"; const CREATED: typeof V1CreateAProjectOutput.Type = { @@ -451,4 +453,54 @@ describe("legacy projects create integration", () => { expect(cache.cached).toBe(false); }).pipe(Effect.provide(layer)); }); + + // Go parity (`apps/cli-go/cmd/projects.go:34-55`): Go's --size EnumFlag is an + // 18-value list that does not include "nano" (or "pico") and rejects any other + // value at flag-parse time. TS previously listed "nano" as a valid choice, + // silently succeeding where Go errors. + it.live("rejects --size nano at flag-parse time, matching Go's 18-value enum", () => { + const root = Command.make("supabase").pipe( + Command.withGlobalFlags(LEGACY_GLOBAL_FLAGS), + Command.withSubcommands([legacyProjectsCreateCommand]), + ); + + return Effect.gen(function* () { + const exit = yield* Effect.exit( + Command.runWith(root, { version: "0.0.0-test" })([ + "create", + "alpha", + "--org-id", + "acme", + "--db-password", + "s3cret-pass", + "--region", + "us-east-1", + "--size", + "nano", + ]), + ); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(rejectsInvalidSizeChoice(Cause.squash(exit.cause))).toBe(true); + } + }) as Effect.Effect; + }); }); + +// Distinguishes "the --size flag itself was rejected at parse time" from any +// other failure (e.g. a missing runtime service in this minimal test setup), +// so the regression test above can't pass for the wrong reason. +function rejectsInvalidSizeChoice(error: unknown): boolean { + if (typeof error !== "object" || error === null || !("errors" in error)) return false; + const { errors } = error; + if (!Array.isArray(errors)) return false; + return errors.some( + (candidate: unknown) => + typeof candidate === "object" && + candidate !== null && + "_tag" in candidate && + candidate._tag === "InvalidValue" && + "option" in candidate && + candidate.option === "size", + ); +} From ab241c95adca7c81c130923a83ff9a7ff0af385b Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Wed, 8 Jul 2026 11:10:12 +0100 Subject: [PATCH 21/79] docs(cli): disclose --high-availability as TS-only on projects create (#5800) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What kind of change does this PR introduce? Docs update (Go parity disclosure) + telemetry-list correctness fix. ## What is the current behavior? `--high-availability` on `projects create` has no Go CLI equivalent — Go's `cmd/projects.go` never registers such a flag, and its `RunE` closure never sets `HighAvailability` on the create request body even though the underlying Management API field exists. Unlike `--reveal` on `projects api-keys` (which explicitly documents itself as a TS-only addition, in a code comment, `SIDE_EFFECTS.md`, and `docs/go-cli-porting-status.md`'s "Flag divergences" list), `--high-availability` was undisclosed everywhere. It was also listed in `safeFlags: ["org-id", "high-availability"]`, implying Go-parity telemetry-safety that doesn't exist (Go's `markFlagTelemetrySafe` only covers `org-id`). Since it's a boolean flag, this had no actual runtime effect — boolean flag values are always logged verbatim by the instrumentation regardless of `safeFlags` membership — but the array wrongly implied otherwise. The linked ticket (CLI-1870) offered two valid resolutions: remove the flag for strict parity, or disclose it as TS-only. This PR disclosure, since `--high-availability` is a real, working, user-facing capability that Go's CLI simply never got around to exposing (not a Go bug being ported forward) — removing it would regress anyone currently relying on it. ## What is the new behavior? `--high-availability` is now disclosed as TS-only in a code comment, `SIDE_EFFECTS.md`, and `docs/go-cli-porting-status.md`'s "Flag divergences" list, matching the `--reveal` precedent exactly. Removed from `safeFlags` (now just `["org-id"]`) to match Go's actual telemetry-safe set. Fixes CLI-1870. --- apps/cli/docs/go-cli-porting-status.md | 3 ++ .../commands/projects/create/SIDE_EFFECTS.md | 30 +++++++++++-------- .../projects/create/create.command.ts | 16 ++++++---- 3 files changed, 31 insertions(+), 18 deletions(-) diff --git a/apps/cli/docs/go-cli-porting-status.md b/apps/cli/docs/go-cli-porting-status.md index 0e596515b4..125c1f795c 100644 --- a/apps/cli/docs/go-cli-porting-status.md +++ b/apps/cli/docs/go-cli-porting-status.md @@ -323,6 +323,9 @@ Flag divergences from the Go reference: `reveal=true` so the Management API returns the full secret keys (`sb_secret_...`) in full instead of redacting them, addressing issue #4775. Default behavior (omitted flag) matches Go exactly. +- `projects create` has a TS-only `--high-availability` flag (no Go equivalent). It sets + `high_availability` in the create request body. Default behavior (omitted flag) matches + Go exactly. Behavioral divergences from the Go reference: diff --git a/apps/cli/src/legacy/commands/projects/create/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/projects/create/SIDE_EFFECTS.md index 73d151ebd5..1a9d18c419 100644 --- a/apps/cli/src/legacy/commands/projects/create/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/projects/create/SIDE_EFFECTS.md @@ -40,22 +40,22 @@ ## Telemetry Events Fired -| Event | When | Notable properties / groups | -| ---------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------------ | -| `cli_command_executed` | post-run, success or failure (via wrapper) | `exit_code`, `duration_ms`, `flags` (`--org-id`, `--high-availability` are telemetry-safe) | +| Event | When | Notable properties / groups | +| ---------------------- | ------------------------------------------ | ------------------------------------------------------------------------------- | +| `cli_command_executed` | post-run, success or failure (via wrapper) | `exit_code`, `duration_ms`, `flags` (`--org-id` is telemetry-safe, matching Go) | ## Flags -| Flag | Type | Required (non-interactive) | Description | -| --------------------- | ------ | -------------------------- | ----------------------------------------------- | -| `[project name]` | arg | yes (non-interactive) | Name of the project (positional argument) | -| `--org-id` | string | yes (non-interactive) | Organization ID (slug) to create the project in | -| `--db-password` | string | yes (non-interactive) | Database password for the project | -| `--region` | enum | yes (non-interactive) | AWS region for the project | -| `--size` | enum | no | Desired instance size | -| `--high-availability` | bool | no | Enable high availability for the project | -| `--interactive` | bool | no (default: true) | Enable interactive mode (hidden flag) | -| `--plan` | string | no | Plan selection (hidden flag) | +| Flag | Type | Required (non-interactive) | Description | +| --------------------- | ------ | -------------------------- | ---------------------------------------------------------------------------- | +| `[project name]` | arg | yes (non-interactive) | Name of the project (positional argument) | +| `--org-id` | string | yes (non-interactive) | Organization ID (slug) to create the project in | +| `--db-password` | string | yes (non-interactive) | Database password for the project | +| `--region` | enum | yes (non-interactive) | AWS region for the project | +| `--size` | enum | no | Desired instance size | +| `--high-availability` | bool | no | Enable high availability for the project (**TS-only, no Go CLI equivalent**) | +| `--interactive` | bool | no (default: true) | Enable interactive mode (hidden flag) | +| `--plan` | string | no | Plan selection (hidden flag) | ## Output @@ -93,4 +93,8 @@ One `result` event on success. flags and the positional project name argument are required. - The `--size` flag, when provided, sets the `desired_instance_size` field in the request body. - The `--high-availability` flag, when provided, sets the `high_availability` field in the request body. + This is a TS-only flag with no Go CLI equivalent: `apps/cli-go/cmd/projects.go`'s `init()` (~line 133) + never registers a `high-availability` flag, and the create command's `RunE` closure (~line 74) never sets + `HighAvailability` on the request body, even though the underlying API field exists — matching how + `--reveal` is disclosed on `projects api-keys`. - The `--plan` flag is hidden and reserved. diff --git a/apps/cli/src/legacy/commands/projects/create/create.command.ts b/apps/cli/src/legacy/commands/projects/create/create.command.ts index d1be7dbb51..29e63c37ad 100644 --- a/apps/cli/src/legacy/commands/projects/create/create.command.ts +++ b/apps/cli/src/legacy/commands/projects/create/create.command.ts @@ -68,6 +68,10 @@ const config = { Flag.withDescription("Select a desired instance size for your project."), Flag.optional, ), + // TS-only, no Go CLI equivalent: `cmd/projects.go`'s `init()` never registers a + // `high-availability` flag, and the `RunE` closure's `api.V1CreateProjectBody{...}` + // never sets `HighAvailability` even though the API field exists — disclosed in + // SIDE_EFFECTS.md, matching how `--reveal` is disclosed on `projects api-keys`. highAvailability: Flag.boolean("high-availability").pipe( Flag.withDescription("Enable high availability for the project."), Flag.optional, @@ -98,11 +102,13 @@ export const legacyProjectsCreateCommand = Command.make("create", config).pipe( ]), Command.withHandler((flags) => legacyProjectsCreate(flags).pipe( - withLegacyCommandInstrumentation({ - flags, - safeFlags: ["org-id", "high-availability"], - config, - }), + // `high-availability` is intentionally not in `safeFlags`: Go marks only + // `org-id` telemetry-safe (`markFlagTelemetrySafe`), and it's a boolean flag + // anyway — boolean values are always logged verbatim by the instrumentation + // regardless of `safeFlags`. See the same pattern on `projects api-keys`'s + // `--reveal`. `config` is passed so `region`/`size` (both `Flag.choice`) + // are auto-detected as telemetry-safe, matching Go's `isEnumFlag`. + withLegacyCommandInstrumentation({ flags, safeFlags: ["org-id"], config }), withJsonErrorHandling, ), ), From 85bf2ce503129146a9c07a4dddbe0de237e6894b Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Wed, 8 Jul 2026 15:37:09 +0100 Subject: [PATCH 22/79] fix(cli): run pg-delta/--experimental gate before mutex check in db schema declarative (#5828) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What changed `db schema declarative generate`/`sync` ran their mutual-exclusivity flag check (`db-url`/`linked`/`local`, `apply`/`no-apply`) BEFORE the pg-delta/`--experimental` gate (`legacyRequirePgDelta`). Go's cobra runs `PersistentPreRunE` (the gate) before `ValidateFlagGroups` (mutex check) — confirmed against `apps/cli-go/cmd/db_schema_declarative.go` and the actual `cobra@v1.10.2` source — so invoking either command with conflicting flags and no `--experimental` surfaced the wrong error in the TS shell vs Go. Same bug class already fixed for `storage ls/cp/mv/rm` in CLI-1855 (#5768); this mirrors that precedent as closely as the code structure allows. Declarative's gate needs a config read (`legacyReadDbToml`) that storage's didn't, so the check lives inline in each handler's body rather than at the `.command.ts` level — moving the config read ahead of the mutex check as part of the same reorder is also more correct (Go's `PersistentPreRunE` loads config unconditionally before validating flag groups too). Swaps the order in both handlers, fixes misleading ordering comments (and two stale Go line-number citations found nearby), documents the precedence in both commands' `SIDE_EFFECTS.md`, and adds regression coverage for the "mutex conflict without `--experimental`" case in both `generate` and `sync`. Fixes CLI-1876 --- .../schema/declarative/declarative.errors.ts | 4 +- .../db/schema/declarative/declarative.gate.ts | 16 ++- .../declarative/generate/SIDE_EFFECTS.md | 8 +- .../declarative/generate/generate.handler.ts | 41 +++--- .../generate/generate.integration.test.ts | 124 ++++++++++++++++- .../schema/declarative/sync/SIDE_EFFECTS.md | 8 +- .../schema/declarative/sync/sync.command.ts | 2 +- .../schema/declarative/sync/sync.handler.ts | 35 +++-- .../declarative/sync/sync.integration.test.ts | 126 +++++++++++++++++- .../legacy-experimental-gate.unit.test.ts | 23 +++- apps/cli/src/shared/legacy/global-flags.ts | 43 +++++- 11 files changed, 377 insertions(+), 53 deletions(-) diff --git a/apps/cli/src/legacy/commands/db/schema/declarative/declarative.errors.ts b/apps/cli/src/legacy/commands/db/schema/declarative/declarative.errors.ts index a97de5224d..4ff6cb9ab5 100644 --- a/apps/cli/src/legacy/commands/db/schema/declarative/declarative.errors.ts +++ b/apps/cli/src/legacy/commands/db/schema/declarative/declarative.errors.ts @@ -29,8 +29,8 @@ export class LegacyDeclarativeNonInteractiveError extends Data.TaggedError( /** * A mutually-exclusive flag group was violated. Reproduces cobra's * `MarkFlagsMutuallyExclusive` `ValidateFlagGroups` error byte-for-byte: - * - `generate`: `db-url`/`linked`/`local` (`apps/cli-go/cmd/db_schema_declarative.go:499`) - * - `sync`: `apply`/`no-apply` (`apps/cli-go/cmd/db_schema_declarative.go:490`) + * - `generate`: `db-url`/`linked`/`local` (`apps/cli-go/cmd/db_schema_declarative.go:570`) + * - `sync`: `apply`/`no-apply` (`apps/cli-go/cmd/db_schema_declarative.go:561`) * Both fail before any side effects run, matching cobra's pre-RunE validation. */ export class LegacyDeclarativeMutuallyExclusiveFlagsError extends Data.TaggedError( diff --git a/apps/cli/src/legacy/commands/db/schema/declarative/declarative.gate.ts b/apps/cli/src/legacy/commands/db/schema/declarative/declarative.gate.ts index 506af3485d..bd4777e545 100644 --- a/apps/cli/src/legacy/commands/db/schema/declarative/declarative.gate.ts +++ b/apps/cli/src/legacy/commands/db/schema/declarative/declarative.gate.ts @@ -28,11 +28,17 @@ export function legacyPgDeltaSuggestion(configPath: string): string { } /** - * The Effect-CLI replacement for Go's `PersistentPreRunE` gate: invoke at the - * top of each declarative leaf handler. Fails with - * `LegacyDeclarativeNotEnabledError` (carrying the byte-exact message + - * suggestion) when neither `--experimental` nor `[experimental.pgdelta]` enables - * pg-delta. + * The Effect-CLI replacement for Go's `dbDeclarativeCmd.PersistentPreRunE` gate + * (`apps/cli-go/cmd/db_schema_declarative.go:49-99`). Cobra runs + * `PersistentPreRunE` BEFORE `ValidateFlagGroups()` (mutual-exclusivity checks) + * and `RunE` (`cobra@v1.10.2/command.go:985,1010,1014`), so this gate must run + * before the `MarkFlagsMutuallyExclusive` check in the same command — `db-url`/ + * `linked`/`local` on `generate` (`:570`), `apply`/`no-apply` on `sync` (`:561`) + * — a closed gate must win over a flag-group conflict, not the other way + * around. Invoke at the top of each declarative leaf handler's body, before + * that handler's mutex check. Fails with `LegacyDeclarativeNotEnabledError` + * (carrying the byte-exact message + suggestion) when neither `--experimental` + * nor `[experimental.pgdelta]` enables pg-delta. */ export const legacyRequirePgDelta = Effect.fnUntraced(function* (opts: { readonly experimental: boolean; diff --git a/apps/cli/src/legacy/commands/db/schema/declarative/generate/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/db/schema/declarative/generate/SIDE_EFFECTS.md index de3e3b967f..bd009a4af3 100644 --- a/apps/cli/src/legacy/commands/db/schema/declarative/generate/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/db/schema/declarative/generate/SIDE_EFFECTS.md @@ -47,11 +47,17 @@ pg-delta catalog (source) against the target database's catalog (target). | Code | Condition | | ---- | --------------------------------------------------------------------- | | `0` | success (files written, or skipped after a declined prompt) | -| `1` | conflicting `--db-url`/`--linked`/`--local` (mutually exclusive) | | `1` | pg-delta not enabled (no `--experimental` / `[experimental.pgdelta]`) | +| `1` | conflicting `--db-url`/`--linked`/`--local` (mutually exclusive) | | `1` | non-interactive mode with no explicit target | | `1` | shadow-database / edge-runtime / export failure | +The pg-delta gate and the mutex check are both raised before any side effects run, +but the gate wins when both conditions apply simultaneously: Go's +`PersistentPreRunE` runs before `ValidateFlagGroups()` +(`cobra@v1.10.2/command.go:985,1010`), so a closed gate (missing `--experimental`) +surfaces before a `--db-url`/`--linked`/`--local` conflict is ever checked. + ## Output Diagnostics (target resolution, prompts, `Declarative schema written to `) diff --git a/apps/cli/src/legacy/commands/db/schema/declarative/generate/generate.handler.ts b/apps/cli/src/legacy/commands/db/schema/declarative/generate/generate.handler.ts index fbcb89a721..3cec545df7 100644 --- a/apps/cli/src/legacy/commands/db/schema/declarative/generate/generate.handler.ts +++ b/apps/cli/src/legacy/commands/db/schema/declarative/generate/generate.handler.ts @@ -1,8 +1,8 @@ import { Effect, FileSystem, Option, Path } from "effect"; import { - LegacyExperimentalFlag, LegacyYesFlag, + legacyResolveExperimentalWithProjectEnv, } from "../../../../../../shared/legacy/global-flags.ts"; import { Output } from "../../../../../../shared/output/output.service.ts"; import { Tty } from "../../../../../../shared/runtime/tty.service.ts"; @@ -10,6 +10,7 @@ import { LegacyCliConfig } from "../../../../../config/legacy-cli-config.service import { legacyBold } from "../../../../../shared/legacy-colors.ts"; import { legacyReadProjectRefFile } from "../../../../../shared/legacy-temp-paths.ts"; import { + legacyLoadProjectEnv, legacyReadDbToml, legacyResolveDeclarativeDir, } from "../../../../../shared/legacy-db-config.toml-read.ts"; @@ -44,7 +45,14 @@ export const legacyDbSchemaDeclarativeGenerate = Effect.fn("legacy.db.schema.dec const cliConfig = yield* LegacyCliConfig; const telemetryState = yield* LegacyTelemetryState; const linkedProjectCache = yield* LegacyLinkedProjectCache; - const experimental = yield* LegacyExperimentalFlag; + // Go's `dbDeclarativeCmd.PersistentPreRunE` calls `flags.LoadConfig` — which runs + // `loadNestedEnv` and `os.Setenv`s each project-.env key — BEFORE reading + // `viper.GetBool("EXPERIMENTAL")` for the gate below (`apps/cli-go/cmd/ + // db_schema_declarative.go:73-78`, `pkg/config/config.go:789`). Load the project env + // first and resolve against it, as `db reset` does for its own experimental gate, so a + // `SUPABASE_EXPERIMENTAL` set only in `supabase/.env` opens the gate too. + const projectEnv = yield* legacyLoadProjectEnv(fs, path, cliConfig.workdir); + const experimental = yield* legacyResolveExperimentalWithProjectEnv(projectEnv); const yes = yield* LegacyYesFlag; // The resolved linked ref (explicit `--linked` only), hoisted so the post-run @@ -52,9 +60,23 @@ export const legacyDbSchemaDeclarativeGenerate = Effect.fn("legacy.db.schema.dec let linkedProjectRef: string | undefined; yield* Effect.gen(function* () { + const baseToml = yield* legacyReadDbToml(fs, path, cliConfig.workdir); + // Gate before the mutex check below — order matters; see + // legacyRequirePgDelta's doc comment for why. The pg-delta gate also runs on + // the BASE config: Go's declarative `PersistentPreRunE` gates before the root + // `ParseDatabaseConfig` reloads any `[remotes.]` block, so a remote + // `experimental.pgdelta.enabled = true` must NOT enable a base-disabled + // command without `--experimental`. + yield* legacyRequirePgDelta({ + experimental, + pgDeltaEnabled: baseToml.pgDelta.enabled, + configPath: path.join("supabase", "config.toml"), + }); + // cobra `MarkFlagsMutuallyExclusive("db-url", "linked", "local")` - // (`apps/cli-go/cmd/db_schema_declarative.go:499`) runs before PreRunE/RunE, - // so reject conflicting targets before reading config or the pg-delta gate. + // (`apps/cli-go/cmd/db_schema_declarative.go:570`) runs via + // `ValidateFlagGroups()`, which cobra invokes AFTER `PersistentPreRunE` (the + // gate above) — see legacyRequirePgDelta's doc comment for the full ordering. // "Set" follows cobra's `Changed`: Option set when `Some`, boolean when `true`. const exclusive: Array = []; if (Option.isSome(flags.dbUrl)) exclusive.push("db-url"); @@ -68,17 +90,6 @@ export const legacyDbSchemaDeclarativeGenerate = Effect.fn("legacy.db.schema.dec ); } - const baseToml = yield* legacyReadDbToml(fs, path, cliConfig.workdir); - // The pg-delta gate runs on the BASE config: Go's declarative `PersistentPreRunE` - // gates before the root `ParseDatabaseConfig` reloads any `[remotes.]` block, - // so a remote `experimental.pgdelta.enabled = true` must NOT enable a - // base-disabled command without `--experimental`. - yield* legacyRequirePgDelta({ - experimental, - pgDeltaEnabled: baseToml.pgDelta.enabled, - configPath: path.join("supabase", "config.toml"), - }); - // Explicit `--linked`: Go re-loads config with the resolved ref (root // `ParseDatabaseConfig` linked branch), so a matching `[remotes.]` block // overrides `experimental.pgdelta.*` (declarative_schema_path / format_options) diff --git a/apps/cli/src/legacy/commands/db/schema/declarative/generate/generate.integration.test.ts b/apps/cli/src/legacy/commands/db/schema/declarative/generate/generate.integration.test.ts index 8241336bd5..48ade9b8a6 100644 --- a/apps/cli/src/legacy/commands/db/schema/declarative/generate/generate.integration.test.ts +++ b/apps/cli/src/legacy/commands/db/schema/declarative/generate/generate.integration.test.ts @@ -12,6 +12,7 @@ import { mockLegacyTelemetryStateTracked, useLegacyTempWorkdir, } from "../../../../../../../tests/helpers/legacy-mocks.ts"; +import { CliArgs } from "../../../../../../shared/cli/cli-args.service.ts"; import { LegacyDnsResolverFlag, LegacyExperimentalFlag, @@ -48,6 +49,7 @@ const EXPORT_JSON = JSON.stringify({ interface SetupOpts { experimental?: boolean; + args?: ReadonlyArray; yes?: boolean; stdinIsTty?: boolean; promptConfirmResponses?: ReadonlyArray; @@ -147,6 +149,7 @@ function setup(workdir: string, opts: SetupOpts = {}) { mockLegacyCliConfig({ workdir, projectId: opts.projectId ?? Option.some("test") }), mockTty({ stdinIsTty: opts.stdinIsTty ?? false, stdoutIsTty: false }), Layer.succeed(LegacyExperimentalFlag, opts.experimental ?? true), + Layer.succeed(CliArgs, { args: opts.args ?? ["db", "schema", "declarative", "generate"] }), Layer.succeed(LegacyYesFlag, opts.yes ?? false), Layer.succeed(LegacyNetworkIdFlag, opts.networkId ?? Option.none()), Layer.succeed(LegacyDnsResolverFlag, "native"), @@ -204,10 +207,11 @@ describe("legacy db schema declarative generate integration", () => { }).pipe(Effect.provide(layer)); }); - it.effect("rejects conflicting targets (--local --linked) before the pg-delta gate", () => { - // cobra MarkFlagsMutuallyExclusive("db-url", "linked", "local") runs before - // PreRunE, so this fails even when pg-delta is not enabled. - const { layer } = setup(tmp.current, { experimental: false }); + it.effect("--local --linked with --experimental fails with the mutex error", () => { + // Go's declarative PersistentPreRunE gate (db_schema_declarative.go:49-99) runs + // BEFORE cobra's ValidateFlagGroups() mutex check (cobra@v1.10.2/command.go:985, + // 1010), so the mutex error only surfaces once the gate is open. + const { layer } = setup(tmp.current, { experimental: true }); return Effect.gen(function* () { const exit = yield* Effect.exit( legacyDbSchemaDeclarativeGenerate( @@ -223,6 +227,118 @@ describe("legacy db schema declarative generate integration", () => { }).pipe(Effect.provide(layer)); }); + it.effect( + "--local --linked without --experimental fails with the gate error, not the mutex error", + () => { + // Mirrors storage's experimental-gate-vs-mutex ordering fix (CLI-1855 / CLI-1876): + // the pg-delta gate runs before the mutex check, so an unopened gate wins even + // when the flags would also violate mutual exclusivity. + const { layer } = setup(tmp.current, { experimental: false }); + return Effect.gen(function* () { + const exit = yield* Effect.exit( + legacyDbSchemaDeclarativeGenerate( + flags({ local: Option.some(true), linked: Option.some(true) }), + ), + ); + expect(Exit.isFailure(exit)).toBe(true); + expect(failError(exit)?.constructor.name).toBe("LegacyDeclarativeNotEnabledError"); + }).pipe(Effect.provide(layer)); + }, + ); + + it.effect( + "--local --linked with SUPABASE_EXPERIMENTAL env (no --experimental flag) fails with the mutex error", + () => { + // Go's gate reads viper.GetBool("EXPERIMENTAL") (db_schema_declarative.go:78), + // which picks up SUPABASE_EXPERIMENTAL via viper.AutomaticEnv (root.go:318-334), + // so an env-only experimental session still opens the gate and lets the mutex + // check fire. legacyResolveExperimental (not the raw LegacyExperimentalFlag) is + // what makes the TS gate honor the env var the same way. + const { layer } = setup(tmp.current, { experimental: false }); + const ENV = "SUPABASE_EXPERIMENTAL"; + return Effect.gen(function* () { + const saved = process.env[ENV]; + process.env[ENV] = "1"; + const exit = yield* Effect.exit( + legacyDbSchemaDeclarativeGenerate( + flags({ local: Option.some(true), linked: Option.some(true) }), + ), + ); + if (saved === undefined) delete process.env[ENV]; + else process.env[ENV] = saved; + expect(Exit.isFailure(exit)).toBe(true); + expect(failError(exit)).toMatchObject({ + _tag: "LegacyDeclarativeMutuallyExclusiveFlagsError", + message: + "if any flags in the group [db-url linked local] are set none of the others can be; [linked local] were all set", + }); + }).pipe(Effect.provide(layer)); + }, + ); + + it.effect( + "an explicit --experimental=false closes the gate even when SUPABASE_EXPERIMENTAL is set", + () => { + // viper's bound-pflag lookup returns the flag value whenever Changed is true — + // BEFORE falling back to AutomaticEnv (viper@v1.21.0/viper.go:1176-1178) — so an + // explicit --experimental=false must win over SUPABASE_EXPERIMENTAL=1, closing the + // gate instead of letting the env value override it. + const { layer } = setup(tmp.current, { + experimental: false, + args: ["db", "schema", "declarative", "generate", "--experimental=false"], + }); + const ENV = "SUPABASE_EXPERIMENTAL"; + return Effect.gen(function* () { + const saved = process.env[ENV]; + process.env[ENV] = "1"; + const exit = yield* Effect.exit( + legacyDbSchemaDeclarativeGenerate(flags({ local: Option.some(true) })), + ); + if (saved === undefined) delete process.env[ENV]; + else process.env[ENV] = saved; + expect(Exit.isFailure(exit)).toBe(true); + expect(failError(exit)?.constructor.name).toBe("LegacyDeclarativeNotEnabledError"); + }).pipe(Effect.provide(layer)); + }, + ); + + it.effect( + "--local --linked with SUPABASE_EXPERIMENTAL set only in the project .env fails with the mutex error", + () => { + // Go's flags.LoadConfig runs loadNestedEnv (which os.Setenv's each project-.env key) + // before dbDeclarativeCmd.PersistentPreRunE reads viper.GetBool("EXPERIMENTAL") + // (apps/cli-go/cmd/db_schema_declarative.go:73-78, pkg/config/config.go:789), so a + // SUPABASE_EXPERIMENTAL set only in supabase/.env opens the gate and lets the mutex + // check fire, same as the shell-env case above. + const saved = process.env["SUPABASE_EXPERIMENTAL"]; + delete process.env["SUPABASE_EXPERIMENTAL"]; + mkdirSync(join(tmp.current, "supabase"), { recursive: true }); + writeFileSync(join(tmp.current, "supabase", ".env"), "SUPABASE_EXPERIMENTAL=true\n"); + const { layer } = setup(tmp.current, { experimental: false }); + return Effect.gen(function* () { + const exit = yield* Effect.exit( + legacyDbSchemaDeclarativeGenerate( + flags({ local: Option.some(true), linked: Option.some(true) }), + ), + ); + expect(Exit.isFailure(exit)).toBe(true); + expect(failError(exit)).toMatchObject({ + _tag: "LegacyDeclarativeMutuallyExclusiveFlagsError", + message: + "if any flags in the group [db-url linked local] are set none of the others can be; [linked local] were all set", + }); + }).pipe( + Effect.provide(layer), + Effect.ensuring( + Effect.sync(() => { + if (saved === undefined) delete process.env["SUPABASE_EXPERIMENTAL"]; + else process.env["SUPABASE_EXPERIMENTAL"] = saved; + }), + ), + ); + }, + ); + it.effect("explicit --local: provisions baseline, exports, writes declarative files", () => { const s = setup(tmp.current, { experimental: true }); return Effect.gen(function* () { diff --git a/apps/cli/src/legacy/commands/db/schema/declarative/sync/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/db/schema/declarative/sync/SIDE_EFFECTS.md index 83250c0487..e2182911be 100644 --- a/apps/cli/src/legacy/commands/db/schema/declarative/sync/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/db/schema/declarative/sync/SIDE_EFFECTS.md @@ -45,12 +45,18 @@ as a new timestamped migration. | Code | Condition | | ---- | --------------------------------------------------------------------------------------------------- | | `0` | success (migration created, applied, or "No schema changes found") | -| `1` | conflicting `--apply`/`--no-apply` (mutually exclusive) | | `1` | pg-delta not enabled | +| `1` | conflicting `--apply`/`--no-apply` (mutually exclusive) | | `1` | no declarative schema files found | | `1` | shadow-database / edge-runtime / diff failure | | `1` | apply failure (when applied) — propagated from the native migration apply (`applyMigrationToLocal`) | +The pg-delta gate and the mutex check are both raised before any side effects run, +but the gate wins when both conditions apply simultaneously: Go's +`PersistentPreRunE` runs before `ValidateFlagGroups()` +(`cobra@v1.10.2/command.go:985,1010`), so a closed gate (missing `--experimental`) +surfaces before an `--apply`/`--no-apply` conflict is ever checked. + ## Output Text mode only. The generated SQL, the created-migration path, drop-statement diff --git a/apps/cli/src/legacy/commands/db/schema/declarative/sync/sync.command.ts b/apps/cli/src/legacy/commands/db/schema/declarative/sync/sync.command.ts index e06c092a1f..db9da924de 100644 --- a/apps/cli/src/legacy/commands/db/schema/declarative/sync/sync.command.ts +++ b/apps/cli/src/legacy/commands/db/schema/declarative/sync/sync.command.ts @@ -33,7 +33,7 @@ const config = { Flag.optional, ), // cobra's `MarkFlagsMutuallyExclusive("apply", "no-apply")` keys off `flag.Changed`, - // not the value (`cmd/db_schema_declarative.go:490`), so model presence with `Option` + // not the value (`cmd/db_schema_declarative.go:561`), so model presence with `Option` // so `--apply=false --no-apply` still trips the conflict. The apply decision below // reads the resolved value via `Option.getOrElse`. apply: Flag.boolean("apply").pipe( diff --git a/apps/cli/src/legacy/commands/db/schema/declarative/sync/sync.handler.ts b/apps/cli/src/legacy/commands/db/schema/declarative/sync/sync.handler.ts index 61f276e4c0..63f8d8a7e4 100644 --- a/apps/cli/src/legacy/commands/db/schema/declarative/sync/sync.handler.ts +++ b/apps/cli/src/legacy/commands/db/schema/declarative/sync/sync.handler.ts @@ -2,9 +2,9 @@ import { Cause, Clock, Effect, Exit, FileSystem, Option, Path } from "effect"; import { LegacyDnsResolverFlag, - LegacyExperimentalFlag, LegacyNetworkIdFlag, LegacyYesFlag, + legacyResolveExperimentalWithProjectEnv, } from "../../../../../../shared/legacy/global-flags.ts"; import { Output } from "../../../../../../shared/output/output.service.ts"; import { Tty } from "../../../../../../shared/runtime/tty.service.ts"; @@ -13,6 +13,7 @@ import { legacyBold, legacyRed, legacyYellow } from "../../../../../shared/legac import { LegacyDbConnection } from "../../../../../shared/legacy-db-connection.service.ts"; import { legacyGetHostname } from "../../../../../shared/legacy-hostname.ts"; import { + legacyLoadProjectEnv, legacyReadDbToml, legacyResolveDeclarativeDir, } from "../../../../../shared/legacy-db-config.toml-read.ts"; @@ -72,7 +73,14 @@ export const legacyDbSchemaDeclarativeSync = Effect.fn("legacy.db.schema.declara const path = yield* Path.Path; const cliConfig = yield* LegacyCliConfig; const telemetryState = yield* LegacyTelemetryState; - const experimental = yield* LegacyExperimentalFlag; + // Go's `dbDeclarativeCmd.PersistentPreRunE` calls `flags.LoadConfig` — which runs + // `loadNestedEnv` and `os.Setenv`s each project-.env key — BEFORE reading + // `viper.GetBool("EXPERIMENTAL")` for the gate below (`apps/cli-go/cmd/ + // db_schema_declarative.go:73-78`, `pkg/config/config.go:789`). Load the project env + // first and resolve against it, as `db reset` does for its own experimental gate, so a + // `SUPABASE_EXPERIMENTAL` set only in `supabase/.env` opens the gate too. + const projectEnv = yield* legacyLoadProjectEnv(fs, path, cliConfig.workdir); + const experimental = yield* legacyResolveExperimentalWithProjectEnv(projectEnv); const yes = yield* LegacyYesFlag; const networkId = yield* LegacyNetworkIdFlag; const dnsResolver = yield* LegacyDnsResolverFlag; @@ -89,10 +97,21 @@ export const legacyDbSchemaDeclarativeSync = Effect.fn("legacy.db.schema.declara let linkedProjectRef: string | undefined; yield* Effect.gen(function* () { + const toml = yield* legacyReadDbToml(fs, path, cliConfig.workdir); + // Gate before the mutex check below — order matters; see + // legacyRequirePgDelta's doc comment for why. + yield* legacyRequirePgDelta({ + experimental, + pgDeltaEnabled: toml.pgDelta.enabled, + configPath: path.join("supabase", "config.toml"), + }); + // cobra `MarkFlagsMutuallyExclusive("apply", "no-apply")` - // (`apps/cli-go/cmd/db_schema_declarative.go:490`) runs before PreRunE/RunE, - // so reject the conflict before reading config or the pg-delta gate, rather - // than letting `--no-apply` silently win in the apply-decision helper. + // (`apps/cli-go/cmd/db_schema_declarative.go:561`) runs via + // `ValidateFlagGroups()`, which cobra invokes AFTER `PersistentPreRunE` (the + // gate above) — see legacyRequirePgDelta's doc comment for the full ordering. + // Reject the conflict here rather than letting `--no-apply` silently win in + // the apply-decision helper. const exclusive: Array = []; if (Option.isSome(flags.apply)) exclusive.push("apply"); if (Option.isSome(flags.noApply)) exclusive.push("no-apply"); @@ -104,12 +123,6 @@ export const legacyDbSchemaDeclarativeSync = Effect.fn("legacy.db.schema.declara ); } - const toml = yield* legacyReadDbToml(fs, path, cliConfig.workdir); - yield* legacyRequirePgDelta({ - experimental, - pgDeltaEnabled: toml.pgDelta.enabled, - configPath: path.join("supabase", "config.toml"), - }); // `path.resolve` (not `path.join`) so an absolute `declarative_schema_path` is // used as-is, matching Go's `config.resolve` (which only prefixes the workdir onto // a relative path). `path.join(workdir, abs)` would mangle the absolute path. diff --git a/apps/cli/src/legacy/commands/db/schema/declarative/sync/sync.integration.test.ts b/apps/cli/src/legacy/commands/db/schema/declarative/sync/sync.integration.test.ts index d1e83e7290..44dd7548b2 100644 --- a/apps/cli/src/legacy/commands/db/schema/declarative/sync/sync.integration.test.ts +++ b/apps/cli/src/legacy/commands/db/schema/declarative/sync/sync.integration.test.ts @@ -11,6 +11,7 @@ import { mockLegacyTelemetryStateTracked, useLegacyTempWorkdir, } from "../../../../../../../tests/helpers/legacy-mocks.ts"; +import { CliArgs } from "../../../../../../shared/cli/cli-args.service.ts"; import { LegacyDnsResolverFlag, LegacyExperimentalFlag, @@ -44,6 +45,7 @@ const EXPORT_JSON = JSON.stringify({ interface SetupOpts { experimental?: boolean; + args?: ReadonlyArray; yes?: boolean; stdinIsTty?: boolean; diffSql?: string; @@ -151,6 +153,7 @@ function setup(workdir: string, opts: SetupOpts = {}) { mockLegacyCliConfig({ workdir, projectId: opts.projectId ?? Option.some("test") }), mockTty({ stdinIsTty: opts.stdinIsTty ?? false, stdoutIsTty: false }), Layer.succeed(LegacyExperimentalFlag, opts.experimental ?? true), + Layer.succeed(CliArgs, { args: opts.args ?? ["db", "schema", "declarative", "sync"] }), Layer.succeed(LegacyYesFlag, opts.yes ?? false), Layer.succeed( LegacyNetworkIdFlag, @@ -199,10 +202,11 @@ describe("legacy db schema declarative sync integration", () => { }).pipe(Effect.provide(layer)); }); - it.effect("rejects --apply and --no-apply together before the pg-delta gate", () => { - // cobra MarkFlagsMutuallyExclusive("apply", "no-apply") runs before PreRunE, - // so this fails even when pg-delta is not enabled. - const { layer } = setup(tmp.current, { experimental: false }); + it.effect("--apply and --no-apply together with --experimental fail with the mutex error", () => { + // Go's declarative PersistentPreRunE gate (db_schema_declarative.go:49-99) runs + // BEFORE cobra's ValidateFlagGroups() mutex check (cobra@v1.10.2/command.go:985, + // 1010), so the mutex error only surfaces once the gate is open. + const { layer } = setup(tmp.current, { experimental: true }); return Effect.gen(function* () { const exit = yield* Effect.exit( legacyDbSchemaDeclarativeSync( @@ -218,10 +222,122 @@ describe("legacy db schema declarative sync integration", () => { }).pipe(Effect.provide(layer)); }); + it.effect( + "--apply and --no-apply together without --experimental fail with the gate error, not the mutex error", + () => { + // Mirrors storage's experimental-gate-vs-mutex ordering fix (CLI-1855 / CLI-1876): + // the pg-delta gate runs before the mutex check, so an unopened gate wins even + // when the flags would also violate mutual exclusivity. + const { layer } = setup(tmp.current, { experimental: false }); + return Effect.gen(function* () { + const exit = yield* Effect.exit( + legacyDbSchemaDeclarativeSync( + flags({ apply: Option.some(true), noApply: Option.some(true) }), + ), + ); + expect(Exit.isFailure(exit)).toBe(true); + expect(failError(exit)?.constructor.name).toBe("LegacyDeclarativeNotEnabledError"); + }).pipe(Effect.provide(layer)); + }, + ); + + it.effect( + "--apply and --no-apply together with SUPABASE_EXPERIMENTAL env (no --experimental flag) fail with the mutex error", + () => { + // Go's gate reads viper.GetBool("EXPERIMENTAL") (db_schema_declarative.go:78), + // which picks up SUPABASE_EXPERIMENTAL via viper.AutomaticEnv (root.go:318-334), + // so an env-only experimental session still opens the gate and lets the mutex + // check fire. legacyResolveExperimental (not the raw LegacyExperimentalFlag) is + // what makes the TS gate honor the env var the same way. + const { layer } = setup(tmp.current, { experimental: false }); + const ENV = "SUPABASE_EXPERIMENTAL"; + return Effect.gen(function* () { + const saved = process.env[ENV]; + process.env[ENV] = "1"; + const exit = yield* Effect.exit( + legacyDbSchemaDeclarativeSync( + flags({ apply: Option.some(true), noApply: Option.some(true) }), + ), + ); + if (saved === undefined) delete process.env[ENV]; + else process.env[ENV] = saved; + expect(Exit.isFailure(exit)).toBe(true); + expect(failError(exit)).toMatchObject({ + _tag: "LegacyDeclarativeMutuallyExclusiveFlagsError", + message: + "if any flags in the group [apply no-apply] are set none of the others can be; [apply no-apply] were all set", + }); + }).pipe(Effect.provide(layer)); + }, + ); + + it.effect( + "an explicit --experimental=false closes the gate even when SUPABASE_EXPERIMENTAL is set", + () => { + // viper's bound-pflag lookup returns the flag value whenever Changed is true — + // BEFORE falling back to AutomaticEnv (viper@v1.21.0/viper.go:1176-1178) — so an + // explicit --experimental=false must win over SUPABASE_EXPERIMENTAL=1, closing the + // gate instead of letting the env value override it. + const { layer } = setup(tmp.current, { + experimental: false, + args: ["db", "schema", "declarative", "sync", "--experimental=false"], + }); + const ENV = "SUPABASE_EXPERIMENTAL"; + return Effect.gen(function* () { + const saved = process.env[ENV]; + process.env[ENV] = "1"; + const exit = yield* Effect.exit(legacyDbSchemaDeclarativeSync(flags())); + if (saved === undefined) delete process.env[ENV]; + else process.env[ENV] = saved; + expect(Exit.isFailure(exit)).toBe(true); + expect(failError(exit)?.constructor.name).toBe("LegacyDeclarativeNotEnabledError"); + }).pipe(Effect.provide(layer)); + }, + ); + + it.effect( + "--apply and --no-apply together with SUPABASE_EXPERIMENTAL set only in the project .env fail with the mutex error", + () => { + // Go's flags.LoadConfig runs loadNestedEnv (which os.Setenv's each project-.env key) + // before dbDeclarativeCmd.PersistentPreRunE reads viper.GetBool("EXPERIMENTAL") + // (apps/cli-go/cmd/db_schema_declarative.go:73-78, pkg/config/config.go:789), so a + // SUPABASE_EXPERIMENTAL set only in supabase/.env opens the gate and lets the mutex + // check fire, same as the shell-env case above. + const saved = process.env["SUPABASE_EXPERIMENTAL"]; + delete process.env["SUPABASE_EXPERIMENTAL"]; + mkdirSync(join(tmp.current, "supabase"), { recursive: true }); + writeFileSync(join(tmp.current, "supabase", ".env"), "SUPABASE_EXPERIMENTAL=true\n"); + const { layer } = setup(tmp.current, { experimental: false }); + return Effect.gen(function* () { + const exit = yield* Effect.exit( + legacyDbSchemaDeclarativeSync( + flags({ apply: Option.some(true), noApply: Option.some(true) }), + ), + ); + expect(Exit.isFailure(exit)).toBe(true); + expect(failError(exit)).toMatchObject({ + _tag: "LegacyDeclarativeMutuallyExclusiveFlagsError", + message: + "if any flags in the group [apply no-apply] are set none of the others can be; [apply no-apply] were all set", + }); + }).pipe( + Effect.provide(layer), + Effect.ensuring( + Effect.sync(() => { + if (saved === undefined) delete process.env["SUPABASE_EXPERIMENTAL"]; + else process.env["SUPABASE_EXPERIMENTAL"] = saved; + }), + ), + ); + }, + ); + it.effect("rejects --apply=false --no-apply as a conflict (Go flag.Changed)", () => { // cobra keys the mutex off flag.Changed, so an explicit `--apply=false` still // counts as set and conflicts with `--no-apply`, even though its value is false. - const { layer } = setup(tmp.current, { experimental: false }); + // The gate runs first (see legacyRequirePgDelta's doc comment), so --experimental + // is required here for the mutex error to be the one that surfaces. + const { layer } = setup(tmp.current, { experimental: true }); return Effect.gen(function* () { const exit = yield* Effect.exit( legacyDbSchemaDeclarativeSync( diff --git a/apps/cli/src/legacy/shared/legacy-experimental-gate.unit.test.ts b/apps/cli/src/legacy/shared/legacy-experimental-gate.unit.test.ts index a359ec4af7..bc07234903 100644 --- a/apps/cli/src/legacy/shared/legacy-experimental-gate.unit.test.ts +++ b/apps/cli/src/legacy/shared/legacy-experimental-gate.unit.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "@effect/vitest"; import { Effect, Layer } from "effect"; +import { CliArgs } from "../../shared/cli/cli-args.service.ts"; import { LegacyExperimentalFlag } from "../../shared/legacy/global-flags.ts"; import { LegacyExperimentalRequiredError, @@ -8,7 +9,8 @@ import { } from "./legacy-experimental-gate.ts"; const ENV = "SUPABASE_EXPERIMENTAL"; -const withFlag = (value: boolean) => Layer.succeed(LegacyExperimentalFlag, value); +const withFlag = (value: boolean, args: ReadonlyArray = []) => + Layer.mergeAll(Layer.succeed(LegacyExperimentalFlag, value), Layer.succeed(CliArgs, { args })); describe("legacyRequireExperimental", () => { it.effect("passes when --experimental is set", () => @@ -43,4 +45,23 @@ describe("legacyRequireExperimental", () => { expect(exit._tag).toBe("Success"); }), ); + + it.effect( + "fails even with SUPABASE_EXPERIMENTAL=1 when --experimental=false is explicit (viper Changed wins)", + () => + Effect.gen(function* () { + // viper's bound-pflag lookup returns the flag value whenever Changed is true — + // BEFORE falling back to AutomaticEnv (viper@v1.21.0/viper.go:1176-1178) — so an + // explicit --experimental=false must win over SUPABASE_EXPERIMENTAL=1. + const saved = process.env[ENV]; + process.env[ENV] = "1"; + const error = yield* legacyRequireExperimental.pipe( + Effect.provide(withFlag(false, ["--experimental=false"])), + Effect.flip, + ); + if (saved === undefined) delete process.env[ENV]; + else process.env[ENV] = saved; + expect(error).toBeInstanceOf(LegacyExperimentalRequiredError); + }), + ); }); diff --git a/apps/cli/src/shared/legacy/global-flags.ts b/apps/cli/src/shared/legacy/global-flags.ts index 03c4a37eea..c53c3ea999 100644 --- a/apps/cli/src/shared/legacy/global-flags.ts +++ b/apps/cli/src/shared/legacy/global-flags.ts @@ -151,29 +151,58 @@ export const legacyResolveYesWithProjectEnv = (projectEnv: Record` (pflag's `ParseBool` + * false set). Mirrors {@link legacyYesFlagExplicitlyFalse}: `--experimental` is bound to + * viper the same way `--yes` is (`apps/cli-go/cmd/root.go:318-334`), and viper's bound-pflag + * lookup returns the flag value whenever `Changed` is true — BEFORE falling back to + * `AutomaticEnv` — regardless of whether that value is `true` or `false` + * (`viper@v1.21.0/viper.go:1176-1178`). A plain boolean can't distinguish an explicit + * `--experimental=false` from the omitted default, so scan the raw argv. Only the `=false` + * form needs special handling: `--experimental` / `--experimental=true` are already `true`, + * so `flag || env` matches Go, and an omitted flag correctly falls through to the env value. + */ +const legacyExperimentalFlagExplicitlyFalse = (args: ReadonlyArray): boolean => + args.some( + (arg) => + arg.startsWith("--experimental=") && + PFLAG_FALSE_VALUES.has(arg.slice("--experimental=".length)), + ); + /** * `--experimental` resolved with Go's viper `AutomaticEnv` fallback: the gate in * `rootCmd.PersistentPreRunE` reads `viper.GetBool("EXPERIMENTAL")` * (`apps/cli-go/cmd/root.go:94`), so `SUPABASE_EXPERIMENTAL` enables experimental - * commands just like the flag. A passed `--experimental` wins over the env. + * commands just like the flag. An explicit `--experimental` — including + * `--experimental=false` — wins over the env, matching viper's bound-pflag precedence. */ export const legacyResolveExperimental = Effect.gen(function* () { const flag = yield* LegacyExperimentalFlag; + const cliArgs = yield* CliArgs; + if (legacyExperimentalFlagExplicitlyFalse(cliArgs.args)) { + return false; + } return flag || legacyViperEnvBool("SUPABASE_EXPERIMENTAL"); }); /** * `--experimental` resolved with the project `.env` consulted too, for commands that load the - * nested project env before branching on the experimental gate (`db reset`). Go's - * `ParseDatabaseConfig` runs `loadNestedEnv` — which `os.Setenv`s each project-.env key — - * before `reset.Run` reads `viper.GetBool("EXPERIMENTAL")`, so a `SUPABASE_EXPERIMENTAL` set - * only in `supabase/.env` enables the experimental path. The shell env still wins over the - * file value; a passed `--experimental` wins over both. `projectEnv` is the loaded map from - * `legacyLoadProjectEnv`. + * nested project env before branching on the experimental gate (`db reset`, + * `db schema declarative generate`/`sync`). Go's `ParseDatabaseConfig` / + * `dbDeclarativeCmd.PersistentPreRunE` run `loadNestedEnv` — which `os.Setenv`s each + * project-.env key — before reading `viper.GetBool("EXPERIMENTAL")`, so a + * `SUPABASE_EXPERIMENTAL` set only in `supabase/.env` enables the experimental path. The + * shell env still wins over the file value; an explicit `--experimental` — including + * `--experimental=false` — wins over both, matching viper's bound-pflag precedence. + * `projectEnv` is the loaded map from `legacyLoadProjectEnv`. */ export const legacyResolveExperimentalWithProjectEnv = (projectEnv: Record) => Effect.gen(function* () { const flag = yield* LegacyExperimentalFlag; + const cliArgs = yield* CliArgs; + if (legacyExperimentalFlagExplicitlyFalse(cliArgs.args)) { + return false; + } return ( flag || legacyViperEnvBool("SUPABASE_EXPERIMENTAL") || From c0ed40c29bbd71a5ac8d385102dc7d5bb0cbb46a Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Wed, 8 Jul 2026 16:28:04 +0100 Subject: [PATCH 23/79] fix(cli): exit 0 for bare group commands to match Go cobra parity (#5827) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What changed Invoking a legacy-shell group command that has subcommands but is itself not runnable — e.g. `supabase branches`, `supabase completion` — with no subcommand and no `--help` exited **1** in the TS legacy shell. Go's cobra CLI exits **0** for the identical invocation: a non-`Runnable()` command with no `RunE` internally returns `flag.ErrHelp`, which cobra's `ExecuteC()` maps to "print help, return nil error". Only explicit `--help` got exit 0 before this fix; the bare/missing-subcommand form did not, even though the printed help text was identical in both cases. `CliError.ShowHelp` (in `effect/unstable/cli`) already declares the correct exit code via Effect's own `Runtime.errorExitCode` marker (`0` for a clean `ShowHelp` with no errors, `1` otherwise) — `apps/cli/src/shared/cli/run.ts` now delegates to `Runtime.getErrorExitCode(Cause.squash(cause))` instead of hand-rolling `ShowHelp` classification, which is simpler and tracks the library's own source of truth for any future `CliError` additions. Also added an integration test driving the real `legacyBranchesCommand` through `Command.runWith` (confirming the actual `ShowHelp` cause shape), and an e2e test asserting the real compiled-binary exit code, since this bug is specifically about the real OS process exit code. ## Known related (not fixed here) A typo'd subcommand under a group (e.g. `supabase branches bogus`) still exits 1 where Go cobra exits 0 for a non-root command — this is pre-existing on `develop`, not a regression from this change, and out of scope for this fix. Filed as a follow-up. Fixes CLI-1906 --- apps/cli/src/shared/cli/run.e2e.test.ts | 24 +++++++ .../src/shared/cli/run.integration.test.ts | 66 +++++++++++++++++++ apps/cli/src/shared/cli/run.ts | 45 ++++++++----- apps/cli/src/shared/cli/run.unit.test.ts | 45 ++++++++++++- 4 files changed, 164 insertions(+), 16 deletions(-) create mode 100644 apps/cli/src/shared/cli/run.e2e.test.ts create mode 100644 apps/cli/src/shared/cli/run.integration.test.ts diff --git a/apps/cli/src/shared/cli/run.e2e.test.ts b/apps/cli/src/shared/cli/run.e2e.test.ts new file mode 100644 index 0000000000..1f399eb37e --- /dev/null +++ b/apps/cli/src/shared/cli/run.e2e.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, test } from "vitest"; +import { runSupabase } from "../../../tests/helpers/cli.ts"; + +/** + * CLI-1906: the real bug here is the actual OS process exit code — + * `ProcessControl.exit` calls real `process.exit(code)`, so only a genuine + * subprocess run proves the shipped binary's exit code changed. Everything + * else about this fix (`exitCodeForFailure`'s classification) is covered by + * `run.unit.test.ts` and `run.integration.test.ts`; this is the one minimal + * case that observes the real subprocess boundary. + */ +describe("legacy CLI process exit codes (CLI-1906)", () => { + test("bare `branches` (no subcommand, no --help) exits 0", async () => { + const { exitCode } = await runSupabase(["branches"], { entrypoint: "legacy" }); + expect(exitCode).toBe(0); + }); + + test("a genuine parse error still exits 1", async () => { + const { exitCode } = await runSupabase(["branches", "--this-flag-does-not-exist"], { + entrypoint: "legacy", + }); + expect(exitCode).toBe(1); + }); +}); diff --git a/apps/cli/src/shared/cli/run.integration.test.ts b/apps/cli/src/shared/cli/run.integration.test.ts new file mode 100644 index 0000000000..9ff4f7fd4e --- /dev/null +++ b/apps/cli/src/shared/cli/run.integration.test.ts @@ -0,0 +1,66 @@ +import { describe, expect, test } from "@effect/vitest"; +import { BunServices } from "@effect/platform-bun"; +import { Effect, Exit, Layer } from "effect"; +import { CliOutput, Command } from "effect/unstable/cli"; +import { legacyBranchesCommand } from "../../legacy/commands/branches/branches.command.ts"; +import { textCliOutputFormatter } from "../output/text-formatter.ts"; +import { CliArgs } from "./cli-args.service.ts"; +import { exitCodeForFailure } from "./run.ts"; + +/** + * CLI-1906: `supabase branches` (a legacy "group" command — subcommands, no + * runnable handler of its own) used to exit 1 when invoked bare, even though + * the printed help was identical to `supabase branches --help`, which already + * exited 0. These tests run the real `legacyBranchesCommand` definition + * through `Command.runWith` (same technique as `version.integration.test.ts`) + * so the `ShowHelp` cause shape is the one the real CLI actually produces, not + * a hand-rolled stand-in. `legacyBranchesCommand` is exercised directly + * (rather than nested under `legacyRoot`) because `legacyRoot`'s + * `Command.provide` (see `Command.ts`'s `provide`/`withSubcommands`) wraps its + * *entire* handle — including the bare/`--help`/parse-error paths exercised + * here — in the production output/proxy layer graph (`Layer.unwrap` reading + * every global flag, resolving the Go proxy binary, etc). `Effect.provide` + * still *builds* that layer graph before running the wrapped handle even on + * these runs; it just never gets *consumed*, because the `ShowHelp` failure + * fires before any leaf subcommand handler body executes. Exercising + * `legacyBranchesCommand` directly avoids needing to provide or mock that + * unused graph for a test that only cares about the `ShowHelp` cause shape. + */ +describe("legacy group command exit codes (CLI-1906)", () => { + const layerFor = (args: ReadonlyArray) => + Layer.mergeAll( + CliOutput.layer(textCliOutputFormatter()), + Layer.succeed(CliArgs, { args }), + BunServices.layer, + ); + + const runBranches = (args: ReadonlyArray) => + Effect.runPromiseExit( + Command.runWith(legacyBranchesCommand, { version: "0.0.0-test" })(args).pipe( + Effect.provide(layerFor(args)), + ), + ); + + test("bare `branches` (no subcommand, no --help) fails with a clean ShowHelp that maps to exit 0", async () => { + const exit = await runBranches([]); + expect(Exit.isFailure(exit)).toBe(true); + if (!Exit.isFailure(exit)) return; + + expect(exitCodeForFailure(exit.cause)).toBe(0); + }); + + test("`branches --help` succeeds outright and exits 0", async () => { + const exit = await runBranches(["--help"]); + // The `--help` global flag is handled as a successful `GlobalFlag.Action`, so this + // never even reaches the ShowHelp-as-failure path bare `branches` goes through above. + expect(Exit.isSuccess(exit)).toBe(true); + }); + + test("`branches` with an unrecognized flag is a genuine parse error that still exits 1", async () => { + const exit = await runBranches(["--this-flag-does-not-exist"]); + expect(Exit.isFailure(exit)).toBe(true); + if (!Exit.isFailure(exit)) return; + + expect(exitCodeForFailure(exit.cause)).toBe(1); + }); +}); diff --git a/apps/cli/src/shared/cli/run.ts b/apps/cli/src/shared/cli/run.ts index 099c9a2ba8..48ab86fa05 100644 --- a/apps/cli/src/shared/cli/run.ts +++ b/apps/cli/src/shared/cli/run.ts @@ -1,7 +1,7 @@ import { BunServices } from "@effect/platform-bun"; import { ProjectConfigStore } from "@supabase/config"; import { unixHttpClientLayer } from "@supabase/stack"; -import { Cause, Effect, Exit, Fiber, Layer, Stdio } from "effect"; +import { Cause, Effect, Exit, Fiber, Layer, Runtime, Stdio } from "effect"; import { CliOutput, Command } from "effect/unstable/cli"; import { CLI_VERSION } from "./version.ts"; import { Credentials } from "../../next/auth/credentials.service.ts"; @@ -93,17 +93,26 @@ function formatterLayerFor( : CliOutput.layer(textCliOutputFormatter(context)); } -function isErrorRecord(error: unknown): error is Record { - return typeof error === "object" && error !== null; -} - -function isExplicitHelpCause(cause: Cause.Cause): boolean { - const error = Cause.findErrorOption(cause); - if (error._tag !== "Some" || !isErrorRecord(error.value)) return false; - if (error.value["_tag"] !== "ShowHelp") return false; - - const errors = error.value["errors"]; - return !Array.isArray(errors) || errors.length === 0; +/** + * Process exit code for a failed CLI run, matching Go cobra's exit-code + * mapping. Delegates to Effect's own `Runtime` exit-code protocol (the same + * one `Runtime.defaultTeardown` uses) rather than hand-rolling `ShowHelp` + * classification: `CliError.ShowHelp` declares + * `[Runtime.errorExitCode] = this.errors.length ? 1 : 0`, so a bare group + * command's default handler failing with `ShowHelp({ errors: [] })` (no + * subcommand given, e.g. `supabase branches`) reads as exit `0` here — matching + * Go cobra's non-`Runnable()` handling, which internally returns + * `flag.ErrHelp` and `ExecuteC()` maps that to "print help, return nil error". + * A `ShowHelp` with a non-empty `errors` array (a genuine parse/validation + * failure) reads as exit `1`, and any other failure (including a `Cause.die` + * defect with no typed `ShowHelp` marker at all) falls back to + * `Runtime.getErrorExitCode`'s default of `1`. An explicit `--help` invocation + * never reaches this function — it's handled earlier as a successful + * `GlobalFlag.Action` and exits 0 via the success path. + */ +export function exitCodeForFailure(cause: Cause.Cause): number { + if (Cause.hasInterruptsOnly(cause)) return 130; + return Runtime.getErrorExitCode(Cause.squash(cause)); } function projectContextLayerFor(runtimeLayer: Layer.Layer) { @@ -232,11 +241,17 @@ export async function runCli(rootCommand: Command.Command.Any, options: RunCliOp const output = yield* Output; const exit = yield* program.pipe(Effect.exit); if (Exit.isFailure(exit)) { - const interrupted = Cause.hasInterruptsOnly(exit.cause); - if (!interrupted && !isExplicitHelpCause(exit.cause)) { + const exitCode = exitCodeForFailure(exit.cause); + // Skip reporting for an interrupted run (130 — a signal, not a + // reportable error) and for a clean `ShowHelp` failure (0). Literal + // `--help` never reaches this branch — it's handled as a successful + // `GlobalFlag.Action` and exits 0 via the success path below. See + // `exitCodeForFailure` for why a "clean" ShowHelp failure (e.g. a bare + // group command with no subcommand) also maps to exit 0. + if (exitCode !== 0 && exitCode !== 130) { yield* output.fail(normalizeCause(exit.cause)); } - return yield* processControl.exit(interrupted ? 130 : 1); + return yield* processControl.exit(exitCode); } const exitCode = yield* processControl.getExitCode; return yield* processControl.exit(exitCode ?? 0); diff --git a/apps/cli/src/shared/cli/run.unit.test.ts b/apps/cli/src/shared/cli/run.unit.test.ts index bd2d80804e..f87d160533 100644 --- a/apps/cli/src/shared/cli/run.unit.test.ts +++ b/apps/cli/src/shared/cli/run.unit.test.ts @@ -1,6 +1,8 @@ +import { Cause } from "effect"; +import { CliError } from "effect/unstable/cli"; import { describe, expect, it } from "vitest"; -import { extractCommandPath, shouldUseGlobalSignalInterrupt } from "./run.ts"; +import { exitCodeForFailure, extractCommandPath, shouldUseGlobalSignalInterrupt } from "./run.ts"; describe("extractCommandPath", () => { it("returns positional command-path tokens", () => { @@ -45,3 +47,44 @@ describe("shouldUseGlobalSignalInterrupt", () => { expect(shouldUseGlobalSignalInterrupt([])).toBe(true); }); }); + +describe("exitCodeForFailure", () => { + // CLI-1906: a group command's default handler (e.g. bare `supabase branches`, which + // has subcommands but no runnable handler of its own) fails with exactly this shape: + // ShowHelp with an empty `errors` array. `CliError.ShowHelp` declares + // `[Runtime.errorExitCode] = this.errors.length ? 1 : 0`, so this reads as exit 0 — + // matching Go cobra's `flag.ErrHelp` handling for non-Runnable commands. Before + // CLI-1906, this case always returned 1. + it("exits 0 for a clean ShowHelp failure (bare group command)", () => { + const cause = Cause.fail(new CliError.ShowHelp({ commandPath: ["branches"], errors: [] })); + expect(exitCodeForFailure(cause)).toBe(0); + }); + + it("exits 1 for a ShowHelp cause carrying a genuine validation error", () => { + const cause = Cause.fail( + new CliError.ShowHelp({ + commandPath: ["branches"], + errors: [new CliError.UnrecognizedOption({ option: "--bogus", suggestions: [] })], + }), + ); + expect(exitCodeForFailure(cause)).toBe(1); + }); + + it("exits 1 for a non-ShowHelp failure", () => { + const cause = Cause.fail(new Error("boom")); + expect(exitCodeForFailure(cause)).toBe(1); + }); + + // `Cause.squash` on a `Die` cause returns the raw defect (a plain `Error`, with no + // `Runtime.errorExitCode` marker at all). This must still fall back to the default + // failure exit code (1), not silently pass through as a "clean" exit — this is the real + // unexpected-crash path through `runCli` that must keep exiting 1. + it("exits 1 for a defect with no typed failure", () => { + const cause = Cause.die(new Error("unexpected crash")); + expect(exitCodeForFailure(cause)).toBe(1); + }); + + it("exits 130 when interrupted, regardless of any other failure reason", () => { + expect(exitCodeForFailure(Cause.interrupt())).toBe(130); + }); +}); From 02201c9fe52d59055cc3badaaec6e6b86548be11 Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Wed, 8 Jul 2026 16:31:21 +0100 Subject: [PATCH 24/79] feat(cli): port supabase stop and status commands to native TypeScript (#5765) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Ports `supabase stop` and `supabase status` from Go-proxy stubs to native TypeScript in the legacy CLI shell (CLI-1324). - Both commands now talk directly to Docker/Podman via subprocess, replicating Go's label-filtering and container-naming scheme byte-for-byte. Legacy `start` is still Go-proxied, so this intentionally does **not** route through `@supabase/stack/effect`'s daemon-based orchestration model — that substrate manages a different set of containers than the ones Go's binary actually creates, and using it would silently no-op against a real running stack. - New shared infrastructure (`legacy-docker-lifecycle`, `legacy-go-jwt`, `legacy-local-config-values`, `legacy-api-url`) is reused by both commands, matching Go's local-dev defaults exactly — including a Go-byte-exact JWT signer, since `@supabase/stack`'s own JWT generator uses a different issuer/claim order than what Go prints for local dev keys. - Adds `*.live.test.ts` as a documented test category (`AGENTS.md`) alongside unit/integration/e2e: black-box subprocess tests run by the `cli-e2e-ci` harness against a real platform. `stop`/`status` don't call the Management API, so their live tests spin up a real local Docker stack instead and verify against it directly (e.g. confirming Docker itself has no containers left after `stop`, not just trusting the CLI's exit code). ## Notable review findings fixed along the way - Table/status output was colorizing based on `stderr`'s TTY status while writing to `stdout` — piping stdout while stderr stayed a TTY (`supabase status | less`) would have corrupted output with ANSI escapes (the same bug class CLI-1546 fixed once before). - `--override-name` was leaking into pretty-mode output; Go's `PrettyPrint` rebuilds a fresh, un-overridden view and ignores it there. - `--backup`/`--no-backup`: Go's `--backup` flag is dead code (declared, never bound to a variable in `cmd/stop.go`) — the port now matches that exactly instead of an intended-but-never-true semantic. - Docker/Podman-both-missing errors now name the actual root cause instead of a generic "failed to ..." string. ## Post-review hardening: scoping and consolidation Two structural fixes on top of the port, addressing drift introduced while iterating on review feedback: - **`next/` no longer inherits Go-parity config semantics.** The four viper-compat behaviors added during review (unconditional `[remotes.*]` project-id checks, the deprecated-provider WARN, the widened `env()` reference pattern, and comma-split coercion into array-typed fields) are now gated behind an opt-in `goViperCompat` flag on `LoadProjectConfigOptions` — default off restores the pre-review behavior for `next/`, `packages/stack`, and other non-parity consumers, following the same opt-in pattern as `tomlOnly`/`skipEnvLocal`. Only legacy-shell callers opt in. Regression tests pin the default-off behavior, including the `next start` config-load path that would otherwise have started hard-failing on a malformed `[remotes.*].project_id`. - **Go's `Config.Validate` now has exactly one TS home.** `legacy/shared/legacy-config-validate.ts` replaces the validation orchestration previously duplicated between `legacy-db-config.toml-read.ts` (db/migration family) and `legacy-local-config-values.ts` (status/stop); both callers build a normalized `LegacyConfigValidationInput` from their own pipelines and call the shared validator, and a cross-caller parity test feeds identical broken configs through both real pipelines asserting identical error strings. Consolidating surfaced and fixed three real divergences between the two copies: `db.major_version = 0` now emits Go's `Missing required field in config: db.major_version` (the db loader previously emitted a non-Go message), and the captcha provider-enum and email `content`-vs-`content_path` checks are now present in both paths. CLOSES CLI-1324 --- apps/cli/AGENTS.md | 20 + apps/cli/docs/go-cli-porting-status.md | 4 +- .../commands/config/push/push.handler.ts | 5 +- .../db/reset/reset.integration.test.ts | 12 +- .../functions/deploy/deploy.handler.ts | 1 + .../commands/functions/new/new.handler.ts | 4 +- .../commands/functions/serve/serve.handler.ts | 1 + .../functions/serve/serve.integration.test.ts | 14 +- .../gen/signing-key/signing-key.handler.ts | 2 +- .../commands/gen/types/types.handler.ts | 4 +- .../commands/secrets/set/set.handler.ts | 20 +- .../secrets/set/set.integration.test.ts | 33 + .../commands/seed/buckets/buckets.handler.ts | 1 + .../legacy/commands/status/SIDE_EFFECTS.md | 205 +- .../legacy/commands/status/status.command.ts | 71 +- .../status/status.command.unit.test.ts | 75 + .../legacy/commands/status/status.errors.ts | 58 + .../legacy/commands/status/status.handler.ts | 354 +++- .../status/status.integration.test.ts | 972 +++++++++- .../commands/status/status.live.test.ts | 54 + .../legacy/commands/status/status.pretty.ts | 276 +++ .../status/status.pretty.unit.test.ts | 267 +++ .../legacy/commands/status/status.values.ts | 495 +++++ .../status/status.values.unit.test.ts | 793 ++++++++ .../src/legacy/commands/stop/SIDE_EFFECTS.md | 119 +- .../src/legacy/commands/stop/stop.command.ts | 36 +- .../src/legacy/commands/stop/stop.errors.ts | 62 + .../src/legacy/commands/stop/stop.handler.ts | 417 +++- .../commands/stop/stop.integration.test.ts | 1133 ++++++++++- .../legacy/commands/stop/stop.live.test.ts | 78 + .../legacy/commands/storage/storage.frame.ts | 4 +- .../legacy/config/legacy-cli-config.layer.ts | 20 +- .../legacy-cli-config.layer.unit.test.ts | 37 + apps/cli/src/legacy/shared/legacy-api-url.ts | 26 + apps/cli/src/legacy/shared/legacy-colors.ts | 33 +- .../legacy/shared/legacy-colors.unit.test.ts | 50 + ...legacy-config-validate.parity.unit.test.ts | 266 +++ .../legacy/shared/legacy-config-validate.ts | 778 ++++++++ .../legacy-config-validate.unit.test.ts | 1034 ++++++++++ .../src/legacy/shared/legacy-container-cli.ts | 134 +- .../shared/legacy-container-cli.unit.test.ts | 129 +- .../shared/legacy-db-config.toml-read.ts | 1017 +++++----- .../legacy-db-config.toml-read.unit.test.ts | 22 + .../src/legacy/shared/legacy-docker-ids.ts | 77 +- .../shared/legacy-docker-ids.unit.test.ts | 78 +- .../legacy/shared/legacy-docker-lifecycle.ts | 259 +++ .../legacy-docker-lifecycle.unit.test.ts | 344 ++++ apps/cli/src/legacy/shared/legacy-go-jwt.ts | 197 ++ .../legacy/shared/legacy-go-jwt.unit.test.ts | 179 ++ apps/cli/src/legacy/shared/legacy-hostname.ts | 136 +- .../shared/legacy-hostname.unit.test.ts | 124 +- .../shared/legacy-local-config-values.ts | 1573 +++++++++++++++ .../legacy-local-config-values.unit.test.ts | 1716 +++++++++++++++++ .../shared/legacy-project-environment.ts | 141 ++ .../legacy-project-environment.unit.test.ts | 300 +++ .../src/legacy/shared/legacy-seed-buckets.ts | 4 +- .../shared/legacy-storage-credentials.ts | 19 +- .../src/legacy/shared/legacy-storage-url.ts | 61 + .../shared/legacy-storage-url.unit.test.ts | 47 + .../shared/legacy-workdir-validation.ts | 52 + .../functions/deploy/deploy.handler.ts | 1 + .../dev/functions-dev-config.unit.test.ts | 77 +- .../commands/start/start.integration.test.ts | 70 + .../src/shared/cli/hidden-flag.unit.test.ts | 10 +- apps/cli/src/shared/functions/deploy.ts | 6 +- apps/cli/src/shared/functions/serve.ts | 22 +- apps/docs/public/cli/config.schema.json | 652 +++---- packages/cli-test-helpers/src/parity.ts | 32 +- packages/config/src/auth/email.ts | 23 +- packages/config/src/auth/providers.ts | 13 +- packages/config/src/auth/sms.ts | 213 +- packages/config/src/base.ts | 29 +- packages/config/src/errors.ts | 12 + packages/config/src/index.ts | 3 + packages/config/src/io.ts | 355 +++- packages/config/src/io.unit.test.ts | 794 +++++++- packages/config/src/lib/env.ts | 128 +- packages/config/src/paths.ts | 35 +- packages/config/src/project.ts | 154 +- packages/config/src/project.unit.test.ts | 248 ++- 80 files changed, 15908 insertions(+), 1412 deletions(-) create mode 100644 apps/cli/src/legacy/commands/status/status.command.unit.test.ts create mode 100644 apps/cli/src/legacy/commands/status/status.errors.ts create mode 100644 apps/cli/src/legacy/commands/status/status.live.test.ts create mode 100644 apps/cli/src/legacy/commands/status/status.pretty.ts create mode 100644 apps/cli/src/legacy/commands/status/status.pretty.unit.test.ts create mode 100644 apps/cli/src/legacy/commands/status/status.values.ts create mode 100644 apps/cli/src/legacy/commands/status/status.values.unit.test.ts create mode 100644 apps/cli/src/legacy/commands/stop/stop.errors.ts create mode 100644 apps/cli/src/legacy/commands/stop/stop.live.test.ts create mode 100644 apps/cli/src/legacy/shared/legacy-api-url.ts create mode 100644 apps/cli/src/legacy/shared/legacy-colors.unit.test.ts create mode 100644 apps/cli/src/legacy/shared/legacy-config-validate.parity.unit.test.ts create mode 100644 apps/cli/src/legacy/shared/legacy-config-validate.ts create mode 100644 apps/cli/src/legacy/shared/legacy-config-validate.unit.test.ts create mode 100644 apps/cli/src/legacy/shared/legacy-docker-lifecycle.ts create mode 100644 apps/cli/src/legacy/shared/legacy-docker-lifecycle.unit.test.ts create mode 100644 apps/cli/src/legacy/shared/legacy-go-jwt.ts create mode 100644 apps/cli/src/legacy/shared/legacy-go-jwt.unit.test.ts create mode 100644 apps/cli/src/legacy/shared/legacy-local-config-values.ts create mode 100644 apps/cli/src/legacy/shared/legacy-local-config-values.unit.test.ts create mode 100644 apps/cli/src/legacy/shared/legacy-project-environment.ts create mode 100644 apps/cli/src/legacy/shared/legacy-project-environment.unit.test.ts create mode 100644 apps/cli/src/legacy/shared/legacy-workdir-validation.ts diff --git a/apps/cli/AGENTS.md b/apps/cli/AGENTS.md index 9989e1e33f..3d136879cd 100644 --- a/apps/cli/AGENTS.md +++ b/apps/cli/AGENTS.md @@ -237,6 +237,10 @@ Concrete examples worth watching for as more commands land: This rule is consistent with the repo-wide **Refactoring Policy** ("delete obsolete helpers, shims, and parallel code paths as part of the refactor") — it just makes the policy concrete for the legacy-port workflow. +### `Config.Validate` parity has one home + +Go's `Config.Validate` (`apps/cli-go/pkg/config/config.go:989-1190`) is ported exactly once: `src/legacy/shared/legacy-config-validate.ts` (`legacyValidateResolvedConfig`). Both the db/migration loader (`legacy-db-config.toml-read.ts`) and the status/stop resolver (`legacy-local-config-values.ts`) build a `LegacyConfigValidationInput` from their own pipelines and call it — do not add per-command reimplementations of these checks. When a Go validation branch or message changes, change it there. `legacy-config-validate.parity.unit.test.ts` feeds the same broken configs through both real pipelines and asserts identical error strings; extend it when adding a branch both callers share. + --- ## Legacy Port: Go CLI Output Parity @@ -403,6 +407,7 @@ Read https://www.effect.solutions/testing for Effect testing patterns. Note that - `*.unit.test.ts` belongs to the `unit` Vitest project and is the default for unit-style and other fast in-process tests. - `*.integration.test.ts` belongs to the `integration` project and is for in-process integration tests that exercise real handler or service behavior with layered dependency replacement. - `*.e2e.test.ts` belongs to the `e2e` Vitest project and is for black-box CLI subprocess tests. +- `*.live.test.ts` belongs to the `live` Vitest project and is for black-box CLI subprocess tests that run against a **real, running Supabase platform or local Docker stack** — see "Live tests" below. ### Testing policy @@ -417,6 +422,21 @@ Read https://www.effect.solutions/testing for Effect testing patterns. Note that - Keep `*.e2e.test.ts` focused on golden paths, CLI surface behavior, and subprocess correctness, not branch-by-branch coverage. - **Forbidden pattern (do not add):** spawning the CLI to assert that `--help` renders a flag. Help text is dynamic over flag wiring and is exercised by the integration test's flag parser. The two backups e2e files removed alongside this guidance update are the canonical example of what not to write. +### Live tests (`*.live.test.ts`) + +Live tests are black-box CLI subprocess tests — like `*.e2e.test.ts`, but run against a **real backend** instead of local fakes/mocks: either the real Management API (a full [supabox](https://github.com/supabase/supabox) platform stack) or a real local Docker dev stack (`supabase start`'s actual containers). They are the highest-fidelity, most expensive tier — reserved for the small set of behaviors that only a genuinely running backend can prove (auth round-trips, real Docker label filtering, real container lifecycle), not for anything an integration test can already cover with mocks. + +- **Where they run:** authored in this repo, but executed by the [`supabase/cli-e2e-ci`](https://github.com/supabase/cli-e2e-ci) harness, which builds this CLI, brings up a full supabox stack (and has a real Docker daemon, since that's how supabox itself runs), and invokes the `live` Vitest project (`nx run-many -t test:live`). They never run as part of the default unit/integration/e2e loop, and locally they no-op unless the live environment is configured (see below) — there is no need to stand up supabox yourself to develop other code. +- **Add one whenever you add or change a command whose correctness genuinely depends on a real backend** — a new Management API command, or a change to `start`/`stop`/`status`'s real Docker interaction. Colocate it with the command, same as `*.e2e.test.ts`: `src/legacy/commands//[/].live.test.ts`. +- **Gating:** every live suite must be wrapped in one of `tests/helpers/live.ts`'s `describe.skipIf` gates so the file is inert (skipped, not failed) outside the cli-e2e-ci runner: + - `describeLive` — runs whenever `SUPABASE_ACCESS_TOKEN` is set (the live env is configured at all). Reuse this even for commands that don't call the Management API themselves (e.g. `stop`/`status`) — it doubles as the "we're in the full cli-e2e-ci runner, which also has a real Docker daemon" signal, and there is no dedicated Docker-availability gate today. + - `describeLiveProject` — additionally requires a provisioned project (`SUPABASE_LIVE_PROJECT_REF`); use for project-scoped Management API commands (branches, functions, project-scoped db). + - `describeLiveDataPlane` — additionally requires the project's own Postgres instance to be `ACTIVE_HEALTHY`; use for commands that talk to the project's data plane (migration, db, storage). +- **Invocation:** use `runSupabaseLive(args, options?)` (wraps `runSupabase` with the `legacy` entrypoint and the live profile/timeout defaults) rather than calling `runSupabase` directly, so every live test picks up the same environment plumbing. +- **Local-dev-stack live tests** (`start`/`stop`/`status`, and anything else that manages real Docker containers rather than calling the Management API) follow the same file/gating convention but don't need `SUPABASE_PROFILE`/project-ref machinery. Pattern: `mkdtemp` a project dir, `runSupabaseLive(["init"], { cwd })` to generate a real Go-schema `config.toml`, `runSupabaseLive(["start", ...])` to bring up (a lightweight subset of) the real stack, exercise the command under test, then clean up in `afterEach` (best-effort `stop --no-backup` + `rm` the temp dir) so a failed assertion never leaks containers onto the CI runner. See `commands/stop/stop.live.test.ts` and `commands/status/status.live.test.ts` for the canonical example. +- **Keep the suite small and golden-path only** — same philosophy as `*.e2e.test.ts`, but even more so given the cost of a real backend. One or two scenarios per command is normal; branch-by-branch coverage belongs in `*.integration.test.ts`. +- Timeouts are generous by default (`testTimeout`/`hookTimeout: 300_000` for the whole `live` project) because real platform/Docker operations are slow — pass an explicit per-`test()` timeout when a scenario needs less (or, for a real local-stack `start`, close to the full budget). + --- ## Go CLI Parity Tracking diff --git a/apps/cli/docs/go-cli-porting-status.md b/apps/cli/docs/go-cli-porting-status.md index 125c1f795c..c1ac3b2c96 100644 --- a/apps/cli/docs/go-cli-porting-status.md +++ b/apps/cli/docs/go-cli-porting-status.md @@ -269,8 +269,8 @@ Legend: | `init` | `ported` | [`../src/legacy/commands/init/init.command.ts`](../src/legacy/commands/init/init.command.ts) | | `services` | `ported` | [`../src/legacy/commands/services/services.command.ts`](../src/legacy/commands/services/services.command.ts) | | `start` | `wrapped` | [`../src/legacy/commands/start/start.command.ts`](../src/legacy/commands/start/start.command.ts) | -| `stop` | `wrapped` | [`../src/legacy/commands/stop/stop.command.ts`](../src/legacy/commands/stop/stop.command.ts) | -| `status` | `wrapped` | [`../src/legacy/commands/status/status.command.ts`](../src/legacy/commands/status/status.command.ts) | +| `stop` | `ported` | [`../src/legacy/commands/stop/stop.command.ts`](../src/legacy/commands/stop/stop.command.ts) — native; talks directly to Docker/Podman via subprocess, replicating Go's label-filter and container-naming scheme | +| `status` | `ported` | [`../src/legacy/commands/status/status.command.ts`](../src/legacy/commands/status/status.command.ts) — native; talks directly to Docker/Podman via subprocess, replicating Go's label-filter and container-naming scheme | | `telemetry enable` | `ported` | [`../src/legacy/commands/telemetry/enable/enable.command.ts`](../src/legacy/commands/telemetry/enable/enable.command.ts) | | `telemetry disable` | `ported` | [`../src/legacy/commands/telemetry/disable/disable.command.ts`](../src/legacy/commands/telemetry/disable/disable.command.ts) | | `telemetry status` | `ported` | [`../src/legacy/commands/telemetry/status/status.command.ts`](../src/legacy/commands/telemetry/status/status.command.ts) | diff --git a/apps/cli/src/legacy/commands/config/push/push.handler.ts b/apps/cli/src/legacy/commands/config/push/push.handler.ts index ebd07baabc..60e1869c1e 100644 --- a/apps/cli/src/legacy/commands/config/push/push.handler.ts +++ b/apps/cli/src/legacy/commands/config/push/push.handler.ts @@ -116,7 +116,10 @@ export const legacyConfigPush = Effect.fn("legacy.config.push")(function* ( // Pass `ref` so a matching `[remotes.*]` block is merged over the base config // before decode (Go's `loadFromFile` with `Config.ProjectId` set). A duplicate // `project_id` across remotes surfaces Go's verbatim message. - const loaded = yield* loadProjectConfig(runtimeInfo.cwd, { projectRef: ref }).pipe( + const loaded = yield* loadProjectConfig(runtimeInfo.cwd, { + projectRef: ref, + goViperCompat: true, + }).pipe( Effect.catchTag( "ProjectConfigParseError", (cause) => diff --git a/apps/cli/src/legacy/commands/db/reset/reset.integration.test.ts b/apps/cli/src/legacy/commands/db/reset/reset.integration.test.ts index 1ddec7f3b3..853f933134 100644 --- a/apps/cli/src/legacy/commands/db/reset/reset.integration.test.ts +++ b/apps/cli/src/legacy/commands/db/reset/reset.integration.test.ts @@ -347,12 +347,14 @@ describe("legacy db reset", () => { }); it.live("finishes a local reset when bucket seeding hits a strict-loader-rejected config", () => { - // The bucket-seeding core re-loads config via the strict `@supabase/config` loader, - // which rejects some Go-valid configs (e.g. `[db.seed] enabled = "env(SEED_ENABLED)"`). - // The seam's Go recreate already validated + rebuilt the DB, so aborting here would - // leave the reset half-done — warn and skip buckets so reset finishes like Go. + // The bucket-seeding core re-loads config via the strict `@supabase/config` loader. + // `SEED_ENABLED=maybe` is invalid for both Go's `strconv.ParseBool` and the TS + // loader's coercion, so the reload fails during decode (unlike e.g. `1`/`true`, + // which both now accept). The seam's Go recreate already validated + rebuilt the + // DB, so aborting here would leave the reset half-done — warn and skip buckets so + // reset finishes like Go. const previous = process.env["SEED_ENABLED"]; - process.env["SEED_ENABLED"] = "1"; + process.env["SEED_ENABLED"] = "maybe"; const { layer, out, seam } = setup(tmp.current, { toml: 'project_id = "test"\n\n[db.seed]\nenabled = "env(SEED_ENABLED)"\n', args: ["db", "reset"], diff --git a/apps/cli/src/legacy/commands/functions/deploy/deploy.handler.ts b/apps/cli/src/legacy/commands/functions/deploy/deploy.handler.ts index ece00daaa3..f1f8e6c4c9 100644 --- a/apps/cli/src/legacy/commands/functions/deploy/deploy.handler.ts +++ b/apps/cli/src/legacy/commands/functions/deploy/deploy.handler.ts @@ -41,6 +41,7 @@ export const legacyFunctionsDeploy = Effect.fn("legacy.functions.deploy")(functi projectRoot: cliConfig.workdir, supabaseDir: join(cliConfig.workdir, "supabase"), dashboardUrl: legacyDashboardUrl(cliConfig.profile), + goViperCompat: true, yes, rawArgs, edgeRuntimeVersion, diff --git a/apps/cli/src/legacy/commands/functions/new/new.handler.ts b/apps/cli/src/legacy/commands/functions/new/new.handler.ts index b448c0307a..5080e4416b 100644 --- a/apps/cli/src/legacy/commands/functions/new/new.handler.ts +++ b/apps/cli/src/legacy/commands/functions/new/new.handler.ts @@ -89,7 +89,9 @@ const listExistingFunctionSlugs = Effect.fnUntraced(function* (workdir: string) }); const resolveTemplateInputs = Effect.fnUntraced(function* (workdir: string, slug: string) { - const loaded = yield* loadProjectConfig(workdir).pipe(Effect.orElseSucceed(() => null)); + const loaded = yield* loadProjectConfig(workdir, { goViperCompat: true }).pipe( + Effect.orElseSucceed(() => null), + ); const port = loaded?.config.api.port ?? DEFAULT_LOCAL_API_PORT; const publishableKey = loaded?.config.auth.publishable_key ?? defaultPublishableKey; return { diff --git a/apps/cli/src/legacy/commands/functions/serve/serve.handler.ts b/apps/cli/src/legacy/commands/functions/serve/serve.handler.ts index 0718862e64..86b51f387b 100644 --- a/apps/cli/src/legacy/commands/functions/serve/serve.handler.ts +++ b/apps/cli/src/legacy/commands/functions/serve/serve.handler.ts @@ -33,5 +33,6 @@ export const legacyFunctionsServe = Effect.fn("legacy.functions.serve")(function debug, networkId, projectIdOverride: cliConfig.projectId, + goViperCompat: true, }).pipe(Effect.ensuring(telemetryState.flush)); }); diff --git a/apps/cli/src/legacy/commands/functions/serve/serve.integration.test.ts b/apps/cli/src/legacy/commands/functions/serve/serve.integration.test.ts index 370af5aacf..8e3354fe2d 100644 --- a/apps/cli/src/legacy/commands/functions/serve/serve.integration.test.ts +++ b/apps/cli/src/legacy/commands/functions/serve/serve.integration.test.ts @@ -1695,7 +1695,7 @@ describe("legacy functions serve integration", () => { "verify_jwt = true", "", "[remotes.override]", - 'project_id = "override-project"', + 'project_id = "overrideprojectaaaaa"', "", "[remotes.override.functions.hello]", "verify_jwt = false", @@ -1710,7 +1710,7 @@ describe("legacy functions serve integration", () => { const { layer } = setupServe({ childSpawner, - projectId: Option.some("override-project"), + projectId: Option.some("overrideprojectaaaaa"), }); const error = yield* legacyFunctionsServe(baseFlags()).pipe( Effect.provide(layer), @@ -1724,20 +1724,20 @@ describe("legacy functions serve integration", () => { expect(deployMockState.volumeCalls).toEqual([ { - volumeName: "supabase_edge_runtime_override-project", - projectId: "override-project", + volumeName: "supabase_edge_runtime_overrideprojectaaaaa", + projectId: "overrideprojectaaaaa", }, ]); expect(deployMockState.networkCalls).toEqual([ { - networkMode: "supabase_network_override-project", - projectId: "override-project", + networkMode: "supabase_network_overrideprojectaaaaa", + projectId: "overrideprojectaaaaa", }, ]); expect(deployMockState.runCalls).toContainEqual( expect.objectContaining({ command: "docker", - args: ["container", "inspect", "supabase_db_override-project"], + args: ["container", "inspect", "supabase_db_overrideprojectaaaaa"], }), ); diff --git a/apps/cli/src/legacy/commands/gen/signing-key/signing-key.handler.ts b/apps/cli/src/legacy/commands/gen/signing-key/signing-key.handler.ts index 775a39140e..defbaad723 100644 --- a/apps/cli/src/legacy/commands/gen/signing-key/signing-key.handler.ts +++ b/apps/cli/src/legacy/commands/gen/signing-key/signing-key.handler.ts @@ -162,7 +162,7 @@ const generatePrivateKey = Effect.fnUntraced(function* (algorithm: SigningAlgori const loadSigningKeysConfig = Effect.fnUntraced(function* (cwd: string) { const path = yield* Path.Path; - const loaded = yield* loadProjectConfig(cwd).pipe( + const loaded = yield* loadProjectConfig(cwd, { goViperCompat: true }).pipe( Effect.catchTag("ProjectConfigParseError", (cause) => Effect.fail( new LegacyGenSigningKeyConfigParseError({ diff --git a/apps/cli/src/legacy/commands/gen/types/types.handler.ts b/apps/cli/src/legacy/commands/gen/types/types.handler.ts index 912f1bb8a0..87113c6a42 100644 --- a/apps/cli/src/legacy/commands/gen/types/types.handler.ts +++ b/apps/cli/src/legacy/commands/gen/types/types.handler.ts @@ -248,9 +248,9 @@ export const legacyGenTypes = Effect.fn("legacy.gen.types")(function* (flags: Le ); } - const loadConfig = () => loadProjectConfig(cliConfig.workdir); + const loadConfig = () => loadProjectConfig(cliConfig.workdir, { goViperCompat: true }); const loadConfigForRef = (projectRef: string) => - loadProjectConfig(cliConfig.workdir, { projectRef }); + loadProjectConfig(cliConfig.workdir, { projectRef, goViperCompat: true }); const schemasFromConfig = (apiSchemas: ReadonlyArray | undefined) => defaultSchemas(apiSchemas); diff --git a/apps/cli/src/legacy/commands/secrets/set/set.handler.ts b/apps/cli/src/legacy/commands/secrets/set/set.handler.ts index e7d9f72d0f..53a46ee15f 100644 --- a/apps/cli/src/legacy/commands/secrets/set/set.handler.ts +++ b/apps/cli/src/legacy/commands/secrets/set/set.handler.ts @@ -183,7 +183,13 @@ export const legacySecretsSet = Effect.fn("legacy.secrets.set")(function* ( // Without this, a schema-decode error on `--project-ref ` // would recover the *base* `[edge_runtime.secrets]` instead of the // explicitly selected remote's override. - const loadedConfig = yield* loadProjectConfig(runtimeInfo.cwd, { projectRef: ref }).pipe( + // `goViperCompat: true` opts into `applyRemoteOverride`'s duplicate- + // `project_id`/format checks (`packages/config/src/io.ts`) — required for + // the `DuplicateRemoteProjectIdError` catch below to ever fire. + const loadedConfig = yield* loadProjectConfig(runtimeInfo.cwd, { + projectRef: ref, + goViperCompat: true, + }).pipe( Effect.flatMap((loaded) => { if (loaded === null) { return Effect.succeed(null); @@ -265,6 +271,17 @@ export const legacySecretsSet = Effect.fn("legacy.secrets.set")(function* ( Effect.catchTag("DuplicateRemoteProjectIdError", (cause) => debugLogger.debug(cause.message).pipe(Effect.as(null)), ), + // A `[remotes.*]` block's `project_id` fails Go's ref-pattern check — + // raised from `Config.Validate` (`pkg/config/config.go:996-1001`), which + // runs inside the same `Config.Load()` call as the duplicate check above + // (`config.go:882`). Go's `flags.LoadConfig` swallows this the same + // non-fatal way (`internal/secrets/set/set.go:22-24`), so a malformed + // remote block must not abort an otherwise-valid `secrets set`. + // `cause.message` already matches Go's string verbatim (see + // `InvalidRemoteProjectIdError`'s field doc). + Effect.catchTag("InvalidRemoteProjectIdError", (cause) => + debugLogger.debug(cause.message).pipe(Effect.as(null)), + ), ); if (loadedConfig !== null) { const projectEnv = yield* loadProjectEnvironment({ @@ -276,6 +293,7 @@ export const legacySecretsSet = Effect.fn("legacy.secrets.set")(function* ( loadedConfig.edge_runtime, projectEnv, "edge_runtime", + { goViperCompat: true }, ); for (const [name, value] of Object.entries(resolved.secrets ?? {})) { // Go's `DecryptSecretHookFunc` (`pkg/config/secret.go:98`) never diff --git a/apps/cli/src/legacy/commands/secrets/set/set.integration.test.ts b/apps/cli/src/legacy/commands/secrets/set/set.integration.test.ts index 91f4fd18a8..4e6a3e4dfd 100644 --- a/apps/cli/src/legacy/commands/secrets/set/set.integration.test.ts +++ b/apps/cli/src/legacy/commands/secrets/set/set.integration.test.ts @@ -724,6 +724,39 @@ project_id = "dupe-project-id" }, ); + it.live( + "tolerates a [remotes.*] block with a malformed project_id and still sets CLI-arg secrets (Go parity)", + () => { + // Go's `flags.LoadConfig` swallows *any* `Load()` error non-fatally + // (`internal/secrets/set/set.go:22-24`), including the invalid-format + // error `Config.Validate` raises for every `[remotes.*].project_id` + // that doesn't match Go's ref pattern (`pkg/config/config.go:996-1001`), + // which runs inside the same `Config.Load()` call (`config.go:882`) as + // the duplicate check above. There is no parsed document to recover a + // subtree from, so config-sourced secrets are dropped entirely — only + // CLI-arg secrets survive. + writeConfig( + `[edge_runtime.secrets] +FROM_CONFIG = "config-value" + +[remotes.a] +project_id = "not-a-valid-ref" +`, + ); + const { layer, api, debugLogger } = setup(); + return Effect.gen(function* () { + yield* legacySecretsSet({ + projectRef: Option.none(), + envFile: Option.none(), + secrets: ["FOO=bar"], + }); + expect(parsePostBody(api.requests[0]?.body)).toEqual([{ name: "FOO", value: "bar" }]); + expect(debugLogger.messages).toHaveLength(1); + expect(debugLogger.messages[0]).toContain("Invalid config for remotes.a.project_id"); + }).pipe(Effect.provide(layer)); + }, + ); + it.live( "does not echo a literal secret value from config.toml into the debug log on a syntax error", () => { diff --git a/apps/cli/src/legacy/commands/seed/buckets/buckets.handler.ts b/apps/cli/src/legacy/commands/seed/buckets/buckets.handler.ts index 24b2543055..f5d04275b7 100644 --- a/apps/cli/src/legacy/commands/seed/buckets/buckets.handler.ts +++ b/apps/cli/src/legacy/commands/seed/buckets/buckets.handler.ts @@ -45,6 +45,7 @@ export const legacySeedBuckets = Effect.fn("legacy.seed.buckets")(function* ( ? yield* projectRefResolver.loadProjectRef(Option.none()) : ""; linkedRef = projectRef; + yield* legacySeedBucketsRun({ projectRef, emitSummary: true }); }).pipe( // Go's root `Execute` caches the linked project + fires org/project group diff --git a/apps/cli/src/legacy/commands/status/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/status/SIDE_EFFECTS.md index 089b2340c3..bc43278256 100644 --- a/apps/cli/src/legacy/commands/status/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/status/SIDE_EFFECTS.md @@ -2,15 +2,17 @@ ## Files Read -| Path | Format | When | -| -------------------------------- | ------ | ---------------------------------------- | -| `/supabase/config.toml` | TOML | always, to resolve project configuration | +| Path | Format | When | +| ------------------------------------------------------------------------------------------------------------------- | --------- | ----------------------------------------------------------------------------- | +| `/supabase/config.toml` | TOML | always, to resolve project configuration | +| `auth.signing_keys_path` (config-relative or absolute) | JSON | only when `auth.signing_keys_path` is set in config.toml | +| `api.tls.cert_path` / `api.tls.key_path` (unconditionally joined with `/supabase`, no absolute-path guard) | raw bytes | only when `api.enabled` and `api.tls.enabled`, and the respective path is set | ## Files Written -| Path | Format | When | -| ---- | ------ | ---- | -| — | — | — | +| Path | Format | When | +| ---------------------------- | ------ | ------ | +| `~/.supabase/telemetry.json` | JSON | always | ## API Routes @@ -18,67 +20,186 @@ | ------ | ---- | ---- | ------------ | ---------------------- | | — | — | — | — | — | +Neither this command nor any of its dependencies make a Management API call — everything is +resolved from local `config.toml` and the local Docker daemon. + ## Environment Variables -| Variable | Purpose | Required? | -| -------- | ------- | --------- | -| — | — | — | +| Variable | Purpose | Required? | +| -------------------------------- | ------------------------------------------------------- | ----------------------------------------------------------------------- | +| `SUPABASE_PROJECT_ID` | overrides the resolved local project id | no (falls back to config.toml `project_id` → workdir basename) | +| `SUPABASE_WORKDIR` | overrides the resolved project workdir | no (falls back to `--workdir` → walk-up search for `config.toml` → cwd) | +| `SUPABASE_SERVICES_HOSTNAME` | overrides the hostname used to build local service URLs | no (falls back to `DOCKER_HOST`'s tcp host → `127.0.0.1`) | +| `SUPABASE_AUTH_JWT_SECRET` | overrides `auth.jwt_secret` | no | +| `SUPABASE_AUTH_PUBLISHABLE_KEY` | overrides `auth.publishable_key` | no | +| `SUPABASE_AUTH_SECRET_KEY` | overrides `auth.secret_key` | no | +| `SUPABASE_AUTH_ANON_KEY` | overrides `auth.anon_key` | no | +| `SUPABASE_AUTH_SERVICE_ROLE_KEY` | overrides `auth.service_role_key` | no | + +The `SUPABASE_AUTH_*` vars mirror Go's Viper `AutomaticEnv` (`SetEnvPrefix("SUPABASE")` + +`.`→`_` key replacer, `pkg/config/config.go:529-535`) and take precedence over the corresponding +`config.toml` value, matching Viper's real precedence order. + +`docker` (or `podman` as a fallback) must be on `PATH`. ## Exit Codes -| Code | Condition | -| ---- | --------------------------------------------- | -| `0` | success — status displayed | -| `1` | malformed config | -| `1` | Docker daemon not running or connection error | +| Code | Condition | +| ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `0` | success — status displayed | +| `0` | **`--ignore-health-check` is set** — skips the health assertion below entirely, so an unhealthy/not-running db never fails the command | +| `1` | `supabase/config.toml` missing or malformed | +| `1` | a malformed `--override-name` entry | +| `1` | listing running containers failed (Docker daemon unreachable, etc.) | +| `1` | the db container inspect call failed (including "not found") — health assertion, skipped by `--ignore-health-check` above | +| `1` | the db container is present but not in the `running` state — health assertion, skipped by `--ignore-health-check` above | +| `1` | the db container is running but its Docker health check isn't `healthy` — health assertion, skipped by `--ignore-health-check` above | +| `1` | `auth.jwt_secret` is configured but shorter than 16 characters (Go's `Config.Validate` rejects this at config-load time) | +| `1` | `auth.signing_keys_path` is configured but the file is missing/malformed, or its first key's algorithm is not `RS256`/`ES256` | +| `1` | `api.enabled` and `api.tls.enabled` are true and only one of `api.tls.cert_path`/`key_path` is set (Go's `Config.Validate` rejects this at config-load time) | +| `1` | `api.enabled` and `api.tls.enabled` are true, both `cert_path` and `key_path` are set, but one of the files can't be read | + +## Telemetry Events Fired + +| Event | When | Notable properties / groups | +| ---------------------- | ------------------------------------------ | ----------------------------------- | +| `cli_command_executed` | post-run, success or failure (via wrapper) | `exit_code`, `duration_ms`, `flags` | ## Output ### `--output-format text` (Go CLI compatible) -Prints a table of service names, container IDs, images, and URLs. +Default (`-o` unset or `-o pretty`): a stderr banner, then 5 grouped rounded-border tables on +stdout. Empty rows (a value with nothing resolved) and entirely empty groups are skipped; a +blank line follows every group, rendered or not. ``` - supabase local development setup is running. - - API URL: http://127.0.0.1:54321 - GraphQL URL: http://127.0.0.1:54321/graphql/v1 - S3 Storage URL: http://127.0.0.1:54321/storage/v1/s3 - DB URL: postgresql://postgres:postgres@127.0.0.1:54322/postgres - Studio URL: http://127.0.0.1:54323 - Inbucket URL: http://127.0.0.1:54324 - JWT secret: super-secret-jwt-token-with-at-least-32-characters-long - anon key: ... -service_role key: ... - S3 Access Key: 625729a08b95bf1b7ff351a663f3a23c - S3 Secret Key: 850181e4652dd023b7a98c58ae0d2d34bd487ee0ead3abe0 - S3 Region: local +supabase local development setup is running. + +╭──────────────────────────────────────╮ +│ 🔧 Development Tools │ +├─────────┬────────────────────────────┤ +│ Studio │ http://127.0.0.1:54323 │ +│ Mailpit │ http://127.0.0.1:54324 │ +│ MCP │ http://127.0.0.1:54321/mcp │ +╰─────────┴────────────────────────────╯ + +╭──────────────────────────────────────────────────────╮ +│ 🌐 APIs │ +├────────────────┬─────────────────────────────────────┤ +│ Project URL │ http://127.0.0.1:54321 │ +│ REST │ http://127.0.0.1:54321/rest/v1 │ +│ GraphQL │ http://127.0.0.1:54321/graphql/v1 │ +│ Edge Functions │ http://127.0.0.1:54321/functions/v1 │ +╰────────────────┴─────────────────────────────────────╯ + +╭───────────────────────────────────────────────────────────────╮ +│ ⛁ Database │ +├─────┬─────────────────────────────────────────────────────────┤ +│ URL │ postgresql://postgres:postgres@127.0.0.1:54322/postgres │ +╰─────┴─────────────────────────────────────────────────────────╯ + +╭──────────────────────────────────────────────────────────────╮ +│ 🔑 Authentication Keys │ +├─────────────┬────────────────────────────────────────────────┤ +│ Publishable │ sb_publishable_ACJWlzQHlZjBrEguHvfOxg_3BJgxAaH │ +│ Secret │ sb_secret_N7UND0UgjKTVK-Uodkm0Hg_xSvEMPvz │ +╰─────────────┴────────────────────────────────────────────────╯ + +╭───────────────────────────────────────────────────────────────────────────────╮ +│ 📦 Storage (S3) │ +├────────────┬──────────────────────────────────────────────────────────────────┤ +│ URL │ http://127.0.0.1:54321/storage/v1/s3 │ +│ Access Key │ 625729a08b95bf1b7ff351a663f3a23c │ +│ Secret Key │ 850181e4652dd023b7a98c58ae0d2d34bd487ee0cc3254aed6eda37307425907 │ +│ Region │ local │ +╰────────────┴──────────────────────────────────────────────────────────────────╯ ``` -### `--output-format json` +Group table cells are colored on a TTY (Aqua for links, Yellow for keys, Green for labels, bold +headers); colors are stripped on non-TTY/piped output. + +`Stopped services: [ ...]` is written to stderr (Go slice format, e.g. +`[supabase_storage_test supabase_studio_test]`) whenever one of the 13 expected service +containers isn't in the running set. + +### `-o env` + +`KEY="VALUE"` lines (unquoted for integer-looking values), one per resolved field, sorted by +key — see `legacy-go-output.encoders.ts`'s `encodeEnv`. + +### `-o json` ```json { "API_URL": "http://127.0.0.1:54321", - "DB_URL": "postgresql://...", + "DB_URL": "postgresql://postgres:postgres@127.0.0.1:54322/postgres", "ANON_KEY": "...", "SERVICE_ROLE_KEY": "...", + "PUBLISHABLE_KEY": "...", + "SECRET_KEY": "...", "JWT_SECRET": "...", - "S3_ACCESS_KEY": "...", - "S3_SECRET_KEY": "...", - "S3_REGION": "local" + "S3_PROTOCOL_ACCESS_KEY_ID": "625729a08b95bf1b7ff351a663f3a23c", + "S3_PROTOCOL_ACCESS_KEY_SECRET": "...", + "S3_PROTOCOL_REGION": "local" } ``` -### `--output-format stream-json` +Top-level keys sorted alphabetically, 2-space indent, trailing newline (Go `encoding/json` +parity). Fields whose owning service is disabled or excluded are omitted entirely (not emitted +as `null`/`""`). + +### `-o yaml` / `-o toml` + +Same value set as `-o json`, encoded via `encodeYaml`/`encodeToml`. + +### `--output-format json` / `stream-json` (when `-o` is unset or `pretty`) -Not applicable. +Additive — no Go CLI equivalent. Emits the same resolved value map via +`output.success("", values)` / the NDJSON `result` event. ## Notes -- `--override-name` flag overrides specific variable names in env output. -- `-o env` output format uses KEY=VALUE pairs. -- `-o json` output format uses a JSON object. -- `-o pretty` (default) uses the human-readable table format. -- `--exclude` (hidden, repeatable) omits named containers from the output; matches Go's `cmd/status.go:39-40`. -- `--ignore-health-check` (hidden, boolean) exits `0` even when a service is unhealthy; matches Go's `cmd/status.go:41-42`. +- `-o`/`--output` (`env|pretty|json|toml|yaml`) takes priority over `--output-format` whenever + it is set, matching the Go-parity checklist's dual-output-flag rule. `-o pretty` (or `-o` + unset) falls through to `--output-format`'s text/json/stream-json handling. +- `--override-name api.url=NEXT_PUBLIC_SUPABASE_URL` remaps a single field's output KEY; the + value and group layout are unaffected. An unknown key or a malformed (non `KEY=VALUE`) entry + fails with `LegacyStatusOverrideParseError`. This only affects the `env`/`json`/`toml`/`yaml` + (`printStatus`) output path — matching Go, the pretty table (`-o pretty` or unset) always + renders with un-overridden names, since Go's `PrettyPrint` unmarshals a fresh, empty `EnvSet{}` + rather than reusing the CLI-supplied, override-populated `CustomName` (`status.go:236-243`). +- When neither `docker` nor `podman` can be spawned at all, the error message names the actual + root cause (e.g. "docker: command not found (podman also not found) — install Docker Desktop or + Podman and ensure it is on PATH") rather than a generic "failed to ..." string. +- `--exclude ` (hidden) omits a service from the value map when `value` matches either its + container id or its default Docker image short name (Go's `ShortContainerImageName`, e.g. + `storage-api` for the storage service, `edge-runtime` for edge functions) — the default image + is read from the same embedded Dockerfile manifest Go parses, so a version bump there is picked + up automatically without needing to read the `.temp/-version` pin file. +- `--ignore-health-check` (hidden) skips the db container health assertion entirely and always + exits `0`, matching Go's early-return in `Run()`. +- Default `auth.anon_key`/`auth.service_role_key`/`auth.jwt_secret` values are generated via a + Go-byte-exact HS256 signer (`legacy-go-jwt.ts`), not `@supabase/stack`'s `generateJwt` — the + latter uses a different issuer, expiry, and claim order that would not match what Go prints + for local dev keys. A configured `auth.jwt_secret` shorter than 16 characters fails the command + (`LegacyStatusInvalidConfigError`), matching Go's `Config.Validate` rejecting it at config-load + time before any command can render output. +- When `auth.signing_keys_path` is set and resolves to a non-empty JWK array, `anon_key`/ + `service_role_key` are instead signed asymmetrically (RS256/ES256) with the file's first key, + matching Go's `generateJWT` (`pkg/config/apikeys.go:76-113`) — a relative path resolves against + `/supabase`. This path is skipped entirely when `auth.anon_key`/`auth.service_role_key` + are explicitly configured. A missing/malformed file, or a first key with an algorithm other than + `RS256`/`ES256`, fails the command (`LegacyStatusInvalidConfigError`). +- `SUPABASE_AUTH_JWT_SECRET`/`SUPABASE_AUTH_PUBLISHABLE_KEY`/`SUPABASE_AUTH_SECRET_KEY`/ + `SUPABASE_AUTH_ANON_KEY`/`SUPABASE_AUTH_SERVICE_ROLE_KEY` override the corresponding + `config.toml` value at higher precedence, matching Go's Viper `AutomaticEnv` — an empty env var + is treated as unset. This is scoped to exactly the 5 auth fields `status` reads; it is not a + general `@supabase/config` port of Viper's `AutomaticEnv` (which applies to every config field). +- `db.password` and the `storage.s3_credentials` triple have no `@supabase/config` schema field; + Go hardcodes both (`"postgres"` and the S3 access key/secret/region seen above), reproduced + identically in `legacy-local-config-values.ts`. +- No e2e test is planned for this command: there is no Docker-daemon-free golden path, and the + e2e harness (`runSupabase()`) does not provision a real local stack. This is a scope reduction + relative to the Linear issue's "E2E compatibility test added" checkbox; see the port plan for + the full justification. diff --git a/apps/cli/src/legacy/commands/status/status.command.ts b/apps/cli/src/legacy/commands/status/status.command.ts index 201bce98f0..12be889ebb 100644 --- a/apps/cli/src/legacy/commands/status/status.command.ts +++ b/apps/cli/src/legacy/commands/status/status.command.ts @@ -1,19 +1,47 @@ +import { Layer } from "effect"; import { Command, Flag } from "effect/unstable/cli"; import type * as CliCommand from "effect/unstable/cli/Command"; + +import { legacyCliConfigLayer } from "../../config/legacy-cli-config.layer.ts"; +import { legacyDebugLoggerLayer } from "../../shared/legacy-debug-logger.layer.ts"; +import { LEGACY_RESOURCE_OUTPUT_FORMATS } from "../../shared/legacy-go-output-flag.ts"; +import { legacyParseStringSliceFlag } from "../../shared/legacy-string-slice-flag.ts"; +import { legacyTelemetryStateLayer } from "../../telemetry/legacy-telemetry-state.layer.ts"; +import { commandRuntimeLayer } from "../../../shared/runtime/command-runtime.layer.ts"; +import { withJsonErrorHandling } from "../../../shared/output/json-error-handling.ts"; +import { withLegacyCommandInstrumentation } from "../../telemetry/legacy-command-instrumentation.ts"; import { legacyStatus } from "./status.handler.ts"; -const config = { - overrideName: Flag.string("override-name").pipe( - Flag.atLeast(0), - Flag.withDescription("Override specific variable names."), - Flag.withDefault([] as ReadonlyArray), - ), - exclude: Flag.string("exclude").pipe( +/** + * Go registers both `--override-name` and `--exclude` as pflag `StringSliceVar` + * (`cmd/status.go:36-37`), which CSV-splits each occurrence and accumulates + * across repeats — `--override-name a=1,b=2` is two overrides, not one. Effect's + * `Flag.atLeast(0)` only handles repetition, so every occurrence needs the same + * `legacyParseStringSliceFlag` normalization already used for `sso`/`postgres-config`. + */ +function csvStringSliceFlag(name: string) { + return Flag.string(name).pipe( Flag.atLeast(0), - Flag.withDescription("Names of containers to omit from output."), + Flag.mapTryCatch( + (rawValues) => legacyParseStringSliceFlag(rawValues), + (err) => (err instanceof Error ? err.message : String(err)), + ), Flag.withDefault([] as ReadonlyArray), - Flag.withHidden, - ), + ); +} + +export const legacyStatusOverrideNameFlag = csvStringSliceFlag("override-name").pipe( + Flag.withDescription("Override specific variable names."), +); + +export const legacyStatusExcludeFlag = csvStringSliceFlag("exclude").pipe( + Flag.withDescription("Names of containers to omit from output."), + Flag.withHidden, +); + +const config = { + overrideName: legacyStatusOverrideNameFlag, + exclude: legacyStatusExcludeFlag, ignoreHealthCheck: Flag.boolean("ignore-health-check").pipe( Flag.withDescription("Ignore unhealthy services and exit 0"), Flag.withHidden, @@ -22,6 +50,18 @@ const config = { export type LegacyStatusFlags = CliCommand.Command.Config.Infer; +// `status` makes no Management API calls (Go's status needs no access token), so +// it deliberately avoids `legacyManagementApiRuntimeLayer` — mirrors `unlink`'s +// runtime shape. `legacyCliConfigLayer` is exposed at the top level directly +// (nothing else in this runtime needs to consume it internally). +const cliConfig = legacyCliConfigLayer.pipe(Layer.provide(legacyDebugLoggerLayer)); + +const legacyStatusRuntimeLayer = Layer.mergeAll( + cliConfig, + legacyTelemetryStateLayer, + commandRuntimeLayer(["status"]), +); + export const legacyStatusCommand = Command.make("status", config).pipe( Command.withDescription("Show status of local Supabase containers."), Command.withShortDescription("Show status of local Supabase containers"), @@ -35,5 +75,14 @@ export const legacyStatusCommand = Command.make("status", config).pipe( description: "Output status as JSON", }, ]), - Command.withHandler((flags) => legacyStatus(flags)), + Command.withHandler((flags) => + legacyStatus(flags).pipe( + withLegacyCommandInstrumentation({ + flags, + outputFormats: LEGACY_RESOURCE_OUTPUT_FORMATS, + }), + withJsonErrorHandling, + ), + ), + Command.provide(legacyStatusRuntimeLayer), ); diff --git a/apps/cli/src/legacy/commands/status/status.command.unit.test.ts b/apps/cli/src/legacy/commands/status/status.command.unit.test.ts new file mode 100644 index 0000000000..257f805382 --- /dev/null +++ b/apps/cli/src/legacy/commands/status/status.command.unit.test.ts @@ -0,0 +1,75 @@ +import { BunServices } from "@effect/platform-bun"; +import { Effect, Exit } from "effect"; +import { describe, expect, test } from "vitest"; +import { legacyStatusExcludeFlag, legacyStatusOverrideNameFlag } from "./status.command.ts"; + +describe("legacy status --override-name flag (pflag StringSlice parity)", () => { + test("splits a comma-separated value into multiple overrides", async () => { + const [, overrideName] = await Effect.runPromise( + legacyStatusOverrideNameFlag + .parse({ + flags: { "override-name": ["api.url=FOO,db.url=BAR"] }, + arguments: [], + }) + .pipe(Effect.provide(BunServices.layer)), + ); + + expect(overrideName).toEqual(["api.url=FOO", "db.url=BAR"]); + }); + + test("accumulates repeated occurrences, each CSV-split", async () => { + const [, overrideName] = await Effect.runPromise( + legacyStatusOverrideNameFlag + .parse({ + flags: { "override-name": ["api.url=FOO,db.url=BAR", "studio.url=BAZ"] }, + arguments: [], + }) + .pipe(Effect.provide(BunServices.layer)), + ); + + expect(overrideName).toEqual(["api.url=FOO", "db.url=BAR", "studio.url=BAZ"]); + }); + + test("defaults to an empty array when unset", async () => { + const [, overrideName] = await Effect.runPromise( + legacyStatusOverrideNameFlag + .parse({ flags: {}, arguments: [] }) + .pipe(Effect.provide(BunServices.layer)), + ); + + expect(overrideName).toEqual([]); + }); + + test("rejects malformed CSV (unterminated quote)", async () => { + const exit = await Effect.runPromise( + legacyStatusOverrideNameFlag + .parse({ flags: { "override-name": ['"api.url=FOO'] }, arguments: [] }) + .pipe(Effect.provide(BunServices.layer)) + .pipe(Effect.exit), + ); + + expect(Exit.isFailure(exit)).toBe(true); + }); +}); + +describe("legacy status --exclude flag (pflag StringSlice parity)", () => { + test("splits a comma-separated value into multiple exclusions", async () => { + const [, exclude] = await Effect.runPromise( + legacyStatusExcludeFlag + .parse({ flags: { exclude: ["kong,auth"] }, arguments: [] }) + .pipe(Effect.provide(BunServices.layer)), + ); + + expect(exclude).toEqual(["kong", "auth"]); + }); + + test("defaults to an empty array when unset", async () => { + const [, exclude] = await Effect.runPromise( + legacyStatusExcludeFlag + .parse({ flags: {}, arguments: [] }) + .pipe(Effect.provide(BunServices.layer)), + ); + + expect(exclude).toEqual([]); + }); +}); diff --git a/apps/cli/src/legacy/commands/status/status.errors.ts b/apps/cli/src/legacy/commands/status/status.errors.ts new file mode 100644 index 0000000000..9e72e14fbb --- /dev/null +++ b/apps/cli/src/legacy/commands/status/status.errors.ts @@ -0,0 +1,58 @@ +import { Data } from "effect"; + +/** + * An explicit `--workdir`/`SUPABASE_WORKDIR` path doesn't exist or isn't a + * directory. Mirrors Go's `ChangeWorkDir` (`apps/cli-go/internal/utils/misc.go: + * 231-250`), which unconditionally `os.Chdir(workdir)`s in `PersistentPreRunE` + * (`apps/cli-go/cmd/root.go:93-105`) — before `status`'s own `PreRunE` + * (override-name parsing) or `RunE`, so a bad explicit workdir must fail here + * first, before config load or any Docker access. + */ +export class LegacyStatusWorkdirError extends Data.TaggedError("LegacyStatusWorkdirError")<{ + readonly message: string; +}> {} + +/** `loadProjectConfig` rejected `supabase/config.toml` (malformed TOML/JSON). */ +export class LegacyStatusConfigLoadError extends Data.TaggedError("LegacyStatusConfigLoadError")<{ + readonly message: string; +}> {} + +/** A `--override-name KEY=VALUE` entry did not parse, mirroring `env.EnvironToEnvSet`. */ +export class LegacyStatusOverrideParseError extends Data.TaggedError( + "LegacyStatusOverrideParseError", +)<{ + readonly message: string; +}> {} + +/** Inspecting the db container failed for a reason other than "not found". */ +export class LegacyStatusDbInspectError extends Data.TaggedError("LegacyStatusDbInspectError")<{ + readonly message: string; +}> {} + +/** The db container is absent or present but not in the `running` state. */ +export class LegacyStatusDbNotRunningError extends Data.TaggedError( + "LegacyStatusDbNotRunningError", +)<{ + readonly message: string; +}> {} + +/** The db container is running but its Docker health check is not `healthy`. */ +export class LegacyStatusDbNotReadyError extends Data.TaggedError("LegacyStatusDbNotReadyError")<{ + readonly message: string; +}> {} + +/** Listing running containers by label failed. */ +export class LegacyStatusListError extends Data.TaggedError("LegacyStatusListError")<{ + readonly message: string; +}> {} + +/** + * `config.toml` resolved to a value `Config.Validate` would reject before status + * ever renders — e.g. an `auth.jwt_secret` shorter than 16 characters + * (`pkg/config/apikeys.go:45-47`). + */ +export class LegacyStatusInvalidConfigError extends Data.TaggedError( + "LegacyStatusInvalidConfigError", +)<{ + readonly message: string; +}> {} diff --git a/apps/cli/src/legacy/commands/status/status.handler.ts b/apps/cli/src/legacy/commands/status/status.handler.ts index 566427260c..7e984a0f93 100644 --- a/apps/cli/src/legacy/commands/status/status.handler.ts +++ b/apps/cli/src/legacy/commands/status/status.handler.ts @@ -1,12 +1,350 @@ -import { Effect } from "effect"; -import { LegacyGoProxy } from "../../../shared/legacy/go-proxy.service.ts"; +import { loadProjectConfig, loadProjectEnvironment, ProjectConfigSchema } from "@supabase/config"; +import { ChildProcessSpawner } from "effect/unstable/process"; +import { Effect, FileSystem, Option, Schema } from "effect"; + +import { LegacyCliConfig } from "../../config/legacy-cli-config.service.ts"; +import { LegacyTelemetryState } from "../../telemetry/legacy-telemetry-state.service.ts"; +import { LegacyOutputFlag } from "../../../shared/legacy/global-flags.ts"; +import { Output } from "../../../shared/output/output.service.ts"; +import { legacyAqua } from "../../shared/legacy-colors.ts"; +import { + legacyCliProjectFilterValue, + legacyResolveLocalProjectId, + legacySanitizeProjectId, + legacyServiceContainerIds, + localDbContainerId, +} from "../../shared/legacy-docker-ids.ts"; +import { + legacyInspectContainerState, + legacyListContainersByLabel, +} from "../../shared/legacy-docker-lifecycle.ts"; +import { + encodeEnv, + encodeGoJson, + encodeToml, + encodeYaml, +} from "../../shared/legacy-go-output.encoders.ts"; +import { legacyGetHostname } from "../../shared/legacy-hostname.ts"; +import { legacyResolveProjectEnvironmentValues } from "../../shared/legacy-project-environment.ts"; +import { legacyValidateWorkdirIsDirectory } from "../../shared/legacy-workdir-validation.ts"; import type { LegacyStatusFlags } from "./status.command.ts"; +import { + LegacyStatusConfigLoadError, + LegacyStatusDbInspectError, + LegacyStatusDbNotReadyError, + LegacyStatusDbNotRunningError, + LegacyStatusInvalidConfigError, + LegacyStatusListError, + LegacyStatusOverrideParseError, + LegacyStatusWorkdirError, +} from "./status.errors.ts"; +import { legacyRenderStatusPretty } from "./status.pretty.ts"; +import { + LEGACY_STATUS_FIELDS, + legacyGateStatusState, + legacyResolveStatusLocalState, + legacyStatusContainerIds, + legacyStatusValuesFromState, +} from "./status.values.ts"; + +/** + * Parses `--override-name api.url=NEXT_PUBLIC_SUPABASE_URL` entries into a + * `fieldKey -> outputName` map, mirroring Go's `env.EnvironToEnvSet` + + * `env.Unmarshal` (`cmd/status.go:21-27`): each entry must be a `KEY=VALUE` + * pair. `env.EnvironToEnvSet` only validates that shape (`go-env`'s + * `ErrInvalidEnviron`); the Netflix `go-env` library's `Unmarshal` then walks + * `CustomName`'s own struct fields and looks up each field's tag in the + * resulting map — it never checks the map for leftover/unmatched keys, so an + * entry whose `KEY` isn't one of the 18 known `CustomName` field keys is + * silently ignored, not an error (verified against `go-env@v0.1.2`'s + * `env.go`/`transform.go`). + */ +function parseOverrides( + entries: ReadonlyArray, +): Effect.Effect, LegacyStatusOverrideParseError> { + const knownKeys = new Set(LEGACY_STATUS_FIELDS.map((field) => field.fieldKey)); + const overrides = new Map(); + for (const entry of entries) { + const separatorIndex = entry.indexOf("="); + if (separatorIndex <= 0) { + return Effect.fail( + new LegacyStatusOverrideParseError({ + message: `invalid override-name entry, expected KEY=VALUE: ${entry}`, + }), + ); + } + const key = entry.slice(0, separatorIndex); + const value = entry.slice(separatorIndex + 1); + if (!knownKeys.has(key)) { + continue; + } + overrides.set(key, value); + } + return Effect.succeed(overrides); +} + +/** Go's `fmt.Fprintln(os.Stderr, "Stopped services:", stopped)` slice format. */ +function formatGoStringSlice(items: ReadonlyArray): string { + return `[${items.join(" ")}]`; +} export const legacyStatus = Effect.fn("legacy.status")(function* (flags: LegacyStatusFlags) { - const proxy = yield* LegacyGoProxy; - const args: string[] = ["status"]; - for (const override of flags.overrideName) args.push("--override-name", override); - for (const name of flags.exclude) args.push("--exclude", name); - if (flags.ignoreHealthCheck) args.push("--ignore-health-check"); - yield* proxy.exec(args); + const output = yield* Output; + const goOutputFlag = yield* LegacyOutputFlag; + const cliConfig = yield* LegacyCliConfig; + const telemetryState = yield* LegacyTelemetryState; + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; + const fs = yield* FileSystem.FileSystem; + + yield* Effect.gen(function* () { + // 0. Go's `ChangeWorkDir` (`apps/cli-go/internal/utils/misc.go:231-250`) + // unconditionally `os.Chdir`s the resolved `--workdir`/`SUPABASE_WORKDIR` + // in `PersistentPreRunE` (`cmd/root.go:93-105`) — before `status`'s own + // `PreRunE` (override-name parsing) or `RunE`. A missing or non-directory + // path fails immediately, so this must win over every later error. + yield* legacyValidateWorkdirIsDirectory(cliConfig.workdir, fs).pipe( + Effect.mapError((error) => new LegacyStatusWorkdirError({ message: error.message })), + ); + + // 1. `--override-name KEY=VALUE` parsing — mirroring Go's Cobra wiring, + // where override validation runs in `PreRunE` (`cmd/status.go:21-27`) and + // Cobra's execute loop returns as soon as `PreRunE` errors, never calling + // `RunE` (`spf13/cobra@v1.10.2/command.go:999-1015`). So a malformed + // `--override-name` entry fails before `status.Run` ever loads config or + // touches Docker (`internal/status/status.go:101-116`) — it must win over + // a config-load error or a Docker/DB health-check error, not be masked by + // either. `overrides` itself is only consumed much later, by + // `legacyStatusValuesFromState` below. + const overrides = yield* parseOverrides(flags.overrideName); + + // 2. `status` always needs config, unlike `stop` (status.go:99-103). An + // ABSENT config.toml is not a hard failure in Go: `flags.LoadConfig` -> + // `Config.Load` -> `loadFromFile` -> `mergeFileConfig` treats a missing + // file as a no-op (`os.ErrNotExist` -> nil, pkg/config/config.go:655-656) + // and proceeds with template defaults (`mergeDefaultValues`, + // pkg/config/config.go:639-648). Only a MALFORMED file is a hard error. + // Mirror that by decoding an empty document through the schema for its + // defaults (matching `packages/config/src/functions-manifest.ts`'s + // `decodeProjectConfig({})` pattern) instead of failing. + // `search: false` on both loaders below: `cliConfig.workdir` already IS + // Go's fully-resolved chdir target (`legacy-cli-config.layer.ts`'s + // `resolveWorkdir` mirrors `ChangeWorkDir`'s explicit-exact-vs-default- + // searched resolution, `apps/cli-go/internal/utils/misc.go:231-247`), so + // letting `@supabase/config`'s `findProjectPaths` climb ancestors again on + // top of that would let an unrelated ancestor project's config.toml win + // when `--workdir`/`SUPABASE_WORKDIR` points at a subdirectory with no + // `supabase/config.toml` of its own — Go never searches past the exact + // (explicit or defaulted) workdir (`NewPathBuilder`, `pkg/config/utils.go: + // 43-48`). + const projectEnv = yield* loadProjectEnvironment({ + cwd: cliConfig.workdir, + baseEnv: process.env, + search: false, + // Go's `loadDefaultEnv` (`apps/cli-go/pkg/config/config.go:1243-1250`) + // omits `.env.local` from its candidate list whenever + // `SUPABASE_ENV=test` — a malformed or intentionally non-test + // `supabase/.env.local` is then invisible to Go and must not fail + // config loading here either. `legacyResolveProjectEnvironmentValues` + // below already applies this same gate for the project-root pass (see + // its `candidateDotenvFilenames`); this mirrors it for the + // `supabase/`-dir pass `loadProjectEnvironment` itself performs. + skipEnvLocal: (process.env["SUPABASE_ENV"] || "development") === "test", + }).pipe( + Effect.mapError( + (cause) => + new LegacyStatusConfigLoadError({ message: `failed to read config: ${String(cause)}` }), + ), + ); + + // `legacyResolveProjectEnvironmentValues` fills the gap between + // `loadProjectEnvironment` (supabase/.env(.local) + ambient only) and Go's + // `loadNestedEnv`, which also loads project-root and `SUPABASE_ENV`-selected + // dotenv files (`pkg/config/config.go:1169-1207`) — see its doc comment for + // the full precedence chain. Resolved BEFORE `loadProjectConfig` decodes + // config.toml (not after) because Go's `Config.Load` runs `loadNestedEnv` + // before `LoadEnvHook` decodes `env(...)` references (`config.go:735-738`); + // an `env(...)` value sourced only from a project-root/`SUPABASE_ENV`- + // selected file must already be visible to the decoder, not just to the + // `SUPABASE_PROJECT_ID`/`SUPABASE_AUTH_*` overrides read further below. + // A malformed extra dotenv file throws here (see `readDotEnvFile`), + // matching Go's `loadNestedEnv` propagating `godotenv`'s parse error + // instead of silently skipping the bad line. `workdir` is passed through so + // dotenv files under `/supabase`/`workdir` are still discovered + // even when `projectEnv` is `null` (no config.toml there) — Go's own + // `loadNestedEnv` runs unconditionally, before `config.toml` is ever + // opened (`pkg/config/config.go:786-793`). + const projectEnvValues = yield* Effect.try({ + try: () => legacyResolveProjectEnvironmentValues(projectEnv, cliConfig.workdir), + catch: (cause) => + new LegacyStatusConfigLoadError({ message: `failed to read config: ${String(cause)}` }), + }); + + const loaded = yield* loadProjectConfig(cliConfig.workdir, { + projectEnv: projectEnv !== null ? { ...projectEnv, values: projectEnvValues } : undefined, + search: false, + // Go's `NewPathBuilder`/`Config.Load` (`pkg/config/utils.go:43-48`) only + // ever resolves `supabase/config.toml` — it has no concept of a JSON + // project config file. Without this, a workdir with a stray + // `config.json` would make `loadProjectConfig` prefer it over + // `config.toml`, reporting ports/keys for a config Go never reads. + tomlOnly: true, + goViperCompat: true, + }).pipe( + Effect.mapError( + (cause) => + new LegacyStatusConfigLoadError({ message: `failed to read config: ${String(cause)}` }), + ), + ); + const config = loaded?.config ?? Schema.decodeUnknownSync(ProjectConfigSchema)({}); + + // 3. Resolve + VALIDATE config-derived state before any Docker call — + // matching Go's `flags.LoadConfig` (config load + `Validate`, + // `internal/utils/flags/config_path.go:12` -> `pkg/config/config.go:882`), + // which runs entirely before `assertContainerHealthy`/container listing + // (`internal/status/status.go:101-116`). `legacyResolveStatusLocalState` + // can throw `LegacyInvalidJwtSecretError` (a short `auth.jwt_secret`), + // `LegacyInvalidPortEnvOverrideError`/`LegacyInvalidBoolEnvOverrideError` + // (a malformed `SUPABASE_*_PORT`/`SUPABASE_*_ENABLED` override), or a + // signing-keys-file read/parse error — all of these must fail here, not + // be masked by a Docker/DB error when the local stack happens to be + // unavailable. `hostname` has no Docker dependency either, so it's + // resolved here rather than later. + const hostname = legacyGetHostname(); + const localState = yield* Effect.try({ + try: () => + legacyResolveStatusLocalState( + config, + hostname, + cliConfig.workdir, + projectEnvValues, + loaded?.document, + ), + catch: (cause) => + new LegacyStatusInvalidConfigError({ + message: cause instanceof Error ? cause.message : String(cause), + }), + }); + + // 4. status has no --project-id flag; resolution is always env → toml → + // workdir basename, then sanitized to match the singleton Go's + // `Config.Validate` produces once at config-load time + // (`pkg/config/config.go:938-944`) — every reader, including the Docker + // LABEL `start` writes (`internal/utils/docker.go:375`), sees that same + // sanitized string, so `status` must filter on it too (see + // `legacyCliProjectFilterValue`'s doc comment). + const projectId = legacySanitizeProjectId( + legacyResolveLocalProjectId( + projectEnvValues["SUPABASE_PROJECT_ID"] ?? process.env["SUPABASE_PROJECT_ID"], + config.project_id, + cliConfig.workdir, + ), + ); + const dbContainerId = localDbContainerId(projectId); + + // 5. Health check, skipped entirely with --ignore-health-check (status.go:104-108). + // Go's `assertContainerHealthy` never special-cases "not found" — an absent + // container fails `ContainerInspect` itself, which surfaces as the generic + // inspect error (status.go:147-150), not the "not running" branch (which + // only applies to a present-but-stopped container, status.go:150-151). + // `legacyInspectContainerState` mirrors that: a missing container is just + // another non-zero exit, mapped below with the real Docker stderr text. + if (!flags.ignoreHealthCheck) { + const state = yield* legacyInspectContainerState(spawner, dbContainerId).pipe( + Effect.mapError((cause) => new LegacyStatusDbInspectError({ message: cause.message })), + ); + if (!state.running) { + return yield* Effect.fail( + new LegacyStatusDbNotRunningError({ + message: `${dbContainerId} container is not running: ${state.status}`, + }), + ); + } + if (state.health !== undefined && state.health !== "healthy") { + return yield* Effect.fail( + new LegacyStatusDbNotReadyError({ + message: `${dbContainerId} container is not ready: ${state.health}`, + }), + ); + } + } + + // 6. List running containers, diff against the 13 expected service ids + // (status.go:125-145), and report any that are stopped. + const filterValue = legacyCliProjectFilterValue(projectId); + const runningNames = yield* legacyListContainersByLabel(spawner, { + projectIdFilter: filterValue, + all: false, + format: "names", + }).pipe(Effect.mapError((cause) => new LegacyStatusListError({ message: cause.message }))); + const runningSet = new Set(runningNames); + const serviceIds = legacyServiceContainerIds(projectId); + const stopped = serviceIds.filter((id) => !runningSet.has(id)); + if (stopped.length > 0) { + yield* output.raw(`Stopped services: ${formatGoStringSlice(stopped)}\n`, "stderr"); + } + + // 7. Merge health-derived exclusions with the user's --exclude flag. + const excluded = [...stopped, ...flags.exclude]; + + // 8. Apply the exclude-based gating on top of the already-validated + // `localState` (Go's `toValues()` exclude filtering, `status.go:55-61`). + // Pure/non-throwing — see `legacyGateStatusState`'s doc comment. Reused + // for both the real and pretty-mode (empty-override) value maps below, + // matching this handler's pre-split behavior. + const containerIds = legacyStatusContainerIds(projectId); + const state = legacyGateStatusState(localState, containerIds, excluded); + const { values } = legacyStatusValuesFromState(state, overrides); + + // Go's `PrettyPrint` (`status.go:236-243`) unmarshals a FRESH, empty + // `EnvSet{}` into a brand-new `CustomName{}` rather than reusing the + // CLI-supplied, override-populated `names` — `--override-name` only ever + // affects `printStatus`'s env/json/toml/yaml path, never the pretty table. + // Remap names from the already-resolved `state` (empty override map) so the + // rendered table matches Go exactly without leaking `--override-name` into + // pretty-mode output, and without a second (throwing) state resolution. + const renderPretty = Effect.fnUntraced(function* () { + yield* output.raw( + `${legacyAqua("supabase")} local development setup is running.\n\n`, + "stderr", + ); + const pretty = legacyStatusValuesFromState(state, new Map()); + yield* output.raw(legacyRenderStatusPretty(pretty.values, pretty.names)); + }); + + // 9. Output branching: Go's -o (env|json|toml|yaml|pretty) is a complete + // format choice and takes priority over --output-format (root.ts:119-121, + // matching functions/list's list.handler.ts:115-118) — only an ABSENT -o + // defers to --output-format for json/stream-json. + const goFmt = Option.getOrUndefined(goOutputFlag); + + if (goFmt === "env") { + yield* output.raw(encodeEnv(values) + "\n"); + return; + } + if (goFmt === "json") { + yield* output.raw(encodeGoJson(values)); + return; + } + if (goFmt === "toml") { + yield* output.raw(encodeToml(values) + "\n"); + return; + } + if (goFmt === "yaml") { + yield* output.raw(encodeYaml(values)); + return; + } + if (goFmt === "pretty") { + yield* renderPretty(); + return; + } + + // goFmt is undefined — defer to TS --output-format for json/stream-json, + // otherwise render the grouped rounded-table (Go's `-o pretty` default). + if (output.format === "json" || output.format === "stream-json") { + yield* output.success("", values); + return; + } + + yield* renderPretty(); + }).pipe(Effect.ensuring(telemetryState.flush)); }); diff --git a/apps/cli/src/legacy/commands/status/status.integration.test.ts b/apps/cli/src/legacy/commands/status/status.integration.test.ts index 1c87049978..d1988d5569 100644 --- a/apps/cli/src/legacy/commands/status/status.integration.test.ts +++ b/apps/cli/src/legacy/commands/status/status.integration.test.ts @@ -1,58 +1,958 @@ +import { generateKeyPairSync } from "node:crypto"; +import { mkdirSync, writeFileSync } from "node:fs"; +import { basename, join } from "node:path"; + +import { BunServices } from "@effect/platform-bun"; import { describe, expect, it } from "@effect/vitest"; -import { Effect, Layer } from "effect"; -import { LegacyGoProxy } from "../../../shared/legacy/go-proxy.service.ts"; -import { legacyStatus } from "./status.handler.ts"; +import { Deferred, Effect, Exit, Layer, Option, PlatformError, Sink, Stream } from "effect"; +import { ChildProcessSpawner } from "effect/unstable/process"; +import { afterEach, vi } from "vitest"; + +import { mockOutput } from "../../../../tests/helpers/mocks.ts"; +import { + mockLegacyCliConfig, + mockLegacyTelemetryStateTracked, + useLegacyTempWorkdir, +} from "../../../../tests/helpers/legacy-mocks.ts"; +import { LegacyOutputFlag } from "../../../shared/legacy/global-flags.ts"; +import { legacyServiceContainerIds, localDbContainerId } from "../../shared/legacy-docker-ids.ts"; import type { LegacyStatusFlags } from "./status.command.ts"; +import { legacyStatus } from "./status.handler.ts"; + +const tempRoot = useLegacyTempWorkdir("supabase-status-int-"); + +afterEach(() => { + delete process.env["SUPABASE_AUTH_JWT_SECRET"]; +}); + +function flags(overrides: Partial = {}): LegacyStatusFlags { + return { + overrideName: [], + exclude: [], + ignoreHealthCheck: false, + ...overrides, + }; +} + +function writeConfig(workdir: string, contents = 'project_id = "demo"\n') { + const supabaseDir = join(workdir, "supabase"); + mkdirSync(supabaseDir, { recursive: true }); + writeFileSync(join(supabaseDir, "config.toml"), contents); +} + +interface SpawnRecord { + readonly command: string; + readonly args: ReadonlyArray; +} + +type RouteResult = { + readonly exitCode?: number; + readonly stdout?: ReadonlyArray; + readonly stderr?: ReadonlyArray; +}; + +/** Same routing-by-argv mock spawner shape as `stop.integration.test.ts`. */ +function mockRoutedContainerCliSpawner( + route: (args: ReadonlyArray) => RouteResult, + opts: { + readonly dockerMissing?: boolean; + readonly failSpawnFor?: (args: ReadonlyArray) => boolean; + } = {}, +) { + const spawned: Array = []; + + const layer = Layer.succeed( + ChildProcessSpawner.ChildProcessSpawner, + ChildProcessSpawner.make((command) => + Effect.gen(function* () { + const cmd = command._tag === "StandardCommand" ? command.command : ""; + const args = command._tag === "StandardCommand" ? command.args : []; + spawned.push({ command: cmd, args }); + + if (opts.dockerMissing === true && cmd === "docker") { + return yield* Effect.fail( + PlatformError.systemError({ + _tag: "NotFound", + module: "ChildProcess", + method: "spawn", + description: "docker not found", + }), + ); + } -function setupLegacyStatus() { - const calls: Array> = []; - const layer = Layer.succeed(LegacyGoProxy, { - exec: (args) => - Effect.sync(() => { - calls.push(args); + if (opts.failSpawnFor?.(args) === true) { + return yield* Effect.fail( + PlatformError.systemError({ + _tag: "NotFound", + module: "ChildProcess", + method: "spawn", + description: "spawn failed", + }), + ); + } + + const encoder = new TextEncoder(); + const result = route(args); + const exitDeferred = yield* Deferred.make(); + yield* Effect.forkDetach( + Effect.gen(function* () { + yield* Effect.sleep("5 millis"); + yield* Deferred.succeed( + exitDeferred, + ChildProcessSpawner.ExitCode(result.exitCode ?? 0), + ); + }), + ); + const stdoutBytes = (result.stdout ?? []).map((line) => encoder.encode(`${line}\n`)); + const stderrBytes = (result.stderr ?? []).map((line) => encoder.encode(`${line}\n`)); + + return ChildProcessSpawner.makeHandle({ + pid: ChildProcessSpawner.ProcessId(5000 + spawned.length), + stdout: Stream.fromIterable(stdoutBytes), + stderr: Stream.fromIterable(stderrBytes), + all: Stream.empty, + exitCode: Deferred.await(exitDeferred), + isRunning: Effect.succeed(false), + stdin: Sink.drain, + kill: () => Effect.void, + unref: Effect.succeed(Effect.void), + getInputFd: () => Sink.drain, + getOutputFd: () => Stream.empty, + }); }), - execCapture: () => Effect.succeed(""), + ), + ); + + return { + layer, + get spawned() { + return spawned; + }, + }; +} + +const ALL_RUNNING_NAMES = legacyServiceContainerIds("demo"); +const HEALTHY_DB_STATE = JSON.stringify({ + Status: "running", + Running: true, + Health: { Status: "healthy" }, +}); + +/** + * Default happy-path router: db container inspect reports healthy+running, `ps` + * (names format) lists every one of the 13 expected services as running. + */ +function defaultRoute( + opts: { + readonly runningNames?: ReadonlyArray; + readonly dbInspectStdout?: string; + readonly dbInspectExitCode?: number; + readonly dbInspectStderr?: ReadonlyArray; + } = {}, +) { + const runningNames = opts.runningNames ?? ALL_RUNNING_NAMES; + return (args: ReadonlyArray): RouteResult => { + if (args[0] === "container" && args[1] === "inspect") { + return { + exitCode: opts.dbInspectExitCode ?? 0, + stdout: [opts.dbInspectStdout ?? HEALTHY_DB_STATE], + stderr: opts.dbInspectStderr, + }; + } + if (args[0] === "ps") return { stdout: runningNames }; + return { exitCode: 0 }; + }; +} + +interface SetupOpts { + readonly format?: "text" | "json" | "stream-json"; + readonly goOutput?: Option.Option<"env" | "pretty" | "json" | "toml" | "yaml">; + readonly route?: (args: ReadonlyArray) => RouteResult; + readonly dockerMissing?: boolean; + readonly failSpawnFor?: (args: ReadonlyArray) => boolean; + readonly skipConfig?: boolean; + readonly configContents?: string; + /** Defaults to `tempRoot.current` — override for `--workdir`-resolution tests. */ + readonly workdir?: string; +} + +function setup(opts: SetupOpts = {}) { + const workdir = opts.workdir ?? tempRoot.current; + if (opts.skipConfig !== true) { + writeConfig(workdir, opts.configContents); + } + const out = mockOutput({ + format: opts.format ?? "text", + interactive: (opts.format ?? "text") === "text", + }); + const telemetry = mockLegacyTelemetryStateTracked(); + const cliConfig = mockLegacyCliConfig({ workdir, projectId: Option.none() }); + const child = mockRoutedContainerCliSpawner(opts.route ?? defaultRoute(), { + dockerMissing: opts.dockerMissing, + failSpawnFor: opts.failSpawnFor, }); - return { layer, calls }; + + const layer = Layer.mergeAll( + BunServices.layer, + out.layer, + cliConfig, + telemetry.layer, + child.layer, + Layer.succeed(LegacyOutputFlag, opts.goOutput ?? Option.none()), + ); + + return { workdir, out, telemetry, child, layer }; } -const baseFlags: LegacyStatusFlags = { - overrideName: [], - exclude: [], - ignoreHealthCheck: false, -}; +describe("legacy status integration", () => { + it.live("shows the running stack as a pretty table", () => { + const { layer, out } = setup(); + return Effect.gen(function* () { + yield* legacyStatus(flags()); + expect(out.stderrText).toContain("local development setup is running."); + expect(out.stdoutText).toContain("🔧 Development Tools"); + expect(out.stdoutText).toContain("🌐 APIs"); + expect(out.stdoutText).toContain("⛁ Database"); + expect(out.stdoutText).toContain("🔑 Authentication Keys"); + expect(out.stdoutText).toContain("📦 Storage (S3)"); + expect(out.stdoutText).toContain("postgresql://postgres:postgres@"); + expect(out.stderrText).not.toContain("Stopped services:"); + }).pipe(Effect.provide(layer)); + }); + + it.live( + "sanitizes a dirty config.toml project_id before filtering, matching start's label", + () => { + // Go's Config.Validate rewrites Config.ProjectId to its sanitized form once + // at config-load time (pkg/config/config.go:938-944); every later reader — + // including the Docker label `start` writes — sees that same sanitized + // string. Filtering/inspecting with the raw value here would target + // containers `start` never created. + const { layer, child } = setup({ configContents: 'project_id = "My App!!"\n' }); + return Effect.gen(function* () { + yield* legacyStatus(flags()); + const inspectCall = child.spawned.find( + (s) => s.args[0] === "container" && s.args[1] === "inspect", + ); + expect(inspectCall?.args[2]).toBe(localDbContainerId("My_App_")); + const psCall = child.spawned.find((s) => s.args[0] === "ps"); + expect(psCall?.args).toContain("label=com.supabase.cli.project=My_App_"); + }).pipe(Effect.provide(layer)); + }, + ); + + it.live("skips the db health check with --ignore-health-check", () => { + const { layer, child } = setup({ + route: (args) => { + // db inspect would fail if called; ps still needs to succeed. + if (args[0] === "container" && args[1] === "inspect") return { exitCode: 1 }; + if (args[0] === "ps") return { stdout: ALL_RUNNING_NAMES }; + return { exitCode: 0 }; + }, + }); + return Effect.gen(function* () { + yield* legacyStatus(flags({ ignoreHealthCheck: true })); + expect(child.spawned.some((s) => s.args[0] === "container" && s.args[1] === "inspect")).toBe( + false, + ); + }).pipe(Effect.provide(layer)); + }); + + it.live( + "succeeds against an unhealthy db when --ignore-health-check is set (status.go:104-108)", + () => { + // Pairs with "fails when the db container is unhealthy" below (ignoreHealthCheck: false, + // the default) to cover both sides of Go's `if !ignoreHealthCheck { assertContainerHealthy }`. + const { layer, child } = setup({ + route: defaultRoute({ + dbInspectStdout: JSON.stringify({ + Status: "running", + Running: true, + Health: { Status: "starting" }, + }), + }), + }); + return Effect.gen(function* () { + yield* legacyStatus(flags({ ignoreHealthCheck: true })); + expect( + child.spawned.some((s) => s.args[0] === "container" && s.args[1] === "inspect"), + ).toBe(false); + }).pipe(Effect.provide(layer)); + }, + ); + + it.live("reports stopped services on stderr", () => { + const { layer, out } = setup({ + route: defaultRoute({ runningNames: ALL_RUNNING_NAMES.slice(1) }), + }); + return Effect.gen(function* () { + yield* legacyStatus(flags()); + const missing = ALL_RUNNING_NAMES[0]; + expect(out.stderrText).toContain(`Stopped services: [${missing}]`); + }).pipe(Effect.provide(layer)); + }); + + it.live("fails when config.toml is malformed", () => { + const workdir = tempRoot.current; + mkdirSync(join(workdir, "supabase"), { recursive: true }); + writeFileSync(join(workdir, "supabase", "config.toml"), "not valid toml ====="); + const { layer, child } = setup({ skipConfig: true }); + return Effect.gen(function* () { + const exit = yield* Effect.exit(legacyStatus(flags())); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(JSON.stringify(exit.cause)).toContain("LegacyStatusConfigLoadError"); + } + expect(child.spawned).toEqual([]); + }).pipe(Effect.provide(layer)); + }); -describe("legacy status", () => { - it.live("forwards no extra flags when defaults are used", () => { - const { layer, calls } = setupLegacyStatus(); + it.live("fails when [remotes.*] has a duplicate project_id, even with no projectRef", () => { + // Go's duplicate-project_id check (config.go:594-602) runs unconditionally + // on every config load, inside the same loop that resolves the [remotes.*] + // override — it is not gated on a caller actually selecting a remote. + // `status` never binds a --project-ref flag, so it must still fail on a + // config-wide duplicate, before ever reaching Docker. + const workdir = tempRoot.current; + mkdirSync(join(workdir, "supabase"), { recursive: true }); + writeFileSync( + join(workdir, "supabase", "config.toml"), + `project_id = "baseref" + +[remotes.a] +project_id = "previewrefaaaaaaaaaa" + +[remotes.b] +project_id = "previewrefaaaaaaaaaa" +`, + ); + const { layer, child } = setup({ skipConfig: true }); return Effect.gen(function* () { - yield* legacyStatus(baseFlags); - expect(calls).toEqual([["status"]]); + const exit = yield* Effect.exit(legacyStatus(flags())); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(JSON.stringify(exit.cause)).toContain("LegacyStatusConfigLoadError"); + } + expect(child.spawned).toEqual([]); }).pipe(Effect.provide(layer)); }); - it.live("forwards --override-name for each provided override", () => { - const { layer, calls } = setupLegacyStatus(); + it.live("fails when a [remotes.*] project_id is not a valid 20-letter ref", () => { + // Go's Config.Validate (config.go:996-1001) checks every [remotes.*].project_id + // against refPattern unconditionally on every config load — not only a + // remote that ends up selected — so this must fail closed before status + // reaches Docker, even with no --project-ref requested. + const workdir = tempRoot.current; + mkdirSync(join(workdir, "supabase"), { recursive: true }); + writeFileSync( + join(workdir, "supabase", "config.toml"), + `project_id = "baseref" + +[remotes.bad] +project_id = "short" +`, + ); + const { layer, child } = setup({ skipConfig: true }); return Effect.gen(function* () { - yield* legacyStatus({ - ...baseFlags, - overrideName: ["api.url=NEXT_PUBLIC_SUPABASE_URL"], + const exit = yield* Effect.exit(legacyStatus(flags())); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(JSON.stringify(exit.cause)).toContain("LegacyStatusConfigLoadError"); + } + expect(child.spawned).toEqual([]); + }).pipe(Effect.provide(layer)); + }); + + it.live( + "decodes a comma-separated string into an array field ([]string) for status to proceed", + () => { + // Go's StringToSliceHookFunc (mapstructure) splits a plain string literal + // into a []string for a []string field like additional_redirect_urls — + // this only runs when goViperCompat is on. Pin that status still proceeds + // past config load (and on to a successful Docker inspect/list) rather + // than treating the string literal as a decode error. + const { layer } = setup({ + configContents: + 'project_id = "demo"\n[auth]\nadditional_redirect_urls = "http://a,http://b"\n', }); - expect(calls).toEqual([["status", "--override-name", "api.url=NEXT_PUBLIC_SUPABASE_URL"]]); + return Effect.gen(function* () { + yield* legacyStatus(flags()); + }).pipe(Effect.provide(layer)); + }, + ); + + it.live("warns on stderr for a deprecated auth.external provider", () => { + // Go's `external.validate()` (config.go:1418-1423) disables a bare + // [auth.external.slack] block and warns — mirrored by + // `normalizeDeprecatedExternalProviders` in packages/config's io.ts, gated + // on `goViperCompat` (confirmed already wired in status.handler.ts). The + // WARN goes out via Effect's `Console.error`, not this file's `Output` + // service, so it must be observed with a raw console.error spy — same + // idiom as packages/config/src/io.unit.test.ts's deprecated-provider tests. + const { layer } = setup({ + configContents: 'project_id = "demo"\n[auth.external.slack]\nenabled = true\n', + }); + const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + return Effect.gen(function* () { + yield* legacyStatus(flags()); + expect(errorSpy).toHaveBeenCalledWith( + expect.stringContaining('WARN: disabling deprecated "slack" provider'), + ); + }).pipe(Effect.provide(layer), Effect.ensuring(Effect.sync(() => errorSpy.mockRestore()))); + }); + + it.live("fails when --workdir/SUPABASE_WORKDIR points at a missing path", () => { + // Go's `ChangeWorkDir` (`apps/cli-go/internal/utils/misc.go:231-250`) + // `os.Chdir`s the explicit workdir in `PersistentPreRunE`, before config + // load or any Docker call — a missing path must fail immediately, not + // fall through to the workdir-basename default and inspect Docker. + const missingWorkdir = join(tempRoot.current, "does-not-exist"); + const { layer, child } = setup({ workdir: missingWorkdir, skipConfig: true }); + return Effect.gen(function* () { + const exit = yield* Effect.exit(legacyStatus(flags())); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(JSON.stringify(exit.cause)).toContain("LegacyStatusWorkdirError"); + expect(JSON.stringify(exit.cause)).toContain( + `failed to change workdir: chdir ${missingWorkdir}: no such file or directory`, + ); + } + expect(child.spawned).toEqual([]); + }).pipe(Effect.provide(layer)); + }); + + it.live("fails when --workdir/SUPABASE_WORKDIR points at a file, not a directory", () => { + const filePath = join(tempRoot.current, "not-a-directory"); + writeFileSync(filePath, ""); + const { layer, child } = setup({ workdir: filePath, skipConfig: true }); + return Effect.gen(function* () { + const exit = yield* Effect.exit(legacyStatus(flags())); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(JSON.stringify(exit.cause)).toContain("LegacyStatusWorkdirError"); + expect(JSON.stringify(exit.cause)).toContain( + `failed to change workdir: chdir ${filePath}: not a directory`, + ); + } + expect(child.spawned).toEqual([]); + }).pipe(Effect.provide(layer)); + }); + + it.live("fails when auth.jwt_secret is configured but shorter than 16 characters", () => { + // Go's Config.Validate rejects this at config-load time + // (pkg/config/apikeys.go:45-47), entirely before assertContainerHealthy/ + // container listing (internal/status/status.go:101-116) — so no Docker + // call happens, same as the malformed config.toml case above. + const { layer, child } = setup({ + configContents: 'project_id = "demo"\n[auth]\njwt_secret = "too-short"\n', + }); + return Effect.gen(function* () { + const exit = yield* Effect.exit(legacyStatus(flags())); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(JSON.stringify(exit.cause)).toContain("LegacyStatusInvalidConfigError"); + expect(JSON.stringify(exit.cause)).toContain( + "Invalid config for auth.jwt_secret. Must be at least 16 characters", + ); + } + expect(child.spawned).toEqual([]); + }).pipe(Effect.provide(layer)); + }); + + it.live("honors SUPABASE_AUTH_JWT_SECRET over a config.toml value with -o env", () => { + // Go's Viper AutomaticEnv gives env vars higher precedence than config.toml + // (pkg/config/config.go:529-535) — a stack started with this env var set + // must report the env-derived secret, not the one in config.toml. + const { layer, out } = setup({ + goOutput: Option.some("env"), + configContents: `project_id = "demo"\n[auth]\njwt_secret = "${"a".repeat(32)}"\n`, + }); + process.env["SUPABASE_AUTH_JWT_SECRET"] = "b".repeat(32); + return Effect.gen(function* () { + yield* legacyStatus(flags()); + expect(out.stdoutText).toContain(`JWT_SECRET="${"b".repeat(32)}"`); + expect(out.stdoutText).not.toContain("a".repeat(32)); + }).pipe(Effect.provide(layer)); + }); + + it.live("signs anon/service_role keys asymmetrically when signing_keys_path is set", () => { + // Go's generateJWT signs with the first key in auth.signing_keys_path + // (RS256/ES256) instead of HMAC when that file resolves to a non-empty JWK + // array (pkg/config/apikeys.go:76-113). + const { layer, out, workdir } = setup({ + goOutput: Option.some("json"), + configContents: 'project_id = "demo"\n[auth]\nsigning_keys_path = "signing_keys.json"\n', + }); + const { privateKey } = generateKeyPairSync("rsa", { modulusLength: 2048 }); + const jwk = { ...privateKey.export({ format: "jwk" }), alg: "RS256", kid: "test-kid" }; + writeFileSync(join(workdir, "supabase", "signing_keys.json"), JSON.stringify([jwk])); + return Effect.gen(function* () { + yield* legacyStatus(flags()); + const parsed = JSON.parse(out.stdoutText) as Record; + const [headerSegment] = parsed.ANON_KEY?.split(".") ?? []; + const header = JSON.parse(Buffer.from(headerSegment ?? "", "base64url").toString()); + expect(header).toEqual({ alg: "RS256", kid: "test-kid", typ: "JWT" }); + }).pipe(Effect.provide(layer)); + }); + + it.live("reports status using schema defaults when config.toml is missing entirely", () => { + // Matches Go: `flags.LoadConfig` -> `Config.Load` -> `loadFromFile` -> + // `mergeFileConfig` treats a missing file as a no-op (`os.ErrNotExist` -> + // nil, pkg/config/config.go:655-656), not an error — `status` proceeds + // using template defaults. Only a malformed file is a hard failure (see + // the sibling "malformed" test above). + // + // Without config.toml, the resolved project id falls back to the workdir + // basename (not the module-level `ALL_RUNNING_NAMES`, which is fixed to + // "demo") — route `ps` off that so the expected services actually show as + // running rather than all appearing "stopped" and excluded. + const projectId = basename(tempRoot.current); + const { layer, out } = setup({ + skipConfig: true, + route: defaultRoute({ runningNames: legacyServiceContainerIds(projectId) }), + }); + return Effect.gen(function* () { + yield* legacyStatus(flags()); + expect(out.stderrText).toContain("local development setup is running."); + expect(out.stdoutText).toContain("Project URL"); + expect(out.stdoutText).toContain("Database"); + }).pipe(Effect.provide(layer)); + }); + + it.live("resolves SUPABASE_PROJECT_ID from supabase/.env over config.toml", () => { + // Go's Config.Load runs loadNestedEnv (supabase/.env(.local) via godotenv) + // before loadFromFile's AutomaticEnv reads SUPABASE_PROJECT_ID + // (pkg/config/config.go:735-738) — an env-file-only value overrides + // config.toml's project_id too, not just an ambient shell export. + const supabaseDir = join(tempRoot.current, "supabase"); + mkdirSync(supabaseDir, { recursive: true }); + writeFileSync(join(supabaseDir, ".env"), "SUPABASE_PROJECT_ID=env-file-project\n"); + const { layer, child } = setup({ + configContents: 'project_id = "toml-project"\n', + route: defaultRoute({ runningNames: legacyServiceContainerIds("env-file-project") }), + }); + return Effect.gen(function* () { + yield* legacyStatus(flags()); + const inspectCall = child.spawned.find( + (s) => s.args[0] === "container" && s.args[1] === "inspect", + ); + expect(inspectCall?.args).toContain(localDbContainerId("env-file-project")); }).pipe(Effect.provide(layer)); }); - it.live("forwards the hidden --exclude and --ignore-health-check flags", () => { - const { layer, calls } = setupLegacyStatus(); + it.live("prefers ambient SUPABASE_PROJECT_ID over supabase/.env", () => { + const supabaseDir = join(tempRoot.current, "supabase"); + mkdirSync(supabaseDir, { recursive: true }); + writeFileSync(join(supabaseDir, ".env"), "SUPABASE_PROJECT_ID=env-file-project\n"); + process.env["SUPABASE_PROJECT_ID"] = "ambient-project"; + const { layer, child } = setup({ + configContents: 'project_id = "toml-project"\n', + route: defaultRoute({ runningNames: legacyServiceContainerIds("ambient-project") }), + }); + return Effect.gen(function* () { + yield* legacyStatus(flags()); + const inspectCall = child.spawned.find( + (s) => s.args[0] === "container" && s.args[1] === "inspect", + ); + expect(inspectCall?.args).toContain(localDbContainerId("ambient-project")); + }).pipe( + Effect.provide(layer), + Effect.ensuring(Effect.sync(() => delete process.env["SUPABASE_PROJECT_ID"])), + ); + }); + + it.live("resolves SUPABASE_PROJECT_ID from a project-root .env file", () => { + // Go's loadNestedEnv walks past supabase/ one more level, to the project + // root/workdir (pkg/config/config.go:1169-1190) — a project-root-only + // dotenv value must override config.toml too, not just supabase/.env. + writeFileSync(join(tempRoot.current, ".env"), "SUPABASE_PROJECT_ID=root-env-project\n"); + const { layer, child } = setup({ + configContents: 'project_id = "toml-project"\n', + route: defaultRoute({ runningNames: legacyServiceContainerIds("root-env-project") }), + }); return Effect.gen(function* () { - yield* legacyStatus({ - overrideName: [], - exclude: ["db", "kong"], - ignoreHealthCheck: true, + yield* legacyStatus(flags()); + const inspectCall = child.spawned.find( + (s) => s.args[0] === "container" && s.args[1] === "inspect", + ); + expect(inspectCall?.args).toContain(localDbContainerId("root-env-project")); + }).pipe(Effect.provide(layer)); + }); + + it.live( + "does not climb to an ancestor project's config.toml when workdir has none of its own", + () => { + // Go's ChangeWorkDir uses an explicit/defaulted workdir exactly, with no + // ancestor search (apps/cli-go/internal/utils/misc.go:231-247) — mirrored + // here by `search: false`. A workdir with no supabase/config.toml of its + // own must fall back to defaults (workdir-basename project id), not an + // ancestor project's config.toml, even though `cliConfig.workdir` sits + // right inside one. + const nestedWorkdir = join(tempRoot.current, "nested"); + mkdirSync(nestedWorkdir, { recursive: true }); + writeConfig(tempRoot.current, 'project_id = "ancestor-project"\n'); + const projectId = basename(nestedWorkdir); + const { layer, child } = setup({ + workdir: nestedWorkdir, + skipConfig: true, + route: defaultRoute({ runningNames: legacyServiceContainerIds(projectId) }), }); - expect(calls).toEqual([ - ["status", "--exclude", "db", "--exclude", "kong", "--ignore-health-check"], - ]); + return Effect.gen(function* () { + yield* legacyStatus(flags()); + const inspectCall = child.spawned.find( + (s) => s.args[0] === "container" && s.args[1] === "inspect", + ); + expect(inspectCall?.args).toContain(localDbContainerId(projectId)); + expect(inspectCall?.args).not.toContain(localDbContainerId("ancestor-project")); + }).pipe(Effect.provide(layer)); + }, + ); + + it.live("resolves SUPABASE_PROJECT_ID from supabase/.env even when config.toml is absent", () => { + // Go's loadNestedEnv runs unconditionally, before config.toml is ever + // opened (pkg/config/config.go:786-793) — a supabase/.env-only project id + // must still be honored even when there's no config.toml to fall back to + // template defaults from. + const supabaseDir = join(tempRoot.current, "supabase"); + mkdirSync(supabaseDir, { recursive: true }); + writeFileSync(join(supabaseDir, ".env"), "SUPABASE_PROJECT_ID=no-config-project\n"); + const { layer, child } = setup({ + skipConfig: true, + route: defaultRoute({ runningNames: legacyServiceContainerIds("no-config-project") }), + }); + return Effect.gen(function* () { + yield* legacyStatus(flags()); + const inspectCall = child.spawned.find( + (s) => s.args[0] === "container" && s.args[1] === "inspect", + ); + expect(inspectCall?.args).toContain(localDbContainerId("no-config-project")); + }).pipe(Effect.provide(layer)); + }); + + it.live("honors SUPABASE_AUTH_JWT_SECRET from supabase/.env, not just the ambient shell", () => { + // Go's Config.Load runs loadNestedEnv (supabase/.env(.local) via godotenv) + // before AutomaticEnv reads SUPABASE_AUTH_JWT_SECRET (config.go:735-738) — + // a dotenv-file-only value must be visible here too, not just an ambient + // shell export (see the sibling "-o env" ambient test above). + const supabaseDir = join(tempRoot.current, "supabase"); + mkdirSync(supabaseDir, { recursive: true }); + writeFileSync(join(supabaseDir, ".env"), `SUPABASE_AUTH_JWT_SECRET=${"c".repeat(32)}\n`); + const { layer, out } = setup({ + goOutput: Option.some("env"), + configContents: `project_id = "demo"\n[auth]\njwt_secret = "${"a".repeat(32)}"\n`, + }); + return Effect.gen(function* () { + yield* legacyStatus(flags()); + expect(out.stdoutText).toContain(`JWT_SECRET="${"c".repeat(32)}"`); + expect(out.stdoutText).not.toContain("a".repeat(32)); + }).pipe(Effect.provide(layer)); + }); + + it.live("fails when both docker and podman are missing", () => { + // Neither container runtime can be spawned at all — distinct from a spawned + // process exiting non-zero (covered by the malformed/unhealthy scenarios + // above). + const { layer } = setup({ failSpawnFor: () => true }); + return Effect.gen(function* () { + const exit = yield* Effect.exit(legacyStatus(flags())); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(JSON.stringify(exit.cause)).toContain("LegacyStatusDbInspectError"); + } + }).pipe(Effect.provide(layer)); + }); + + it.live("falls back to podman when docker is absent", () => { + const { layer, child } = setup({ dockerMissing: true }); + return Effect.gen(function* () { + yield* legacyStatus(flags()); + // The failed `docker` attempt is recorded before the `podman` fallback fires + // (`spawnContainerCli`'s `Effect.catch` retries the same argv), so the last + // matching record for a given argv is the successful one. + const psCalls = child.spawned.filter((s) => s.args[0] === "ps"); + expect(psCalls.at(-1)?.command).toBe("podman"); + expect(psCalls.some((s) => s.command === "docker")).toBe(true); + }).pipe(Effect.provide(layer)); + }); + + it.live("fails when listing running containers errors", () => { + const { layer } = setup({ + route: (args) => { + if (args[0] === "container" && args[1] === "inspect") { + return { exitCode: 0, stdout: [HEALTHY_DB_STATE] }; + } + if (args[0] === "ps") return { exitCode: 1, stderr: ["daemon down"] }; + return { exitCode: 0 }; + }, + }); + return Effect.gen(function* () { + const exit = yield* Effect.exit(legacyStatus(flags())); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(JSON.stringify(exit.cause)).toContain("LegacyStatusListError"); + } + }).pipe(Effect.provide(layer)); + }); + + it.live("fails when the db container is not running", () => { + const { layer } = setup({ + route: defaultRoute({ + dbInspectStdout: JSON.stringify({ Status: "exited", Running: false }), + }), + }); + return Effect.gen(function* () { + const exit = yield* Effect.exit(legacyStatus(flags())); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + const serialized = JSON.stringify(exit.cause); + expect(serialized).toContain("LegacyStatusDbNotRunningError"); + expect(serialized).toContain(localDbContainerId("demo")); + } + }).pipe(Effect.provide(layer)); + }); + + it.live( + "succeeds against a paused-but-healthy db, matching Go's boolean-based running gate", + () => { + // Go's `assertContainerHealthy` (`status.go:150`) gates on the boolean + // `resp.State.Running`, not the status string — a paused container can + // report `Running: true` alongside `Status: "paused"`, and Go continues + // past the not-running branch to the health check in that case. + const { layer } = setup({ + route: defaultRoute({ + dbInspectStdout: JSON.stringify({ + Status: "paused", + Running: true, + Health: { Status: "healthy" }, + }), + }), + }); + return Effect.gen(function* () { + yield* legacyStatus(flags()); + }).pipe(Effect.provide(layer)); + }, + ); + + it.live("fails when the db container is absent, preserving the real Docker stderr text", () => { + // Go's `assertContainerHealthy` never special-cases "not found" — it wraps + // whatever `ContainerInspect` returns (`status.go:148-149`), so the real + // Docker stderr must flow through rather than a hardcoded TS string. + const { layer } = setup({ + route: defaultRoute({ + dbInspectExitCode: 1, + dbInspectStderr: ["Error response from daemon: No such container: x"], + }), + }); + return Effect.gen(function* () { + const exit = yield* Effect.exit(legacyStatus(flags())); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + const serialized = JSON.stringify(exit.cause); + expect(serialized).toContain("LegacyStatusDbInspectError"); + expect(serialized).toContain( + "failed to inspect container health: Error response from daemon: No such container: x", + ); + } + }).pipe(Effect.provide(layer)); + }); + + it.live("fails when the db container is unhealthy", () => { + const { layer } = setup({ + route: defaultRoute({ + dbInspectStdout: JSON.stringify({ + Status: "running", + Running: true, + Health: { Status: "starting" }, + }), + }), + }); + return Effect.gen(function* () { + const exit = yield* Effect.exit(legacyStatus(flags())); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(JSON.stringify(exit.cause)).toContain("LegacyStatusDbNotReadyError"); + } + }).pipe(Effect.provide(layer)); + }); + + it.live("fails when db inspect errors for a reason other than not-found", () => { + const { layer } = setup({ + route: defaultRoute({ dbInspectExitCode: 1, dbInspectStderr: ["permission denied"] }), + }); + return Effect.gen(function* () { + const exit = yield* Effect.exit(legacyStatus(flags())); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(JSON.stringify(exit.cause)).toContain("LegacyStatusDbInspectError"); + } + }).pipe(Effect.provide(layer)); + }); + + it.live("outputs env vars with -o env", () => { + const { layer, out } = setup({ goOutput: Option.some("env") }); + return Effect.gen(function* () { + yield* legacyStatus(flags()); + expect(out.stdoutText).toContain('API_URL="http://127.0.0.1:54321"'); + expect(out.stdoutText).toContain("DB_URL="); + }).pipe(Effect.provide(layer)); + }); + + it.live("outputs a json object with -o json", () => { + const { layer, out } = setup({ goOutput: Option.some("json") }); + return Effect.gen(function* () { + yield* legacyStatus(flags()); + const parsed = JSON.parse(out.stdoutText) as Record; + expect(parsed.API_URL).toBe("http://127.0.0.1:54321"); + expect(parsed.DB_URL).toContain("postgresql://postgres:postgres@"); + }).pipe(Effect.provide(layer)); + }); + + it.live("omits excluded services from -o json", () => { + const { layer, out } = setup({ goOutput: Option.some("json") }); + return Effect.gen(function* () { + const storageId = legacyServiceContainerIds("demo")[5]!; + yield* legacyStatus(flags({ exclude: [storageId] })); + const parsed = JSON.parse(out.stdoutText) as Record; + expect(parsed.STORAGE_S3_URL).toBeUndefined(); + expect(parsed.API_URL).toBeDefined(); + }).pipe(Effect.provide(layer)); + }); + + it.live("omits every service named across multiple --exclude entries", () => { + const { layer, out } = setup({ goOutput: Option.some("json") }); + return Effect.gen(function* () { + const authId = legacyServiceContainerIds("demo")[1]!; + const storageId = legacyServiceContainerIds("demo")[5]!; + yield* legacyStatus(flags({ exclude: [authId, storageId] })); + const parsed = JSON.parse(out.stdoutText) as Record; + expect(parsed.PUBLISHABLE_KEY).toBeUndefined(); + expect(parsed.STORAGE_S3_URL).toBeUndefined(); + expect(parsed.API_URL).toBeDefined(); + }).pipe(Effect.provide(layer)); + }); + + it.live("merges an auto-detected stopped service with a --exclude entry (status.go:116)", () => { + // Go's `excluded := append(stopped, exclude...)` merges the health-derived + // stopped list with the user-supplied --exclude list — both must take effect + // together, not just whichever one the command would have applied alone. + const { layer, out } = setup({ + goOutput: Option.some("json"), + // kong (index 0) is absent from the running set, so it's auto-detected as stopped. + route: defaultRoute({ runningNames: ALL_RUNNING_NAMES.slice(1) }), + }); + return Effect.gen(function* () { + const authId = legacyServiceContainerIds("demo")[1]!; + yield* legacyStatus(flags({ exclude: [authId] })); + const parsed = JSON.parse(out.stdoutText) as Record; + expect(parsed.API_URL).toBeUndefined(); // excluded via the auto-detected stopped kong + expect(parsed.PUBLISHABLE_KEY).toBeUndefined(); // excluded via --exclude + expect(parsed.DB_URL).toBeDefined(); // db.url is set unconditionally, before any gating + }).pipe(Effect.provide(layer)); + }); + + it.live("outputs yaml with -o yaml", () => { + const { layer, out } = setup({ goOutput: Option.some("yaml") }); + return Effect.gen(function* () { + yield* legacyStatus(flags()); + expect(out.stdoutText).toContain("API_URL:"); + }).pipe(Effect.provide(layer)); + }); + + it.live("outputs toml with -o toml", () => { + const { layer, out } = setup({ goOutput: Option.some("toml") }); + return Effect.gen(function* () { + yield* legacyStatus(flags()); + expect(out.stdoutText).toContain("API_URL ="); + }).pipe(Effect.provide(layer)); + }); + + it.live("remaps an output key with --override-name api.url=NEXT_PUBLIC_SUPABASE_URL", () => { + const { layer, out } = setup({ goOutput: Option.some("json") }); + return Effect.gen(function* () { + yield* legacyStatus(flags({ overrideName: ["api.url=NEXT_PUBLIC_SUPABASE_URL"] })); + const parsed = JSON.parse(out.stdoutText) as Record; + expect(parsed.NEXT_PUBLIC_SUPABASE_URL).toBe("http://127.0.0.1:54321"); + expect(parsed.API_URL).toBeUndefined(); + }).pipe(Effect.provide(layer)); + }); + + it.live("fails on a malformed --override-name entry", () => { + const { layer } = setup(); + return Effect.gen(function* () { + const exit = yield* Effect.exit(legacyStatus(flags({ overrideName: ["not-a-kv-pair"] }))); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(JSON.stringify(exit.cause)).toContain("LegacyStatusOverrideParseError"); + } + }).pipe(Effect.provide(layer)); + }); + + it.live("silently ignores an --override-name entry with an unknown field key", () => { + // Matches Go: `env.Unmarshal` (Netflix go-env) walks CustomName's own struct + // fields and looks up each field's tag in the override map — it never checks + // for leftover/unmatched keys, so an unrecognized key is a no-op, not an error. + const { layer, out } = setup({ goOutput: Option.some("json") }); + return Effect.gen(function* () { + yield* legacyStatus(flags({ overrideName: ["not.a.real.field=NAME"] })); + const parsed = JSON.parse(out.stdoutText) as Record; + expect(parsed.NAME).toBeUndefined(); + expect(parsed.API_URL).toBe("http://127.0.0.1:54321"); + }).pipe(Effect.provide(layer)); + }); + + it.live("applies a valid --override-name entry alongside an unknown one", () => { + const { layer, out } = setup({ goOutput: Option.some("json") }); + return Effect.gen(function* () { + yield* legacyStatus( + flags({ overrideName: ["not.a.real.field=NAME", "api.url=NEXT_PUBLIC_SUPABASE_URL"] }), + ); + const parsed = JSON.parse(out.stdoutText) as Record; + expect(parsed.NEXT_PUBLIC_SUPABASE_URL).toBe("http://127.0.0.1:54321"); + expect(parsed.NAME).toBeUndefined(); + }).pipe(Effect.provide(layer)); + }); + + it.live("emits a machine result with --output-format json when -o is unset", () => { + const { layer, out } = setup({ format: "json" }); + return Effect.gen(function* () { + yield* legacyStatus(flags()); + const success = out.messages.find((m) => m.type === "success"); + expect(success?.data).toMatchObject({ API_URL: "http://127.0.0.1:54321" }); + expect(out.stdoutText).not.toContain("\x1b[?25l"); + }).pipe(Effect.provide(layer)); + }); + + it.live("-o takes priority over --output-format when both are passed", () => { + const { layer, out } = setup({ format: "json", goOutput: Option.some("env") }); + return Effect.gen(function* () { + yield* legacyStatus(flags()); + // -o env wins: raw KEY="VALUE" text on stdout, not a structured success message. + expect(out.stdoutText).toContain('API_URL="http://127.0.0.1:54321"'); + expect(out.messages.find((m) => m.type === "success")).toBeUndefined(); + }).pipe(Effect.provide(layer)); + }); + + it.live("lets --output pretty win over --output-format json", () => { + // Explicit `-o pretty` is a complete Go format choice (root.ts:119-121, + // matching functions/list) and must render the table, not defer to the + // TS-only --output-format json/stream-json branch. + const { layer, out } = setup({ format: "json", goOutput: Option.some("pretty") }); + return Effect.gen(function* () { + yield* legacyStatus(flags()); + expect(out.stderrText).toContain("local development setup is running."); + expect(out.stdoutText).toContain("🌐 APIs"); + expect(out.messages.find((m) => m.type === "success")).toBeUndefined(); + }).pipe(Effect.provide(layer)); + }); + + it.live("flushes telemetry via ensuring even on failure", () => { + const { layer, telemetry } = setup({ + route: (args) => + args[0] === "container" && args[1] === "inspect" ? { exitCode: 1 } : { exitCode: 0 }, + }); + return Effect.gen(function* () { + yield* Effect.exit(legacyStatus(flags())); + expect(telemetry.flushed).toBe(true); }).pipe(Effect.provide(layer)); }); }); diff --git a/apps/cli/src/legacy/commands/status/status.live.test.ts b/apps/cli/src/legacy/commands/status/status.live.test.ts new file mode 100644 index 0000000000..64569c118c --- /dev/null +++ b/apps/cli/src/legacy/commands/status/status.live.test.ts @@ -0,0 +1,54 @@ +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { afterEach, expect, test } from "vitest"; + +import { describeLive, runSupabaseLive } from "../../../../tests/helpers/live.ts"; + +const START_TIMEOUT_MS = 280_000; + +// See stop.live.test.ts for why `describeLive` (not a Management-API gate) is +// the right reuse here: `status` never calls the Management API, only the real +// Docker daemon the cli-e2e-ci runner provides. See AGENTS.md's "Live tests" +// section for the full convention. +describeLive("supabase status (live)", () => { + let projectDir: string | undefined; + + afterEach(async () => { + if (projectDir === undefined) return; + await runSupabaseLive(["stop", "--no-backup"], { cwd: projectDir }).catch(() => undefined); + await rm(projectDir, { recursive: true, force: true }).catch(() => undefined); + projectDir = undefined; + }); + + test( + "reports a running local stack in pretty and json modes", + { timeout: START_TIMEOUT_MS }, + async () => { + projectDir = await mkdtemp(path.join(tmpdir(), "sb-status-live-")); + + const init = await runSupabaseLive(["init"], { cwd: projectDir }); + expect(init.exitCode, `stdout:\n${init.stdout}\nstderr:\n${init.stderr}`).toBe(0); + + const start = await runSupabaseLive( + ["start", "--exclude", "studio", "--exclude", "analytics", "--exclude", "vector"], + { cwd: projectDir, exitTimeoutMs: START_TIMEOUT_MS }, + ); + expect(start.exitCode, `stdout:\n${start.stdout}\nstderr:\n${start.stderr}`).toBe(0); + + const pretty = await runSupabaseLive(["status"], { cwd: projectDir }); + expect(pretty.exitCode, `stdout:\n${pretty.stdout}\nstderr:\n${pretty.stderr}`).toBe(0); + expect(`${pretty.stdout}${pretty.stderr}`).toContain("is running"); + expect(pretty.stdout).toContain("Project URL"); + expect(pretty.stdout).toContain("Database"); + + const json = await runSupabaseLive(["status", "-o", "json"], { cwd: projectDir }); + expect(json.exitCode, `stdout:\n${json.stdout}\nstderr:\n${json.stderr}`).toBe(0); + const parsed: unknown = JSON.parse(json.stdout); + expect(parsed).toMatchObject({ + API_URL: expect.stringContaining("http"), + DB_URL: expect.stringContaining("postgresql://"), + }); + }, + ); +}); diff --git a/apps/cli/src/legacy/commands/status/status.pretty.ts b/apps/cli/src/legacy/commands/status/status.pretty.ts new file mode 100644 index 0000000000..2999e4ef50 --- /dev/null +++ b/apps/cli/src/legacy/commands/status/status.pretty.ts @@ -0,0 +1,276 @@ +import { legacyAqua, legacyBold, legacyGreen, legacyYellow } from "../../shared/legacy-colors.ts"; +import type { LegacyStatusOutputNames } from "./status.values.ts"; + +/** + * Port of Go's `PrettyPrint` / `OutputGroup.printTable` + * (`apps/cli-go/internal/status/status.go:236-392`), reproducing + * `tablewriter.NewTable` with `tw.StyleRounded` byte-for-byte for the fixed + * 5-group, 2-column layout `status` needs. This is not a general tablewriter + * port — column sizing, wrapping, and merge behavior are only implemented to the + * extent this command's rounded box needs them. + * + * Column 0 (the label column) is capped at 16 display columns + * (`ColMaxWidths.PerColumn[0] = 16`, `status.go:344`); a label wider than that + * word-wraps across multiple lines, leaving column 1 blank on the continuation + * lines (verified against a real `tablewriter@v1.1.4` render — see the port + * plan). None of the fixed labels below reach 17 characters today, so this is + * defensive parity rather than an observed case. + * + * This does not reuse `legacy/output/legacy-glamour-table.ts` — that helper + * byte-matches Go's `glamour.RenderTable(..., AsciiStyle)`, a single ASCII table + * with a different border style used by other commands. `status`'s Go source + * renders with `tablewriter`/`tw.StyleRounded` into 5 separate grouped, colored, + * Unicode-rounded-box tables, which is a different rendering contract entirely. + * + * Every color call below styles text written to **stdout** (via `output.raw` + * with no stream argument in `status.handler.ts`), so each one explicitly passes + * `process.stdout` to `legacy-colors.ts`'s helpers — they default to + * `process.stderr`, which would check the wrong stream's TTY status here. + */ + +type OutputKind = "text" | "link" | "key"; + +interface OutputItem { + readonly label: string; + readonly value: string; + readonly kind: OutputKind; +} + +interface OutputGroup { + readonly name: string; + readonly items: ReadonlyArray; +} + +const COLUMN_0_MAX_WIDTH = 16; + +/** + * Builds the 5 fixed groups Go's `PrettyPrint` declares (`status.go:245-285`), + * looking up each label's value by output KEY from the resolved value map — + * `--override-name` remaps the KEY but never the group layout, matching Go + * (`values[names.StudioURL]`, not a hardcoded default name). + */ +function buildGroups( + values: Readonly>, + names: LegacyStatusOutputNames, +): ReadonlyArray { + const at = (key: string) => values[key] ?? ""; + return [ + { + name: "🔧 Development Tools", + items: [ + { label: "Studio", value: at(names.studioUrl), kind: "link" }, + { label: "Mailpit", value: at(names.mailpitUrl), kind: "link" }, + { label: "MCP", value: at(names.mcpUrl), kind: "link" }, + ], + }, + { + name: "🌐 APIs", + items: [ + { label: "Project URL", value: at(names.apiUrl), kind: "link" }, + { label: "REST", value: at(names.restUrl), kind: "link" }, + { label: "GraphQL", value: at(names.graphqlUrl), kind: "link" }, + { label: "Edge Functions", value: at(names.functionsUrl), kind: "link" }, + ], + }, + { + name: "⛁ Database", + items: [{ label: "URL", value: at(names.dbUrl), kind: "link" }], + }, + { + name: "🔑 Authentication Keys", + items: [ + { label: "Publishable", value: at(names.publishableKey), kind: "key" }, + { label: "Secret", value: at(names.secretKey), kind: "key" }, + ], + }, + { + name: "📦 Storage (S3)", + items: [ + { label: "URL", value: at(names.storageS3Url), kind: "link" }, + { label: "Access Key", value: at(names.storageS3AccessKeyId), kind: "key" }, + { label: "Secret Key", value: at(names.storageS3SecretAccessKey), kind: "key" }, + { label: "Region", value: at(names.storageS3Region), kind: "text" }, + ], + }, + ]; +} + +/** + * Display width, matching `go-runewidth`'s treatment closely enough for this + * command's inputs: URLs/keys/labels are always plain ASCII, so every rune is + * width 1. The only non-ASCII runes ever rendered are the 5 fixed group-title + * emoji, whose exact rendered widths are hardcoded in {@link HEADER_DISPLAY_WIDTH} + * below rather than computed generically (avoids taking a full Unicode + * East-Asian-Width dependency for a 5-value constant table). + */ +function displayWidth(text: string): number { + return [...text].length; +} + +/** Go-rendered display width of each fixed group title (see `status.pretty.unit.test.ts`). */ +const HEADER_DISPLAY_WIDTH: Readonly> = { + "🔧 Development Tools": 20, + "🌐 APIs": 7, + "⛁ Database": 10, + "🔑 Authentication Keys": 22, + "📦 Storage (S3)": 15, +}; + +/** + * Exported only for direct unit coverage of the fallback branch (a group title + * outside the 5-entry {@link HEADER_DISPLAY_WIDTH} table) — every call site in + * this file only ever passes one of those 5 fixed titles. + */ +export function legacyStatusHeaderWidth(name: string): number { + return HEADER_DISPLAY_WIDTH[name] ?? displayWidth(name); +} + +/** + * Greedy word-wrap to `width` columns, mirroring tablewriter's column wrapping. + * Exported only for direct unit coverage of the >16-char defensive-wrap branch + * (see the file-level doc comment) — none of this command's real labels reach + * that width today, so `legacyRenderStatusPretty` never exercises it end to end. + */ +export function legacyWrapStatusLabel(text: string, width: number): ReadonlyArray { + if (displayWidth(text) <= width) return [text]; + const words = text.split(" "); + const lines: string[] = []; + let current = ""; + for (const word of words) { + const candidate = current.length === 0 ? word : `${current} ${word}`; + if (displayWidth(candidate) <= width) { + current = candidate; + } else { + if (current.length > 0) lines.push(current); + current = word; + } + } + if (current.length > 0) lines.push(current); + return lines.length > 0 ? lines : [text]; +} + +/** + * Value coloring, mirroring the `switch row.Type` in `printTable` + * (`status.go:372-377`): `Link` → Aqua, `Key` → Yellow, `Text` → unstyled (the + * switch has no `Text` case, so `value` keeps its raw pre-switch assignment). + */ +function colorValue(kind: OutputKind, value: string): string { + switch (kind) { + case "link": + return legacyAqua(value, process.stdout); + case "key": + return legacyYellow(value, process.stdout); + case "text": + return value; + } +} + +interface ColumnLayout { + readonly col0Padded: number; + readonly col1Padded: number; + readonly targetInner: number; +} + +/** + * Computes the padded column widths and total inner (header) width for a group, + * mirroring tablewriter's column-sizing pass: each column is sized from its + * widest content cell (col 0 capped at 16), then both columns widen evenly + * (col 0 taking the larger half of an odd remainder) if the header text is + * wider than the data-driven layout. Exported only for direct unit coverage of + * the header-widens-the-table branch — none of this command's 5 fixed group + * titles are wider than their data today, so `legacyRenderStatusPretty` never + * exercises it end to end (see the file-level doc comment). + */ +export function legacyStatusColumnLayout( + headerWidthValue: number, + col0Contents: ReadonlyArray, + col1Contents: ReadonlyArray, +): ColumnLayout { + const col0Content = Math.min( + COLUMN_0_MAX_WIDTH, + Math.max(...col0Contents.map((text) => displayWidth(text))), + ); + const col1Content = Math.max(...col1Contents.map((text) => displayWidth(text))); + + let col0Padded = col0Content + 2; + let col1Padded = col1Content + 2; + const dataInner = col0Padded + 1 + col1Padded; + const targetInner = Math.max(dataInner, headerWidthValue + 2); + const extra = targetInner - dataInner; + if (extra > 0) { + col0Padded += Math.ceil(extra / 2); + col1Padded += Math.floor(extra / 2); + } + return { col0Padded, col1Padded, targetInner }; +} + +function renderGroupTable(group: OutputGroup): string | undefined { + const rows = group.items.filter((item) => item.value.length > 0); + if (rows.length === 0) return undefined; + + // Column 0 wraps at 16; column 1 is never capped (Go only sets PerColumn[0]). + // Kept as plain text here — color is applied only after padding, below, so an + // ANSI escape is never counted toward the padded display width. + const wrappedRows = rows.map((row) => ({ + lines: legacyWrapStatusLabel(row.label, COLUMN_0_MAX_WIDTH), + kind: row.kind, + value: row.value, + })); + + const { col0Padded, col1Padded, targetInner } = legacyStatusColumnLayout( + legacyStatusHeaderWidth(group.name), + rows.map((row) => row.label), + rows.map((row) => row.value), + ); + const col0Width = col0Padded - 2; + const col1Width = col1Padded - 2; + + // Pad on the plain text first, then apply color/bold — an active ANSI escape + // must never be counted toward the padded display width. + const pad = (text: string, width: number) => + text + " ".repeat(Math.max(0, width - displayWidth(text))); + // The header uses `headerWidth` (the hardcoded emoji-aware width table) rather + // than `displayWidth`, so its padding lines up with the border math above, + // which sized `targetInner` off the same `headerWidth` call. + const padHeader = (text: string, width: number) => + text + " ".repeat(Math.max(0, width - legacyStatusHeaderWidth(text))); + + const lines: string[] = []; + lines.push(`╭${"─".repeat(col0Padded + 1 + col1Padded)}╮`); + lines.push(`│ ${legacyBold(padHeader(group.name, targetInner - 2), process.stdout)} │`); + lines.push(`├${"─".repeat(col0Padded)}┬${"─".repeat(col1Padded)}┤`); + for (const row of wrappedRows) { + row.lines.forEach((line, index) => { + // Only the first wrapped line carries the value; Go's continuation lines + // (from a >16-char label wrapping) leave column 1 blank. + const labelCell = legacyGreen(pad(line, col0Width), process.stdout); + const paddedValue = pad(index === 0 ? row.value : "", col1Width); + const valueCell = index === 0 ? colorValue(row.kind, paddedValue) : paddedValue; + lines.push(`│ ${labelCell} │ ${valueCell} │`); + }); + } + lines.push(`╰${"─".repeat(col0Padded)}┴${"─".repeat(col1Padded)}╯`); + return lines.join("\n"); +} + +/** + * Port of Go's `PrettyPrint` (`status.go:236-294`): renders the 5 fixed groups + * as rounded-border tables, skipping empty rows and empty groups, with a blank + * line after every group (rendered or not — Go's loop always + * `fmt.Fprintln(w)`s after a nil-error `printTable`, even when nothing rendered). + */ +export function legacyRenderStatusPretty( + values: Readonly>, + names: LegacyStatusOutputNames, +): string { + const groups = buildGroups(values, names); + const lines: string[] = []; + for (const group of groups) { + const table = renderGroupTable(group); + if (table !== undefined) { + lines.push(table); + } + lines.push(""); + } + return lines.join("\n"); +} diff --git a/apps/cli/src/legacy/commands/status/status.pretty.unit.test.ts b/apps/cli/src/legacy/commands/status/status.pretty.unit.test.ts new file mode 100644 index 0000000000..be44ac4c75 --- /dev/null +++ b/apps/cli/src/legacy/commands/status/status.pretty.unit.test.ts @@ -0,0 +1,267 @@ +import { describe, expect, it } from "vitest"; + +import { + legacyRenderStatusPretty, + legacyStatusColumnLayout, + legacyStatusHeaderWidth, + legacyWrapStatusLabel, +} from "./status.pretty.ts"; +import type { LegacyStatusOutputNames } from "./status.values.ts"; + +// The renderer applies Go-parity ANSI styling via `legacy-colors.ts`, which +// no-ops on a real non-TTY stream but the vitest process presents its stderr +// as color-capable. Strip escapes so these assertions target the plain +// structural output — the golden contract per the port plan — not whichever +// TTY heuristic the test runner happens to report. +// eslint-disable-next-line no-control-regex +const stripAnsi = (text: string) => text.replace(/\x1b\[[0-9;]*m/gu, ""); + +// Default (un-overridden) output names, matching `status.values.ts`'s +// `resolveOutputNames` with an empty override map — the KEYs the pretty +// renderer looks values up by. +const NAMES: LegacyStatusOutputNames = { + apiUrl: "API_URL", + restUrl: "REST_URL", + graphqlUrl: "GRAPHQL_URL", + storageS3Url: "STORAGE_S3_URL", + mcpUrl: "MCP_URL", + functionsUrl: "FUNCTIONS_URL", + dbUrl: "DB_URL", + studioUrl: "STUDIO_URL", + mailpitUrl: "MAILPIT_URL", + publishableKey: "PUBLISHABLE_KEY", + secretKey: "SECRET_KEY", + storageS3AccessKeyId: "S3_PROTOCOL_ACCESS_KEY_ID", + storageS3SecretAccessKey: "S3_PROTOCOL_ACCESS_KEY_SECRET", + storageS3Region: "S3_PROTOCOL_REGION", +}; + +const FULL_VALUES: Record = { + API_URL: "http://127.0.0.1:54321", + REST_URL: "http://127.0.0.1:54321/rest/v1", + GRAPHQL_URL: "http://127.0.0.1:54321/graphql/v1", + STORAGE_S3_URL: "http://127.0.0.1:54321/storage/v1/s3", + MCP_URL: "http://127.0.0.1:54321/mcp", + FUNCTIONS_URL: "http://127.0.0.1:54321/functions/v1", + DB_URL: "postgresql://postgres:postgres@127.0.0.1:54322/postgres", + STUDIO_URL: "http://127.0.0.1:54323", + MAILPIT_URL: "http://127.0.0.1:54324", + PUBLISHABLE_KEY: "sb_publishable_ACJWlzQHlZjBrEguHvfOxg_3BJgxAaH", + SECRET_KEY: "sb_secret_N7UND0UgjKTVK-Uodkm0Hg_xSvEMPvz", + S3_PROTOCOL_ACCESS_KEY_ID: "625729a08b95bf1b7ff351a663f3a23c", + S3_PROTOCOL_ACCESS_KEY_SECRET: "850181e4652dd023b7a98c58ae0d2d34bd487ee0cc3254aed6eda37307425907", + S3_PROTOCOL_REGION: "local", +}; + +describe("legacyRenderStatusPretty", () => { + // Byte-for-byte parity with a real `tablewriter@v1.1.4` + `tw.StyleRounded` + // render of Go's `PrettyPrint` group layout (verified by running the actual + // vendored Go module against this exact value set — see the port plan). + it("matches the Go rounded-table fixture for a fully running stack", () => { + const out = stripAnsi(legacyRenderStatusPretty(FULL_VALUES, NAMES)); + + const expected = [ + "╭──────────────────────────────────────╮", + "│ 🔧 Development Tools │", + "├─────────┬────────────────────────────┤", + "│ Studio │ http://127.0.0.1:54323 │", + "│ Mailpit │ http://127.0.0.1:54324 │", + "│ MCP │ http://127.0.0.1:54321/mcp │", + "╰─────────┴────────────────────────────╯", + "", + "╭──────────────────────────────────────────────────────╮", + "│ 🌐 APIs │", + "├────────────────┬─────────────────────────────────────┤", + "│ Project URL │ http://127.0.0.1:54321 │", + "│ REST │ http://127.0.0.1:54321/rest/v1 │", + "│ GraphQL │ http://127.0.0.1:54321/graphql/v1 │", + "│ Edge Functions │ http://127.0.0.1:54321/functions/v1 │", + "╰────────────────┴─────────────────────────────────────╯", + "", + "╭───────────────────────────────────────────────────────────────╮", + "│ ⛁ Database │", + "├─────┬─────────────────────────────────────────────────────────┤", + "│ URL │ postgresql://postgres:postgres@127.0.0.1:54322/postgres │", + "╰─────┴─────────────────────────────────────────────────────────╯", + "", + "╭──────────────────────────────────────────────────────────────╮", + "│ 🔑 Authentication Keys │", + "├─────────────┬────────────────────────────────────────────────┤", + "│ Publishable │ sb_publishable_ACJWlzQHlZjBrEguHvfOxg_3BJgxAaH │", + "│ Secret │ sb_secret_N7UND0UgjKTVK-Uodkm0Hg_xSvEMPvz │", + "╰─────────────┴────────────────────────────────────────────────╯", + "", + "╭───────────────────────────────────────────────────────────────────────────────╮", + "│ 📦 Storage (S3) │", + "├────────────┬──────────────────────────────────────────────────────────────────┤", + "│ URL │ http://127.0.0.1:54321/storage/v1/s3 │", + "│ Access Key │ 625729a08b95bf1b7ff351a663f3a23c │", + "│ Secret Key │ 850181e4652dd023b7a98c58ae0d2d34bd487ee0cc3254aed6eda37307425907 │", + "│ Region │ local │", + "╰────────────┴──────────────────────────────────────────────────────────────────╯", + "", + ].join("\n"); + + expect(out).toBe(expected); + }); + + // Byte-for-byte parity with a real render of a single-row group (Database), + // confirming the header-vs-single-short-row column sizing. All other groups + // are empty in this fixture, so only the Database box should appear. + it("matches the Go rounded-table fixture for a single-row group", () => { + const out = stripAnsi( + legacyRenderStatusPretty({ DB_URL: FULL_VALUES.DB_URL ?? "" }, { ...NAMES, dbUrl: "DB_URL" }), + ); + + const expectedTable = [ + "╭───────────────────────────────────────────────────────────────╮", + "│ ⛁ Database │", + "├─────┬─────────────────────────────────────────────────────────┤", + "│ URL │ postgresql://postgres:postgres@127.0.0.1:54322/postgres │", + "╰─────┴─────────────────────────────────────────────────────────╯", + ].join("\n"); + + expect(out).toContain(expectedTable); + expect(out).toBe(["", "", expectedTable, "", "", ""].join("\n")); + }); + + // All other groups are empty in this fixture, so only the APIs box appears + // (only Project URL, the rest of the group's rows are excluded/disabled). + it("matches the Go rounded-table fixture for a partial APIs group", () => { + const out = stripAnsi(legacyRenderStatusPretty({ API_URL: "http://127.0.0.1:54321" }, NAMES)); + + const expectedTable = [ + "╭──────────────────────────────────────╮", + "│ 🌐 APIs │", + "├─────────────┬────────────────────────┤", + "│ Project URL │ http://127.0.0.1:54321 │", + "╰─────────────┴────────────────────────╯", + ].join("\n"); + + expect(out).toBe(["", expectedTable, "", "", "", ""].join("\n")); + }); + + it("skips a row whose value is missing from the value map", () => { + // Only Studio present; Mailpit/MCP absent from the map entirely (excluded + // or disabled upstream in `status.values.ts`) — same as an empty string. + const out = stripAnsi( + legacyRenderStatusPretty({ STUDIO_URL: "http://127.0.0.1:54323" }, NAMES), + ); + + expect(out).toContain("Studio"); + expect(out).not.toContain("Mailpit"); + expect(out).not.toContain("MCP"); + }); + + it("skips an entirely empty group but still emits its trailing blank line", () => { + // Nothing present for Development Tools; only the Database URL is set. + const out = stripAnsi(legacyRenderStatusPretty({ DB_URL: FULL_VALUES.DB_URL ?? "" }, NAMES)); + const lines = out.split("\n"); + + // No rounded-box characters before the Database group's own box. + expect(lines[0]).not.toMatch(/[╭│╰]/); + expect(lines[0]).toBe(""); + expect(out).not.toContain("Development Tools"); + expect(out).toContain("⛁ Database"); + }); + + it("returns only blank lines when every group is empty", () => { + const out = stripAnsi(legacyRenderStatusPretty({}, NAMES)); + // One blank line per group (5 groups), none of them rendering a table. + expect(out).toBe(["", "", "", "", ""].join("\n")); + }); + + // `legacyRenderStatusPretty` is a pure lookup: it renders whatever `values` + // are reachable through `names`' keys, with no opinion on how the caller + // derived either. This is NOT asserting that `--override-name` reaches + // pretty-mode output in production — `status.handler.ts` deliberately always + // calls this function with un-overridden names (matching Go's `PrettyPrint`, + // which unmarshals a fresh empty `EnvSet{}` rather than the CLI's overridden + // `CustomName`). This test only proves the renderer's KEY-based lookup itself + // works correctly for an arbitrary names/values pairing. + it("resolves values through whatever KEY the names parameter specifies", () => { + const overriddenNames: LegacyStatusOutputNames = { + ...NAMES, + apiUrl: "NEXT_PUBLIC_SUPABASE_URL", + }; + const out = stripAnsi( + legacyRenderStatusPretty( + { NEXT_PUBLIC_SUPABASE_URL: "http://127.0.0.1:54321" }, + overriddenNames, + ), + ); + expect(out).toContain("http://127.0.0.1:54321"); + }); +}); + +// None of `status`'s 18 fixed field labels or 5 fixed group titles are wide +// enough to exercise these two branches through the public +// `legacyRenderStatusPretty` API today (see the file-level doc comment on +// `status.pretty.ts`) — covered directly here as defensive Go-parity logic. +describe("legacyWrapStatusLabel", () => { + it("returns the text unwrapped when it fits within the width", () => { + expect(legacyWrapStatusLabel("Edge Functions", 16)).toEqual(["Edge Functions"]); + }); + + it("word-wraps a label wider than the column width", () => { + expect(legacyWrapStatusLabel("This Is A Very Long Label Name", 16)).toEqual([ + "This Is A Very", + "Long Label Name", + ]); + }); + + it("hard-breaks a single word wider than the column width", () => { + expect(legacyWrapStatusLabel("ThisIsAVeryLongSingleWordLabel", 16)).toEqual([ + "ThisIsAVeryLongSingleWordLabel", + ]); + }); + + it("does not emit a leading empty line when the very first word already overflows", () => { + expect(legacyWrapStatusLabel("SuperLongFirstWord Short", 10)).toEqual([ + "SuperLongFirstWord", + "Short", + ]); + }); + + it("returns the input unchanged for an empty label", () => { + expect(legacyWrapStatusLabel("", 10)).toEqual([""]); + }); + + it("returns the input unchanged for a whitespace-only label wider than the column", () => { + // Every "word" from splitting on spaces is itself empty, so `current` never + // accumulates anything to flush after the loop — the `lines` array stays + // empty and the function falls back to the original text. + expect(legacyWrapStatusLabel(" ", 2)).toEqual([" "]); + }); +}); + +describe("legacyStatusColumnLayout", () => { + it("sizes columns from content alone when the header already fits", () => { + const layout = legacyStatusColumnLayout(10, ["URL"], ["postgresql://short"]); + expect(layout.targetInner).toBe(3 + 2 + 1 + "postgresql://short".length + 2); + }); + + it("widens both columns evenly when the header is wider than the data", () => { + // Base data-driven layout: col0="a"(1+2=3), col1="b"(1+2=3), dataInner=3+1+3=7. + // A 10-char header needs innerWidth=12, so 5 extra columns split 3/2. + const layout = legacyStatusColumnLayout(10, ["a"], ["b"]); + expect(layout.targetInner).toBe(12); + expect(layout.col0Padded).toBe(6); + expect(layout.col1Padded).toBe(5); + }); + + it("caps column 0's content width at 16 even when a label is longer", () => { + const layout = legacyStatusColumnLayout(0, ["a".repeat(30)], ["b"]); + expect(layout.col0Padded).toBe(18); + }); +}); + +describe("legacyStatusHeaderWidth", () => { + it("uses the hardcoded emoji-aware width for a known fixed group title", () => { + expect(legacyStatusHeaderWidth("⛁ Database")).toBe(10); + }); + + it("falls back to code-point length for a title outside the fixed table", () => { + expect(legacyStatusHeaderWidth("Plain Title")).toBe("Plain Title".length); + }); +}); diff --git a/apps/cli/src/legacy/commands/status/status.values.ts b/apps/cli/src/legacy/commands/status/status.values.ts new file mode 100644 index 0000000000..4759a75af6 --- /dev/null +++ b/apps/cli/src/legacy/commands/status/status.values.ts @@ -0,0 +1,495 @@ +import type { ProjectConfig } from "@supabase/config"; + +import { dockerfileServiceImage } from "../../../shared/services/dockerfile-images.ts"; +import { legacyServiceContainerIds } from "../../shared/legacy-docker-ids.ts"; +import { + legacyEnvOverrideBool, + legacyResolveLocalConfigValues, + type LegacyLocalConfigValues, +} from "../../shared/legacy-local-config-values.ts"; + +/** + * Port of Go's `status.CustomName` + `toValues()` (`internal/status/status.go:29-97`). + * Each field's Go `env:"..."` tag carries two things: the dotted key + * `--override-name =` matches against (`fieldKey` below), and the + * default output env-var name (`defaultName`). `deprecated` fields (`inbucket`, + * `jwt_secret`, `anon_key`, `service_role_key`) are still emitted — Go's + * `deprecated` tag only affects a startup warning it never wires up for `status` + * (only `env.Unmarshal` reads the tag, and it does not warn), so no divergence here. + */ +export interface LegacyStatusField { + readonly fieldKey: string; + readonly defaultName: string; +} + +const API_URL: LegacyStatusField = { fieldKey: "api.url", defaultName: "API_URL" }; +const REST_URL: LegacyStatusField = { fieldKey: "api.rest_url", defaultName: "REST_URL" }; +const GRAPHQL_URL: LegacyStatusField = { fieldKey: "api.graphql_url", defaultName: "GRAPHQL_URL" }; +const STORAGE_S3_URL: LegacyStatusField = { + fieldKey: "api.storage_s3_url", + defaultName: "STORAGE_S3_URL", +}; +const MCP_URL: LegacyStatusField = { fieldKey: "api.mcp_url", defaultName: "MCP_URL" }; +const FUNCTIONS_URL: LegacyStatusField = { + fieldKey: "api.functions_url", + defaultName: "FUNCTIONS_URL", +}; +const DB_URL: LegacyStatusField = { fieldKey: "db.url", defaultName: "DB_URL" }; +const STUDIO_URL: LegacyStatusField = { fieldKey: "studio.url", defaultName: "STUDIO_URL" }; +const INBUCKET_URL: LegacyStatusField = { fieldKey: "inbucket.url", defaultName: "INBUCKET_URL" }; +const MAILPIT_URL: LegacyStatusField = { fieldKey: "mailpit.url", defaultName: "MAILPIT_URL" }; +const PUBLISHABLE_KEY: LegacyStatusField = { + fieldKey: "auth.publishable_key", + defaultName: "PUBLISHABLE_KEY", +}; +const SECRET_KEY: LegacyStatusField = { fieldKey: "auth.secret_key", defaultName: "SECRET_KEY" }; +const JWT_SECRET: LegacyStatusField = { fieldKey: "auth.jwt_secret", defaultName: "JWT_SECRET" }; +const ANON_KEY: LegacyStatusField = { fieldKey: "auth.anon_key", defaultName: "ANON_KEY" }; +const SERVICE_ROLE_KEY: LegacyStatusField = { + fieldKey: "auth.service_role_key", + defaultName: "SERVICE_ROLE_KEY", +}; +const STORAGE_S3_ACCESS_KEY_ID: LegacyStatusField = { + fieldKey: "storage.s3_access_key_id", + defaultName: "S3_PROTOCOL_ACCESS_KEY_ID", +}; +const STORAGE_S3_SECRET_ACCESS_KEY: LegacyStatusField = { + fieldKey: "storage.s3_secret_access_key", + defaultName: "S3_PROTOCOL_ACCESS_KEY_SECRET", +}; +const STORAGE_S3_REGION: LegacyStatusField = { + fieldKey: "storage.s3_region", + defaultName: "S3_PROTOCOL_REGION", +}; + +/** All 18 fields, in `CustomName` struct declaration order. */ +export const LEGACY_STATUS_FIELDS: ReadonlyArray = [ + API_URL, + REST_URL, + GRAPHQL_URL, + STORAGE_S3_URL, + MCP_URL, + FUNCTIONS_URL, + DB_URL, + STUDIO_URL, + INBUCKET_URL, + MAILPIT_URL, + PUBLISHABLE_KEY, + SECRET_KEY, + JWT_SECRET, + ANON_KEY, + SERVICE_ROLE_KEY, + STORAGE_S3_ACCESS_KEY_ID, + STORAGE_S3_SECRET_ACCESS_KEY, + STORAGE_S3_REGION, +]; + +/** The subset of {@link LEGACY_STATUS_FIELDS} the pretty renderer looks up by field. */ +export interface LegacyStatusOutputNames { + readonly apiUrl: string; + readonly restUrl: string; + readonly graphqlUrl: string; + readonly storageS3Url: string; + readonly mcpUrl: string; + readonly functionsUrl: string; + readonly dbUrl: string; + readonly studioUrl: string; + readonly mailpitUrl: string; + readonly publishableKey: string; + readonly secretKey: string; + readonly storageS3AccessKeyId: string; + readonly storageS3SecretAccessKey: string; + readonly storageS3Region: string; +} + +/** + * Resolves each field's output KEY, applying `--override-name =` + * remaps over the Go default names. `overrides` maps `fieldKey` (e.g. `"api.url"`) + * to the replacement output name, mirroring `env.Unmarshal`'s `default=` override. + */ +function resolveOutputNames(overrides: ReadonlyMap): LegacyStatusOutputNames { + const nameFor = (field: LegacyStatusField) => overrides.get(field.fieldKey) ?? field.defaultName; + return { + apiUrl: nameFor(API_URL), + restUrl: nameFor(REST_URL), + graphqlUrl: nameFor(GRAPHQL_URL), + storageS3Url: nameFor(STORAGE_S3_URL), + mcpUrl: nameFor(MCP_URL), + functionsUrl: nameFor(FUNCTIONS_URL), + dbUrl: nameFor(DB_URL), + studioUrl: nameFor(STUDIO_URL), + mailpitUrl: nameFor(MAILPIT_URL), + publishableKey: nameFor(PUBLISHABLE_KEY), + secretKey: nameFor(SECRET_KEY), + storageS3AccessKeyId: nameFor(STORAGE_S3_ACCESS_KEY_ID), + storageS3SecretAccessKey: nameFor(STORAGE_S3_SECRET_ACCESS_KEY), + storageS3Region: nameFor(STORAGE_S3_REGION), + }; +} + +/** + * Container ids `toValues()` gates each group on, taken from + * `legacyServiceContainerIds`'s alias order (`kong`, `auth`, `inbucket`, ..., + * `edge_runtime`, ...) — see `legacy-docker-ids.ts`. + */ +export interface LegacyStatusContainerIds { + readonly kong: string; + readonly auth: string; + readonly inbucket: string; + readonly rest: string; + readonly storage: string; + readonly studio: string; + readonly edgeRuntime: string; +} + +// Positional indices into `legacyServiceContainerIds`'s fixed 13-element +// array (`legacy-docker-ids.ts`'s `GetDockerIds()` order), named so a caller +// never has to destructure the array positionally. +const CONTAINER_INDEX = { + kong: 0, + auth: 1, + inbucket: 2, + rest: 4, + storage: 5, + studio: 8, + edgeRuntime: 9, +} as const; + +/** + * Derives {@link LegacyStatusContainerIds} from `legacyServiceContainerIds`'s + * flat array for a given project id. The array's length and order are a fixed + * Go-parity contract (13 elements, `GetDockerIds()` order), so every named + * index here is guaranteed present — this only exists to give the handler a + * named-field view instead of positional array destructuring. + */ +export function legacyStatusContainerIds(projectId: string): LegacyStatusContainerIds { + const ids = legacyServiceContainerIds(projectId); + const at = (index: number) => ids[index] ?? ""; + return { + kong: at(CONTAINER_INDEX.kong), + auth: at(CONTAINER_INDEX.auth), + inbucket: at(CONTAINER_INDEX.inbucket), + rest: at(CONTAINER_INDEX.rest), + storage: at(CONTAINER_INDEX.storage), + studio: at(CONTAINER_INDEX.studio), + edgeRuntime: at(CONTAINER_INDEX.edgeRuntime), + }; +} + +/** + * Port of Go's `utils.ShortContainerImageName` (`internal/utils/misc.go:33-39,75`): + * extracts the repo name between the (first) `/` and the (last) `:`, falling back to + * the full string when the image ref doesn't match (no slash, or no tag). + */ +export function legacyShortContainerImageName(imageName: string): string { + const match = /\/(.*):/.exec(imageName); + return match?.[1] ?? imageName; +} + +// Default image short names Go's `--exclude` also matches against +// (`internal/status/status.go:55-61`), one per gated service. Sourced from the same +// embedded Dockerfile manifest Go parses (`dockerfileServiceImage`), so a version bump +// there is picked up automatically. Pinned-version substitution +// (`legacy-db-image.ts`'s `replaceImageTag`) only ever rewrites the portion after the +// first `:`, which `legacyShortContainerImageName` discards — so these are invariant to +// version pinning and no `.temp/-version` file needs to be read here. +const KONG_IMAGE_NAME = legacyShortContainerImageName(dockerfileServiceImage("kong")); +const POSTGREST_IMAGE_NAME = legacyShortContainerImageName(dockerfileServiceImage("postgrest")); +const STUDIO_IMAGE_NAME = legacyShortContainerImageName(dockerfileServiceImage("studio")); +const GOTRUE_IMAGE_NAME = legacyShortContainerImageName(dockerfileServiceImage("gotrue")); +const MAILPIT_IMAGE_NAME = legacyShortContainerImageName(dockerfileServiceImage("mailpit")); +const STORAGE_IMAGE_NAME = legacyShortContainerImageName(dockerfileServiceImage("storage")); +const EDGE_RUNTIME_IMAGE_NAME = legacyShortContainerImageName( + dockerfileServiceImage("edgeruntime"), +); + +export interface LegacyStatusValuesResult { + readonly values: Record; + readonly names: LegacyStatusOutputNames; + readonly local: LegacyLocalConfigValues; +} + +/** + * Everything `toValues()` needs that does NOT depend on `--override-name` — + * i.e. every field except the output KEY remapping. Resolving this once and + * reusing it for both the env/json/toml/yaml values (real overrides) and the + * pretty-table values (Go always recomputes with an empty override map, + * `status.go:236-243`) avoids re-reading `auth.signing_keys_path` and + * re-signing the anon/service_role JWTs a second time per invocation. + */ +export interface LegacyStatusState { + readonly config: ProjectConfig; + readonly local: LegacyLocalConfigValues; + readonly kongEnabled: boolean; + readonly postgrestEnabled: boolean; + readonly studioEnabled: boolean; + readonly authEnabled: boolean; + readonly inbucketEnabled: boolean; + readonly storageEnabled: boolean; + readonly functionsEnabled: boolean; + readonly storageS3ProtocolEnabled: boolean; +} + +/** + * The config-load/`Validate`-equivalent half of {@link LegacyStatusState} — + * everything that can THROW, and none of it depends on `excluded`/ + * `containerIds`. Split out so `status.handler.ts` can resolve and validate + * this before any Docker call, matching Go's `flags.LoadConfig` (config load + * + `Validate`, `internal/utils/flags/config_path.go:12` -> + * `pkg/config/config.go:882`) running entirely before `assertContainerHealthy`/ + * container listing (`internal/status/status.go:101-116`) — a bad + * `auth.jwt_secret` or malformed `SUPABASE_*_PORT`/`SUPABASE_*_ENABLED` + * override must fail here, not be masked by a Docker/DB error when the local + * stack happens to be unavailable. + */ +export interface LegacyStatusLocalState { + readonly config: ProjectConfig; + readonly local: LegacyLocalConfigValues; + readonly apiEnabled: boolean; + readonly studioSectionEnabled: boolean; + readonly authSectionEnabled: boolean; + readonly inbucketSectionEnabled: boolean; + readonly storageSectionEnabled: boolean; + readonly edgeRuntimeEnabled: boolean; + readonly storageS3ProtocolEnabled: boolean; +} + +/** + * Port of the throwing, non-Docker-dependent half of Go's + * `(*CustomName).toValues(exclude...)` (`internal/status/status.go:50-97`): + * resolves local config values (URLs, keys — can throw, see + * {@link legacyResolveLocalConfigValues}) and the per-service `.enabled` gates, + * with NO reference to `excluded`/`containerIds` — see {@link legacyGateStatusState} + * for the Docker-dependent, non-throwing half this composes with (in + * `status.handler.ts`, or via {@link legacyStatusValues} for callers that + * don't need to run validation before Docker calls). + * + * Each `.enabled` gate is read through {@link legacyEnvOverrideBool}, not the + * raw decoded `config.
.enabled`, because Go's `status.toValues()` + * (`status.go:55-61`) reads `utils.Config.*.Enabled` — a package-level struct + * Viper has already applied any `SUPABASE_
_ENABLED` env/dotenv + * override to (`SetEnvPrefix("SUPABASE")` + `AutomaticEnv()` + + * `ExperimentalBindStruct()`, `pkg/config/config.go:580-586`) — generically, + * not just for `auth.enabled`. Skipping this would mean a stack Go started + * with e.g. `SUPABASE_API_ENABLED=true` over a `false` TOML value has Kong/ + * PostgREST running while native `status` omits them entirely. + * + * @throws {LegacyInvalidJwtSecretError} when `auth.jwt_secret` is set but too short. + * @throws {LegacyInvalidPortEnvOverrideError} when a `SUPABASE_*_PORT` env/dotenv + * override doesn't parse as a valid port. + * @throws {LegacyInvalidBoolEnvOverrideError} when a `SUPABASE_*_ENABLED` env/dotenv + * override doesn't parse as a valid bool. + * @throws when `auth.signing_keys_path` is set but the file is missing, malformed, + * or its first key is unsupported — see {@link legacyGenerateAsymmetricGoJwt}. + */ +export function legacyResolveStatusLocalState( + config: ProjectConfig, + hostname: string, + workdir: string, + projectEnvValues: Readonly> | undefined = undefined, + /** `LoadedProjectConfig.document` — see {@link legacyResolveLocalConfigValues}'s doc comment. */ + document: Readonly> | undefined = undefined, +): LegacyStatusLocalState { + const local = legacyResolveLocalConfigValues( + config, + hostname, + workdir, + projectEnvValues, + document, + ); + + const apiEnabled = legacyEnvOverrideBool( + "SUPABASE_API_ENABLED", + config.api.enabled, + "api.enabled", + projectEnvValues, + ); + const studioSectionEnabled = legacyEnvOverrideBool( + "SUPABASE_STUDIO_ENABLED", + config.studio.enabled, + "studio.enabled", + projectEnvValues, + ); + const authSectionEnabled = legacyEnvOverrideBool( + "SUPABASE_AUTH_ENABLED", + config.auth.enabled, + "auth.enabled", + projectEnvValues, + ); + const inbucketSectionEnabled = legacyEnvOverrideBool( + "SUPABASE_LOCAL_SMTP_ENABLED", + config.local_smtp.enabled, + "local_smtp.enabled", + projectEnvValues, + ); + const storageSectionEnabled = legacyEnvOverrideBool( + "SUPABASE_STORAGE_ENABLED", + config.storage.enabled, + "storage.enabled", + projectEnvValues, + ); + const edgeRuntimeEnabled = legacyEnvOverrideBool( + "SUPABASE_EDGE_RUNTIME_ENABLED", + config.edge_runtime.enabled, + "edge_runtime.enabled", + projectEnvValues, + ); + const storageS3ProtocolEnabled = legacyEnvOverrideBool( + "SUPABASE_STORAGE_S3_PROTOCOL_ENABLED", + config.storage.s3_protocol.enabled, + "storage.s3_protocol.enabled", + projectEnvValues, + ); + + return { + config, + local, + apiEnabled, + studioSectionEnabled, + authSectionEnabled, + inbucketSectionEnabled, + storageSectionEnabled, + edgeRuntimeEnabled, + storageS3ProtocolEnabled, + }; +} + +/** + * The Docker-dependent, non-throwing half of Go's `toValues()`: applies + * `excluded` (matching each gated service by its container id + * (`legacyStatusContainerIds`) OR its default Docker image short name + * (`legacyShortContainerImageName` above) — the 6 relevant Go config fields + * (`Api.KongImage`, `Api.Image`, `Studio.Image`, `Auth.Image`, `Inbucket.Image`, + * `Storage.Image`, `EdgeRuntime.Image`) all carry `toml:"-"`, so they're never + * user-overridable and the default image is always the one to check) on top of + * an already-resolved {@link LegacyStatusLocalState}. Pure: every throwing + * concern already ran in {@link legacyResolveStatusLocalState}. + */ +export function legacyGateStatusState( + localState: LegacyStatusLocalState, + containerIds: LegacyStatusContainerIds, + excluded: ReadonlyArray, +): LegacyStatusState { + const { config, local } = localState; + const { apiEnabled, studioSectionEnabled, authSectionEnabled } = localState; + const { inbucketSectionEnabled, storageSectionEnabled } = localState; + const { edgeRuntimeEnabled, storageS3ProtocolEnabled } = localState; + const isExcluded = (id: string) => excluded.includes(id); + + const kongEnabled = apiEnabled && !isExcluded(containerIds.kong) && !isExcluded(KONG_IMAGE_NAME); + const postgrestEnabled = + kongEnabled && !isExcluded(containerIds.rest) && !isExcluded(POSTGREST_IMAGE_NAME); + const studioEnabled = + studioSectionEnabled && !isExcluded(containerIds.studio) && !isExcluded(STUDIO_IMAGE_NAME); + const authEnabled = + authSectionEnabled && !isExcluded(containerIds.auth) && !isExcluded(GOTRUE_IMAGE_NAME); + const inbucketEnabled = + inbucketSectionEnabled && !isExcluded(containerIds.inbucket) && !isExcluded(MAILPIT_IMAGE_NAME); + const storageEnabled = + storageSectionEnabled && !isExcluded(containerIds.storage) && !isExcluded(STORAGE_IMAGE_NAME); + const functionsEnabled = + edgeRuntimeEnabled && + !isExcluded(containerIds.edgeRuntime) && + !isExcluded(EDGE_RUNTIME_IMAGE_NAME); + + return { + config, + local, + kongEnabled, + postgrestEnabled, + studioEnabled, + authEnabled, + inbucketEnabled, + storageEnabled, + functionsEnabled, + storageS3ProtocolEnabled, + }; +} + +/** + * Applies `--override-name` remapping to an already-resolved {@link LegacyStatusState}. + * Pure and non-throwing — every failure mode of `toValues()` lives in + * {@link legacyResolveStatusLocalState}, which runs once per `status` invocation. + */ +export function legacyStatusValuesFromState( + state: LegacyStatusState, + overrides: ReadonlyMap, +): LegacyStatusValuesResult { + const { local, kongEnabled, postgrestEnabled, studioEnabled, authEnabled } = state; + const { inbucketEnabled, storageEnabled, functionsEnabled, storageS3ProtocolEnabled } = state; + const names = resolveOutputNames(overrides); + + // Go always sets db.url unconditionally, before any gating (status.go:52). + const values: Record = { + [names.dbUrl]: local.dbUrl, + }; + + if (kongEnabled) { + values[names.apiUrl] = local.apiUrl; + if (postgrestEnabled) { + values[names.restUrl] = local.restUrl; + values[names.graphqlUrl] = local.graphqlUrl; + } + if (functionsEnabled) { + values[names.functionsUrl] = local.functionsUrl; + } + if (studioEnabled) { + values[names.mcpUrl] = local.mcpUrl; + } + } + if (studioEnabled) { + values[names.studioUrl] = local.studioUrl; + } + if (authEnabled) { + values[names.publishableKey] = local.publishableKey; + values[names.secretKey] = local.secretKey; + values[overrides.get(JWT_SECRET.fieldKey) ?? JWT_SECRET.defaultName] = local.jwtSecret; + values[overrides.get(ANON_KEY.fieldKey) ?? ANON_KEY.defaultName] = local.anonKey; + values[overrides.get(SERVICE_ROLE_KEY.fieldKey) ?? SERVICE_ROLE_KEY.defaultName] = + local.serviceRoleKey; + } + if (inbucketEnabled) { + values[names.mailpitUrl] = local.mailpitUrl; + values[overrides.get(INBUCKET_URL.fieldKey) ?? INBUCKET_URL.defaultName] = local.mailpitUrl; + } + if (storageEnabled && storageS3ProtocolEnabled) { + values[names.storageS3Url] = local.storageS3Url; + values[names.storageS3AccessKeyId] = local.storageS3AccessKeyId; + values[names.storageS3SecretAccessKey] = local.storageS3SecretAccessKey; + values[names.storageS3Region] = local.storageS3Region; + } + + return { values, names, local }; +} + +/** + * Convenience wrapper combining {@link legacyResolveStatusLocalState} + + * {@link legacyGateStatusState} + {@link legacyStatusValuesFromState} in one + * call — used directly by tests that only need a single override map. + * `status.handler.ts` calls the three separately instead, so it can resolve + + * validate `localState` before any Docker call (see + * {@link legacyResolveStatusLocalState}'s doc comment), and reuse the gated + * `state` for both the real and pretty-mode (empty-override) value maps + * without recomputing `local`. + */ +export function legacyStatusValues( + config: ProjectConfig, + containerIds: LegacyStatusContainerIds, + hostname: string, + excluded: ReadonlyArray, + overrides: ReadonlyMap, + workdir: string, + projectEnvValues: Readonly> | undefined = undefined, + /** `LoadedProjectConfig.document` — see {@link legacyResolveLocalConfigValues}'s doc comment. */ + document: Readonly> | undefined = undefined, +): LegacyStatusValuesResult { + const localState = legacyResolveStatusLocalState( + config, + hostname, + workdir, + projectEnvValues, + document, + ); + const state = legacyGateStatusState(localState, containerIds, excluded); + return legacyStatusValuesFromState(state, overrides); +} diff --git a/apps/cli/src/legacy/commands/status/status.values.unit.test.ts b/apps/cli/src/legacy/commands/status/status.values.unit.test.ts new file mode 100644 index 0000000000..3ae5f60004 --- /dev/null +++ b/apps/cli/src/legacy/commands/status/status.values.unit.test.ts @@ -0,0 +1,793 @@ +import { ProjectConfigSchema, type ProjectConfig } from "@supabase/config"; +import { Schema } from "effect"; +import { describe, expect, it } from "vitest"; + +import { + legacyShortContainerImageName, + legacyStatusContainerIds, + legacyStatusValues, + type LegacyStatusContainerIds, +} from "./status.values.ts"; + +const decodeConfig = Schema.decodeUnknownSync(ProjectConfigSchema); + +function baseConfig(overrides: Record = {}): ProjectConfig { + return decodeConfig({ project_id: "test", ...overrides }); +} + +const CONTAINER_IDS: LegacyStatusContainerIds = { + kong: "supabase_kong_test", + auth: "supabase_auth_test", + inbucket: "supabase_inbucket_test", + rest: "supabase_rest_test", + storage: "supabase_storage_test", + studio: "supabase_studio_test", + edgeRuntime: "supabase_edge_runtime_test", +}; + +const HOSTNAME = "127.0.0.1"; +const NONE: ReadonlyArray = []; +const NO_OVERRIDES = new Map(); +const WORKDIR = "/tmp/status-values-test"; + +describe("legacyStatusValues", () => { + it("emits DB_URL unconditionally, even when every other service is disabled/excluded", () => { + const config = baseConfig({ + api: { enabled: false }, + studio: { enabled: false }, + auth: { enabled: false }, + local_smtp: { enabled: false }, + storage: { enabled: false }, + edge_runtime: { enabled: false }, + }); + const { values } = legacyStatusValues( + config, + CONTAINER_IDS, + HOSTNAME, + NONE, + NO_OVERRIDES, + WORKDIR, + ); + expect(Object.keys(values)).toEqual(["DB_URL"]); + expect(values.DB_URL).toContain("postgresql://postgres:postgres@127.0.0.1"); + }); + + describe("api / kong gating", () => { + it("includes API_URL when api.enabled", () => { + const { values } = legacyStatusValues( + baseConfig(), + CONTAINER_IDS, + HOSTNAME, + NONE, + NO_OVERRIDES, + WORKDIR, + ); + expect(values.API_URL).toBeDefined(); + }); + + it("omits API_URL when api.enabled is false", () => { + const config = baseConfig({ api: { enabled: false } }); + const { values } = legacyStatusValues( + config, + CONTAINER_IDS, + HOSTNAME, + NONE, + NO_OVERRIDES, + WORKDIR, + ); + expect(values.API_URL).toBeUndefined(); + }); + + it("omits API_URL when the kong container id is excluded", () => { + const { values } = legacyStatusValues( + baseConfig(), + CONTAINER_IDS, + HOSTNAME, + [CONTAINER_IDS.kong], + NO_OVERRIDES, + WORKDIR, + ); + expect(values.API_URL).toBeUndefined(); + }); + + it("omits API_URL when the kong image short name is excluded", () => { + const { values } = legacyStatusValues( + baseConfig(), + CONTAINER_IDS, + HOSTNAME, + ["kong"], + NO_OVERRIDES, + WORKDIR, + ); + expect(values.API_URL).toBeUndefined(); + }); + + it("omits REST/GraphQL when kong is disabled even though postgrest is enabled", () => { + const config = baseConfig({ api: { enabled: false } }); + const { values } = legacyStatusValues( + config, + CONTAINER_IDS, + HOSTNAME, + NONE, + NO_OVERRIDES, + WORKDIR, + ); + expect(values.REST_URL).toBeUndefined(); + expect(values.GRAPHQL_URL).toBeUndefined(); + }); + + it("omits REST/GraphQL when only the rest container id is excluded", () => { + const { values } = legacyStatusValues( + baseConfig(), + CONTAINER_IDS, + HOSTNAME, + [CONTAINER_IDS.rest], + NO_OVERRIDES, + WORKDIR, + ); + expect(values.API_URL).toBeDefined(); + expect(values.REST_URL).toBeUndefined(); + expect(values.GRAPHQL_URL).toBeUndefined(); + }); + + it("includes REST/GraphQL when kong and postgrest are both enabled", () => { + const { values } = legacyStatusValues( + baseConfig(), + CONTAINER_IDS, + HOSTNAME, + NONE, + NO_OVERRIDES, + WORKDIR, + ); + expect(values.REST_URL).toBeDefined(); + expect(values.GRAPHQL_URL).toBeDefined(); + }); + + it("omits REST/GraphQL when the postgrest image short name is excluded", () => { + const { values } = legacyStatusValues( + baseConfig(), + CONTAINER_IDS, + HOSTNAME, + ["postgrest"], + NO_OVERRIDES, + WORKDIR, + ); + expect(values.API_URL).toBeDefined(); + expect(values.REST_URL).toBeUndefined(); + expect(values.GRAPHQL_URL).toBeUndefined(); + }); + }); + + describe("functions gating", () => { + it("includes FUNCTIONS_URL when kong and edge_runtime are both enabled", () => { + const { values } = legacyStatusValues( + baseConfig(), + CONTAINER_IDS, + HOSTNAME, + NONE, + NO_OVERRIDES, + WORKDIR, + ); + expect(values.FUNCTIONS_URL).toBeDefined(); + }); + + it("omits FUNCTIONS_URL when edge_runtime.enabled is false", () => { + const config = baseConfig({ edge_runtime: { enabled: false } }); + const { values } = legacyStatusValues( + config, + CONTAINER_IDS, + HOSTNAME, + NONE, + NO_OVERRIDES, + WORKDIR, + ); + expect(values.FUNCTIONS_URL).toBeUndefined(); + }); + + it("omits FUNCTIONS_URL when kong is disabled even though edge_runtime is enabled", () => { + const config = baseConfig({ api: { enabled: false } }); + const { values } = legacyStatusValues( + config, + CONTAINER_IDS, + HOSTNAME, + NONE, + NO_OVERRIDES, + WORKDIR, + ); + expect(values.FUNCTIONS_URL).toBeUndefined(); + }); + + it("omits FUNCTIONS_URL when the edge_runtime container id is excluded", () => { + const { values } = legacyStatusValues( + baseConfig(), + CONTAINER_IDS, + HOSTNAME, + [CONTAINER_IDS.edgeRuntime], + NO_OVERRIDES, + WORKDIR, + ); + expect(values.FUNCTIONS_URL).toBeUndefined(); + }); + + it("omits FUNCTIONS_URL when the edge-runtime image short name is excluded", () => { + // The image repo name (`supabase/edge-runtime`) differs from the Dockerfile's + // build alias (`edgeruntime`) — the short name Go matches against is the former. + const { values } = legacyStatusValues( + baseConfig(), + CONTAINER_IDS, + HOSTNAME, + ["edge-runtime"], + NO_OVERRIDES, + WORKDIR, + ); + expect(values.FUNCTIONS_URL).toBeUndefined(); + }); + }); + + describe("studio / mcp gating", () => { + it("includes STUDIO_URL when studio.enabled", () => { + const { values } = legacyStatusValues( + baseConfig(), + CONTAINER_IDS, + HOSTNAME, + NONE, + NO_OVERRIDES, + WORKDIR, + ); + expect(values.STUDIO_URL).toBeDefined(); + }); + + it("omits STUDIO_URL when studio.enabled is false", () => { + const config = baseConfig({ studio: { enabled: false } }); + const { values } = legacyStatusValues( + config, + CONTAINER_IDS, + HOSTNAME, + NONE, + NO_OVERRIDES, + WORKDIR, + ); + expect(values.STUDIO_URL).toBeUndefined(); + }); + + it("omits STUDIO_URL when the studio container id is excluded", () => { + const { values } = legacyStatusValues( + baseConfig(), + CONTAINER_IDS, + HOSTNAME, + [CONTAINER_IDS.studio], + NO_OVERRIDES, + WORKDIR, + ); + expect(values.STUDIO_URL).toBeUndefined(); + }); + + it("omits STUDIO_URL when the studio image short name is excluded", () => { + const { values } = legacyStatusValues( + baseConfig(), + CONTAINER_IDS, + HOSTNAME, + ["studio"], + NO_OVERRIDES, + WORKDIR, + ); + expect(values.STUDIO_URL).toBeUndefined(); + }); + + it("includes MCP_URL only when both kong and studio are enabled", () => { + const { values } = legacyStatusValues( + baseConfig(), + CONTAINER_IDS, + HOSTNAME, + NONE, + NO_OVERRIDES, + WORKDIR, + ); + expect(values.MCP_URL).toBeDefined(); + }); + + it("omits MCP_URL when kong is disabled", () => { + const config = baseConfig({ api: { enabled: false } }); + const { values } = legacyStatusValues( + config, + CONTAINER_IDS, + HOSTNAME, + NONE, + NO_OVERRIDES, + WORKDIR, + ); + expect(values.MCP_URL).toBeUndefined(); + }); + + it("omits MCP_URL when studio is disabled", () => { + const config = baseConfig({ studio: { enabled: false } }); + const { values } = legacyStatusValues( + config, + CONTAINER_IDS, + HOSTNAME, + NONE, + NO_OVERRIDES, + WORKDIR, + ); + expect(values.MCP_URL).toBeUndefined(); + }); + }); + + describe("auth gating", () => { + it("includes all 5 auth fields when auth.enabled", () => { + const { values } = legacyStatusValues( + baseConfig(), + CONTAINER_IDS, + HOSTNAME, + NONE, + NO_OVERRIDES, + WORKDIR, + ); + expect(values.PUBLISHABLE_KEY).toBeDefined(); + expect(values.SECRET_KEY).toBeDefined(); + expect(values.JWT_SECRET).toBeDefined(); + expect(values.ANON_KEY).toBeDefined(); + expect(values.SERVICE_ROLE_KEY).toBeDefined(); + }); + + it("omits all 5 auth fields when auth.enabled is false", () => { + const config = baseConfig({ auth: { enabled: false } }); + const { values } = legacyStatusValues( + config, + CONTAINER_IDS, + HOSTNAME, + NONE, + NO_OVERRIDES, + WORKDIR, + ); + expect(values.PUBLISHABLE_KEY).toBeUndefined(); + expect(values.SECRET_KEY).toBeUndefined(); + expect(values.JWT_SECRET).toBeUndefined(); + expect(values.ANON_KEY).toBeUndefined(); + expect(values.SERVICE_ROLE_KEY).toBeUndefined(); + }); + + it("omits all 5 auth fields when the auth container id is excluded", () => { + const { values } = legacyStatusValues( + baseConfig(), + CONTAINER_IDS, + HOSTNAME, + [CONTAINER_IDS.auth], + NO_OVERRIDES, + WORKDIR, + ); + expect(values.PUBLISHABLE_KEY).toBeUndefined(); + }); + + it("omits all 5 auth fields when the gotrue image short name is excluded", () => { + const { values } = legacyStatusValues( + baseConfig(), + CONTAINER_IDS, + HOSTNAME, + ["gotrue"], + NO_OVERRIDES, + WORKDIR, + ); + expect(values.PUBLISHABLE_KEY).toBeUndefined(); + }); + }); + + describe("inbucket/mailpit gating", () => { + it("includes MAILPIT_URL and the deprecated INBUCKET_URL alias when local_smtp.enabled", () => { + const { values } = legacyStatusValues( + baseConfig(), + CONTAINER_IDS, + HOSTNAME, + NONE, + NO_OVERRIDES, + WORKDIR, + ); + expect(values.MAILPIT_URL).toBeDefined(); + expect(values.INBUCKET_URL).toBe(values.MAILPIT_URL); + }); + + it("omits MAILPIT_URL/INBUCKET_URL when local_smtp.enabled is false", () => { + const config = baseConfig({ local_smtp: { enabled: false } }); + const { values } = legacyStatusValues( + config, + CONTAINER_IDS, + HOSTNAME, + NONE, + NO_OVERRIDES, + WORKDIR, + ); + expect(values.MAILPIT_URL).toBeUndefined(); + expect(values.INBUCKET_URL).toBeUndefined(); + }); + + it("omits MAILPIT_URL/INBUCKET_URL when the inbucket container id is excluded", () => { + const { values } = legacyStatusValues( + baseConfig(), + CONTAINER_IDS, + HOSTNAME, + [CONTAINER_IDS.inbucket], + NO_OVERRIDES, + WORKDIR, + ); + expect(values.MAILPIT_URL).toBeUndefined(); + }); + + it("omits MAILPIT_URL/INBUCKET_URL when the mailpit image short name is excluded", () => { + const { values } = legacyStatusValues( + baseConfig(), + CONTAINER_IDS, + HOSTNAME, + ["mailpit"], + NO_OVERRIDES, + WORKDIR, + ); + expect(values.MAILPIT_URL).toBeUndefined(); + }); + }); + + describe("storage / s3 gating", () => { + it("includes all 4 storage S3 fields when storage.enabled and s3_protocol.enabled", () => { + const { values } = legacyStatusValues( + baseConfig(), + CONTAINER_IDS, + HOSTNAME, + NONE, + NO_OVERRIDES, + WORKDIR, + ); + expect(values.STORAGE_S3_URL).toBeDefined(); + expect(values.S3_PROTOCOL_ACCESS_KEY_ID).toBeDefined(); + expect(values.S3_PROTOCOL_ACCESS_KEY_SECRET).toBeDefined(); + expect(values.S3_PROTOCOL_REGION).toBeDefined(); + }); + + it("omits storage S3 fields when storage.enabled is false", () => { + const config = baseConfig({ storage: { enabled: false } }); + const { values } = legacyStatusValues( + config, + CONTAINER_IDS, + HOSTNAME, + NONE, + NO_OVERRIDES, + WORKDIR, + ); + expect(values.STORAGE_S3_URL).toBeUndefined(); + }); + + it("omits storage S3 fields when the storage container id is excluded", () => { + const { values } = legacyStatusValues( + baseConfig(), + CONTAINER_IDS, + HOSTNAME, + [CONTAINER_IDS.storage], + NO_OVERRIDES, + WORKDIR, + ); + expect(values.STORAGE_S3_URL).toBeUndefined(); + }); + + it("omits storage S3 fields when the storage-api image short name is excluded", () => { + // The image repo name (`supabase/storage-api`) differs from the Dockerfile's + // build alias (`storage`) — the short name Go matches against is the former. + const { values } = legacyStatusValues( + baseConfig(), + CONTAINER_IDS, + HOSTNAME, + ["storage-api"], + NO_OVERRIDES, + WORKDIR, + ); + expect(values.STORAGE_S3_URL).toBeUndefined(); + }); + + it("omits storage S3 fields when storage.s3_protocol.enabled is false", () => { + const config = baseConfig({ storage: { s3_protocol: { enabled: false } } }); + const { values } = legacyStatusValues( + config, + CONTAINER_IDS, + HOSTNAME, + NONE, + NO_OVERRIDES, + WORKDIR, + ); + expect(values.STORAGE_S3_URL).toBeUndefined(); + expect(values.S3_PROTOCOL_ACCESS_KEY_ID).toBeUndefined(); + }); + }); + + describe("SUPABASE_*_ENABLED env overrides", () => { + // Go's `status.toValues()` (`status.go:55-61`) reads `utils.Config.*.Enabled` + // AFTER Viper's `SetEnvPrefix("SUPABASE")` + `AutomaticEnv()` binding + // (`pkg/config/config.go:580-586`) has already applied any + // `SUPABASE_
_ENABLED` override — generically, not just for auth. + // `legacyResolveStatusLocalState` must read the same post-override value + // for every gate, not the raw decoded `config.
.enabled`. + + it("includes API_URL/REST_URL when SUPABASE_API_ENABLED overrides a disabled api.enabled", () => { + const config = baseConfig({ api: { enabled: false } }); + const { values } = legacyStatusValues( + config, + CONTAINER_IDS, + HOSTNAME, + NONE, + NO_OVERRIDES, + WORKDIR, + { + SUPABASE_API_ENABLED: "true", + }, + ); + expect(values.API_URL).toBeDefined(); + expect(values.REST_URL).toBeDefined(); + }); + + it("omits API_URL when SUPABASE_API_ENABLED=false overrides an enabled api.enabled", () => { + const { values } = legacyStatusValues( + baseConfig(), + CONTAINER_IDS, + HOSTNAME, + NONE, + NO_OVERRIDES, + WORKDIR, + { SUPABASE_API_ENABLED: "false" }, + ); + expect(values.API_URL).toBeUndefined(); + }); + + it("includes STUDIO_URL when SUPABASE_STUDIO_ENABLED overrides a disabled studio.enabled", () => { + const config = baseConfig({ studio: { enabled: false } }); + const { values } = legacyStatusValues( + config, + CONTAINER_IDS, + HOSTNAME, + NONE, + NO_OVERRIDES, + WORKDIR, + { + SUPABASE_STUDIO_ENABLED: "true", + }, + ); + expect(values.STUDIO_URL).toBeDefined(); + }); + + it("includes the 5 auth fields when SUPABASE_AUTH_ENABLED overrides a disabled auth.enabled", () => { + // Reproduces the exact scenario a Go-started stack can hit: TOML says + // auth is disabled, but the running stack was actually started with + // SUPABASE_AUTH_ENABLED=true from the shell/dotenv, so Auth is up and + // status must still print its credentials. + const config = baseConfig({ auth: { enabled: false } }); + const { values } = legacyStatusValues( + config, + CONTAINER_IDS, + HOSTNAME, + NONE, + NO_OVERRIDES, + WORKDIR, + { + SUPABASE_AUTH_ENABLED: "true", + }, + ); + expect(values.PUBLISHABLE_KEY).toBeDefined(); + expect(values.ANON_KEY).toBeDefined(); + expect(values.SERVICE_ROLE_KEY).toBeDefined(); + }); + + it("omits the 5 auth fields when SUPABASE_AUTH_ENABLED=false overrides an enabled auth.enabled", () => { + const { values } = legacyStatusValues( + baseConfig(), + CONTAINER_IDS, + HOSTNAME, + NONE, + NO_OVERRIDES, + WORKDIR, + { SUPABASE_AUTH_ENABLED: "false" }, + ); + expect(values.PUBLISHABLE_KEY).toBeUndefined(); + }); + + it("includes MAILPIT_URL when SUPABASE_LOCAL_SMTP_ENABLED overrides a disabled local_smtp.enabled", () => { + const config = baseConfig({ local_smtp: { enabled: false } }); + const { values } = legacyStatusValues( + config, + CONTAINER_IDS, + HOSTNAME, + NONE, + NO_OVERRIDES, + WORKDIR, + { + SUPABASE_LOCAL_SMTP_ENABLED: "true", + }, + ); + expect(values.MAILPIT_URL).toBeDefined(); + }); + + it("includes storage S3 fields when SUPABASE_STORAGE_ENABLED overrides a disabled storage.enabled", () => { + const config = baseConfig({ storage: { enabled: false } }); + const { values } = legacyStatusValues( + config, + CONTAINER_IDS, + HOSTNAME, + NONE, + NO_OVERRIDES, + WORKDIR, + { + SUPABASE_STORAGE_ENABLED: "true", + }, + ); + expect(values.STORAGE_S3_URL).toBeDefined(); + }); + + it("includes FUNCTIONS_URL when SUPABASE_EDGE_RUNTIME_ENABLED overrides a disabled edge_runtime.enabled", () => { + const config = baseConfig({ edge_runtime: { enabled: false } }); + const { values } = legacyStatusValues( + config, + CONTAINER_IDS, + HOSTNAME, + NONE, + NO_OVERRIDES, + WORKDIR, + { + SUPABASE_EDGE_RUNTIME_ENABLED: "true", + }, + ); + expect(values.FUNCTIONS_URL).toBeDefined(); + }); + + it("includes storage S3 fields when SUPABASE_STORAGE_S3_PROTOCOL_ENABLED overrides a disabled s3_protocol.enabled", () => { + const config = baseConfig({ storage: { s3_protocol: { enabled: false } } }); + const { values } = legacyStatusValues( + config, + CONTAINER_IDS, + HOSTNAME, + NONE, + NO_OVERRIDES, + WORKDIR, + { + SUPABASE_STORAGE_S3_PROTOCOL_ENABLED: "true", + }, + ); + expect(values.STORAGE_S3_URL).toBeDefined(); + }); + + it("omits storage S3 fields when SUPABASE_STORAGE_S3_PROTOCOL_ENABLED=false overrides an enabled s3_protocol.enabled", () => { + const { values } = legacyStatusValues( + baseConfig(), + CONTAINER_IDS, + HOSTNAME, + NONE, + NO_OVERRIDES, + WORKDIR, + { SUPABASE_STORAGE_S3_PROTOCOL_ENABLED: "false" }, + ); + expect(values.STORAGE_S3_URL).toBeUndefined(); + }); + }); + + describe("--override-name remapping", () => { + it("remaps a field's output KEY while leaving the value unchanged", () => { + const overrides = new Map([["api.url", "NEXT_PUBLIC_SUPABASE_URL"]]); + const { values } = legacyStatusValues( + baseConfig(), + CONTAINER_IDS, + HOSTNAME, + NONE, + overrides, + WORKDIR, + ); + expect(values.API_URL).toBeUndefined(); + expect(values.NEXT_PUBLIC_SUPABASE_URL).toBe("http://127.0.0.1:54321"); + }); + + it("remaps every field independently when multiple overrides are given", () => { + const overrides = new Map([ + ["api.url", "CUSTOM_API_URL"], + ["db.url", "CUSTOM_DB_URL"], + ]); + const { values } = legacyStatusValues( + baseConfig(), + CONTAINER_IDS, + HOSTNAME, + NONE, + overrides, + WORKDIR, + ); + expect(values.CUSTOM_API_URL).toBeDefined(); + expect(values.CUSTOM_DB_URL).toBeDefined(); + expect(values.API_URL).toBeUndefined(); + expect(values.DB_URL).toBeUndefined(); + }); + + it("leaves unrelated fields at their default name when only one is overridden", () => { + const overrides = new Map([["api.url", "CUSTOM_API_URL"]]); + const { values } = legacyStatusValues( + baseConfig(), + CONTAINER_IDS, + HOSTNAME, + NONE, + overrides, + WORKDIR, + ); + expect(values.REST_URL).toBeDefined(); + }); + + it("remaps the deprecated auth.jwt_secret/anon_key/service_role_key keys", () => { + const overrides = new Map([ + ["auth.jwt_secret", "CUSTOM_JWT_SECRET"], + ["auth.anon_key", "CUSTOM_ANON_KEY"], + ["auth.service_role_key", "CUSTOM_SERVICE_ROLE_KEY"], + ]); + const { values } = legacyStatusValues( + baseConfig(), + CONTAINER_IDS, + HOSTNAME, + NONE, + overrides, + WORKDIR, + ); + expect(values.CUSTOM_JWT_SECRET).toBeDefined(); + expect(values.CUSTOM_ANON_KEY).toBeDefined(); + expect(values.CUSTOM_SERVICE_ROLE_KEY).toBeDefined(); + expect(values.JWT_SECRET).toBeUndefined(); + expect(values.ANON_KEY).toBeUndefined(); + expect(values.SERVICE_ROLE_KEY).toBeUndefined(); + }); + + it("remaps the deprecated inbucket.url key independently of mailpit.url", () => { + const overrides = new Map([["inbucket.url", "CUSTOM_INBUCKET_URL"]]); + const { values } = legacyStatusValues( + baseConfig(), + CONTAINER_IDS, + HOSTNAME, + NONE, + overrides, + WORKDIR, + ); + expect(values.CUSTOM_INBUCKET_URL).toBeDefined(); + expect(values.MAILPIT_URL).toBeDefined(); + expect(values.INBUCKET_URL).toBeUndefined(); + }); + }); + + it("combines stopped-service exclusions with --exclude flag exclusions", () => { + // Both `stopped` (from the health-check diff) and `--exclude` (user flag) + // funnel into the same `excluded` array in the handler; the pure function + // only sees the merged list. + const excluded = [CONTAINER_IDS.storage, CONTAINER_IDS.studio]; + const { values } = legacyStatusValues( + baseConfig(), + CONTAINER_IDS, + HOSTNAME, + excluded, + NO_OVERRIDES, + WORKDIR, + ); + expect(values.STORAGE_S3_URL).toBeUndefined(); + expect(values.STUDIO_URL).toBeUndefined(); + expect(values.API_URL).toBeDefined(); + }); +}); + +describe("legacyShortContainerImageName", () => { + it("extracts the repo name between the first slash and the last colon", () => { + expect(legacyShortContainerImageName("supabase/storage-api:v1.61.9")).toBe("storage-api"); + expect(legacyShortContainerImageName("library/kong:2.8.1")).toBe("kong"); + }); + + it("falls back to the full string when there is no slash/tag to extract", () => { + expect(legacyShortContainerImageName("kong")).toBe("kong"); + }); +}); + +describe("legacyStatusContainerIds", () => { + it("derives every named field from legacyServiceContainerIds's fixed array order", () => { + const ids = legacyStatusContainerIds("demo"); + expect(ids).toEqual({ + kong: "supabase_kong_demo", + auth: "supabase_auth_demo", + inbucket: "supabase_inbucket_demo", + rest: "supabase_rest_demo", + storage: "supabase_storage_demo", + studio: "supabase_studio_demo", + edgeRuntime: "supabase_edge_runtime_demo", + }); + }); +}); diff --git a/apps/cli/src/legacy/commands/stop/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/stop/SIDE_EFFECTS.md index 09cf725258..31de3f8e06 100644 --- a/apps/cli/src/legacy/commands/stop/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/stop/SIDE_EFFECTS.md @@ -1,16 +1,21 @@ # `supabase stop` +Native TypeScript port of Go's `internal/stop`. Talks directly to Docker via subprocess +(`docker`/`podman`), replicating Go's label-filtering and container-naming scheme +byte-for-byte — it does not go through `@supabase/stack/effect`'s orchestration model +(see the CLI-1324 plan's "Critical architectural finding" for why). + ## Files Read -| Path | Format | When | -| -------------------------------- | ------ | ---------------------------------------- | -| `/supabase/config.toml` | TOML | always, to resolve project configuration | +| Path | Format | When | +| -------------------------------- | ------ | -------------------------------------------------------------------------- | +| `/supabase/config.toml` | TOML | default path only — skipped entirely when `--project-id` or `--all` is set | ## Files Written -| Path | Format | When | -| ---- | ------ | ---- | -| — | — | — | +| Path | Format | When | +| ---------------------------- | ------ | ----------------------------------------------------------- | +| `~/.supabase/telemetry.json` | JSON | always (in `Effect.ensuring`) at end of command — Go parity | ## API Routes @@ -18,37 +23,109 @@ | ------ | ---- | ---- | ------------ | ---------------------- | | — | — | — | — | — | +Neither `stop` nor its Go counterpart make any Management API call. Everything is local +Docker + local `config.toml`. + ## Environment Variables -| Variable | Purpose | Required? | -| -------- | ------- | --------- | -| — | — | — | +| Variable | Purpose | Required? | +| --------------------- | -------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | +| `SUPABASE_PROJECT_ID` | overrides the resolved local project id on the default path (env → config.toml → workdir basename) | no | +| `SUPABASE_WORKDIR` | resolves `LegacyCliConfig.workdir`, which locates `config.toml` on the default path | no (falls back to walking up from cwd for `supabase/config.toml`) | + +`docker`/`podman` must be resolvable on `PATH` (or reachable via the configured Docker +context) — `spawnContainerCli` tries `docker` first and falls back to `podman`. When +neither can be spawned at all, the error message names the actual root cause (e.g. +"docker: command not found (podman also not found) — install Docker Desktop or Podman +and ensure it is on PATH") rather than a generic "failed to ..." string. ## Exit Codes -| Code | Condition | -| ---- | --------------------------------------------- | -| `0` | success — all containers stopped | -| `1` | Docker daemon not running or connection error | +| Code | Condition | +| ---- | -------------------------------------------------------------------------------------------------------- | +| `0` | success — containers/volumes/networks pruned | +| `1` | `--project-id` and `--all` both set (`LegacyStopMutuallyExclusiveError`) | +| `1` | `config.toml` present but malformed (`LegacyStopConfigLoadError`) — an **absent** file is not an error | +| `1` | listing containers failed (`LegacyStopListError`) | +| `1` | stopping one or more containers failed (`LegacyStopContainerError`) | +| `1` | `docker container prune` failed (`LegacyStopContainerPruneError`) | +| `1` | `docker volume prune` failed, only reached when volumes are being deleted (`LegacyStopVolumePruneError`) | +| `1` | `docker network prune` failed (`LegacyStopNetworkPruneError`) | +| `1` | `docker`/`podman` both absent from `PATH` (surfaces as one of the errors above) | + +## Telemetry Events Fired + +| Event | When | Notable properties / groups | +| ---------------------- | ------------------------------------------ | ----------------------------------- | +| `cli_command_executed` | post-run, success or failure (via wrapper) | `exit_code`, `duration_ms`, `flags` | + +Matches `apps/cli-go/internal/stop/`. Go does not fire any custom telemetry event for +this command. ## Output +Go's `stop.RunE` never reads `-o`/`--output` itself, but the flag is still registered +on the root command as a `PersistentFlags()` enum (`cmd/root.go:330`, +`env|pretty|json|toml|yaml`) that every subcommand inherits, so `stop -o csv`/`-o table` +is rejected by pflag at parse time, before `RunE` runs — Go never reaches this command's +body with an unsupported value. `stop.command.ts` matches this: it wraps the handler +with `withLegacyCommandInstrumentation`, whose default `outputFormats` +(`LEGACY_RESOURCE_OUTPUT_FORMATS`, same `env|pretty|json|toml|yaml` set) validates and +rejects the flag before the handler runs — not a divergence, just enforced one layer up +rather than read inside this handler. Only the TS-native `--output-format` is consulted +by this handler's own logic below. + ### `--output-format text` (Go CLI compatible) -Prints "Stopped supabase local development setup." on success. +- stdout: `Stopping containers...` (printed unconditionally before any Docker call, + matching Go's `fmt.Fprintln` — see `docker.go:97`) +- stdout: `Stopped supabase local development setup.` (`supabase` rendered in Aqua/cyan + when the output stream is a TTY, plain otherwise) +- stderr (conditional): when any Docker volume still carries the project's + `com.supabase.cli.project` label after stopping, an additional suggestion line: + - with a project id filter: `Local data are backed up to docker volume. Use docker to show them: docker volume ls --filter label=com.supabase.cli.project=` + - with `--all` (empty filter): `Local data are backed up to docker volume. Use docker to show them: docker volume ls --filter label=com.supabase.cli.project` ### `--output-format json` -Not applicable — stop is a local-dev workflow command. +Additive — no Go CLI equivalent. Single JSON object via `Output.success`: + +```json +{ "project_id_filter": "demo", "backup": true } +``` ### `--output-format stream-json` -Not applicable — stop is a local-dev workflow command. +Same payload as `json`, delivered as a `result` NDJSON event. ## Notes -- `--no-backup` deletes all data volumes after stopping. -- `--project-id` targets a specific local project ID to stop. -- `--all` stops all local Supabase instances across all projects on the machine. -- `--project-id` and `--all` are mutually exclusive. -- The hidden `--backup` flag (default true) is the inverse of `--no-backup`. +- `--project-id` and `--all` are **directory-independent** pure Docker-label filters — + neither reads `config.toml`. Only the no-flags default path resolves the project id + from `LegacyCliConfig.workdir` (env → config.toml `project_id` → workdir basename). +- The hidden `--backup` flag exists only for Go CLI surface parity — it has **no effect**. + Go declares it via `flags.Bool("backup", true, ...)` (`cmd/stop.go:26`) but never binds + the return value to a variable, so `RunE` always passes `!noBackup` to `stop.Run` + regardless of `--backup`. The TS port matches this exactly: `deleteVolumes = +flags.noBackup`. `--backup=false` alone does **not** delete volumes; only + `--no-backup` does. +- Volume prune gates `--all` on the Docker daemon's API version (`legacy-container-cli.ts`'s + `legacyDockerSupportsVolumePruneAllFlag`, checked via `docker version --format +'{{.Server.APIVersion}}'`), matching Go's `Docker.ClientVersion() >= "1.42"` check + (`docker.go:126-133`) exactly. This isn't cosmetic: Docker CLI's own `--all` flag on + `volume prune` is annotated `version: "1.42"` and enforced by Cobra's `Args` validator + _before_ pruning runs, so sending it unconditionally on a pre-1.42 daemon hard-fails the + whole call instead of just pruning a narrower set. On the Podman fallback, `--all` is + omitted unconditionally instead: no released Podman `volume prune` (checked v4.3 through + the current v5.7) accepts that flag, and Podman already prunes every unused volume by + default, so dropping it there is lossless. Podman itself is a TS-only fallback (Go never + shells out to a `docker`/`podman` binary), so this has no Go-parity implication either way. +- Containers are stopped concurrently (`Effect.all(..., { concurrency: "unbounded" })`), + mirroring Go's `WaitAll` goroutine fan-out. Every container's failure is checked before + failing the command (rather than stopping at the first failure), matching Go's + `errors.Join` over the full result set — though the surfaced message is a single fixed + string rather than a joined list of per-container errors, since Docker CLI subprocess + stderr isn't captured per-container the way Go's SDK error is. +- No e2e test is planned: there is no Docker-daemon-free golden path for this command, + and the e2e harness (`runSupabase()`) does not provision a real local stack. See the + CLI-1324 plan's "E2e tests" section for the full justification. diff --git a/apps/cli/src/legacy/commands/stop/stop.command.ts b/apps/cli/src/legacy/commands/stop/stop.command.ts index c89e0c3d1d..64fb443c6b 100644 --- a/apps/cli/src/legacy/commands/stop/stop.command.ts +++ b/apps/cli/src/legacy/commands/stop/stop.command.ts @@ -1,5 +1,13 @@ +import { Layer } from "effect"; import { Command, Flag } from "effect/unstable/cli"; import type * as CliCommand from "effect/unstable/cli/Command"; + +import { commandRuntimeLayer } from "../../../shared/runtime/command-runtime.layer.ts"; +import { withJsonErrorHandling } from "../../../shared/output/json-error-handling.ts"; +import { legacyCliConfigLayer } from "../../config/legacy-cli-config.layer.ts"; +import { legacyDebugLoggerLayer } from "../../shared/legacy-debug-logger.layer.ts"; +import { legacyTelemetryStateLayer } from "../../telemetry/legacy-telemetry-state.layer.ts"; +import { withLegacyCommandInstrumentation } from "../../telemetry/legacy-command-instrumentation.ts"; import { legacyStop } from "./stop.handler.ts"; const config = { @@ -17,15 +25,41 @@ const config = { noBackup: Flag.boolean("no-backup").pipe( Flag.withDescription("Deletes all data volumes after stopping."), ), + // Modelled as `Option` (presence = pflag `Changed`), not a plain + // boolean: Cobra's `MarkFlagsMutuallyExclusive("project-id", "all")` + // (`apps/cli-go/cmd/stop.go:31`) rejects the command whenever BOTH flags + // were explicitly set, regardless of the value `--all` was set to — the + // vendored cobra@v1.10.2 `flag_groups.go:139` check is + // `groupStatus[group][name] = flag.Changed`, not the flag's boolean value. + // A plain `Flag.boolean` here would make `--project-id x --all=false` + // indistinguishable from `--project-id x` (no `--all` at all), silently + // accepting a combination Go rejects. all: Flag.boolean("all").pipe( Flag.withDescription("Stop all local Supabase instances from all projects across the machine."), + Flag.optional, ), } as const; export type LegacyStopFlags = CliCommand.Command.Config.Infer; +// `stop` makes no Management API calls (Go's stop needs no access token) and talks +// directly to Docker, so it deliberately avoids `legacyManagementApiRuntimeLayer` — +// it provides only the services the handler + instrumentation consume. +// `ChildProcessSpawner` is not listed here: it comes from `BunServices` in the root +// runtime (`shared/cli/run.ts`), the same way `gen types`/`unlink` rely on it. +const cliConfig = legacyCliConfigLayer.pipe(Layer.provide(legacyDebugLoggerLayer)); + +const legacyStopRuntimeLayer = Layer.mergeAll( + cliConfig, + legacyTelemetryStateLayer, + commandRuntimeLayer(["stop"]), +); + export const legacyStopCommand = Command.make("stop", config).pipe( Command.withDescription("Stop all local Supabase containers."), Command.withShortDescription("Stop all local Supabase containers"), - Command.withHandler((flags) => legacyStop(flags)), + Command.withHandler((flags) => + legacyStop(flags).pipe(withLegacyCommandInstrumentation({ flags }), withJsonErrorHandling), + ), + Command.provide(legacyStopRuntimeLayer), ); diff --git a/apps/cli/src/legacy/commands/stop/stop.errors.ts b/apps/cli/src/legacy/commands/stop/stop.errors.ts new file mode 100644 index 0000000000..ac8d49db97 --- /dev/null +++ b/apps/cli/src/legacy/commands/stop/stop.errors.ts @@ -0,0 +1,62 @@ +import { Data } from "effect"; + +/** + * An explicit `--workdir`/`SUPABASE_WORKDIR` path doesn't exist or isn't a + * directory. Mirrors Go's `ChangeWorkDir` (`apps/cli-go/internal/utils/misc.go: + * 231-250`), which unconditionally `os.Chdir(workdir)`s in `PersistentPreRunE` + * (`apps/cli-go/cmd/root.go:93-105`) — before `stop`'s own flag validation or + * `RunE`, so a bad explicit workdir must fail here first, before config load + * or any Docker access. + */ +export class LegacyStopWorkdirError extends Data.TaggedError("LegacyStopWorkdirError")<{ + readonly message: string; +}> {} + +/** + * `--project-id` and `--all` were both set. Best-effort match of cobra's + * `MarkFlagsMutuallyExclusive` message shape (`stopCmd.MarkFlagsMutuallyExclusive("project-id", + * "all")`, `apps/cli-go/cmd/stop.go`). Cobra isn't vendored in this repo, so the exact + * wording could not be verified against source; this mirrors the same phrasing already + * used for `gen types`'s mutually-exclusive flag groups (`types.handler.ts`). + */ +export class LegacyStopMutuallyExclusiveError extends Data.TaggedError( + "LegacyStopMutuallyExclusiveError", +)<{ + readonly message: string; +}> {} + +/** Loading `config.toml` failed for a reason other than the file being absent (malformed TOML). */ +export class LegacyStopConfigLoadError extends Data.TaggedError("LegacyStopConfigLoadError")<{ + readonly message: string; +}> {} + +/** + * Listing containers to stop failed. `stop`-specific wrapper over + * `LegacyDockerLifecycleListError` (see `legacy-docker-lifecycle.ts`) so this command's + * errors are all in one file with a `LegacyStop*` tag, matching the plan's error list. + */ +export class LegacyStopListError extends Data.TaggedError("LegacyStopListError")<{ + readonly message: string; +}> {} + +/** Stopping one or more containers failed (`DockerRemoveAll`'s `WaitAll` step). */ +export class LegacyStopContainerError extends Data.TaggedError("LegacyStopContainerError")<{ + readonly message: string; +}> {} + +/** `docker container prune` failed. */ +export class LegacyStopContainerPruneError extends Data.TaggedError( + "LegacyStopContainerPruneError", +)<{ + readonly message: string; +}> {} + +/** `docker volume prune` failed (only run when `--no-backup`/`--backup=false`). */ +export class LegacyStopVolumePruneError extends Data.TaggedError("LegacyStopVolumePruneError")<{ + readonly message: string; +}> {} + +/** `docker network prune` failed. */ +export class LegacyStopNetworkPruneError extends Data.TaggedError("LegacyStopNetworkPruneError")<{ + readonly message: string; +}> {} diff --git a/apps/cli/src/legacy/commands/stop/stop.handler.ts b/apps/cli/src/legacy/commands/stop/stop.handler.ts index ab7f5f9ad8..3e31196de6 100644 --- a/apps/cli/src/legacy/commands/stop/stop.handler.ts +++ b/apps/cli/src/legacy/commands/stop/stop.handler.ts @@ -1,15 +1,410 @@ -import { Effect, Option } from "effect"; -import { LegacyGoProxy } from "../../../shared/legacy/go-proxy.service.ts"; +import { loadProjectConfig, loadProjectEnvironment, ProjectConfigSchema } from "@supabase/config"; +import { ChildProcessSpawner } from "effect/unstable/process"; +import { Effect, FileSystem, Option, Result, Schema } from "effect"; + +import { Output } from "../../../shared/output/output.service.ts"; +import { LegacyCliConfig } from "../../config/legacy-cli-config.service.ts"; +import { LegacyTelemetryState } from "../../telemetry/legacy-telemetry-state.service.ts"; +import { legacyAqua } from "../../shared/legacy-colors.ts"; +import { + containerCliExitCode, + legacyDescribeContainerCliFailure, + legacyDockerSupportsVolumePruneAllFlag, +} from "../../shared/legacy-container-cli.ts"; +import { + legacyCliProjectFilterValue, + legacyResolveLocalProjectId, + legacySanitizeProjectId, +} from "../../shared/legacy-docker-ids.ts"; +import { + legacyListContainersByLabel, + legacyListVolumesByLabel, +} from "../../shared/legacy-docker-lifecycle.ts"; +import { legacyGetHostname } from "../../shared/legacy-hostname.ts"; +import { legacyResolveLocalConfigValues } from "../../shared/legacy-local-config-values.ts"; +import { legacyResolveProjectEnvironmentValues } from "../../shared/legacy-project-environment.ts"; +import { legacyValidateWorkdirIsDirectory } from "../../shared/legacy-workdir-validation.ts"; import type { LegacyStopFlags } from "./stop.command.ts"; +import { + LegacyStopConfigLoadError, + LegacyStopContainerError, + LegacyStopContainerPruneError, + LegacyStopListError, + LegacyStopMutuallyExclusiveError, + LegacyStopNetworkPruneError, + LegacyStopVolumePruneError, + LegacyStopWorkdirError, +} from "./stop.errors.ts"; + +/** + * Resolve the Docker label filter `stop` searches on. Go's flag precedence + * (`stop.go:14-22`): `--all` bypasses config entirely with an empty filter; + * `--project-id` overrides `Config.ProjectId` directly, also bypassing + * config.toml; otherwise `flags.LoadConfig` reads config.toml and + * `Config.ProjectId` (env → toml → workdir basename) is used. + * + * "env" is Go's post-`loadNestedEnv` value, not just the ambient shell + * environment: `Config.Load` loads `supabase/.env`/`.env.local` *and* + * project-root/`SUPABASE_ENV`-selected dotenv files into the process env via + * `godotenv.Load` (`pkg/config/config.go:735-738,1169-1207`; godotenv never + * overrides an already-set var) *before* Viper's `AutomaticEnv` reads + * `SUPABASE_PROJECT_ID` (`config.go:534-535`) — so an env-file-only value + * overrides config.toml too, not only an ambient shell export. + * `legacyResolveProjectEnvironmentValues` implements that full precedence + * chain (see its doc comment) on top of `loadProjectEnvironment`'s + * `supabase/`-dir-only result, so it's used here instead of reading + * `process.env` directly. It still returns a usable map (falling back to + * `/supabase`/`workdir` and `process.env` itself) even when no + * `supabase/` config file exists at `workdir`, matching Go's `loadNestedEnv` + * running unconditionally before `config.toml` is ever opened + * (`pkg/config/config.go:786-793`) — the `?? process.env[...]` fallback below + * only still matters for keys neither source produced. + * + * The config/env-derived (default) branch is sanitized with + * {@link legacySanitizeProjectId} before it's used as a filter value, + * matching Go's `Config.Validate` sanitizing the `Config.ProjectId` + * singleton once at config-load time (`pkg/config/config.go:938-944`) — every + * later reader, including the Docker LABEL `start` writes + * (`internal/utils/docker.go:375`), sees that same sanitized string. The + * explicit `--project-id` bypass stays RAW to match: Go assigns the flag + * value straight to `Config.ProjectId` without going through `Validate` + * (`internal/stop/stop.go:19-20`). + * + * Go's check is `len(projectId) > 0` (`internal/stop/stop.go:18`), not merely + * "was the flag set" — an explicit but empty `--project-id ""` falls through + * to the config.toml branch exactly like an absent flag, so that's mirrored + * here with a non-empty check rather than `Option.isSome` alone. + */ +const resolveSearchProjectIdFilter = Effect.fn("legacy.stop.resolveSearchProjectIdFilter")( + function* (flags: LegacyStopFlags, cliConfig: LegacyCliConfig["Service"]) { + // `internal/stop/stop.go:17`'s `if !all` reads the resolved value (not + // presence), so this branch stays value-based — `Option.getOrElse` mirrors + // Cobra's `BoolVar` default of `false` when `--all` was never passed. + if (Option.getOrElse(flags.all, () => false)) return ""; + if (Option.isSome(flags.projectId) && flags.projectId.value.length > 0) { + return flags.projectId.value; + } + + // `search: false`: `cliConfig.workdir` already IS Go's fully-resolved chdir + // target (`legacy-cli-config.layer.ts`'s `resolveWorkdir` mirrors + // `ChangeWorkDir`'s explicit-exact-vs-default-searched resolution, + // `apps/cli-go/internal/utils/misc.go:231-247`), so letting + // `@supabase/config`'s `findProjectPaths` climb ancestors again on top of + // that would let an unrelated ancestor project's config.toml win when + // `--workdir`/`SUPABASE_WORKDIR` points at a subdirectory with no + // `supabase/config.toml` of its own — Go never searches past the exact + // (explicit or defaulted) workdir (`NewPathBuilder`, `pkg/config/utils.go: + // 43-48`). + const projectEnv = yield* loadProjectEnvironment({ + cwd: cliConfig.workdir, + baseEnv: process.env, + search: false, + // Go's `loadDefaultEnv` (`apps/cli-go/pkg/config/config.go:1243-1250`) + // omits `.env.local` from its candidate list whenever + // `SUPABASE_ENV=test` — a malformed or intentionally non-test + // `supabase/.env.local` is then invisible to Go and must not fail + // config loading here either. `legacyResolveProjectEnvironmentValues` + // below already applies this same gate for the project-root pass (see + // its `candidateDotenvFilenames`); this mirrors it for the + // `supabase/`-dir pass `loadProjectEnvironment` itself performs. + skipEnvLocal: (process.env["SUPABASE_ENV"] || "development") === "test", + }).pipe( + Effect.mapError( + (cause) => + new LegacyStopConfigLoadError({ message: `failed to read config: ${String(cause)}` }), + ), + ); + + // Resolved BEFORE `loadProjectConfig` decodes config.toml (not after): + // Go's `Config.Load` runs `loadNestedEnv` before `LoadEnvHook` decodes + // `env(...)` references (`config.go:735-738`), so an `env(...)`-valued + // `project_id` sourced only from a project-root/`SUPABASE_ENV`-selected + // file must already be visible to the decoder, not just to the + // `SUPABASE_PROJECT_ID` override read below. A malformed extra dotenv + // file throws here (see `readDotEnvFile`), matching Go's `loadNestedEnv` + // propagating `godotenv`'s parse error instead of silently skipping the + // bad line. `workdir` is passed through so dotenv files under + // `/supabase`/`workdir` are still discovered even when + // `projectEnv` is `null` (no config.toml there) — Go's own `loadNestedEnv` + // runs unconditionally, before `config.toml` is ever opened + // (`pkg/config/config.go:786-793`). + const projectEnvValues = yield* Effect.try({ + try: () => legacyResolveProjectEnvironmentValues(projectEnv, cliConfig.workdir), + catch: (cause) => + new LegacyStopConfigLoadError({ message: `failed to read config: ${String(cause)}` }), + }); + + // An absent config.toml is not a failure — Go's `flags.LoadConfig` still + // resolves a project id via the workdir basename default. Only a + // malformed file (`loadProjectConfig` failing rather than returning + // `null`) is a hard error, matching `gen types`'s `loadConfig()` pattern. + const loaded = yield* loadProjectConfig(cliConfig.workdir, { + projectEnv: projectEnv !== null ? { ...projectEnv, values: projectEnvValues } : undefined, + search: false, + // Go's `NewPathBuilder`/`Config.Load` (`pkg/config/utils.go:43-48`) only + // ever resolves `supabase/config.toml` — it has no concept of a JSON + // project config file. Without this, a workdir with a stray + // `config.json` would make `loadProjectConfig` prefer it over + // `config.toml`, potentially stopping containers for the wrong project. + tomlOnly: true, + goViperCompat: true, + }).pipe( + Effect.mapError( + (cause) => + new LegacyStopConfigLoadError({ message: `failed to read config: ${String(cause)}` }), + ), + ); + const config = loaded?.config ?? Schema.decodeUnknownSync(ProjectConfigSchema)({}); + + // VALIDATE config before any Docker call, matching Go's `flags.LoadConfig` + // (config load + `Validate`, `internal/utils/flags/config_path.go:10-14` -> + // `pkg/config/config.go:882`), which the default `stop` path runs in full + // (`internal/stop/stop.go:15-25`) before ever touching Docker — unlike the + // `--all`/`--project-id` branches above, which bypass config loading + // entirely and so must NOT run this. `legacyResolveLocalConfigValues` is + // reused purely for its throwing side effects (its resolved URLs/keys are + // discarded); it gives `stop` the same partial-but-growing `Config.Validate` + // parity `status` already has (`status.handler.ts`), rather than a one-off + // re-implementation. `legacyGetHostname` has no Docker dependency, so it's + // safe to call speculatively here too. + yield* Effect.try({ + try: () => + legacyResolveLocalConfigValues( + config, + legacyGetHostname(), + cliConfig.workdir, + projectEnvValues, + loaded?.document, + ), + catch: (cause) => + new LegacyStopConfigLoadError({ + message: cause instanceof Error ? cause.message : String(cause), + }), + }); + + const resolved = legacyResolveLocalProjectId( + projectEnvValues["SUPABASE_PROJECT_ID"] ?? process.env["SUPABASE_PROJECT_ID"], + config.project_id, + cliConfig.workdir, + ); + return legacySanitizeProjectId(resolved); + }, +); export const legacyStop = Effect.fn("legacy.stop")(function* (flags: LegacyStopFlags) { - const proxy = yield* LegacyGoProxy; - const args: string[] = ["stop"]; - if (Option.isSome(flags.projectId)) args.push("--project-id", flags.projectId.value); - // `--backup` defaults to true; only forward when explicitly disabled, which - // matches the Go CLI semantics (`!noBackup` && `--backup=false`). - if (!flags.backup) args.push("--backup=false"); - if (flags.noBackup) args.push("--no-backup"); - if (flags.all) args.push("--all"); - yield* proxy.exec(args); + const output = yield* Output; + const cliConfig = yield* LegacyCliConfig; + const telemetryState = yield* LegacyTelemetryState; + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; + const fs = yield* FileSystem.FileSystem; + + yield* Effect.gen(function* () { + // Go's `ChangeWorkDir` (`apps/cli-go/internal/utils/misc.go:231-250`) + // unconditionally `os.Chdir`s the resolved `--workdir`/`SUPABASE_WORKDIR` + // in `PersistentPreRunE` (`cmd/root.go:93-105`) — before any of `stop`'s + // own flag validation or `RunE`. A missing or non-directory path fails + // immediately, so this must win over every later error, including the + // `--project-id`/`--all` mutual-exclusivity check below. + yield* legacyValidateWorkdirIsDirectory(cliConfig.workdir, fs).pipe( + Effect.mapError((error) => new LegacyStopWorkdirError({ message: error.message })), + ); + + // Presence-based, matching Cobra's `Changed` check (see the doc comment on + // `all`'s flag definition in `stop.command.ts`) — `--project-id x --all=false` + // must reject too, not just `--all`/`--all=true`. + if (Option.isSome(flags.projectId) && Option.isSome(flags.all)) { + return yield* Effect.fail( + new LegacyStopMutuallyExclusiveError({ + // Cobra's `validateExclusiveFlagGroups` (spf13/cobra flag_groups.go): + // the group name keeps declaration order (`strings.Join(flagNames, " ")`), + // but the "were all set" list is `sort.Strings`-ed — verified against + // the vendored cobra@v1.10.2 source, not guessed. + message: + "if any flags in the group [project-id all] are set none of the others can be; [all project-id] were all set", + }), + ); + } + + const searchProjectIdFilter = yield* resolveSearchProjectIdFilter(flags, cliConfig); + // Go's hidden `--backup` flag is declared via `flags.Bool("backup", true, ...)` + // (`cmd/stop.go:26`) but its return value is discarded — never bound to a + // variable, so `RunE` always passes `!noBackup` to `stop.Run` regardless of + // `--backup`'s value. `--backup=false` is a no-op in the real Go binary + // today; only `--no-backup` deletes volumes. Matching that exactly (not the + // seemingly-intended-but-dead semantics of the flag's own description). + const deleteVolumes = flags.noBackup; + const filterValue = legacyCliProjectFilterValue(searchProjectIdFilter); + + // Go prints this line unconditionally and immediately — `docker.go:97`'s + // `fmt.Fprintln(w, "Stopping containers...")`, where `w` is a + // `StatusWriter` that `fmt.Println`s straight to stdout in non-interactive + // mode (`tea.go:59-60,87-90`) before any Docker call runs. The debounced + // `output.task` spinner used elsewhere in this codebase gates its message + // behind a delay, which drops this line whenever the underlying calls + // resolve faster than that threshold — exactly what happens against the + // mocked/replayed Docker CLI. Print it directly so it always appears. + if (output.format === "text") { + yield* output.raw("Stopping containers...\n"); + } + + yield* Effect.gen(function* () { + const containerIds = yield* legacyListContainersByLabel(spawner, { + projectIdFilter: filterValue, + all: true, + format: "id", + }).pipe(Effect.mapError((cause) => new LegacyStopListError({ message: cause.message }))); + + // Go stops containers concurrently via `WaitAll`, joining every failure + // rather than short-circuiting on the first one (`docker.go:96-146`). + // + // `stdout`/`stderr: "ignore"` on every exit-code-only call below: none of + // these read the child's own output, and the default `"pipe"` stdio + // otherwise leaves an OS pipe unread — once `docker`/`podman` write + // enough to it (e.g. `container prune`'s "Deleted Containers" ID list on + // a host with many stale containers, most likely under `stop --all`), + // the child blocks on write() and `stop` hangs. Matches the existing + // `stdio: "ignore"` precedent for the same "exit-code-only" shape in + // `legacy-pgdelta.seam.layer.ts`. + const stopResults = yield* Effect.all( + containerIds.map((id) => + containerCliExitCode(spawner, ["stop", id], { + stdin: "ignore", + stdout: "ignore", + stderr: "ignore", + }).pipe(Effect.result), + ), + { concurrency: "unbounded" }, + ); + const failedStop = stopResults.find( + (result) => Result.isFailure(result) || result.success !== 0, + ); + if (failedStop !== undefined) { + return yield* Effect.fail( + new LegacyStopContainerError({ + message: `failed to stop container: ${ + Result.isFailure(failedStop) + ? legacyDescribeContainerCliFailure(failedStop.failure) + : `exit ${failedStop.success}` + }`, + }), + ); + } + + const containerPruneExitCode = yield* containerCliExitCode( + spawner, + ["container", "prune", "--force", "--filter", `label=${filterValue}`], + { stdin: "ignore", stdout: "ignore", stderr: "ignore" }, + ).pipe( + Effect.mapError( + (cause) => + new LegacyStopContainerPruneError({ + message: `failed to prune containers: ${legacyDescribeContainerCliFailure(cause)}`, + }), + ), + ); + if (containerPruneExitCode !== 0) { + return yield* Effect.fail( + new LegacyStopContainerPruneError({ message: "failed to prune containers" }), + ); + } + + if (deleteVolumes) { + // Go gates the `--all` filter arg on Docker API >= 1.42 + // (`docker.go:126-133`, `Docker.ClientVersion() >= "1.42"`): Docker + // CLI's own `volume prune --all` flag is annotated `version: "1.42"` + // (`docker/cli@v28.5.2` `cli/command/volume/prune.go:53`) and enforced + // by Cobra's `Args` validator *before* `RunE` runs + // (`cmd/docker/docker.go:659-660`) — on an older daemon, passing + // `--all` unconditionally would hard-fail this whole call and prune + // nothing, not just prune a narrower set. There's no persistent + // Engine API client here to ask the negotiated version directly (Go + // talks to the Docker Engine API, never a `docker` binary), so + // {@link legacyDockerSupportsVolumePruneAllFlag} asks the `docker` CLI + // itself via `docker version` and mirrors Go's gate exactly. + // + // Podman is a Docker-CLI-compatible fallback this port adds, not something + // Go itself has, so there's no Go behavior to match on that path — but + // `--all` isn't a real flag on any released Podman `volume prune` (only + // `--filter`/`--force`/`--help`, checked v4.3 through the current v5.7; + // `--all` only exists in unreleased dev docs), so it hard-fails on a real + // Podman-only host. Podman already prunes every unused volume by default, + // so omitting `--all` on the Podman fallback is a lossless fix. + const dockerSupportsAll = yield* legacyDockerSupportsVolumePruneAllFlag(spawner); + const volumePruneExitCode = yield* containerCliExitCode( + spawner, + [ + "volume", + "prune", + "--force", + ...(dockerSupportsAll ? ["--all"] : []), + "--filter", + `label=${filterValue}`, + ], + { stdin: "ignore", stdout: "ignore", stderr: "ignore" }, + ["volume", "prune", "--force", "--filter", `label=${filterValue}`], + ).pipe( + Effect.mapError( + (cause) => + new LegacyStopVolumePruneError({ + message: `failed to prune volumes: ${legacyDescribeContainerCliFailure(cause)}`, + }), + ), + ); + if (volumePruneExitCode !== 0) { + return yield* Effect.fail( + new LegacyStopVolumePruneError({ message: "failed to prune volumes" }), + ); + } + } + + const networkPruneExitCode = yield* containerCliExitCode( + spawner, + ["network", "prune", "--force", "--filter", `label=${filterValue}`], + { stdin: "ignore", stdout: "ignore", stderr: "ignore" }, + ).pipe( + Effect.mapError( + (cause) => + new LegacyStopNetworkPruneError({ + message: `failed to prune networks: ${legacyDescribeContainerCliFailure(cause)}`, + }), + ), + ); + if (networkPruneExitCode !== 0) { + return yield* Effect.fail( + new LegacyStopNetworkPruneError({ message: "failed to prune networks" }), + ); + } + }); + + if (output.format === "text") { + // Written to stdout (no stream arg): `legacyAqua` must target stdout's own + // TTY status, not stderr's — see `legacy-colors.ts`'s doc comment. + yield* output.raw( + `Stopped ${legacyAqua("supabase", process.stdout)} local development setup.\n`, + ); + } else { + yield* output.success("Stopped supabase local development setup.", { + project_id_filter: searchProjectIdFilter, + backup: !deleteVolumes, + }); + } + + // Post-run suggestion (stop.go:26-37): only meaningful in text mode — json/ + // stream-json payloads have no equivalent field to carry this hint. + if (output.format === "text") { + const remainingVolumes = yield* legacyListVolumesByLabel(spawner, filterValue).pipe( + Effect.orElseSucceed(() => []), + ); + if (remainingVolumes.length > 0) { + const listVolumeCommand = + searchProjectIdFilter.length > 0 + ? `docker volume ls --filter label=com.supabase.cli.project=${searchProjectIdFilter}` + : "docker volume ls --filter label=com.supabase.cli.project"; + yield* output.raw( + `Local data are backed up to docker volume. Use docker to show them: ${legacyAqua(listVolumeCommand)}\n`, + "stderr", + ); + } + } + }).pipe(Effect.ensuring(telemetryState.flush)); }); diff --git a/apps/cli/src/legacy/commands/stop/stop.integration.test.ts b/apps/cli/src/legacy/commands/stop/stop.integration.test.ts index c9c9d83fea..3ac32c623c 100644 --- a/apps/cli/src/legacy/commands/stop/stop.integration.test.ts +++ b/apps/cli/src/legacy/commands/stop/stop.integration.test.ts @@ -1,55 +1,1122 @@ +import { mkdirSync, writeFileSync } from "node:fs"; +import { basename, join } from "node:path"; + +import { BunServices } from "@effect/platform-bun"; import { describe, expect, it } from "@effect/vitest"; -import { Effect, Layer, Option } from "effect"; -import { LegacyGoProxy } from "../../../shared/legacy/go-proxy.service.ts"; +import { Deferred, Effect, Exit, Layer, Option, PlatformError, Sink, Stream } from "effect"; +import { ChildProcessSpawner } from "effect/unstable/process"; +import { vi } from "vitest"; + +import { mockOutput } from "../../../../tests/helpers/mocks.ts"; +import { + mockLegacyCliConfig, + mockLegacyTelemetryStateTracked, + useLegacyTempWorkdir, +} from "../../../../tests/helpers/legacy-mocks.ts"; import { legacyStop } from "./stop.handler.ts"; import type { LegacyStopFlags } from "./stop.command.ts"; -function setupLegacyStop() { - const calls: Array> = []; - const layer = Layer.succeed(LegacyGoProxy, { - exec: (args) => - Effect.sync(() => { - calls.push(args); +const tempRoot = useLegacyTempWorkdir("supabase-stop-int-"); + +function flags(overrides: Partial = {}): LegacyStopFlags { + return { + projectId: Option.none(), + backup: true, + noBackup: false, + all: Option.none(), + ...overrides, + }; +} + +function writeConfig(workdir: string, projectId: string) { + const supabaseDir = join(workdir, "supabase"); + mkdirSync(supabaseDir, { recursive: true }); + writeFileSync(join(supabaseDir, "config.toml"), `project_id = "${projectId}"\n`); +} + +function writeEnvFile(workdir: string, fileName: ".env" | ".env.local", contents: string) { + const supabaseDir = join(workdir, "supabase"); + mkdirSync(supabaseDir, { recursive: true }); + writeFileSync(join(supabaseDir, fileName), contents); +} + +interface SpawnRecord { + readonly command: string; + readonly args: ReadonlyArray; +} + +type RouteResult = { + readonly exitCode?: number; + readonly stdout?: ReadonlyArray; + readonly stderr?: ReadonlyArray; +}; + +/** + * Routes each spawned invocation to a caller-supplied result by matching argv + * (rather than a fixed call sequence): `stop` issues five distinct docker + * subcommands (`ps`, `stop`, `container prune`, `volume prune`, `network prune`, + * `volume ls`) whose relative order/count varies per scenario (N `stop` calls for + * N listed containers), so a routing table is a better fit than the sequential + * step-array mock `gen types` uses for its single linear pipeline. + */ +function mockRoutedContainerCliSpawner( + route: (args: ReadonlyArray) => RouteResult, + opts: { + readonly dockerMissing?: boolean; + // Fails BOTH docker and podman spawn attempts for argv matching this predicate, + // simulating a runtime that cannot be spawned at all (as opposed to a spawned + // process exiting non-zero) — exercises the `Effect.mapError`/`orElseSucceed` + // spawn-failure branches distinct from the exit-code-checking branches. + readonly failSpawnFor?: (args: ReadonlyArray) => boolean; + } = {}, +) { + const spawned: Array = []; + + const layer = Layer.succeed( + ChildProcessSpawner.ChildProcessSpawner, + ChildProcessSpawner.make((command) => + Effect.gen(function* () { + const cmd = command._tag === "StandardCommand" ? command.command : ""; + const args = command._tag === "StandardCommand" ? command.args : []; + spawned.push({ command: cmd, args }); + + if (opts.dockerMissing === true && cmd === "docker") { + return yield* Effect.fail( + PlatformError.systemError({ + _tag: "NotFound", + module: "ChildProcess", + method: "spawn", + description: "docker not found", + }), + ); + } + + if (opts.failSpawnFor?.(args) === true) { + return yield* Effect.fail( + PlatformError.systemError({ + _tag: "NotFound", + module: "ChildProcess", + method: "spawn", + description: "spawn failed", + }), + ); + } + + const encoder = new TextEncoder(); + const result = route(args); + const exitDeferred = yield* Deferred.make(); + yield* Effect.forkDetach( + Effect.gen(function* () { + yield* Effect.sleep("5 millis"); + yield* Deferred.succeed( + exitDeferred, + ChildProcessSpawner.ExitCode(result.exitCode ?? 0), + ); + }), + ); + const stdoutBytes = (result.stdout ?? []).map((line) => encoder.encode(`${line}\n`)); + const stderrBytes = (result.stderr ?? []).map((line) => encoder.encode(`${line}\n`)); + + return ChildProcessSpawner.makeHandle({ + pid: ChildProcessSpawner.ProcessId(4000 + spawned.length), + stdout: Stream.fromIterable(stdoutBytes), + stderr: Stream.fromIterable(stderrBytes), + all: Stream.empty, + exitCode: Deferred.await(exitDeferred), + isRunning: Effect.succeed(false), + stdin: Sink.drain, + kill: () => Effect.void, + unref: Effect.succeed(Effect.void), + getInputFd: () => Sink.drain, + getOutputFd: () => Stream.empty, + }); }), - execCapture: () => Effect.succeed(""), + ), + ); + + return { + layer, + get spawned() { + return spawned; + }, + }; +} + +/** + * Default happy-path router: `ps` lists one container, `docker version` reports + * an API version comfortably at/above the `volume prune --all` gate (1.42, see + * `legacyDockerSupportsVolumePruneAllFlag`), everything else succeeds empty. + */ +function defaultRoute( + opts: { + readonly containerIds?: ReadonlyArray; + readonly volumeNames?: ReadonlyArray; + readonly dockerApiVersion?: string; + } = {}, +) { + const containerIds = opts.containerIds ?? ["c1"]; + const volumeNames = opts.volumeNames ?? []; + const dockerApiVersion = opts.dockerApiVersion ?? "1.45"; + return (args: ReadonlyArray): RouteResult => { + if (args[0] === "ps") return { stdout: containerIds }; + if (args[0] === "volume" && args[1] === "ls") return { stdout: volumeNames }; + if (args[0] === "version") return { stdout: [dockerApiVersion] }; + return { exitCode: 0 }; + }; +} + +interface SetupOpts { + readonly format?: "text" | "json" | "stream-json"; + readonly route?: (args: ReadonlyArray) => RouteResult; + readonly dockerMissing?: boolean; + readonly failSpawnFor?: (args: ReadonlyArray) => boolean; + readonly configuredProjectId?: string; + readonly skipConfig?: boolean; + /** Defaults to `tempRoot.current` — override for `--workdir`-resolution tests. */ + readonly workdir?: string; +} + +function setup(opts: SetupOpts = {}) { + const workdir = opts.workdir ?? tempRoot.current; + if (opts.skipConfig !== true) { + writeConfig(workdir, opts.configuredProjectId ?? "demo"); + } + const out = mockOutput({ + format: opts.format ?? "text", + interactive: (opts.format ?? "text") === "text", + }); + const telemetry = mockLegacyTelemetryStateTracked(); + const cliConfig = mockLegacyCliConfig({ workdir, projectId: Option.none() }); + const child = mockRoutedContainerCliSpawner(opts.route ?? defaultRoute(), { + dockerMissing: opts.dockerMissing, + failSpawnFor: opts.failSpawnFor, }); - return { layer, calls }; + + const layer = Layer.mergeAll( + BunServices.layer, + out.layer, + cliConfig, + telemetry.layer, + child.layer, + ); + + return { workdir, out, telemetry, child, layer }; } -const baseFlags: LegacyStopFlags = { - projectId: Option.none(), - backup: true, - noBackup: false, - all: false, -}; +describe("legacy stop integration", () => { + it.live( + "stops the current project's containers with backup and suggests the volume command", + () => { + const { layer, out, child } = setup({ + configuredProjectId: "demo", + route: defaultRoute({ containerIds: ["c1", "c2"], volumeNames: ["supabase_db_demo"] }), + }); + return Effect.gen(function* () { + yield* legacyStop(flags()); + const psCall = child.spawned.find((s) => s.args[0] === "ps"); + expect(psCall?.args).toEqual([ + "ps", + "--filter", + "label=com.supabase.cli.project=demo", + "--all", + "--format", + "{{.ID}}", + ]); + const stopCalls = child.spawned.filter((s) => s.args[0] === "stop"); + expect(stopCalls.map((s) => s.args)).toEqual([ + ["stop", "c1"], + ["stop", "c2"], + ]); + expect(out.stdoutText).toContain("Stopping containers..."); + expect(out.stdoutText).toContain("Stopped"); + expect(out.stdoutText).toContain("local development setup."); + expect(out.stderrText).toContain( + "Local data are backed up to docker volume. Use docker to show them:", + ); + expect(out.stderrText).toContain( + "docker volume ls --filter label=com.supabase.cli.project=demo", + ); + }).pipe(Effect.provide(layer)); + }, + ); + + it.live( + "sanitizes a dirty config.toml project_id before filtering, matching start's label", + () => { + // Go's Config.Validate rewrites Config.ProjectId to its sanitized form once + // at config-load time (pkg/config/config.go:938-944); every later reader — + // including the Docker label `start` writes — sees that same sanitized + // string. Filtering on the raw value here would match nothing `start` + // ever labeled. + const { layer, child } = setup({ + configuredProjectId: "My App!!", + route: defaultRoute(), + }); + return Effect.gen(function* () { + yield* legacyStop(flags()); + const psCall = child.spawned.find((s) => s.args[0] === "ps"); + expect(psCall?.args).toEqual([ + "ps", + "--filter", + "label=com.supabase.cli.project=My_App_", + "--all", + "--format", + "{{.ID}}", + ]); + }).pipe(Effect.provide(layer)); + }, + ); + + it.live("keeps an explicit --project-id raw, unsanitized (Go's bypass)", () => { + // Go assigns the --project-id flag value straight to Config.ProjectId + // without going through Validate (internal/stop/stop.go:19-20), so this + // path must NOT sanitize even though the default (config-derived) path does. + const { layer, child } = setup({ skipConfig: true, route: defaultRoute() }); + return Effect.gen(function* () { + yield* legacyStop(flags({ projectId: Option.some("Raw Value!!") })); + const psCall = child.spawned.find((s) => s.args[0] === "ps"); + expect(psCall?.args).toEqual([ + "ps", + "--filter", + "label=com.supabase.cli.project=Raw Value!!", + "--all", + "--format", + "{{.ID}}", + ]); + }).pipe(Effect.provide(layer)); + }); + + it.live("stops every project's containers with --all without reading config.toml", () => { + const { layer, child } = setup({ skipConfig: true, route: defaultRoute() }); + return Effect.gen(function* () { + yield* legacyStop(flags({ all: Option.some(true) })); + const psCall = child.spawned.find((s) => s.args[0] === "ps"); + expect(psCall?.args).toEqual([ + "ps", + "--filter", + "label=com.supabase.cli.project", + "--all", + "--format", + "{{.ID}}", + ]); + const pruneCalls = child.spawned.filter( + (s) => s.args[0] === "container" && s.args[1] === "prune", + ); + expect(pruneCalls[0]?.args).toEqual([ + "container", + "prune", + "--force", + "--filter", + "label=com.supabase.cli.project", + ]); + }).pipe(Effect.provide(layer)); + }); + + it.live("suggests the bare-label volume command with --all when volumes remain", () => { + const { layer, out } = setup({ + skipConfig: true, + route: defaultRoute({ volumeNames: ["supabase_db_demo"] }), + }); + return Effect.gen(function* () { + yield* legacyStop(flags({ all: Option.some(true) })); + expect(out.stderrText).toContain( + "Local data are backed up to docker volume. Use docker to show them:", + ); + expect(out.stderrText).toContain("docker volume ls --filter label=com.supabase.cli.project"); + expect(out.stderrText).not.toContain("com.supabase.cli.project="); + }).pipe(Effect.provide(layer)); + }); + + it.live("stops a named project with --project-id without reading config.toml", () => { + const { layer, child } = setup({ skipConfig: true, route: defaultRoute() }); + return Effect.gen(function* () { + yield* legacyStop(flags({ projectId: Option.some("other-project") })); + const psCall = child.spawned.find((s) => s.args[0] === "ps"); + expect(psCall?.args).toEqual([ + "ps", + "--filter", + "label=com.supabase.cli.project=other-project", + "--all", + "--format", + "{{.ID}}", + ]); + }).pipe(Effect.provide(layer)); + }); + + it.live("falls back to config.toml when --project-id is an empty string", () => { + // Go's check is `len(projectId) > 0` (internal/stop/stop.go:18), not just + // "was --project-id set" — an empty value must fall through to config.toml + // exactly like an absent flag, not resolve to the bare/all-projects filter. + const { layer, child } = setup({ configuredProjectId: "demo", route: defaultRoute() }); + return Effect.gen(function* () { + yield* legacyStop(flags({ projectId: Option.some("") })); + const psCall = child.spawned.find((s) => s.args[0] === "ps"); + expect(psCall?.args).toEqual([ + "ps", + "--filter", + "label=com.supabase.cli.project=demo", + "--all", + "--format", + "{{.ID}}", + ]); + }).pipe(Effect.provide(layer)); + }); + + it.live("resolves SUPABASE_PROJECT_ID from supabase/.env over config.toml", () => { + // Go's Config.Load runs loadNestedEnv (supabase/.env(.local) via godotenv) + // before loadFromFile's AutomaticEnv reads SUPABASE_PROJECT_ID + // (pkg/config/config.go:735-738) — an env-file-only value overrides + // config.toml's project_id too, not just an ambient shell export. + const { layer, child } = setup({ configuredProjectId: "toml-project", route: defaultRoute() }); + writeEnvFile(tempRoot.current, ".env", "SUPABASE_PROJECT_ID=env-file-project\n"); + return Effect.gen(function* () { + yield* legacyStop(flags()); + const psCall = child.spawned.find((s) => s.args[0] === "ps"); + expect(psCall?.args).toEqual([ + "ps", + "--filter", + "label=com.supabase.cli.project=env-file-project", + "--all", + "--format", + "{{.ID}}", + ]); + }).pipe(Effect.provide(layer)); + }); + + it.live("prefers ambient SUPABASE_PROJECT_ID over supabase/.env", () => { + const { layer, child } = setup({ configuredProjectId: "toml-project", route: defaultRoute() }); + writeEnvFile(tempRoot.current, ".env", "SUPABASE_PROJECT_ID=env-file-project\n"); + process.env["SUPABASE_PROJECT_ID"] = "ambient-project"; + return Effect.gen(function* () { + yield* legacyStop(flags()); + const psCall = child.spawned.find((s) => s.args[0] === "ps"); + expect(psCall?.args).toEqual([ + "ps", + "--filter", + "label=com.supabase.cli.project=ambient-project", + "--all", + "--format", + "{{.ID}}", + ]); + }).pipe( + Effect.provide(layer), + Effect.ensuring(Effect.sync(() => delete process.env["SUPABASE_PROJECT_ID"])), + ); + }); + + it.live( + "does not climb to an ancestor project's config.toml when workdir has none of its own", + () => { + // Go's ChangeWorkDir uses an explicit/defaulted workdir exactly, with no + // ancestor search (apps/cli-go/internal/utils/misc.go:231-247) — mirrored + // here by `search: false`. A workdir with no supabase/config.toml of its + // own must fall back to defaults (workdir-basename project id), not an + // ancestor project's config.toml, even though `cliConfig.workdir` sits + // right inside one. + const nestedWorkdir = join(tempRoot.current, "nested"); + mkdirSync(nestedWorkdir, { recursive: true }); + writeConfig(tempRoot.current, "ancestor-project"); + const projectId = basename(nestedWorkdir); + const { layer, child } = setup({ + workdir: nestedWorkdir, + skipConfig: true, + route: defaultRoute(), + }); + return Effect.gen(function* () { + yield* legacyStop(flags()); + const psCall = child.spawned.find((s) => s.args[0] === "ps"); + expect(psCall?.args).toEqual([ + "ps", + "--filter", + `label=com.supabase.cli.project=${projectId}`, + "--all", + "--format", + "{{.ID}}", + ]); + }).pipe(Effect.provide(layer)); + }, + ); + + it.live("resolves SUPABASE_PROJECT_ID from supabase/.env even when config.toml is absent", () => { + // Go's loadNestedEnv runs unconditionally, before config.toml is ever + // opened (pkg/config/config.go:786-793) — a supabase/.env-only project id + // must still be honored even when there's no config.toml to fall back to + // template defaults from. + const { layer, child } = setup({ skipConfig: true, route: defaultRoute() }); + writeEnvFile(tempRoot.current, ".env", "SUPABASE_PROJECT_ID=no-config-project\n"); + return Effect.gen(function* () { + yield* legacyStop(flags()); + const psCall = child.spawned.find((s) => s.args[0] === "ps"); + expect(psCall?.args).toEqual([ + "ps", + "--filter", + "label=com.supabase.cli.project=no-config-project", + "--all", + "--format", + "{{.ID}}", + ]); + }).pipe(Effect.provide(layer)); + }); + + it.live("resolves SUPABASE_PROJECT_ID from a project-root .env file", () => { + // Go's loadNestedEnv walks past supabase/ one more level, to the project + // root/workdir (pkg/config/config.go:1169-1190) — a project-root-only + // dotenv value must override config.toml too, not just supabase/.env. + const { layer, child } = setup({ configuredProjectId: "toml-project", route: defaultRoute() }); + writeFileSync(join(tempRoot.current, ".env"), "SUPABASE_PROJECT_ID=root-env-project\n"); + return Effect.gen(function* () { + yield* legacyStop(flags()); + const psCall = child.spawned.find((s) => s.args[0] === "ps"); + expect(psCall?.args).toEqual([ + "ps", + "--filter", + "label=com.supabase.cli.project=root-env-project", + "--all", + "--format", + "{{.ID}}", + ]); + }).pipe(Effect.provide(layer)); + }); + + it.live("fails when --workdir/SUPABASE_WORKDIR points at a missing path", () => { + // Go's `ChangeWorkDir` (`apps/cli-go/internal/utils/misc.go:231-250`) + // `os.Chdir`s the explicit workdir in `PersistentPreRunE`, before any of + // `stop`'s own flag validation, config load, or Docker access — a missing + // path must fail immediately, not fall through to the workdir-basename + // default and prune under that name. + const missingWorkdir = join(tempRoot.current, "does-not-exist"); + const { layer, child } = setup({ workdir: missingWorkdir, skipConfig: true }); + return Effect.gen(function* () { + const exit = yield* Effect.exit(legacyStop(flags())); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(JSON.stringify(exit.cause)).toContain("LegacyStopWorkdirError"); + expect(JSON.stringify(exit.cause)).toContain( + `failed to change workdir: chdir ${missingWorkdir}: no such file or directory`, + ); + } + expect(child.spawned).toEqual([]); + }).pipe(Effect.provide(layer)); + }); + + it.live("fails when --workdir/SUPABASE_WORKDIR points at a file, not a directory", () => { + const filePath = join(tempRoot.current, "not-a-directory"); + writeFileSync(filePath, ""); + const { layer, child } = setup({ workdir: filePath, skipConfig: true }); + return Effect.gen(function* () { + const exit = yield* Effect.exit(legacyStop(flags())); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(JSON.stringify(exit.cause)).toContain("LegacyStopWorkdirError"); + expect(JSON.stringify(exit.cause)).toContain( + `failed to change workdir: chdir ${filePath}: not a directory`, + ); + } + expect(child.spawned).toEqual([]); + }).pipe(Effect.provide(layer)); + }); + + it.live("rejects --project-id together with --all", () => { + const { layer, child } = setup({ skipConfig: true, route: defaultRoute() }); + return Effect.gen(function* () { + const exit = yield* Effect.exit( + legacyStop(flags({ projectId: Option.some("other-project"), all: Option.some(true) })), + ); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(JSON.stringify(exit.cause)).toContain("LegacyStopMutuallyExclusiveError"); + } + expect(child.spawned).toEqual([]); + }).pipe(Effect.provide(layer)); + }); + + // Cobra's `MarkFlagsMutuallyExclusive` mutex is presence-based (`Changed`), + // not value-based — `--all=false` still counts as "set" alongside + // `--project-id`, so this must reject too, not just `--all`/`--all=true`. + it.live("rejects --project-id together with an explicit --all=false", () => { + const { layer, child } = setup({ skipConfig: true, route: defaultRoute() }); + return Effect.gen(function* () { + const exit = yield* Effect.exit( + legacyStop(flags({ projectId: Option.some("other-project"), all: Option.some(false) })), + ); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(JSON.stringify(exit.cause)).toContain("LegacyStopMutuallyExclusiveError"); + } + expect(child.spawned).toEqual([]); + }).pipe(Effect.provide(layer)); + }); + + it.live("deletes data volumes with --no-backup", () => { + const { layer, child } = setup({ configuredProjectId: "demo", route: defaultRoute() }); + return Effect.gen(function* () { + yield* legacyStop(flags({ noBackup: true })); + const volumePrune = child.spawned.find( + (s) => s.args[0] === "volume" && s.args[1] === "prune", + ); + expect(volumePrune?.args).toEqual([ + "volume", + "prune", + "--force", + "--all", + "--filter", + "label=com.supabase.cli.project=demo", + ]); + }).pipe(Effect.provide(layer)); + }); + + it.live( + "omits --all from docker's volume prune on a pre-1.42 API host, matching Go's gate", + () => { + // Docker CLI's own `volume prune --all` flag requires API >= 1.42 and + // hard-fails (pruning nothing) on an older daemon — Go avoids ever + // sending it by checking `Docker.ClientVersion() >= "1.42"` + // (docker.go:126-133). This mirrors that gate via `docker version`. + const { layer, child } = setup({ + configuredProjectId: "demo", + route: defaultRoute({ dockerApiVersion: "1.41" }), + }); + return Effect.gen(function* () { + yield* legacyStop(flags({ noBackup: true })); + const volumePrune = child.spawned.find( + (s) => s.command === "docker" && s.args[0] === "volume" && s.args[1] === "prune", + ); + expect(volumePrune?.args).toEqual([ + "volume", + "prune", + "--force", + "--filter", + "label=com.supabase.cli.project=demo", + ]); + }).pipe(Effect.provide(layer)); + }, + ); + + it.live("includes --all in docker's volume prune when the API is exactly 1.42", () => { + const { layer, child } = setup({ + configuredProjectId: "demo", + route: defaultRoute({ dockerApiVersion: "1.42" }), + }); + return Effect.gen(function* () { + yield* legacyStop(flags({ noBackup: true })); + const volumePrune = child.spawned.find( + (s) => s.command === "docker" && s.args[0] === "volume" && s.args[1] === "prune", + ); + expect(volumePrune?.args).toEqual([ + "volume", + "prune", + "--force", + "--all", + "--filter", + "label=com.supabase.cli.project=demo", + ]); + }).pipe(Effect.provide(layer)); + }); + + it.live("--backup=false alone does not delete data volumes, matching Go's dead flag", () => { + // Go's `--backup` is declared but never bound to a variable (`cmd/stop.go:26`) — + // `RunE` always passes `!noBackup`, so `--backup=false` has zero effect in the + // real Go binary today. Only `--no-backup` deletes volumes. + const { layer, child } = setup({ configuredProjectId: "demo", route: defaultRoute() }); + return Effect.gen(function* () { + yield* legacyStop(flags({ backup: false })); + const volumePrune = child.spawned.find( + (s) => s.args[0] === "volume" && s.args[1] === "prune", + ); + expect(volumePrune).toBeUndefined(); + }).pipe(Effect.provide(layer)); + }); + + it.live("--no-backup still deletes data volumes even when --backup stays true", () => { + const { layer, child } = setup({ configuredProjectId: "demo", route: defaultRoute() }); + return Effect.gen(function* () { + yield* legacyStop(flags({ backup: true, noBackup: true })); + const volumePrune = child.spawned.find( + (s) => s.args[0] === "volume" && s.args[1] === "prune", + ); + expect(volumePrune?.args).toEqual([ + "volume", + "prune", + "--force", + "--all", + "--filter", + "label=com.supabase.cli.project=demo", + ]); + }).pipe(Effect.provide(layer)); + }); + + it.live("keeps data volumes by default (no volume prune call)", () => { + const { layer, child } = setup({ configuredProjectId: "demo", route: defaultRoute() }); + return Effect.gen(function* () { + yield* legacyStop(flags()); + const volumePrune = child.spawned.find( + (s) => s.args[0] === "volume" && s.args[1] === "prune", + ); + expect(volumePrune).toBeUndefined(); + }).pipe(Effect.provide(layer)); + }); + + it.live("fails when config.toml is malformed", () => { + const workdir = tempRoot.current; + mkdirSync(join(workdir, "supabase"), { recursive: true }); + writeFileSync(join(workdir, "supabase", "config.toml"), "not valid toml ====="); + const { layer, child } = setup({ skipConfig: true, route: defaultRoute() }); + return Effect.gen(function* () { + const exit = yield* Effect.exit(legacyStop(flags())); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(JSON.stringify(exit.cause)).toContain("LegacyStopConfigLoadError"); + } + expect(child.spawned).toEqual([]); + }).pipe(Effect.provide(layer)); + }); -describe("legacy stop", () => { - it.live("forwards no extra flags when defaults are used", () => { - const { layer, calls } = setupLegacyStop(); + it.live("fails when [remotes.*] has a duplicate project_id, even with no projectRef", () => { + // Go's Config.Validate builds the duplicate map across all [remotes.*] + // blocks unconditionally (config.go:503-518), so this must fail before + // stop ever selects a remote or touches Docker — not just when a + // matching --project-ref is requested. + const workdir = tempRoot.current; + mkdirSync(join(workdir, "supabase"), { recursive: true }); + writeFileSync( + join(workdir, "supabase", "config.toml"), + `project_id = "baseref" + +[remotes.a] +project_id = "aaaaaaaaaaaaaaaaaaaa" + +[remotes.b] +project_id = "aaaaaaaaaaaaaaaaaaaa" +`, + ); + const { layer, child } = setup({ skipConfig: true, route: defaultRoute() }); return Effect.gen(function* () { - yield* legacyStop(baseFlags); - expect(calls).toEqual([["stop"]]); + const exit = yield* Effect.exit(legacyStop(flags())); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(JSON.stringify(exit.cause)).toContain("LegacyStopConfigLoadError"); + } + expect(child.spawned).toEqual([]); }).pipe(Effect.provide(layer)); }); - it.live("forwards --backup=false when the hidden --backup flag is disabled", () => { - const { layer, calls } = setupLegacyStop(); + it.live("fails when a [remotes.*] project_id is not a valid 20-letter ref", () => { + // Go's Config.Validate (config.go:996-1001) checks every [remotes.*].project_id + // against refPattern unconditionally on every config load, so an invalid + // format must fail closed before stop reaches Docker. + const workdir = tempRoot.current; + mkdirSync(join(workdir, "supabase"), { recursive: true }); + writeFileSync( + join(workdir, "supabase", "config.toml"), + `project_id = "baseref" + +[remotes.bad] +project_id = "short" +`, + ); + const { layer, child } = setup({ skipConfig: true, route: defaultRoute() }); return Effect.gen(function* () { - yield* legacyStop({ ...baseFlags, backup: false }); - expect(calls).toEqual([["stop", "--backup=false"]]); + const exit = yield* Effect.exit(legacyStop(flags())); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(JSON.stringify(exit.cause)).toContain("LegacyStopConfigLoadError"); + } + expect(child.spawned).toEqual([]); }).pipe(Effect.provide(layer)); }); - it.live("forwards --no-backup, --project-id and --all", () => { - const { layer, calls } = setupLegacyStop(); + it.live( + "decodes a comma-separated string into an array field ([]string) for stop to proceed", + () => { + // Go's `newDecodeHook` wires `mapstructure.StringToSliceHookFunc(",")` + // unconditionally, so a plain string value for a `[]string` field like + // `additional_redirect_urls` decodes fine and must not block stop from + // reaching Docker. + const workdir = tempRoot.current; + mkdirSync(join(workdir, "supabase"), { recursive: true }); + writeFileSync( + join(workdir, "supabase", "config.toml"), + `project_id = "demo" + +[auth] +additional_redirect_urls = "http://a,http://b" +`, + ); + const { layer, child } = setup({ skipConfig: true, route: defaultRoute() }); + return Effect.gen(function* () { + yield* legacyStop(flags()); + const psCall = child.spawned.find((s) => s.args[0] === "ps"); + expect(psCall?.args).toEqual([ + "ps", + "--filter", + "label=com.supabase.cli.project=demo", + "--all", + "--format", + "{{.ID}}", + ]); + }).pipe(Effect.provide(layer)); + }, + ); + + it.live("warns on stderr for a deprecated auth.external provider", () => { + // `normalizeDeprecatedExternalProviders` (packages/config/src/io.ts) emits + // this WARN via `Console.error` only when `goViperCompat` is set — verify + // legacy stop keeps that Go-parity behavior wired on. + const workdir = tempRoot.current; + mkdirSync(join(workdir, "supabase"), { recursive: true }); + writeFileSync( + join(workdir, "supabase", "config.toml"), + `project_id = "demo" + +[auth.external.slack] +enabled = true +`, + ); + const { layer } = setup({ skipConfig: true, route: defaultRoute() }); + const warnings: Array = []; + const errorSpy = vi.spyOn(console, "error").mockImplementation((...args) => { + warnings.push(args.map((a) => String(a)).join(" ")); + }); return Effect.gen(function* () { - yield* legacyStop({ - projectId: Option.some("abc"), - backup: true, - noBackup: true, - all: true, + yield* legacyStop(flags()); + expect(warnings.some((m) => m.includes('WARN: disabling deprecated "slack" provider'))).toBe( + true, + ); + }).pipe(Effect.provide(layer), Effect.ensuring(Effect.sync(() => errorSpy.mockRestore()))); + }); + + it.live( + "fails and never spawns docker when config.toml has an unsupported db.major_version", + () => { + // Matches Go's default `stop` path, which runs `flags.LoadConfig` (config + // load + `Validate`) entirely before any Docker call + // (`internal/stop/stop.go:15-25` -> `pkg/config/config.go:882`) — a config + // Go rejects must fail `stop` before it touches containers, not just when + // reading `project_id`. + const workdir = tempRoot.current; + mkdirSync(join(workdir, "supabase"), { recursive: true }); + writeFileSync( + join(workdir, "supabase", "config.toml"), + 'project_id = "demo"\n[db]\nmajor_version = 12\n', + ); + const { layer, child } = setup({ skipConfig: true, route: defaultRoute() }); + return Effect.gen(function* () { + const exit = yield* Effect.exit(legacyStop(flags())); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(JSON.stringify(exit.cause)).toContain("LegacyStopConfigLoadError"); + expect(JSON.stringify(exit.cause)).toContain("Postgres version 12.x is unsupported"); + } + expect(child.spawned).toEqual([]); + }).pipe(Effect.provide(layer)); + }, + ); + + it.live("does not run config Validate for --all (bypasses config entirely)", () => { + // `internal/stop/stop.go:15-25`: the `--all` branch never calls + // `flags.LoadConfig`, so an otherwise-invalid config.toml must not block it. + const workdir = tempRoot.current; + mkdirSync(join(workdir, "supabase"), { recursive: true }); + writeFileSync( + join(workdir, "supabase", "config.toml"), + 'project_id = "demo"\n[db]\nmajor_version = 12\n', + ); + const { layer, child } = setup({ skipConfig: true, route: defaultRoute() }); + return Effect.gen(function* () { + const exit = yield* Effect.exit(legacyStop(flags({ all: Option.some(true) }))); + expect(Exit.isSuccess(exit)).toBe(true); + const psCall = child.spawned.find((s) => s.args[0] === "ps"); + expect(psCall?.args).toContain("label=com.supabase.cli.project"); + }).pipe(Effect.provide(layer)); + }); + + it.live("does not run config Validate for --project-id (bypasses config entirely)", () => { + // `internal/stop/stop.go:15-25`: an explicit `--project-id` sets + // `Config.ProjectId` directly and never calls `flags.LoadConfig`. + const workdir = tempRoot.current; + mkdirSync(join(workdir, "supabase"), { recursive: true }); + writeFileSync( + join(workdir, "supabase", "config.toml"), + 'project_id = "demo"\n[db]\nmajor_version = 12\n', + ); + const { layer, child } = setup({ skipConfig: true, route: defaultRoute() }); + return Effect.gen(function* () { + const exit = yield* Effect.exit(legacyStop(flags({ projectId: Option.some("explicit") }))); + expect(Exit.isSuccess(exit)).toBe(true); + const psCall = child.spawned.find((s) => s.args[0] === "ps"); + expect(psCall?.args).toContain("label=com.supabase.cli.project=explicit"); + }).pipe(Effect.provide(layer)); + }); + + it.live("fails when stopping a container errors", () => { + const { layer } = setup({ + configuredProjectId: "demo", + route: (args) => { + if (args[0] === "ps") return { stdout: ["c1"] }; + if (args[0] === "stop") return { exitCode: 1, stderr: ["boom"] }; + return { exitCode: 0 }; + }, + }); + return Effect.gen(function* () { + const exit = yield* Effect.exit(legacyStop(flags())); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(JSON.stringify(exit.cause)).toContain("LegacyStopContainerError"); + } + }).pipe(Effect.provide(layer)); + }); + + it.live("fails when a container cannot be spawned to stop it at all", () => { + // Distinct from a spawned `docker stop` exiting non-zero (covered above) — + // this exercises the branch where docker AND podman both fail to spawn for + // the `stop ` argv specifically. + const { layer } = setup({ + configuredProjectId: "demo", + route: (args) => (args[0] === "ps" ? { stdout: ["c1"] } : { exitCode: 0 }), + failSpawnFor: (args) => args[0] === "stop", + }); + return Effect.gen(function* () { + const exit = yield* Effect.exit(legacyStop(flags())); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(JSON.stringify(exit.cause)).toContain("LegacyStopContainerError"); + } + }).pipe(Effect.provide(layer)); + }); + + it.live( + "fails the same way in json mode, where 'Stopping containers...' is never printed", + () => { + // The `output.format === "text"` gate around the "Stopping containers..." + // line means json mode skips it entirely; this exercises that the + // list/stop/prune failure path is unaffected by that gate. + const { layer } = setup({ + format: "json", + configuredProjectId: "demo", + route: (args) => { + if (args[0] === "ps") return { stdout: ["c1"] }; + if (args[0] === "stop") return { exitCode: 1, stderr: ["boom"] }; + return { exitCode: 0 }; + }, }); - expect(calls).toEqual([["stop", "--project-id", "abc", "--no-backup", "--all"]]); + return Effect.gen(function* () { + const exit = yield* Effect.exit(legacyStop(flags())); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(JSON.stringify(exit.cause)).toContain("LegacyStopContainerError"); + } + }).pipe(Effect.provide(layer)); + }, + ); + + it.live("fails when container prune errors", () => { + const { layer } = setup({ + configuredProjectId: "demo", + route: (args) => { + if (args[0] === "container" && args[1] === "prune") return { exitCode: 1 }; + return defaultRoute()(args); + }, + }); + return Effect.gen(function* () { + const exit = yield* Effect.exit(legacyStop(flags())); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(JSON.stringify(exit.cause)).toContain("LegacyStopContainerPruneError"); + } + }).pipe(Effect.provide(layer)); + }); + + it.live("fails when volume prune errors", () => { + const { layer } = setup({ + configuredProjectId: "demo", + route: (args) => { + if (args[0] === "volume" && args[1] === "prune") return { exitCode: 1 }; + return defaultRoute()(args); + }, + }); + return Effect.gen(function* () { + const exit = yield* Effect.exit(legacyStop(flags({ noBackup: true }))); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(JSON.stringify(exit.cause)).toContain("LegacyStopVolumePruneError"); + } + }).pipe(Effect.provide(layer)); + }); + + it.live("fails when network prune errors", () => { + const { layer } = setup({ + configuredProjectId: "demo", + route: (args) => { + if (args[0] === "network" && args[1] === "prune") return { exitCode: 1 }; + return defaultRoute()(args); + }, + }); + return Effect.gen(function* () { + const exit = yield* Effect.exit(legacyStop(flags())); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(JSON.stringify(exit.cause)).toContain("LegacyStopNetworkPruneError"); + } + }).pipe(Effect.provide(layer)); + }); + + it.live("fails when the container list errors", () => { + const { layer } = setup({ + configuredProjectId: "demo", + route: (args) => { + if (args[0] === "ps") return { exitCode: 1, stderr: ["daemon down"] }; + return { exitCode: 0 }; + }, + }); + return Effect.gen(function* () { + const exit = yield* Effect.exit(legacyStop(flags())); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(JSON.stringify(exit.cause)).toContain("LegacyStopListError"); + } + }).pipe(Effect.provide(layer)); + }); + + it.live("falls back to podman when docker is absent", () => { + const { layer, child } = setup({ + configuredProjectId: "demo", + route: defaultRoute(), + dockerMissing: true, + }); + return Effect.gen(function* () { + yield* legacyStop(flags()); + // The failed `docker` attempt is recorded before the `podman` fallback fires + // (`spawnContainerCli`'s `Effect.catch` retries the same argv), so the + // successful call is the LAST matching record, not the first. + const psCalls = child.spawned.filter((s) => s.args[0] === "ps"); + expect(psCalls.at(-1)?.command).toBe("podman"); + expect(psCalls.some((s) => s.command === "docker")).toBe(true); + }).pipe(Effect.provide(layer)); + }); + + it.live("omits --all from podman's volume prune (not a real Podman flag)", () => { + // No released Podman `volume prune` accepts `--all` (only `--filter`/`--force`/ + // `--help`), so passing Docker's `--all` argv straight through to the Podman + // fallback would hard-fail after containers are already stopped. Podman prunes + // every unused volume by default, so dropping `--all` there is lossless. + const { layer, child } = setup({ + configuredProjectId: "demo", + route: defaultRoute(), + dockerMissing: true, + }); + return Effect.gen(function* () { + yield* legacyStop(flags({ noBackup: true })); + const volumePruneCalls = child.spawned.filter( + (s) => s.args[0] === "volume" && s.args[1] === "prune", + ); + expect(volumePruneCalls.at(-1)?.command).toBe("podman"); + expect(volumePruneCalls.at(-1)?.args).toEqual([ + "volume", + "prune", + "--force", + "--filter", + "label=com.supabase.cli.project=demo", + ]); + }).pipe(Effect.provide(layer)); + }); + + it.live("emits a machine result in json mode without spinner text", () => { + const { layer, out } = setup({ + format: "json", + configuredProjectId: "demo", + route: defaultRoute({ volumeNames: ["supabase_db_demo"] }), + }); + return Effect.gen(function* () { + yield* legacyStop(flags()); + const success = out.messages.find((m) => m.type === "success"); + expect(success?.data).toMatchObject({ project_id_filter: "demo", backup: true }); + expect(out.stdoutText).not.toContain("\x1b[?25l"); + // json mode has no volume-suggestion equivalent — only text mode emits it. + expect(out.stderrText).not.toContain("Local data are backed up"); + }).pipe(Effect.provide(layer)); + }); + + it.live("shows no volume suggestion when no volumes remain", () => { + const { layer, out } = setup({ + configuredProjectId: "demo", + route: defaultRoute({ volumeNames: [] }), + }); + return Effect.gen(function* () { + yield* legacyStop(flags()); + expect(out.stderrText).not.toContain("Local data are backed up"); + }).pipe(Effect.provide(layer)); + }); + + it.live("flushes telemetry via ensuring even on failure", () => { + const { layer, telemetry } = setup({ + configuredProjectId: "demo", + route: (args) => (args[0] === "ps" ? { exitCode: 1 } : { exitCode: 0 }), + }); + return Effect.gen(function* () { + yield* Effect.exit(legacyStop(flags())); + expect(telemetry.flushed).toBe(true); + }).pipe(Effect.provide(layer)); + }); + + it.live("fails when container prune cannot spawn any container runtime", () => { + const { layer } = setup({ + configuredProjectId: "demo", + route: defaultRoute(), + failSpawnFor: (args) => args[0] === "container" && args[1] === "prune", + }); + return Effect.gen(function* () { + const exit = yield* Effect.exit(legacyStop(flags())); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(JSON.stringify(exit.cause)).toContain("LegacyStopContainerPruneError"); + } + }).pipe(Effect.provide(layer)); + }); + + it.live("fails when volume prune cannot spawn any container runtime", () => { + const { layer } = setup({ + configuredProjectId: "demo", + route: defaultRoute(), + failSpawnFor: (args) => args[0] === "volume" && args[1] === "prune", + }); + return Effect.gen(function* () { + const exit = yield* Effect.exit(legacyStop(flags({ noBackup: true }))); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(JSON.stringify(exit.cause)).toContain("LegacyStopVolumePruneError"); + } + }).pipe(Effect.provide(layer)); + }); + + it.live("fails when network prune cannot spawn any container runtime", () => { + const { layer } = setup({ + configuredProjectId: "demo", + route: defaultRoute(), + failSpawnFor: (args) => args[0] === "network" && args[1] === "prune", + }); + return Effect.gen(function* () { + const exit = yield* Effect.exit(legacyStop(flags())); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(JSON.stringify(exit.cause)).toContain("LegacyStopNetworkPruneError"); + } + }).pipe(Effect.provide(layer)); + }); + + it.live("still reports success when the post-run volume listing fails", () => { + // The volume-suggestion check is best-effort (`Effect.orElseSucceed`): a + // failure listing volumes after a successful stop must not fail the command, + // matching Go's `if resp, err := ...; err == nil && ...` (stop.go:29) — a + // listing error there is silently ignored, not surfaced. + const { layer, out } = setup({ + configuredProjectId: "demo", + route: defaultRoute(), + failSpawnFor: (args) => args[0] === "volume" && args[1] === "ls", + }); + return Effect.gen(function* () { + yield* legacyStop(flags()); + expect(out.stdoutText).toContain("Stopped"); + expect(out.stderrText).not.toContain("Local data are backed up"); }).pipe(Effect.provide(layer)); }); }); diff --git a/apps/cli/src/legacy/commands/stop/stop.live.test.ts b/apps/cli/src/legacy/commands/stop/stop.live.test.ts new file mode 100644 index 0000000000..95452c78c7 --- /dev/null +++ b/apps/cli/src/legacy/commands/stop/stop.live.test.ts @@ -0,0 +1,78 @@ +import { execFile } from "node:child_process"; +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { promisify } from "node:util"; +import { afterEach, expect, test } from "vitest"; + +import { describeLive, runSupabaseLive } from "../../../../tests/helpers/live.ts"; + +const execFileAsync = promisify(execFile); + +const START_TIMEOUT_MS = 280_000; + +// `stop` never calls the Management API — it talks directly to the real local +// Docker stack `start` (still a Go-proxy) creates. `describeLive` is reused +// purely as the "we're in the full cli-e2e-ci runner" signal (it also has a +// real Docker daemon, since that's how supabox itself runs); the +// SUPABASE_ACCESS_TOKEN it gates on is otherwise irrelevant here. See +// AGENTS.md's "Live tests" section for the full convention. +describeLive("supabase stop (live)", () => { + let projectDir: string | undefined; + let projectId: string | undefined; + + afterEach(async () => { + if (projectDir === undefined) return; + // Best-effort cleanup even if an assertion above failed mid-lifecycle — a + // leaked local stack would otherwise pollute the CI runner for later jobs. + await runSupabaseLive(["stop", "--no-backup"], { cwd: projectDir }).catch(() => undefined); + await rm(projectDir, { recursive: true, force: true }).catch(() => undefined); + projectDir = undefined; + projectId = undefined; + }); + + test( + "starts a real local stack, then stops it and removes its containers", + { timeout: START_TIMEOUT_MS }, + async () => { + projectDir = await mkdtemp(path.join(tmpdir(), "sb-stop-live-")); + // No `project_id` override, so the cli resolves it from the workdir + // basename — matching Go's precedence exactly (see legacy-docker-ids.ts). + projectId = path.basename(projectDir); + + const init = await runSupabaseLive(["init"], { cwd: projectDir }); + expect(init.exitCode, `stdout:\n${init.stdout}\nstderr:\n${init.stderr}`).toBe(0); + + // Exclude the heaviest, least relevant services (Next.js Studio build, the + // logging pipeline) — `stop`'s Docker label-filtering logic doesn't care + // which services are running, only that at least one real container + // exists to stop. + const start = await runSupabaseLive( + ["start", "--exclude", "studio", "--exclude", "analytics", "--exclude", "vector"], + { cwd: projectDir, exitTimeoutMs: START_TIMEOUT_MS }, + ); + expect(start.exitCode, `stdout:\n${start.stdout}\nstderr:\n${start.stderr}`).toBe(0); + + // Sanity: confirm the stack is actually up before testing `stop` against it. + const before = await runSupabaseLive(["status"], { cwd: projectDir }); + expect(before.exitCode, `stdout:\n${before.stdout}\nstderr:\n${before.stderr}`).toBe(0); + + const stop = await runSupabaseLive(["stop"], { cwd: projectDir }); + expect(stop.exitCode, `stdout:\n${stop.stdout}\nstderr:\n${stop.stderr}`).toBe(0); + expect(stop.stdout).toContain("Stopped"); + + // The real Docker daemon must agree: no container carrying this project's + // label survives `stop` — the actual behavior under test, not just the + // cli's own exit code. + const { stdout: remaining } = await execFileAsync("docker", [ + "ps", + "-a", + "--filter", + `label=com.supabase.cli.project=${projectId}`, + "--format", + "{{.ID}}", + ]); + expect(remaining.trim()).toBe(""); + }, + ); +}); diff --git a/apps/cli/src/legacy/commands/storage/storage.frame.ts b/apps/cli/src/legacy/commands/storage/storage.frame.ts index f4d5fd15f9..dfad46cac2 100644 --- a/apps/cli/src/legacy/commands/storage/storage.frame.ts +++ b/apps/cli/src/legacy/commands/storage/storage.frame.ts @@ -50,8 +50,8 @@ export const legacyLoadStorageConfig = Effect.fnUntraced(function* ( workdir: string, projectRef: string, ) { - const loadOptions: LoadProjectConfigOptions | undefined = - projectRef !== "" ? { projectRef } : undefined; + const loadOptions: LoadProjectConfigOptions = + projectRef !== "" ? { projectRef, goViperCompat: true } : { goViperCompat: true }; const loaded = yield* loadProjectConfig(workdir, loadOptions).pipe( Effect.catchTag( "ProjectConfigParseError", diff --git a/apps/cli/src/legacy/config/legacy-cli-config.layer.ts b/apps/cli/src/legacy/config/legacy-cli-config.layer.ts index 9ced346d5f..843059bf59 100644 --- a/apps/cli/src/legacy/config/legacy-cli-config.layer.ts +++ b/apps/cli/src/legacy/config/legacy-cli-config.layer.ts @@ -166,6 +166,22 @@ function resolveProfile( }); } +/** + * Go's `ChangeWorkDir` (`apps/cli-go/internal/utils/misc.go:231-250`) always + * `os.Chdir(workdir)`s using the raw `--workdir`/`SUPABASE_WORKDIR` string, + * which can be relative (e.g. `.`) — but every later reader of the resolved + * workdir (including the `Config.ProjectId` cwd-basename default, `Eject`, + * `pkg/config/config.go:561-570`, run on every `Config.Load()` via + * `mergeDefaultValues`, `config.go:690-699`) reads `os.Getwd()`, the real + * ABSOLUTE directory, never the raw configured string. `os.Chdir(".")` is a + * no-op syscall-wise, so Go's `cwd` is unaffected by the flag/env value being + * relative. This resolves the flag/env value against the real process `cwd` + * the same way, so `LegacyCliConfig.workdir` is always absolute — matching + * Go's invariant that basename-ing it (e.g. `legacyResolveLocalProjectId`'s + * workdir-basename fallback) operates on a real directory name, not a + * relative-path fragment like `.` (which would sanitize to an empty project + * id and build a bare, all-projects-matching Docker label filter). + */ function resolveWorkdir( flagValue: Option.Option, envValue: string | undefined, @@ -175,10 +191,10 @@ function resolveWorkdir( ): Effect.Effect { return Effect.gen(function* () { if (Option.isSome(flagValue) && flagValue.value.length > 0) { - return flagValue.value; + return path.resolve(cwd, flagValue.value); } if (envValue !== undefined && envValue.length > 0) { - return envValue; + return path.resolve(cwd, envValue); } let current = cwd; // Walk up until we hit a directory containing supabase/config.toml or the FS root. diff --git a/apps/cli/src/legacy/config/legacy-cli-config.layer.unit.test.ts b/apps/cli/src/legacy/config/legacy-cli-config.layer.unit.test.ts index f6d8ca20f8..0bb042b435 100644 --- a/apps/cli/src/legacy/config/legacy-cli-config.layer.unit.test.ts +++ b/apps/cli/src/legacy/config/legacy-cli-config.layer.unit.test.ts @@ -278,6 +278,43 @@ describe("legacyCliConfigLayer", () => { ), ); + // Go's `ChangeWorkDir` (`apps/cli-go/internal/utils/misc.go:231-250`) always + // `os.Chdir`s the raw flag/env value, but every later reader — including the + // `Config.ProjectId` cwd-basename default (`Eject`, `pkg/config/config.go: + // 561-570`) — reads `os.Getwd()`, the real absolute directory, never the raw + // string. A relative `--workdir .`/`SUPABASE_WORKDIR=.` must therefore resolve + // to an absolute path here too, not stay `"."` (which would later basename to + // an empty project id). + it.effect("resolves a relative --workdir flag against the real cwd", () => + Effect.gen(function* () { + const config = yield* LegacyCliConfig; + expect(config.workdir).toBe(tempRoot); + }).pipe(Effect.provide(makeLayer({ workdirFlag: Option.some("."), cwd: tempRoot }))), + ); + + it.effect("resolves a relative --workdir flag with a subdirectory against the real cwd", () => + Effect.gen(function* () { + const config = yield* LegacyCliConfig; + expect(config.workdir).toBe(join(tempRoot, "sub")); + }).pipe(Effect.provide(makeLayer({ workdirFlag: Option.some("sub"), cwd: tempRoot }))), + ); + + it.effect("resolves a relative SUPABASE_WORKDIR env value against the real cwd", () => + Effect.gen(function* () { + const config = yield* LegacyCliConfig; + expect(config.workdir).toBe(tempRoot); + }).pipe(Effect.provide(makeLayer({ env: { SUPABASE_WORKDIR: "." }, cwd: tempRoot }))), + ); + + it.effect("keeps an absolute --workdir flag unchanged", () => + Effect.gen(function* () { + const config = yield* LegacyCliConfig; + expect(config.workdir).toBe("/flag/workdir"); + }).pipe( + Effect.provide(makeLayer({ workdirFlag: Option.some("/flag/workdir"), cwd: tempRoot })), + ), + ); + it.effect("walks up from CWD looking for supabase/config.toml", () => { const projectRoot = join(tempRoot, "project"); const nested = join(projectRoot, "deep", "child"); diff --git a/apps/cli/src/legacy/shared/legacy-api-url.ts b/apps/cli/src/legacy/shared/legacy-api-url.ts new file mode 100644 index 0000000000..c48b73ac44 --- /dev/null +++ b/apps/cli/src/legacy/shared/legacy-api-url.ts @@ -0,0 +1,26 @@ +/** + * Local API URL derivation, mirroring Go's `config.go:634-644` + `misc.go:298`: + * an explicit `api.external_url` wins, otherwise `://:` + * where the scheme follows `api.tls.enabled` and the port is `api.port`. + * Hoisted here because `legacy-storage-credentials.ts` and + * `legacy-local-config-values.ts` both need this exact computation. + */ +export function legacyResolveApiExternalUrl( + config: { + readonly external_url?: string; + readonly port: number; + readonly tls: { readonly enabled: boolean }; + }, + hostname: string, +): string { + if (config.external_url !== undefined && config.external_url.length > 0) { + return config.external_url; + } + const scheme = config.tls.enabled ? "https" : "http"; + // Go builds host:port with net.JoinHostPort (config.go:636-638), bracketing an + // IPv6 host. + const hostPort = hostname.includes(":") + ? `[${hostname}]:${config.port}` + : `${hostname}:${config.port}`; + return `${scheme}://${hostPort}`; +} diff --git a/apps/cli/src/legacy/shared/legacy-colors.ts b/apps/cli/src/legacy/shared/legacy-colors.ts index c41cfae7d4..25d32d7c42 100644 --- a/apps/cli/src/legacy/shared/legacy-colors.ts +++ b/apps/cli/src/legacy/shared/legacy-colors.ts @@ -6,27 +6,38 @@ import { styleText } from "node:util"; * Go uses lipgloss, which auto-detects the output profile and renders **plain** * text when the stream is not a TTY (piped output, CI, tests). `styleText` * mirrors that: with `validateStream` (the default) it checks the target stream - * and `NO_COLOR`, returning the unstyled string when colour is unsupported. We - * point it at `process.stderr` because the bootstrap progress / suggestion lines - * these style are written to stderr. + * and `NO_COLOR`, returning the unstyled string when colour is unsupported. + * + * `stream` defaults to `process.stderr` because every original call site styles + * progress/suggestion lines written to stderr. A caller styling content that is + * itself written to **stdout** (e.g. `status`'s pretty table) must pass + * `process.stdout` explicitly — otherwise the TTY check runs against the wrong + * stream, and piping stdout while stderr stays a TTY (`supabase status | less`) + * would corrupt the piped output with ANSI escapes (the same bug class CLI-1546 + * fixed for the progress spinner). * * lipgloss colour "14" is bright cyan; `"cyan"` is the closest faithful match, * matching `branches.prompt.ts`'s existing port of `utils.Aqua`. */ -export function legacyAqua(text: string): string { - return styleText("cyan", text, { stream: process.stderr }); +export function legacyAqua(text: string, stream: NodeJS.WriteStream = process.stderr): string { + return styleText("cyan", text, { stream }); } -export function legacyBold(text: string): string { - return styleText("bold", text, { stream: process.stderr }); +export function legacyBold(text: string, stream: NodeJS.WriteStream = process.stderr): string { + return styleText("bold", text, { stream }); } /** Port of Go's `utils.Yellow` — lipgloss colour "11" (bright yellow). */ -export function legacyYellow(text: string): string { - return styleText("yellow", text, { stream: process.stderr }); +export function legacyYellow(text: string, stream: NodeJS.WriteStream = process.stderr): string { + return styleText("yellow", text, { stream }); } /** Port of Go's `utils.Red` — lipgloss colour "9" (bright red). */ -export function legacyRed(text: string): string { - return styleText("red", text, { stream: process.stderr }); +export function legacyRed(text: string, stream: NodeJS.WriteStream = process.stderr): string { + return styleText("red", text, { stream }); +} + +/** Port of Go's `utils.Green` — lipgloss colour "10" (bright green). */ +export function legacyGreen(text: string, stream: NodeJS.WriteStream = process.stderr): string { + return styleText("green", text, { stream }); } diff --git a/apps/cli/src/legacy/shared/legacy-colors.unit.test.ts b/apps/cli/src/legacy/shared/legacy-colors.unit.test.ts new file mode 100644 index 0000000000..e1f5e7873b --- /dev/null +++ b/apps/cli/src/legacy/shared/legacy-colors.unit.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it } from "vitest"; + +import { legacyAqua, legacyBold, legacyGreen, legacyRed, legacyYellow } from "./legacy-colors.ts"; + +// These tests only assert that each helper runs without throwing and returns a +// string containing the input text — actual color application depends on the +// stream's live TTY/NO_COLOR state, which isn't controllable from a test +// process. The behavior worth protecting here is the `stream` parameter +// threading through to `styleText`, not a specific ANSI byte sequence. +describe("legacy-colors", () => { + it("legacyAqua defaults to stderr when no stream is given", () => { + expect(legacyAqua("supabase")).toContain("supabase"); + }); + + it("legacyAqua accepts an explicit stream", () => { + expect(legacyAqua("supabase", process.stdout)).toContain("supabase"); + }); + + it("legacyBold defaults to stderr when no stream is given", () => { + expect(legacyBold("text")).toContain("text"); + }); + + it("legacyBold accepts an explicit stream", () => { + expect(legacyBold("text", process.stdout)).toContain("text"); + }); + + it("legacyYellow defaults to stderr when no stream is given", () => { + expect(legacyYellow("warning")).toContain("warning"); + }); + + it("legacyYellow accepts an explicit stream", () => { + expect(legacyYellow("warning", process.stdout)).toContain("warning"); + }); + + it("legacyRed defaults to stderr when no stream is given", () => { + expect(legacyRed("error")).toContain("error"); + }); + + it("legacyRed accepts an explicit stream", () => { + expect(legacyRed("error", process.stdout)).toContain("error"); + }); + + it("legacyGreen defaults to stderr when no stream is given", () => { + expect(legacyGreen("label")).toContain("label"); + }); + + it("legacyGreen accepts an explicit stream", () => { + expect(legacyGreen("label", process.stdout)).toContain("label"); + }); +}); diff --git a/apps/cli/src/legacy/shared/legacy-config-validate.parity.unit.test.ts b/apps/cli/src/legacy/shared/legacy-config-validate.parity.unit.test.ts new file mode 100644 index 0000000000..d62db6a815 --- /dev/null +++ b/apps/cli/src/legacy/shared/legacy-config-validate.parity.unit.test.ts @@ -0,0 +1,266 @@ +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { BunServices } from "@effect/platform-bun"; +import { describe, expect, it } from "@effect/vitest"; +import { ProjectConfigSchema, type ProjectConfig } from "@supabase/config"; +import { Effect, Exit, FileSystem, Path, Schema } from "effect"; + +import { legacyReadDbToml } from "./legacy-db-config.toml-read.ts"; +import { legacyResolveLocalConfigValues } from "./legacy-local-config-values.ts"; + +/** + * Cross-caller parity coverage: for a table of Go-parity misconfigurations, drives BOTH real + * pipelines — D (`legacyReadDbToml`, Effect/raw-TOML) and L (`legacyResolveLocalConfigValues`, + * `@supabase/config`-decoded) — and asserts they fail with the SAME shared error-message + * substring, since both now route through the single `legacyValidateResolvedConfig`. The two + * pipelines don't need byte-identical exception wrapping, just the same core Go-parity message + * text (`.toContain(...)` on both sides with the same expected string). + * + * D's harness replicates the `withConfig`/`read`/`failsWith` pattern from + * `legacy-db-config.toml-read.unit.test.ts` (file-local there, not exported — faithfully + * reproduced here rather than imported). L's harness replicates the `baseConfig`/`WORKDIR` + * pattern from `legacy-local-config-values.unit.test.ts` (same reasoning). + */ + +function withConfig(content: string) { + const dir = mkdtempSync(join(tmpdir(), "legacy-config-validate-parity-")); + mkdirSync(join(dir, "supabase"), { recursive: true }); + writeFileSync(join(dir, "supabase", "config.toml"), content); + return dir; +} + +const readD = (workdir: string) => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + return yield* legacyReadDbToml(fs, path, workdir); + }).pipe(Effect.provide(BunServices.layer)); + +/** Drives D's real pipeline and asserts the failure message contains `message`. */ +function failsWithD(tomlLines: ReadonlyArray, message: string) { + return Effect.gen(function* () { + const dir = withConfig(tomlLines.join("\n")); + const exit = yield* readD(dir).pipe(Effect.exit); + expect(Exit.isFailure(exit), `D: expected failure containing: ${message}`).toBe(true); + if (Exit.isFailure(exit)) { + expect(JSON.stringify(exit.cause)).toContain(message); + } + rmSync(dir, { recursive: true, force: true }); + }); +} + +const decodeConfig = Schema.decodeUnknownSync(ProjectConfigSchema); +const WORKDIR = "/tmp/legacy-config-validate-parity-test"; + +function baseConfig(overrides: Record = {}): ProjectConfig { + return decodeConfig({ project_id: "test", ...overrides }); +} + +/** Drives L's real pipeline and asserts the failure message contains `message`. */ +function failsWithL( + overrides: Record, + message: string, + document?: Readonly>, +) { + const config = baseConfig(overrides); + expect(() => + legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document), + ).toThrow(message); +} + +interface ParityScenario { + readonly name: string; + readonly toml: ReadonlyArray; + readonly overrides: Record; + readonly document?: Readonly>; + readonly message: string; +} + +const scenarios: ReadonlyArray = [ + { + name: "db.port = 0", + toml: ["[db]", "port = 0"], + overrides: { db: { port: 0 } }, + message: "Missing required field in config: db.port", + }, + { + name: "db.major_version = 0", + toml: ["[db]", "major_version = 0"], + overrides: { db: { major_version: 0 } }, + message: "Missing required field in config: db.major_version", + }, + { + name: "db.major_version unsupported (16)", + toml: ["[db]", "major_version = 16"], + overrides: { db: { major_version: 16 } }, + message: "Failed reading config: Invalid db.major_version: 16.", + }, + { + name: "storage bucket name with an invalid pattern", + toml: ['[storage.buckets."bad#name"]'], + overrides: { storage: { buckets: { "bad#name": {} } } }, + message: + "Invalid Bucket name: bad#name. Only lowercase letters, numbers, dots, hyphens, and spaces are allowed.", + }, + { + name: "function slug with an invalid pattern", + toml: ["[functions.123]"], + overrides: { functions: { "123": {} } }, + message: + "Invalid Function name: 123. Must start with at least one letter, and only include alphanumeric characters, underscores, and hyphens.", + }, + { + name: "edge_runtime.deno_version = 0", + toml: ["[edge_runtime]", "deno_version = 0"], + overrides: { edge_runtime: { deno_version: 0 } }, + message: "Missing required field in config: edge_runtime.deno_version", + }, + { + name: "edge_runtime.deno_version unsupported (3)", + toml: ["[edge_runtime]", "deno_version = 3"], + overrides: { edge_runtime: { deno_version: 3 } }, + message: "Failed reading config: Invalid edge_runtime.deno_version: 3.", + }, + { + name: "auth.site_url empty with auth enabled", + toml: ["[auth]", "enabled = true", 'site_url = ""'], + overrides: { auth: { enabled: true, site_url: "" } }, + message: "Missing required field in config: auth.site_url", + }, + { + name: "auth.captcha enabled without a provider", + toml: ["[auth.captcha]", "enabled = true"], + overrides: { + auth: { + enabled: true, + site_url: "http://localhost:3000", + captcha: { enabled: true }, + }, + }, + message: "Missing required field in config: auth.captcha.provider", + }, + { + name: "auth.captcha enabled with a provider but no secret", + toml: ["[auth.captcha]", "enabled = true", 'provider = "hcaptcha"'], + overrides: { + auth: { + enabled: true, + site_url: "http://localhost:3000", + captcha: { enabled: true, provider: "hcaptcha" }, + }, + }, + message: "Missing required field in config: auth.captcha.secret", + }, + { + name: "auth.hook.* with a badly-formatted secret", + toml: [ + "[auth.hook.custom_access_token]", + "enabled = true", + 'uri = "https://example.test/hook"', + 'secrets = "not-a-valid-secret"', + ], + overrides: { + auth: { + enabled: true, + site_url: "http://localhost:3000", + hook: { + custom_access_token: { + enabled: true, + uri: "https://example.test/hook", + secrets: "not-a-valid-secret", + }, + }, + }, + }, + // D's assertion goes through `JSON.stringify(exit.cause)`, which backslash-escapes the + // message's embedded double quotes — trim the substring to the quote-free prefix, same + // convention D's own suite uses for this message. + message: "auth.hook.custom_access_token.secrets must be formatted as", + }, + { + name: "auth.mfa.* enroll_enabled without verify_enabled", + toml: ["[auth.mfa.totp]", "enroll_enabled = true", "verify_enabled = false"], + overrides: { + auth: { + enabled: true, + site_url: "http://localhost:3000", + mfa: { totp: { enroll_enabled: true, verify_enabled: false } }, + }, + }, + message: "Invalid MFA config: auth.mfa.totp.enroll_enabled requires verify_enabled", + }, + { + name: "auth.third_party.* enabled without its required field", + toml: ["[auth.third_party.firebase]", "enabled = true"], + overrides: { + auth: { + enabled: true, + site_url: "http://localhost:3000", + third_party: { firebase: { enabled: true } }, + }, + }, + message: "Invalid config: auth.third_party.firebase is enabled but without a project_id.", + }, + { + name: "auth.third_party.* more than one provider enabled", + toml: [ + "[auth.third_party.firebase]", + "enabled = true", + 'project_id = "proj"', + "[auth.third_party.auth0]", + "enabled = true", + 'tenant = "tenant"', + ], + overrides: { + auth: { + enabled: true, + site_url: "http://localhost:3000", + third_party: { + firebase: { enabled: true, project_id: "proj" }, + auth0: { enabled: true, tenant: "tenant" }, + }, + }, + }, + message: "Invalid config: Only one third_party provider allowed to be enabled at a time.", + }, + { + name: "auth.email.smtp present table missing a required field", + // Both pipelines read every smtp field straight off the raw TOML/document rather than a + // schema-decoded, always-defaulted value (Go's presence-based `enabled` default, + // config.go:743-748) — L needs the raw `document` (5th param) for this, matching D's raw + // smol-toml document. + toml: ["[auth.email.smtp]", 'user = "u"'], + overrides: { auth: { enabled: true, site_url: "http://localhost:3000" } }, + document: { auth: { email: { smtp: { user: "u" } } } }, + message: "Missing required field in config: auth.email.smtp.host", + }, + { + name: "experimental.pgdelta.format_options invalid JSON", + toml: ["[experimental.pgdelta]", 'format_options = "{not json"'], + overrides: { experimental: { pgdelta: { format_options: "{not json" } } }, + message: "Invalid config for experimental.pgdelta.format_options: must be valid JSON", + }, +]; + +// Explicitly SKIPPED (only one caller runs the branch, or the branch isn't exercised the same +// way by both — see the module header in `legacy-config-validate.ts` for the full explicitly +// out-of-scope list): +// - `remotes[*].project_id`, `auth.sms`, `auth.external` — D-only, never part of the shared +// validator (`LegacyConfigValidationInput` has no fields for these at all). +// - `api.tls`, `project_id`, `studio`, `local_smtp` — L-only, D has no equivalent sections. +// - `experimental.webhooks` — L reads webhooks presence from a raw `document` that D's +// `legacyReadDbToml` doesn't thread through `legacyValidateResolvedConfig` at all (D has no +// `experimental` presence-based input field for this — verified: `legacy-db-config.toml-read.ts` +// never sets `experimental.webhooksPresent`/`webhooksEnabled` on its `LegacyExperimentalInput`), +// so this branch is D-unreachable and skipped here too. +describe("legacyValidateResolvedConfig cross-caller parity (D vs L)", () => { + for (const scenario of scenarios) { + it.effect(`${scenario.name}: D and L fail with the same message`, () => + Effect.gen(function* () { + yield* failsWithD(scenario.toml, scenario.message); + failsWithL(scenario.overrides, scenario.message, scenario.document); + }), + ); + } +}); diff --git a/apps/cli/src/legacy/shared/legacy-config-validate.ts b/apps/cli/src/legacy/shared/legacy-config-validate.ts new file mode 100644 index 0000000000..f833f95300 --- /dev/null +++ b/apps/cli/src/legacy/shared/legacy-config-validate.ts @@ -0,0 +1,778 @@ +import { isAbsolute, join } from "node:path"; + +import { legacyGoUrlParse } from "./legacy-storage-url.ts"; + +/** + * Single home for Go's `Config.Validate` parity (`apps/cli-go/pkg/config/config.go:989-1192`), + * consolidating the two independent TypeScript ports of that logic: + * + * - **D** = `legacy-db-config.toml-read.ts` — raw smol-toml document + `EnvLookup`, + * Effect-based, fails with `LegacyDbConfigLoadError`. Feeds ~15 db/migration commands via + * `legacy-db-config.layer.ts`. + * - **L** = `legacy-local-config-values.ts` — decoded `@supabase/config` `ProjectConfig`, + * synchronous `node:fs`, throws plain `Error`. Feeds `status/status.values.ts` and + * `stop/stop.handler.ts`. + * + * **This file is the SINGLE home for `Config.Validate` parity going forward. + * Per-command reimplementations of any branch below are forbidden** — hoist here instead, + * per `apps/cli/AGENTS.md`'s "Hoist Before You Duplicate" policy. + * + * ## Status of this commit + * + * {@link legacyValidateResolvedConfig} is now IMPLEMENTED and fully wired into BOTH callers: L + * (`legacy-local-config-values.ts`'s `legacyResolveLocalConfigValues`) and D + * (`legacy-db-config.toml-read.ts`'s `legacyReadDbToml`) each build a + * {@link LegacyConfigValidationInput} from their own decoded config (a `ProjectConfig` + raw + * `document` for L, a raw smol-toml document + `EnvLookup` for D) and call this function once, + * at the correct Go position. Wiring D through this module also fixed D's `db.major_version + * === 0` divergence (D used to fall through to the generic invalid-value message; it now + * throws the same "Missing required field in config: db.major_version" as Go and as L already + * did). + * + * ## Full eventual scope: every `Config.Validate` branch this module owns + * + * In Go's exact `Validate()` order (`config.go:989-1192`), first-failure-wins: + * + * | Go line(s) | Check | + * |--------------------------------|-------| + * | 990-991 | `project_id` required | + * | 1006-1027 | `api.port` / `api.tls.{cert,key}_path` presence (the actual file reads stay per-caller I/O) | + * | 1031-1062 | `db.port`, `db.major_version` (0 / 12 / 13-17 switch) | + * | 1064-1068, pattern @ 1549-1554 | `storage.buckets.*` names vs `LEGACY_BUCKET_NAME_PATTERN` | + * | 1070-1079 | `studio.port` / `studio.api_url` (L-only — D has no studio section) | + * | 1081-1085 | `local_smtp.port` (L-only) | + * | 1087-1153 | `auth.*` sub-sequence, in order: site_url (1088-1090); captcha enum + presence (1099-1109, enum itself decode-time per `auth.go:58-71`); signing_keys read (1110-1116, caller-side I/O); passkey/webauthn (1117-1134); hooks (1136-1138, checks @ 1453-1521, vs `LEGACY_HOOK_SECRET_PATTERN`); mfa (1139-1141, checks @ 1523-1534); email template/notification content-vs-content_path (1293-1323, caller-side I/O) + smtp (1325-1344); third_party (1151-1153, checks @ 1635-1683, vs `LEGACY_CLERK_DOMAIN_PATTERN`) | + * | 1159-1163, pattern @ 1539-1544 | `functions.*` slugs vs `LEGACY_FUNCTION_SLUG_PATTERN` | + * | 1164-1173 | `edge_runtime.deno_version` (0 / 1 / 2 switch) | + * | (decode-time enum) | `analytics.backend` must be `postgres`/`bigquery` | + * | 1175-1187 | `analytics.gcp_*` fields, gated on `backend === "bigquery"` | + * | 1846-1854 | `experimental.webhooks` / `experimental.pgdelta.format_options` | + * + * ## Explicitly OUT of scope forever (D-only, NEVER part of this module) + * + * - `remotes[*].project_id` pattern (`config.go:997-1001`, vs `LEGACY_PROJECT_REF_PATTERN`) — + * D's own remote-merge-phase check (`findInvalidRemoteProjectId`), never shared with L. + * - `auth.sms` (`config.go:1145-1147`/`1348-1417`) — stays 100% inline in D; L instead relies on + * `@supabase/config`'s `sms` schema enforcing the same provider-switch priority at decode time + * (`packages/config/src/auth/sms.ts`), since L decodes through that schema and D doesn't. + * - `auth.external` (`config.go:1148-1150`/`1419-1451`) — inline in BOTH D + * (`legacy-db-config.toml-read.ts`'s "B5: external providers") and L + * ({@link legacyResolveLocalConfigValues}'s `validateAuthExternalProviders`, called after this + * module's shared check, same ordering tradeoff as sms below) — never routed through this + * shared module, since it needs the RAW pre-decode document to see provider names + * `@supabase/config`'s schema doesn't model. + * - `auth.jwt_secret` length check (`apikeys.go:43-73`, `generateAPIKeys`) — each caller's own + * key-generation flow (D and L both already implement this separately), not part of + * `Config.Validate`'s pure-check set. + * + * `legacyExpandEnv` also stays in D (env-substitution machinery, not a validation leaf). + * + * ## Known ordering tradeoff (accepted — do not "fix") + * + * Go's real auth-block order is site_url → captcha → signing_keys[IO] → passkey → hooks → mfa → + * email[IO]+smtp → **sms → external** → third_party. Since sms/external are D-only and never + * part of this module, but third_party IS shared, D cannot call + * {@link legacyValidateResolvedConfig} in a way that preserves relative ordering across the + * sms/external ↔ third_party boundary without complex multi-phase calls. Decision (applies once + * D is wired up in a follow-up commit): D calls {@link legacyValidateResolvedConfig} ONCE with + * the full input (including third_party), positioned after D's own signing-keys and + * email-template I/O reads; D's inline sms/external checks then run AFTER that single call + * succeeds. This means: if third_party is broken, its error surfaces (matching Go); D's + * sms/external checks never run in that case. The only real behavior change from today: for the + * (untested, unrealistic) case where sms/external AND third_party are BOTH simultaneously + * broken in the same config.toml, Go/today's-D would report the sms/external error first, but + * the refactored D reports third_party's error first, since third_party is checked inside the + * single earlier shared call. This is an accepted, narrow, documented parity gap. + * + * The same category of tradeoff now also applies to L: `legacyResolveLocalConfigValues` calls + * {@link legacyValidateResolvedConfig} exactly ONCE, at the very end, after every value this + * module needs has been derived — including L's 3 I/O reads (signing keys, `api.tls` cert/key, + * email template/notification content), which stay at their original textual position (per-caller + * I/O, same as D's). Every pure check this module owns is therefore checked in Go's exact + * relative order against every OTHER pure check, but an I/O read that in L's source sits + * between two pure sections (e.g. the signing-keys read sits between the captcha check and the + * passkey/hooks/mfa/email/smtp/third_party checks) now effectively runs BEFORE any of those + * later pure checks, rather than interleaved at its original relative position — the same + * narrow, accepted, documented tradeoff, not something to "fix" by splitting this function into + * multiple calls. Every existing test constructs exactly one validation failure at a time, so + * this has zero effect on any real test. + */ + +// Go's project-ref pattern (`apps/cli-go/pkg/config/config.go:470`): exactly 20 lowercase +// ASCII letters. Exported from this module (was private in D before this relocation) as the +// canonical home; D's `findInvalidRemoteProjectId` is today the only consumer — the +// `remotes[*].project_id` check itself stays D-only forever, see the module header above. +export const LEGACY_PROJECT_REF_PATTERN = /^[a-z]{20}$/; + +// Go's storage bucket-name pattern (`apps/cli-go/pkg/config/config.go:1382`). +// `config.Validate` runs `ValidateBucketName` over every `[storage.buckets.*]` key +// during config load (`config.go:898-903`), aborting before any db command when a +// name does not match. The source string is reused verbatim in the error message via +// `.source` so it byte-matches Go's `bucketNamePattern.String()`. Used by both D +// (`legacy-db-config.toml-read.ts`) and L (`legacy-local-config-values.ts`), and internally by +// {@link legacyValidateResolvedConfig}'s storage-bucket-names step (`config.go:1064-1068`). +export const LEGACY_BUCKET_NAME_PATTERN = /^(\w|!|-|\.|\*|'|\(|\)| |&|\$|@|=|;|:|\+|,|\?)*$/; + +// Go's function-slug pattern (`apps/cli-go/pkg/config/config.go:1372`). `config.Validate` +// runs `ValidateFunctionSlug` over every `[functions.*]` key during config load +// (`config.go:993-998`), rejecting the config before any db command. `.source` is reused +// in the message so it byte-matches Go's `funcSlugPattern.String()`. Used by both D and L +// (same reason as {@link LEGACY_BUCKET_NAME_PATTERN} above), and internally by +// {@link legacyValidateResolvedConfig}'s function-slugs step (`config.go:1159-1163`). +export const LEGACY_FUNCTION_SLUG_PATTERN = /^[A-Za-z][A-Za-z0-9_-]*$/; + +// Go's `hookSecretPattern` (`apps/cli-go/pkg/config/config.go:1436`). Used by both D and L +// (same reason as {@link LEGACY_BUCKET_NAME_PATTERN} above), and internally by +// {@link legacyValidateResolvedConfig}'s hooks step (`config.go:1453-1521`). +export const LEGACY_HOOK_SECRET_PATTERN = /^v1,whsec_[A-Za-z0-9+/=]{32,88}$/u; + +// Go's `clerkDomainPattern` (`apps/cli-go/pkg/config/config.go:1553`). Used by both D and L +// (same reason as {@link LEGACY_BUCKET_NAME_PATTERN} above), and internally by +// {@link legacyValidateResolvedConfig}'s third_party step (`config.go:1635-1683`). +export const LEGACY_CLERK_DOMAIN_PATTERN = + /^(clerk([.][a-z0-9-]+){2,}|([a-z0-9-]+[.])+clerk[.]accounts[.]dev)$/u; + +// Go's `strconv.ParseBool` accepted forms (`go-viper/mapstructure` `decodeBool` under +// viper's forced `WeaklyTypedInput`): a string decodes to bool via ParseBool, an empty +// string is `false`, and any other value is a parse error. +const GO_BOOL_TRUE = new Set(["1", "t", "T", "TRUE", "true", "True"]); +const GO_BOOL_FALSE = new Set(["0", "f", "F", "FALSE", "false", "False", ""]); + +/** + * Parse a config bool the way Go does (`strconv.ParseBool` via mapstructure's weakly + * typed decode). Returns the bool, or `undefined` for a malformed value (which Go + * surfaces as a `failed to parse config` error). + * + * Used by both D (`legacy-db-config.toml-read.ts`'s `resolveBool`/`resolveBoolOrFail`) and + * L (`legacy-local-config-values.ts`'s `legacyEnvOverrideBool`) for their `SUPABASE_*` + * bool-flavored env overrides and TOML bool decoding. + */ +export function legacyParseGoBool(value: string): boolean | undefined { + if (GO_BOOL_TRUE.has(value)) return true; + if (GO_BOOL_FALSE.has(value)) return false; + return undefined; +} + +/** + * Thrown by {@link legacyValidateResolvedConfig}. Deliberately does NOT override `.name` in a + * constructor — it stays the inherited `"Error"` — so `.toString()`/`.name`/`instanceof Error` + * checks are indistinguishable from a plain `new Error(message)`. Both D and L's existing + * callers/tests observe only `.message` (via `cause instanceof Error ? cause.message : ...` or + * `.toThrow("substring")`), so swapping their inline `throw new Error(...)` calls for this class + * is a byte-identical, purely internal refactor. + */ +export class LegacyConfigValidateError extends Error {} + +/** One `[api.tls]` section, post-env-override. See {@link LegacyConfigValidationInput}. */ +export interface LegacyApiInput { + readonly enabled: boolean; + readonly port: number; + readonly tls: { + readonly enabled: boolean; + readonly certPath: string | undefined; + readonly keyPath: string | undefined; + }; +} + +/** `[db]`, post-env-override. Required — Go validates `db.port`/`db.major_version` unconditionally. */ +export interface LegacyDbInput { + readonly port: number; + readonly majorVersion: number; +} + +/** `[studio]`, post-env-override. L-only — D has no studio section. */ +export interface LegacyStudioInput { + readonly enabled: boolean; + readonly port: number; + readonly apiUrl: string; +} + +/** `[local_smtp]` (Go's `Inbucket`), post-env-override. L-only. */ +export interface LegacyLocalSmtpInput { + readonly enabled: boolean; + readonly port: number; +} + +/** `[auth.captcha]`. `provider` is deliberately `string | undefined`, not a narrow union — see + * divergence #2 in the module's port plan: D passes a raw, untyped TOML string (the enum check + * is live for D); L's `@supabase/config`-decoded value is already schema-narrowed to + * `"hcaptcha" | "turnstile" | undefined` before this function ever sees it, making the branch + * dead-but-harmless for L specifically, while still needing the same widened type to keep this + * field honest and reusable across both callers. + */ +export interface LegacyCaptchaInput { + readonly enabled: boolean; + readonly provider: string | undefined; + readonly secret: string | undefined; +} + +/** `[auth.passkey]` + `[auth.webauthn]`. Present iff `passkey.enabled === true`. */ +export interface LegacyPasskeyInput { + readonly webauthnPresent: boolean; + readonly rpId: string | undefined; + readonly rpOrigins: ReadonlyArray | undefined; +} + +/** One enabled `[auth.hook.]` entry. Caller pre-filters to enabled-only and pre-orders + * per Go's fixed hook-type iteration order (`config.go:1453-1485`). */ +export interface LegacyHookInput { + readonly type: + | "mfa_verification_attempt" + | "password_verification_attempt" + | "custom_access_token" + | "send_sms" + | "send_email" + | "before_user_created"; + /** Post-env-expand; `""` = absent. */ + readonly uri: string; + /** Post-env-expand; `""` = absent. */ + readonly secrets: string; +} + +/** One `[auth.mfa.]` entry. Caller pre-orders totp, phone, web_authn. */ +export interface LegacyMfaFactorInput { + readonly label: "totp" | "phone" | "web_authn"; + readonly enrollEnabled: boolean; + readonly verifyEnabled: boolean; +} + +/** `[auth.email.smtp]`. Present iff the raw TOML table itself is present (Go's presence-based + * `enabled` default, `config.go:743-748` — NOT the decoded, always-defaulted value). */ +export interface LegacySmtpInput { + readonly enabled: boolean; + readonly host: string; + readonly port: number; + readonly user: string; + readonly pass: string; + readonly adminEmail: string; +} + +/** One enabled `[auth.third_party.]` entry. Caller pre-filters to enabled-only and + * pre-orders per Go's fixed provider order (firebase, auth0, cognito, clerk, workos). */ +export interface LegacyThirdPartyInput { + readonly provider: "firebase" | "auth0" | "cognito" | "clerk" | "workos"; + /** `project_id` / `tenant` / `user_pool_id` / `domain` / `issuer_url`, per provider. */ + readonly requiredField: string; + /** cognito's second required field only. */ + readonly cognitoUserPoolRegion?: string; +} + +/** `[auth]`. Present in {@link LegacyConfigValidationInput} iff auth is enabled — matches Go's + * `if c.Auth.Enabled` gate wrapping this entire sub-sequence (`config.go:1087-1153`). */ +export interface LegacyAuthInput { + readonly siteUrl: string; + readonly captcha?: LegacyCaptchaInput; + readonly passkey?: LegacyPasskeyInput; + readonly hooks: ReadonlyArray; + readonly mfa: ReadonlyArray; + readonly smtp?: LegacySmtpInput; + readonly thirdParty: ReadonlyArray; +} + +/** `[analytics]`, post-env-override. Unconditional entry — internally gated on `enabled` + + * `backend === "bigquery"`. `backend` is `string | undefined` for the same dead-but-harmless-for-L + * reason as {@link LegacyCaptchaInput.provider} — see divergence #2. */ +export interface LegacyAnalyticsInput { + readonly enabled: boolean; + readonly backend: string | undefined; + readonly gcpProjectId: string; + readonly gcpProjectNumber: string; + readonly gcpJwtPath: string; +} + +/** `[experimental]`. Unconditional entry — internally gated. `webhooksPresent`/`webhooksEnabled` + * hinge on TOML-section PRESENCE (not the decoded, always-defaulted `enabled` value) — see + * `config.go:1846-1854` and the callers' own doc comments for why. */ +export interface LegacyExperimentalInput { + readonly webhooksPresent?: boolean; + readonly webhooksEnabled?: boolean; + readonly pgdeltaFormatOptions: string; +} + +/** + * Normalized POST-env-override primitives mirroring Go's decoded config, for VALIDATED fields + * only. Every section is OPTIONAL — an absent section means "this caller doesn't run that Go + * branch, skip it" (e.g. D omits `studio`/`localSmtp` entirely; both D and L omit `auth` when + * auth is disabled). See the module header for the full ported-branch table and out-of-scope + * list. + */ +export interface LegacyConfigValidationInput { + /** L only — D's `project_id` isn't part of `Config.Validate`'s shared surface. */ + readonly projectId?: string; + /** L only — D has no `[api]` section. */ + readonly api?: LegacyApiInput; + /** Both, unconditional in Go. */ + readonly db: LegacyDbInput; + /** Both, unconditional (`[]` = none). */ + readonly storageBucketNames: ReadonlyArray; + /** L only. */ + readonly studio?: LegacyStudioInput; + /** L only. */ + readonly localSmtp?: LegacyLocalSmtpInput; + /** Both, present iff auth is enabled. */ + readonly auth?: LegacyAuthInput; + /** Both, unconditional (`[]` = none). */ + readonly functionSlugs: ReadonlyArray; + /** Both, unconditional. */ + readonly edgeRuntimeDenoVersion: number; + /** Both, unconditional entry (internally gated). */ + readonly analytics: LegacyAnalyticsInput; + /** Both, unconditional entry (internally gated). */ + readonly experimental: LegacyExperimentalInput; +} + +function messageOf(cause: unknown): string { + return cause instanceof Error ? cause.message : String(cause); +} + +/** + * Runs every `Config.Validate` branch this module owns (see the module header's table), in + * Go's exact order, first-failure-wins. Pure — no I/O, no Effect. Callers own their own + * per-section I/O reads (signing keys, `api.tls` cert/key, email template/notification content) + * at the correct Go position themselves, using the pure helpers exported below. + */ +export function legacyValidateResolvedConfig(input: LegacyConfigValidationInput): void { + // config.go:990-991 — checked FIRST, before every other field. + if (input.projectId !== undefined && input.projectId.length === 0) { + throw new LegacyConfigValidateError("Missing required field in config: project_id"); + } + + // config.go:1006-1027 — api.port / api.tls.{cert,key}_path, gated on api.enabled. The actual + // cert/key file reads are caller-side I/O (see legacyResolveApiTlsPath below); this only + // checks the "exactly one of cert/key set" presence rule. + if (input.api?.enabled) { + if (input.api.port === 0) { + throw new LegacyConfigValidateError("Missing required field in config: api.port"); + } + if (input.api.tls.enabled) { + const hasCert = input.api.tls.certPath !== undefined && input.api.tls.certPath.length > 0; + const hasKey = input.api.tls.keyPath !== undefined && input.api.tls.keyPath.length > 0; + if (hasCert && !hasKey) { + throw new LegacyConfigValidateError("Missing required field in config: api.tls.key_path"); + } + if (hasKey && !hasCert) { + throw new LegacyConfigValidateError("Missing required field in config: api.tls.cert_path"); + } + } + } + + // config.go:1031-1033 — db.port, unconditional, no `enabled` gate. + if (input.db.port === 0) { + throw new LegacyConfigValidateError("Missing required field in config: db.port"); + } + // config.go:1034-1062 — db.major_version switch: 0 / 12 have dedicated messages, 13/14/15/17 + // are supported, anything else is the generic invalid-value message. + if (input.db.majorVersion === 0) { + throw new LegacyConfigValidateError("Missing required field in config: db.major_version"); + } + if (input.db.majorVersion === 12) { + throw new LegacyConfigValidateError( + "Postgres version 12.x is unsupported. To use the CLI, either start a new project or follow project migration steps here: https://supabase.com/docs/guides/database#migrating-between-projects.", + ); + } + if (![13, 14, 15, 17].includes(input.db.majorVersion)) { + throw new LegacyConfigValidateError( + `Failed reading config: Invalid db.major_version: ${input.db.majorVersion}.`, + ); + } + + // config.go:1064-1068, pattern @ 1549-1554 — every [storage.buckets.*] key, unconditional. + for (const name of input.storageBucketNames) { + if (!LEGACY_BUCKET_NAME_PATTERN.test(name)) { + throw new LegacyConfigValidateError( + `Invalid Bucket name: ${name}. Only lowercase letters, numbers, dots, hyphens, and spaces are allowed. (${LEGACY_BUCKET_NAME_PATTERN.source})`, + ); + } + } + + // config.go:1070-1079 — studio.port / studio.api_url, gated on studio.enabled. L-only. + if (input.studio?.enabled) { + if (input.studio.port === 0) { + throw new LegacyConfigValidateError("Missing required field in config: studio.port"); + } + try { + legacyGoUrlParse(input.studio.apiUrl); + } catch (cause) { + throw new LegacyConfigValidateError(`Invalid config for studio.api_url: ${messageOf(cause)}`); + } + } + + // config.go:1081-1085 — local_smtp.port, gated on local_smtp.enabled. L-only. + if (input.localSmtp?.enabled && input.localSmtp.port === 0) { + throw new LegacyConfigValidateError("Missing required field in config: local_smtp.port"); + } + + // config.go:1087-1153 — the auth.* sub-sequence, all inside `if c.Auth.Enabled`. + if (input.auth !== undefined) { + const auth = input.auth; + + // config.go:1088-1090 — auth.site_url. + if (auth.siteUrl.length === 0) { + throw new LegacyConfigValidateError("Missing required field in config: auth.site_url"); + } + + // config.go:1099-1109 + auth.go:58-71 — auth.captcha. The provider enum check runs FIRST, + // regardless of `enabled` (it's actually a decode-time check in Go, reproduced here so both + // callers see it from one place); only then does the `enabled`-gated presence check run. + if (auth.captcha !== undefined) { + const provider = auth.captcha.provider; + if ( + provider !== undefined && + provider.length > 0 && + provider !== "hcaptcha" && + provider !== "turnstile" + ) { + throw new LegacyConfigValidateError( + "failed to parse config: decoding failed due to the following error(s):\n\n'auth.captcha.provider' must be one of [hcaptcha turnstile]", + ); + } + if (auth.captcha.enabled) { + if (auth.captcha.provider === undefined) { + throw new LegacyConfigValidateError( + "Missing required field in config: auth.captcha.provider", + ); + } + if (auth.captcha.secret === undefined || auth.captcha.secret.length === 0) { + throw new LegacyConfigValidateError( + "Missing required field in config: auth.captcha.secret", + ); + } + } + } + + // config.go:1110-1116 — signing_keys read is caller-side I/O, not part of this function. + + // config.go:1117-1134 — auth.passkey / auth.webauthn. Caller only builds `passkey` when + // `[auth.passkey] enabled` is true. + if (auth.passkey !== undefined) { + if (!auth.passkey.webauthnPresent) { + throw new LegacyConfigValidateError( + "Missing required config section: auth.webauthn (required when auth.passkey.enabled is true)", + ); + } + if (auth.passkey.rpId === undefined || auth.passkey.rpId.length === 0) { + throw new LegacyConfigValidateError( + "Missing required field in config: auth.webauthn.rp_id", + ); + } + if (auth.passkey.rpOrigins === undefined || auth.passkey.rpOrigins.length === 0) { + throw new LegacyConfigValidateError( + "Missing required field in config: auth.webauthn.rp_origins", + ); + } + } + + // config.go:1136-1138, checks @ 1453-1521 — auth.hook.*, caller pre-filtered to + // enabled-only and pre-ordered per Go's fixed hook-type iteration order. + for (const hook of auth.hooks) { + if (hook.uri.length === 0) { + throw new LegacyConfigValidateError( + `Missing required field in config: auth.hook.${hook.type}.uri`, + ); + } + // Go calls `url.Parse` before the scheme switch (`config.go:1497-1499`) and fails the + // whole load on a malformed URI (e.g. an unterminated IPv6 host like `http://[::1`) — + // a bare scheme-prefix regex would accept that. Reuse `legacyGoUrlParse` (the same + // `net/url.Parse` port already used for `studio.api_url` above) instead of re-deriving + // a scheme by hand. + let scheme: string; + try { + scheme = legacyGoUrlParse(hook.uri).scheme; + } catch (cause) { + throw new LegacyConfigValidateError(`failed to parse template url: ${messageOf(cause)}`); + } + if (scheme === "http" || scheme === "https") { + if (hook.secrets.length === 0) { + throw new LegacyConfigValidateError( + `Missing required field in config: auth.hook.${hook.type}.secrets`, + ); + } + for (const secret of hook.secrets.split("|")) { + if (!LEGACY_HOOK_SECRET_PATTERN.test(secret)) { + throw new LegacyConfigValidateError( + `Invalid hook config: auth.hook.${hook.type}.secrets must be formatted as "v1,whsec_" with a minimum length of 32 characters.`, + ); + } + } + } else if (scheme === "pg-functions") { + if (hook.secrets.length > 0) { + throw new LegacyConfigValidateError( + `Invalid hook config: auth.hook.${hook.type}.secrets is unsupported for pg-functions URI`, + ); + } + } else { + throw new LegacyConfigValidateError( + `Invalid hook config: auth.hook.${hook.type}.uri should be a HTTP, HTTPS, or pg-functions URI`, + ); + } + } + + // config.go:1139-1141, checks @ 1523-1534 — auth.mfa.*, caller pre-ordered totp/phone/web_authn. + for (const factor of auth.mfa) { + if (factor.enrollEnabled && !factor.verifyEnabled) { + throw new LegacyConfigValidateError( + `Invalid MFA config: auth.mfa.${factor.label}.enroll_enabled requires verify_enabled`, + ); + } + } + + // config.go:1293-1323 — email template/notification content read + exclusivity is + // caller-side, via legacyResolveEmailTemplateContentPath below. + + // config.go:1325-1344 — auth.email.smtp, gated on the raw table being present AND enabled. + if (auth.smtp !== undefined && auth.smtp.enabled) { + if (auth.smtp.host.length === 0) { + throw new LegacyConfigValidateError( + "Missing required field in config: auth.email.smtp.host", + ); + } + if (auth.smtp.port === 0) { + throw new LegacyConfigValidateError( + "Missing required field in config: auth.email.smtp.port", + ); + } + if (auth.smtp.user.length === 0) { + throw new LegacyConfigValidateError( + "Missing required field in config: auth.email.smtp.user", + ); + } + if (auth.smtp.pass.length === 0) { + throw new LegacyConfigValidateError( + "Missing required field in config: auth.email.smtp.pass", + ); + } + if (auth.smtp.adminEmail.length === 0) { + throw new LegacyConfigValidateError( + "Missing required field in config: auth.email.smtp.admin_email", + ); + } + } + + // config.go:1151-1153, checks @ 1635-1683 — auth.third_party.*, caller pre-filtered to + // enabled-only and pre-ordered firebase, auth0, cognito, clerk, workos. Each provider's + // required field(s) are checked as encountered; the "more than one enabled" check runs only + // after every entry has individually validated. + for (const thirdParty of auth.thirdParty) { + switch (thirdParty.provider) { + case "firebase": { + if (thirdParty.requiredField.length === 0) { + throw new LegacyConfigValidateError( + "Invalid config: auth.third_party.firebase is enabled but without a project_id.", + ); + } + break; + } + case "auth0": { + if (thirdParty.requiredField.length === 0) { + throw new LegacyConfigValidateError( + "Invalid config: auth.third_party.auth0 is enabled but without a tenant.", + ); + } + break; + } + case "cognito": { + if (thirdParty.requiredField.length === 0) { + throw new LegacyConfigValidateError( + "Invalid config: auth.third_party.cognito is enabled but without a user_pool_id.", + ); + } + if ( + thirdParty.cognitoUserPoolRegion === undefined || + thirdParty.cognitoUserPoolRegion.length === 0 + ) { + throw new LegacyConfigValidateError( + "Invalid config: auth.third_party.cognito is enabled but without a user_pool_region.", + ); + } + break; + } + case "clerk": { + if (thirdParty.requiredField.length === 0) { + throw new LegacyConfigValidateError( + "Invalid config: auth.third_party.clerk is enabled but without a domain.", + ); + } + if (!LEGACY_CLERK_DOMAIN_PATTERN.test(thirdParty.requiredField)) { + throw new LegacyConfigValidateError( + "Invalid config: auth.third_party.clerk has invalid domain, it usually is like clerk.example.com or example.clerk.accounts.dev. Check https://clerk.com/setup/supabase on how to find the correct value.", + ); + } + break; + } + case "workos": { + if (thirdParty.requiredField.length === 0) { + throw new LegacyConfigValidateError( + "Invalid config: auth.third_party.workos is enabled but without a issuer_url.", + ); + } + break; + } + } + } + if (auth.thirdParty.length > 1) { + throw new LegacyConfigValidateError( + "Invalid config: Only one third_party provider allowed to be enabled at a time.", + ); + } + } + + // config.go:1159-1163, pattern @ 1539-1544 — every [functions.*] key, unconditional, not + // gated on auth.enabled. + for (const slug of input.functionSlugs) { + if (!LEGACY_FUNCTION_SLUG_PATTERN.test(slug)) { + throw new LegacyConfigValidateError( + `Invalid Function name: ${slug}. Must start with at least one letter, and only include alphanumeric characters, underscores, and hyphens. (${LEGACY_FUNCTION_SLUG_PATTERN.source})`, + ); + } + } + + // config.go:1164-1173 — edge_runtime.deno_version switch, unconditional, not gated on + // edge_runtime.enabled. + if (input.edgeRuntimeDenoVersion === 0) { + throw new LegacyConfigValidateError( + "Missing required field in config: edge_runtime.deno_version", + ); + } + if (input.edgeRuntimeDenoVersion !== 1 && input.edgeRuntimeDenoVersion !== 2) { + throw new LegacyConfigValidateError( + `Failed reading config: Invalid edge_runtime.deno_version: ${input.edgeRuntimeDenoVersion}.`, + ); + } + + // Decode-time enum (`LogflareBackend.UnmarshalText`, config.go:60-65) — reproduced here so + // both callers' env-override paths (which bypass their own decode-time schema guard) see it. + const backend = input.analytics.backend; + if ( + backend !== undefined && + backend.length > 0 && + backend !== "postgres" && + backend !== "bigquery" + ) { + throw new LegacyConfigValidateError( + "failed to parse config: decoding failed due to the following error(s):\n\n'analytics.backend' must be one of [postgres bigquery]", + ); + } + // config.go:1175-1187 — analytics.gcp_*, gated on enabled && backend === "bigquery". + if (input.analytics.enabled && backend === "bigquery") { + if (input.analytics.gcpProjectId.length === 0) { + throw new LegacyConfigValidateError( + "Missing required field in config: analytics.gcp_project_id", + ); + } + if (input.analytics.gcpProjectNumber.length === 0) { + throw new LegacyConfigValidateError( + "Missing required field in config: analytics.gcp_project_number", + ); + } + if (input.analytics.gcpJwtPath.length === 0) { + throw new LegacyConfigValidateError( + "Path to GCP Service Account Key must be provided in config, relative to config.toml: analytics.gcp_jwt_path", + ); + } + } + + // config.go:1847-1848 — experimental.webhooks, hinges on TOML-section PRESENCE, not the + // decoded (always-defaulted) `enabled` value. + if (input.experimental.webhooksPresent === true && input.experimental.webhooksEnabled !== true) { + throw new LegacyConfigValidateError( + "Webhooks cannot be deactivated. [experimental.webhooks] enabled can either be true or left undefined", + ); + } + // config.go:1850-1851 — experimental.pgdelta.format_options, must be valid JSON when set. + if ( + input.experimental.pgdeltaFormatOptions !== "" && + !isValidJson(input.experimental.pgdeltaFormatOptions) + ) { + throw new LegacyConfigValidateError( + "Invalid config for experimental.pgdelta.format_options: must be valid JSON", + ); + } +} + +function isValidJson(value: string): boolean { + try { + JSON.parse(value); + return true; + } catch { + return false; + } +} + +// ── signing keys (config.go:1110-1116, path rule config.go:877-878 filepath.IsAbs guard) ── + +/** Absolute → verbatim; relative → join(workdir, "supabase", p). */ +export function legacyResolveSigningKeysPath(workdir: string, signingKeysPath: string): string { + return isAbsolute(signingKeysPath) ? signingKeysPath : join(workdir, "supabase", signingKeysPath); +} + +/** `failed to read signing keys: ${msg(cause)}` */ +export function legacySigningKeysReadErrorMessage(cause: unknown): string { + return `failed to read signing keys: ${messageOf(cause)}`; +} + +/** `failed to decode signing keys: ${msg(cause)}` */ +export function legacySigningKeysDecodeErrorMessage(cause: unknown): string { + return `failed to decode signing keys: ${messageOf(cause)}`; +} +// D only asserts Array.isArray(JSON.parse(text)); L further decodes into LegacyJwk[] to sign +// with the first key — that JWK-specific decode/signing logic stays in L, unrelated to parity. + +// ── email template / notification (config.go:1293-1323) ── + +/** + * Pure exclusivity decision + path to read for one template/notification entry. Throws + * {@link LegacyConfigValidateError} with the exclusivity message when `contentPath === ""` and + * `contentPresent`. Returns the absolute path to read, or `undefined` when there's nothing to + * read (both `contentPath` and `content` absent — skip, not an error). `contentPath` set (even + * when `content` is ALSO set) always wins — Go does not reject "both set", `content_path` + * silently wins/overwrites. + * + * `base` is caller-resolved: TEMPLATE section → workdir; NOTIFICATION section → + * join(workdir, "supabase") (this asymmetry is real, intentional Go behavior — config.go's own + * FIXME comment flags it, do not "fix" it). + */ +export function legacyResolveEmailTemplateContentPath(args: { + readonly section: "template" | "notification"; + readonly name: string; + /** Post-env-expand; `""` = absent. */ + readonly contentPath: string; + /** Raw `content` key present in the TOML document. */ + readonly contentPresent: boolean; + readonly base: string; +}): string | undefined { + if (args.contentPath.length === 0) { + if (args.contentPresent) { + throw new LegacyConfigValidateError( + `Invalid config for auth.email.${args.section}.${args.name}.content: please use content_path instead`, + ); + } + return undefined; + } + return isAbsolute(args.contentPath) ? args.contentPath : join(args.base, args.contentPath); +} + +/** `Invalid config for auth.email.${section}.${name}.content_path: ${msg(cause)}` */ +export function legacyEmailContentPathReadErrorMessage( + section: "template" | "notification", + name: string, + cause: unknown, +): string { + return `Invalid config for auth.email.${section}.${name}.content_path: ${messageOf(cause)}`; +} + +// ── api.tls cert/key (config.go:1016-1026, path rule ~961-965, NO isAbsolute guard) ── + +/** Unconditional join(workdir, "supabase", p) — Go's path.Join absorbs a leading "/" too. */ +export function legacyResolveApiTlsPath(workdir: string, p: string): string { + return join(workdir, "supabase", p); +} + +/** `failed to read TLS cert: ${msg(cause)}` */ +export function legacyApiTlsCertReadErrorMessage(cause: unknown): string { + return `failed to read TLS cert: ${messageOf(cause)}`; +} + +/** `failed to read TLS key: ${msg(cause)}` */ +export function legacyApiTlsKeyReadErrorMessage(cause: unknown): string { + return `failed to read TLS key: ${messageOf(cause)}`; +} diff --git a/apps/cli/src/legacy/shared/legacy-config-validate.unit.test.ts b/apps/cli/src/legacy/shared/legacy-config-validate.unit.test.ts new file mode 100644 index 0000000000..c66d1db6c9 --- /dev/null +++ b/apps/cli/src/legacy/shared/legacy-config-validate.unit.test.ts @@ -0,0 +1,1034 @@ +import { describe, expect, it } from "vitest"; + +import { + LEGACY_BUCKET_NAME_PATTERN, + LEGACY_CLERK_DOMAIN_PATTERN, + LEGACY_FUNCTION_SLUG_PATTERN, + LEGACY_HOOK_SECRET_PATTERN, + LEGACY_PROJECT_REF_PATTERN, + type LegacyAuthInput, + type LegacyConfigValidationInput, + legacyParseGoBool, + legacyValidateResolvedConfig, +} from "./legacy-config-validate.ts"; + +// Starter suite for the symbols relocated from `legacy-db-config.toml-read.ts` in an earlier +// commit (see the module header in `legacy-config-validate.ts`). The bulk of `Config.Validate` +// behavioral coverage — direct calls to `legacyValidateResolvedConfig`, covering every branch +// this module owns regardless of which caller (D or L) exercises it — lives further down this +// file; it was consolidated here from `legacy-local-config-values.unit.test.ts`, where it used +// to be exercised only indirectly through `legacyResolveLocalConfigValues`. + +describe("legacyParseGoBool", () => { + it("accepts Go's strconv.ParseBool true forms", () => { + for (const value of ["1", "t", "T", "TRUE", "true", "True"]) { + expect(legacyParseGoBool(value)).toBe(true); + } + }); + + it("accepts Go's strconv.ParseBool false forms, including the empty string", () => { + for (const value of ["0", "f", "F", "FALSE", "false", "False", ""]) { + expect(legacyParseGoBool(value)).toBe(false); + } + }); + + it("returns undefined for a value outside Go's strconv.ParseBool acceptance set", () => { + expect(legacyParseGoBool("yes")).toBeUndefined(); + expect(legacyParseGoBool("2")).toBeUndefined(); + }); +}); + +describe("LEGACY_PROJECT_REF_PATTERN", () => { + it("matches a valid 20-character lowercase project ref", () => { + expect(LEGACY_PROJECT_REF_PATTERN.test("abcdefghijklmnopqrst")).toBe(true); + }); + + it("rejects refs of the wrong length or case", () => { + expect(LEGACY_PROJECT_REF_PATTERN.test("short")).toBe(false); + expect(LEGACY_PROJECT_REF_PATTERN.test("ABCDEFGHIJKLMNOPQRST")).toBe(false); + }); +}); + +describe("LEGACY_BUCKET_NAME_PATTERN", () => { + it("matches Go-legal bucket name characters", () => { + expect(LEGACY_BUCKET_NAME_PATTERN.test("my-bucket.1")).toBe(true); + }); + + it("rejects characters outside Go's bucketNamePattern", () => { + expect(LEGACY_BUCKET_NAME_PATTERN.test("bad#name")).toBe(false); + expect(LEGACY_BUCKET_NAME_PATTERN.test("bad/name")).toBe(false); + }); +}); + +describe("LEGACY_FUNCTION_SLUG_PATTERN", () => { + it("matches a valid function slug (letters, digits, _ and -)", () => { + expect(LEGACY_FUNCTION_SLUG_PATTERN.test("my-function")).toBe(true); + expect(LEGACY_FUNCTION_SLUG_PATTERN.test("function_1")).toBe(true); + }); + + it("rejects a slug that doesn't start with a letter", () => { + expect(LEGACY_FUNCTION_SLUG_PATTERN.test("123")).toBe(false); + expect(LEGACY_FUNCTION_SLUG_PATTERN.test("1bad")).toBe(false); + }); +}); + +describe("LEGACY_HOOK_SECRET_PATTERN", () => { + it("matches a valid v1,whsec_ secret", () => { + expect(LEGACY_HOOK_SECRET_PATTERN.test(`v1,whsec_${"a".repeat(32)}`)).toBe(true); + }); + + it("rejects a secret that doesn't match Go's hookSecretPattern", () => { + expect(LEGACY_HOOK_SECRET_PATTERN.test("not-a-valid-secret")).toBe(false); + }); +}); + +describe("LEGACY_CLERK_DOMAIN_PATTERN", () => { + it("matches a valid clerk.example.com domain", () => { + expect(LEGACY_CLERK_DOMAIN_PATTERN.test("clerk.example.com")).toBe(true); + }); + + it("matches a valid .clerk.accounts.dev domain", () => { + expect(LEGACY_CLERK_DOMAIN_PATTERN.test("example.clerk.accounts.dev")).toBe(true); + }); + + it("rejects a domain that doesn't match Go's clerkDomainPattern", () => { + expect(LEGACY_CLERK_DOMAIN_PATTERN.test("not-a-clerk-domain")).toBe(false); + }); +}); + +/** + * A trivially-passing full input. Every test below spreads/overrides only the field(s) its + * check cares about, matching the fixture-building style of `legacy-local-config-values.unit + * .test.ts`'s own `baseConfig()` helper. + */ +function minimalInput( + overrides: Partial = {}, +): LegacyConfigValidationInput { + return { + db: { port: 5432, majorVersion: 17 }, + storageBucketNames: [], + functionSlugs: [], + edgeRuntimeDenoVersion: 2, + analytics: { + enabled: false, + backend: undefined, + gcpProjectId: "", + gcpProjectNumber: "", + gcpJwtPath: "", + }, + experimental: { pgdeltaFormatOptions: "" }, + ...overrides, + }; +} + +/** A trivially-passing `[auth]` section — auth enabled, nothing else configured. */ +function minimalAuthInput(overrides: Partial = {}): LegacyAuthInput { + return { + siteUrl: "http://localhost:3000", + hooks: [], + mfa: [], + thirdParty: [], + ...overrides, + }; +} + +// Moved from `legacy-local-config-values.unit.test.ts`: these describe blocks exercise checks +// that now live entirely inside `legacyValidateResolvedConfig` and can be phrased as direct +// calls with a hand-built `LegacyConfigValidationInput` — no `ProjectConfig`/schema decode, no +// env-override machinery, no file I/O, no `document` threading. Everything that still needs +// one of those (value derivation, env-override mechanics, the 3 I/O checks' actual file reads) +// stays in `legacy-local-config-values.unit.test.ts`. +describe("legacyValidateResolvedConfig", () => { + // config.go:1034-1062 — db.major_version switch. The env-override + // (SUPABASE_DB_MAJOR_VERSION) variants stay in legacy-local-config-values.unit.test.ts. + describe("db.major_version", () => { + it("rejects a configured major_version of 0", () => { + expect(() => + legacyValidateResolvedConfig(minimalInput({ db: { port: 5432, majorVersion: 0 } })), + ).toThrow("Missing required field in config: db.major_version"); + }); + + it("rejects the unsupported Postgres 12.x major_version with Go's dedicated message", () => { + expect(() => + legacyValidateResolvedConfig(minimalInput({ db: { port: 5432, majorVersion: 12 } })), + ).toThrow("Postgres version 12.x is unsupported."); + }); + + it.each([13, 14, 15, 17])("accepts the supported major_version %d", (majorVersion) => { + expect(() => + legacyValidateResolvedConfig(minimalInput({ db: { port: 5432, majorVersion } })), + ).not.toThrow(); + }); + + it("rejects an unsupported major_version with the generic invalid-value message", () => { + expect(() => + legacyValidateResolvedConfig(minimalInput({ db: { port: 5432, majorVersion: 16 } })), + ).toThrow("Failed reading config: Invalid db.major_version: 16."); + }); + }); + + // config.go:1064-1068, pattern @ 1549-1554 — unconditional, no storage.enabled-style gate. + describe("storage.buckets", () => { + it("rejects a bucket name Go's ValidateBucketName refuses", () => { + expect(() => + legacyValidateResolvedConfig(minimalInput({ storageBucketNames: ["bad/name"] })), + ).toThrow("Invalid Bucket name: bad/name."); + }); + + it("does not throw for a valid bucket name", () => { + expect(() => + legacyValidateResolvedConfig(minimalInput({ storageBucketNames: ["avatars.public"] })), + ).not.toThrow(); + }); + + it("does not throw when no buckets are configured", () => { + expect(() => legacyValidateResolvedConfig(minimalInput())).not.toThrow(); + }); + }); + + // config.go:1164-1173 — edge_runtime.deno_version switch, unconditional, not gated on + // edge_runtime.enabled (there is no such field on LegacyConfigValidationInput at all). The + // env-override (SUPABASE_EDGE_RUNTIME_DENO_VERSION) variants stay in + // legacy-local-config-values.unit.test.ts. + describe("edge_runtime.deno_version", () => { + it("rejects a configured deno_version of 0", () => { + expect(() => + legacyValidateResolvedConfig(minimalInput({ edgeRuntimeDenoVersion: 0 })), + ).toThrow("Missing required field in config: edge_runtime.deno_version"); + }); + + it.each([1, 2])("accepts the supported deno_version %d", (denoVersion) => { + expect(() => + legacyValidateResolvedConfig(minimalInput({ edgeRuntimeDenoVersion: denoVersion })), + ).not.toThrow(); + }); + + it("rejects an unsupported deno_version with the generic invalid-value message", () => { + expect(() => + legacyValidateResolvedConfig(minimalInput({ edgeRuntimeDenoVersion: 3 })), + ).toThrow("Failed reading config: Invalid edge_runtime.deno_version: 3."); + }); + + it("rejects an invalid deno_version even when edge_runtime is disabled", () => { + // There is no `edgeRuntime.enabled`-style gate on `LegacyConfigValidationInput` at + // all — this is identical to the "rejects a configured deno_version of 0" case above, + // which is itself the point: Go never gates this check on edge_runtime.enabled. + expect(() => + legacyValidateResolvedConfig(minimalInput({ edgeRuntimeDenoVersion: 0 })), + ).toThrow("Missing required field in config: edge_runtime.deno_version"); + }); + }); + + // config.go:1174-1187 — analytics.gcp_*, gated on enabled && backend === "bigquery". The + // env-override (SUPABASE_ANALYTICS_*) variants stay in legacy-local-config-values.unit.test.ts. + describe("analytics (BigQuery backend required fields)", () => { + it("rejects an enabled bigquery backend without gcp_project_id", () => { + expect(() => + legacyValidateResolvedConfig( + minimalInput({ + analytics: { + enabled: true, + backend: "bigquery", + gcpProjectId: "", + gcpProjectNumber: "", + gcpJwtPath: "", + }, + }), + ), + ).toThrow("Missing required field in config: analytics.gcp_project_id"); + }); + + it("rejects an enabled bigquery backend without gcp_project_number", () => { + expect(() => + legacyValidateResolvedConfig( + minimalInput({ + analytics: { + enabled: true, + backend: "bigquery", + gcpProjectId: "proj", + gcpProjectNumber: "", + gcpJwtPath: "", + }, + }), + ), + ).toThrow("Missing required field in config: analytics.gcp_project_number"); + }); + + it("rejects an enabled bigquery backend without gcp_jwt_path", () => { + expect(() => + legacyValidateResolvedConfig( + minimalInput({ + analytics: { + enabled: true, + backend: "bigquery", + gcpProjectId: "proj", + gcpProjectNumber: "123", + gcpJwtPath: "", + }, + }), + ), + ).toThrow( + "Path to GCP Service Account Key must be provided in config, relative to config.toml: analytics.gcp_jwt_path", + ); + }); + + it("does not throw when an enabled bigquery backend has all three GCP fields", () => { + expect(() => + legacyValidateResolvedConfig( + minimalInput({ + analytics: { + enabled: true, + backend: "bigquery", + gcpProjectId: "proj", + gcpProjectNumber: "123", + gcpJwtPath: "gcp.json", + }, + }), + ), + ).not.toThrow(); + }); + + it("does not throw for the postgres backend, however incomplete the GCP fields are", () => { + expect(() => + legacyValidateResolvedConfig( + minimalInput({ + analytics: { + enabled: true, + backend: "postgres", + gcpProjectId: "", + gcpProjectNumber: "", + gcpJwtPath: "", + }, + }), + ), + ).not.toThrow(); + }); + + it("does not throw when analytics is disabled, however incomplete the GCP fields are", () => { + expect(() => + legacyValidateResolvedConfig( + minimalInput({ + analytics: { + enabled: false, + backend: "bigquery", + gcpProjectId: "", + gcpProjectNumber: "", + gcpJwtPath: "", + }, + }), + ), + ).not.toThrow(); + }); + }); + + // config.go:1846-1854 — experimental.validate(), unconditional, internally gated. + describe("experimental.*", () => { + it("rejects a present [experimental.webhooks] section with enabled omitted", () => { + expect(() => + legacyValidateResolvedConfig( + minimalInput({ experimental: { webhooksPresent: true, pgdeltaFormatOptions: "" } }), + ), + ).toThrow( + "Webhooks cannot be deactivated. [experimental.webhooks] enabled can either be true or left undefined", + ); + }); + + it("rejects a present [experimental.webhooks] section with enabled = false", () => { + expect(() => + legacyValidateResolvedConfig( + minimalInput({ + experimental: { + webhooksPresent: true, + webhooksEnabled: false, + pgdeltaFormatOptions: "", + }, + }), + ), + ).toThrow( + "Webhooks cannot be deactivated. [experimental.webhooks] enabled can either be true or left undefined", + ); + }); + + it("does not throw when [experimental.webhooks] enabled = true", () => { + expect(() => + legacyValidateResolvedConfig( + minimalInput({ + experimental: { + webhooksPresent: true, + webhooksEnabled: true, + pgdeltaFormatOptions: "", + }, + }), + ), + ).not.toThrow(); + }); + + it("does not throw when [experimental.webhooks] is absent entirely", () => { + expect(() => legacyValidateResolvedConfig(minimalInput())).not.toThrow(); + }); + + it("rejects invalid JSON in experimental.pgdelta.format_options", () => { + expect(() => + legacyValidateResolvedConfig( + minimalInput({ experimental: { pgdeltaFormatOptions: "{not json" } }), + ), + ).toThrow("Invalid config for experimental.pgdelta.format_options: must be valid JSON"); + }); + + it("does not throw for valid JSON in experimental.pgdelta.format_options", () => { + expect(() => + legacyValidateResolvedConfig( + minimalInput({ experimental: { pgdeltaFormatOptions: '{"keywordCase":"upper"}' } }), + ), + ).not.toThrow(); + }); + + it("does not throw when experimental.pgdelta.format_options is unset", () => { + expect(() => + legacyValidateResolvedConfig(minimalInput({ experimental: { pgdeltaFormatOptions: "" } })), + ).not.toThrow(); + }); + }); + + // config.go:1088-1090 — auth.site_url, checked first inside `if c.Auth.Enabled`. An absent + // `auth` section on `LegacyConfigValidationInput` IS "auth disabled" from this function's + // perspective. The SUPABASE_AUTH_ENABLED/SUPABASE_AUTH_SITE_URL env-override variants stay in + // legacy-local-config-values.unit.test.ts. + describe("auth.site_url", () => { + it("rejects an explicit empty site_url when auth is enabled", () => { + expect(() => + legacyValidateResolvedConfig(minimalInput({ auth: minimalAuthInput({ siteUrl: "" }) })), + ).toThrow("Missing required field in config: auth.site_url"); + }); + + it("does not throw when site_url is set and auth is enabled", () => { + expect(() => + legacyValidateResolvedConfig( + minimalInput({ auth: minimalAuthInput({ siteUrl: "http://localhost:3000" }) }), + ), + ).not.toThrow(); + }); + + it("does not throw an explicit empty site_url when auth is disabled", () => { + expect(() => legacyValidateResolvedConfig(minimalInput())).not.toThrow(); + }); + }); + + // config.go:1099-1109 + auth.go:58-71 — auth.captcha, checked right after auth.site_url. + describe("auth.captcha", () => { + it("rejects an enabled captcha without a provider", () => { + expect(() => + legacyValidateResolvedConfig( + minimalInput({ + auth: minimalAuthInput({ + captcha: { enabled: true, provider: undefined, secret: undefined }, + }), + }), + ), + ).toThrow("Missing required field in config: auth.captcha.provider"); + }); + + it("rejects an enabled captcha with a provider but no secret", () => { + expect(() => + legacyValidateResolvedConfig( + minimalInput({ + auth: minimalAuthInput({ + captcha: { enabled: true, provider: "hcaptcha", secret: undefined }, + }), + }), + ), + ).toThrow("Missing required field in config: auth.captcha.secret"); + }); + + it("does not throw when an enabled captcha has both provider and secret", () => { + expect(() => + legacyValidateResolvedConfig( + minimalInput({ + auth: minimalAuthInput({ + captcha: { enabled: true, provider: "hcaptcha", secret: "shh" }, + }), + }), + ), + ).not.toThrow(); + }); + + it("does not throw when captcha is disabled, however incomplete", () => { + expect(() => + legacyValidateResolvedConfig( + minimalInput({ + auth: minimalAuthInput({ + captcha: { enabled: false, provider: undefined, secret: undefined }, + }), + }), + ), + ).not.toThrow(); + }); + + it("does not throw an enabled captcha without provider/secret when auth is disabled", () => { + expect(() => legacyValidateResolvedConfig(minimalInput())).not.toThrow(); + }); + }); + + // config.go:1117-1134 — auth.passkey/auth.webauthn, right after the (caller-side) signing-keys + // read. + describe("auth.passkey / auth.webauthn", () => { + it("rejects passkey.enabled without an [auth.webauthn] section", () => { + expect(() => + legacyValidateResolvedConfig( + minimalInput({ + auth: minimalAuthInput({ + passkey: { webauthnPresent: false, rpId: undefined, rpOrigins: undefined }, + }), + }), + ), + ).toThrow( + "Missing required config section: auth.webauthn (required when auth.passkey.enabled is true)", + ); + }); + + it("rejects passkey.enabled with [auth.webauthn] missing rp_id", () => { + expect(() => + legacyValidateResolvedConfig( + minimalInput({ + auth: minimalAuthInput({ + passkey: { + webauthnPresent: true, + rpId: undefined, + rpOrigins: ["http://localhost:3000"], + }, + }), + }), + ), + ).toThrow("Missing required field in config: auth.webauthn.rp_id"); + }); + + it("rejects passkey.enabled with [auth.webauthn] missing rp_origins", () => { + expect(() => + legacyValidateResolvedConfig( + minimalInput({ + auth: minimalAuthInput({ + passkey: { webauthnPresent: true, rpId: "localhost", rpOrigins: undefined }, + }), + }), + ), + ).toThrow("Missing required field in config: auth.webauthn.rp_origins"); + }); + + it("does not throw when passkey.enabled has a complete [auth.webauthn] section", () => { + expect(() => + legacyValidateResolvedConfig( + minimalInput({ + auth: minimalAuthInput({ + passkey: { + webauthnPresent: true, + rpId: "localhost", + rpOrigins: ["http://localhost:3000"], + }, + }), + }), + ), + ).not.toThrow(); + }); + + it("does not throw when passkey is absent from the input", () => { + expect(() => + legacyValidateResolvedConfig( + minimalInput({ auth: minimalAuthInput({ passkey: undefined }) }), + ), + ).not.toThrow(); + }); + + it("does not throw when auth carries no passkey data at all", () => { + // Distinct from the previous test only in the original (L-level) caller's derivation — + // "webauthn absent from the document" vs. "no document was threaded through at all". + // Both collapse to `passkey: undefined` at this shared, direct-call layer. + expect(() => + legacyValidateResolvedConfig(minimalInput({ auth: minimalAuthInput() })), + ).not.toThrow(); + }); + + it("does not throw an enabled passkey without webauthn when auth is disabled", () => { + expect(() => legacyValidateResolvedConfig(minimalInput())).not.toThrow(); + }); + }); + + // config.go:1325-1344 — auth.email.smtp, gated on the raw table being present AND enabled. + describe("auth.email.smtp", () => { + it("rejects a present [auth.email.smtp] table with no fields", () => { + expect(() => + legacyValidateResolvedConfig( + minimalInput({ + auth: minimalAuthInput({ + smtp: { enabled: true, host: "", port: 0, user: "", pass: "", adminEmail: "" }, + }), + }), + ), + ).toThrow("Missing required field in config: auth.email.smtp.host"); + }); + + it("rejects a present [auth.email.smtp] table missing port/user/pass/admin_email", () => { + expect(() => + legacyValidateResolvedConfig( + minimalInput({ + auth: minimalAuthInput({ + smtp: { + enabled: true, + host: "smtp.example.com", + port: 0, + user: "", + pass: "", + adminEmail: "", + }, + }), + }), + ), + ).toThrow("Missing required field in config: auth.email.smtp.port"); + }); + + it("does not throw when [auth.email.smtp] explicitly sets enabled = false", () => { + expect(() => + legacyValidateResolvedConfig( + minimalInput({ + auth: minimalAuthInput({ + smtp: { + enabled: false, + host: "smtp.example.com", + port: 0, + user: "", + pass: "", + adminEmail: "", + }, + }), + }), + ), + ).not.toThrow(); + }); + + it("does not throw when [auth.email.smtp] is a complete table", () => { + expect(() => + legacyValidateResolvedConfig( + minimalInput({ + auth: minimalAuthInput({ + smtp: { + enabled: true, + host: "smtp.example.com", + port: 587, + user: "user", + pass: "pass", + adminEmail: "admin@example.com", + }, + }), + }), + ), + ).not.toThrow(); + }); + + it("does not throw when [auth.email.smtp] is absent from the input", () => { + expect(() => + legacyValidateResolvedConfig(minimalInput({ auth: minimalAuthInput({ smtp: undefined }) })), + ).not.toThrow(); + }); + + it("does not throw when auth carries no smtp data at all", () => { + // See the equivalent passkey note above — both collapse to `smtp: undefined` here. + expect(() => + legacyValidateResolvedConfig(minimalInput({ auth: minimalAuthInput() })), + ).not.toThrow(); + }); + + it("does not throw a present but incomplete [auth.email.smtp] table when auth is disabled", () => { + expect(() => legacyValidateResolvedConfig(minimalInput())).not.toThrow(); + }); + }); + + // config.go:1136-1138, checks @ 1453-1521 — auth.hook.*, caller pre-filters to enabled-only. + describe("auth.hook.*", () => { + it("rejects an enabled hook without a uri", () => { + expect(() => + legacyValidateResolvedConfig( + minimalInput({ + auth: minimalAuthInput({ + hooks: [{ type: "custom_access_token", uri: "", secrets: "" }], + }), + }), + ), + ).toThrow("Missing required field in config: auth.hook.custom_access_token.uri"); + }); + + it("rejects an http(s) hook uri without secrets", () => { + expect(() => + legacyValidateResolvedConfig( + minimalInput({ + auth: minimalAuthInput({ + hooks: [ + { type: "custom_access_token", uri: "https://example.test/hook", secrets: "" }, + ], + }), + }), + ), + ).toThrow("Missing required field in config: auth.hook.custom_access_token.secrets"); + }); + + it("rejects an http(s) hook secret that doesn't match Go's hookSecretPattern", () => { + expect(() => + legacyValidateResolvedConfig( + minimalInput({ + auth: minimalAuthInput({ + hooks: [ + { + type: "custom_access_token", + uri: "https://example.test/hook", + secrets: "not-a-valid-secret", + }, + ], + }), + }), + ), + ).toThrow( + 'auth.hook.custom_access_token.secrets must be formatted as "v1,whsec_"', + ); + }); + + it("does not throw for a valid http(s) hook secret", () => { + expect(() => + legacyValidateResolvedConfig( + minimalInput({ + auth: minimalAuthInput({ + hooks: [ + { + type: "custom_access_token", + uri: "https://example.test/hook", + secrets: `v1,whsec_${"a".repeat(32)}`, + }, + ], + }), + }), + ), + ).not.toThrow(); + }); + + it("rejects a pg-functions hook uri with secrets set", () => { + expect(() => + legacyValidateResolvedConfig( + minimalInput({ + auth: minimalAuthInput({ + hooks: [ + { + type: "custom_access_token", + uri: "pg-functions://postgres/public/hook", + secrets: `v1,whsec_${"a".repeat(32)}`, + }, + ], + }), + }), + ), + ).toThrow("auth.hook.custom_access_token.secrets is unsupported for pg-functions URI"); + }); + + it("does not throw for a pg-functions hook uri without secrets", () => { + expect(() => + legacyValidateResolvedConfig( + minimalInput({ + auth: minimalAuthInput({ + hooks: [ + { + type: "custom_access_token", + uri: "pg-functions://postgres/public/hook", + secrets: "", + }, + ], + }), + }), + ), + ).not.toThrow(); + }); + + it("rejects a hook uri with an unsupported scheme", () => { + expect(() => + legacyValidateResolvedConfig( + minimalInput({ + auth: minimalAuthInput({ + hooks: [{ type: "custom_access_token", uri: "ftp://example.test/hook", secrets: "" }], + }), + }), + ), + ).toThrow("auth.hook.custom_access_token.uri should be a HTTP, HTTPS, or pg-functions URI"); + }); + + // Go calls `url.Parse` before the scheme switch (config.go:1497-1499) and fails the whole + // load on a malformed URI, rather than treating any `http:`/`https:` prefix as valid. + it("rejects a hook uri that fails Go's url.Parse (malformed IPv6 host)", () => { + expect(() => + legacyValidateResolvedConfig( + minimalInput({ + auth: minimalAuthInput({ + hooks: [{ type: "custom_access_token", uri: "http://[::1", secrets: "" }], + }), + }), + ), + ).toThrow("failed to parse template url:"); + }); + + it("does not throw for a disabled hook, however incomplete", () => { + // The caller pre-filters to enabled-only hooks — a disabled hook is simply absent from + // `hooks`, matching an empty array here. + expect(() => + legacyValidateResolvedConfig(minimalInput({ auth: minimalAuthInput({ hooks: [] }) })), + ).not.toThrow(); + }); + + it("does not throw an enabled hook without a uri when auth is disabled", () => { + expect(() => legacyValidateResolvedConfig(minimalInput())).not.toThrow(); + }); + }); + + // config.go:1139-1141, checks @ 1523-1534 — auth.mfa.*, fixed totp/phone/web_authn order. + describe("auth.mfa.*", () => { + it.each([ + ["totp", "auth.mfa.totp.enroll_enabled requires verify_enabled"], + ["phone", "auth.mfa.phone.enroll_enabled requires verify_enabled"], + ["web_authn", "auth.mfa.web_authn.enroll_enabled requires verify_enabled"], + ] as const)("rejects %s enroll_enabled without verify_enabled", (label, message) => { + expect(() => + legacyValidateResolvedConfig( + minimalInput({ + auth: minimalAuthInput({ + mfa: [{ label, enrollEnabled: true, verifyEnabled: false }], + }), + }), + ), + ).toThrow(message); + }); + + it("does not throw when enroll_enabled and verify_enabled are both true", () => { + expect(() => + legacyValidateResolvedConfig( + minimalInput({ + auth: minimalAuthInput({ + mfa: [{ label: "totp", enrollEnabled: true, verifyEnabled: true }], + }), + }), + ), + ).not.toThrow(); + }); + + it("does not throw an enroll_enabled MFA factor without verify_enabled when auth is disabled", () => { + expect(() => legacyValidateResolvedConfig(minimalInput())).not.toThrow(); + }); + }); + + // config.go:1151-1153, checks @ 1635-1683 — auth.third_party.*, fixed provider order, caller + // pre-filters to enabled-only. + describe("auth.third_party.*", () => { + it("rejects firebase enabled without a project_id", () => { + expect(() => + legacyValidateResolvedConfig( + minimalInput({ + auth: minimalAuthInput({ thirdParty: [{ provider: "firebase", requiredField: "" }] }), + }), + ), + ).toThrow("Invalid config: auth.third_party.firebase is enabled but without a project_id."); + }); + + it("rejects auth0 enabled without a tenant", () => { + expect(() => + legacyValidateResolvedConfig( + minimalInput({ + auth: minimalAuthInput({ thirdParty: [{ provider: "auth0", requiredField: "" }] }), + }), + ), + ).toThrow("Invalid config: auth.third_party.auth0 is enabled but without a tenant."); + }); + + it("rejects aws_cognito enabled without a user_pool_id", () => { + expect(() => + legacyValidateResolvedConfig( + minimalInput({ + auth: minimalAuthInput({ thirdParty: [{ provider: "cognito", requiredField: "" }] }), + }), + ), + ).toThrow("Invalid config: auth.third_party.cognito is enabled but without a user_pool_id."); + }); + + it("rejects aws_cognito enabled with a user_pool_id but no user_pool_region", () => { + expect(() => + legacyValidateResolvedConfig( + minimalInput({ + auth: minimalAuthInput({ + thirdParty: [ + { provider: "cognito", requiredField: "pool-1", cognitoUserPoolRegion: undefined }, + ], + }), + }), + ), + ).toThrow( + "Invalid config: auth.third_party.cognito is enabled but without a user_pool_region.", + ); + }); + + it("rejects clerk enabled without a domain", () => { + expect(() => + legacyValidateResolvedConfig( + minimalInput({ + auth: minimalAuthInput({ thirdParty: [{ provider: "clerk", requiredField: "" }] }), + }), + ), + ).toThrow("Invalid config: auth.third_party.clerk is enabled but without a domain."); + }); + + it("rejects clerk enabled with a domain that doesn't match Go's clerkDomainPattern", () => { + expect(() => + legacyValidateResolvedConfig( + minimalInput({ + auth: minimalAuthInput({ + thirdParty: [{ provider: "clerk", requiredField: "not-a-clerk-domain" }], + }), + }), + ), + ).toThrow("Invalid config: auth.third_party.clerk has invalid domain"); + }); + + it("does not throw for a valid clerk.example.com domain", () => { + expect(() => + legacyValidateResolvedConfig( + minimalInput({ + auth: minimalAuthInput({ + thirdParty: [{ provider: "clerk", requiredField: "clerk.example.com" }], + }), + }), + ), + ).not.toThrow(); + }); + + it("rejects workos enabled without an issuer_url", () => { + expect(() => + legacyValidateResolvedConfig( + minimalInput({ + auth: minimalAuthInput({ thirdParty: [{ provider: "workos", requiredField: "" }] }), + }), + ), + ).toThrow("Invalid config: auth.third_party.workos is enabled but without a issuer_url."); + }); + + it("rejects more than one third_party provider enabled at once", () => { + expect(() => + legacyValidateResolvedConfig( + minimalInput({ + auth: minimalAuthInput({ + thirdParty: [ + { provider: "firebase", requiredField: "proj" }, + { provider: "auth0", requiredField: "tenant" }, + ], + }), + }), + ), + ).toThrow("Invalid config: Only one third_party provider allowed to be enabled at a time."); + }); + + it("does not throw when no third_party provider is enabled", () => { + expect(() => + legacyValidateResolvedConfig(minimalInput({ auth: minimalAuthInput() })), + ).not.toThrow(); + }); + + it("does not throw an enabled third_party provider missing its required field when auth is disabled", () => { + expect(() => legacyValidateResolvedConfig(minimalInput())).not.toThrow(); + }); + }); + + // config.go:1159-1163, pattern @ 1539-1544 — every [functions.*] key, unconditional, not + // gated on auth.enabled. + describe("functions.*", () => { + it("rejects a function slug Go's ValidateFunctionSlug refuses", () => { + expect(() => legacyValidateResolvedConfig(minimalInput({ functionSlugs: ["1bad"] }))).toThrow( + "Invalid Function name: 1bad.", + ); + }); + + it("does not throw for a valid function slug", () => { + expect(() => + legacyValidateResolvedConfig(minimalInput({ functionSlugs: ["hello-world_v2"] })), + ).not.toThrow(); + }); + + it("does not throw when no functions are configured", () => { + expect(() => legacyValidateResolvedConfig(minimalInput())).not.toThrow(); + }); + + it("rejects an invalid function slug even when auth is disabled", () => { + expect(() => legacyValidateResolvedConfig(minimalInput({ functionSlugs: ["1bad"] }))).toThrow( + "Invalid Function name: 1bad.", + ); + }); + }); + + // config.go:1006-1027 — only the "exactly one of cert/key set" presence rule; the actual + // file reads and the disabled-skip/env-override tests stay in + // legacy-local-config-values.unit.test.ts. + describe("api.tls", () => { + it("rejects cert_path set without key_path", () => { + expect(() => + legacyValidateResolvedConfig( + minimalInput({ + api: { + enabled: true, + port: 54321, + tls: { enabled: true, certPath: "cert.pem", keyPath: undefined }, + }, + }), + ), + ).toThrow("Missing required field in config: api.tls.key_path"); + }); + + it("rejects key_path set without cert_path", () => { + expect(() => + legacyValidateResolvedConfig( + minimalInput({ + api: { + enabled: true, + port: 54321, + tls: { enabled: true, certPath: undefined, keyPath: "key.pem" }, + }, + }), + ), + ).toThrow("Missing required field in config: api.tls.cert_path"); + }); + }); + + // Direct-shared-level regression/divergence coverage (Part C) — behavior that's either new + // (the D fix) or only meaningfully testable at this exact layer (the captcha enum), not a + // move from either caller's own suite. + describe("Config.Validate divergence regression coverage", () => { + it("throws the Go-parity missing-required message for db.major_version = 0 (regression for the D fix in 0c62a914)", () => { + // D used to fall through to the generic "Invalid db.major_version: 0" message; both D + // and L now go through this exact branch, so pin it directly here too, not just via + // D's/L's own suites. + expect(() => + legacyValidateResolvedConfig({ ...minimalInput(), db: { port: 5432, majorVersion: 0 } }), + ).toThrow("Missing required field in config: db.major_version"); + }); + + it("throws Go's decode-time enum message for an invalid auth.captcha.provider, regardless of enabled", () => { + // This scenario is only meaningful at this direct shared-validator level: it's + // unreachable through L's real ProjectConfig-typed flow — `@supabase/config`'s schema + // (packages/config/src/auth/captcha.ts, stringEnum(["hcaptcha", "turnstile"])) already + // narrows `provider` to "hcaptcha" | "turnstile" | undefined before it ever reaches + // `legacyResolveLocalConfigValues`, so an invalid provider value would fail schema + // decoding first, on a completely different code path. D's real TOML flow CAN reach + // this branch (an untyped raw string) — D's own suite + // (`legacy-db-config.toml-read.unit.test.ts`) covers that separately. This test's job is + // only to pin the shared function's own behavior directly. + expect(() => + legacyValidateResolvedConfig( + minimalInput({ + auth: minimalAuthInput({ + captcha: { enabled: false, provider: "not-a-real-provider", secret: undefined }, + }), + }), + ), + ).toThrow( + "failed to parse config: decoding failed due to the following error(s):\n\n'auth.captcha.provider' must be one of [hcaptcha turnstile]", + ); + }); + }); +}); diff --git a/apps/cli/src/legacy/shared/legacy-container-cli.ts b/apps/cli/src/legacy/shared/legacy-container-cli.ts index 3bf5f4244f..53bfb6e947 100644 --- a/apps/cli/src/legacy/shared/legacy-container-cli.ts +++ b/apps/cli/src/legacy/shared/legacy-container-cli.ts @@ -1,4 +1,4 @@ -import { Effect } from "effect"; +import { Data, Effect, Stream } from "effect"; import * as ChildProcess from "effect/unstable/process/ChildProcess"; import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner"; @@ -14,6 +14,36 @@ import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner type Spawner = ChildProcessSpawner["Service"]; +/** + * Raised when neither `docker` nor `podman` can be spawned at all (e.g. neither + * is installed or on `PATH`) — distinct from a spawned process exiting non-zero. + * Not exported: callers never need to match on this type directly, they fold it + * into their own tagged error via {@link legacyDescribeContainerCliFailure} so + * the "no runtime found" root cause survives instead of collapsing into a + * generic "failed to ..." message. + */ +class LegacyContainerRuntimeNotFoundError extends Data.TaggedError( + "LegacyContainerRuntimeNotFoundError", +)<{ + readonly message: string; +}> {} + +const RUNTIME_NOT_FOUND_MESSAGE = + "docker: command not found (podman also not found) — install Docker Desktop or Podman and ensure it is on PATH"; + +/** + * Renders a caller-facing suffix for a `spawnContainerCli`/`containerCliExitCode` + * failure: the clear "neither runtime found" message when that's the cause, + * otherwise the underlying cause's own message (falling back to `String(cause)` + * for non-`Error` causes) so callers never collapse a real failure reason into a + * bare, uninformative "failed to ..." string. + */ +export function legacyDescribeContainerCliFailure(cause: unknown): string { + if (cause instanceof LegacyContainerRuntimeNotFoundError) return cause.message; + if (cause instanceof Error) return cause.message; + return String(cause); +} + /** * Spawn a container-CLI command and return the process handle. Use when the * caller needs to read stdout/stderr or await the exit code itself. @@ -25,17 +55,115 @@ export const spawnContainerCli = ( ) => spawner .spawn(ChildProcess.make("docker", args, options)) - .pipe(Effect.catch(() => spawner.spawn(ChildProcess.make("podman", args, options)))); + .pipe( + Effect.catch(() => + spawner + .spawn(ChildProcess.make("podman", args, options)) + .pipe( + Effect.catch(() => + Effect.fail( + new LegacyContainerRuntimeNotFoundError({ message: RUNTIME_NOT_FOUND_MESSAGE }), + ), + ), + ), + ), + ); /** * Run a container-CLI command and resolve to its exit code, mirroring the * spawner's `exitCode` convenience for callers that only need the status. + * + * `podmanArgs` lets a caller pass different argv to the Podman fallback than to + * Docker, for the rare case where the two aren't drop-in compatible on a given + * subcommand (e.g. `volume prune --all` — Docker-only, see + * `stop.handler.ts`'s volume-prune call). Defaults to reusing `args` unchanged, + * which is correct for every other call site. */ export const containerCliExitCode = ( spawner: Spawner, args: ReadonlyArray, options?: ChildProcess.CommandOptions, + podmanArgs?: ReadonlyArray, ) => spawner .exitCode(ChildProcess.make("docker", args, options)) - .pipe(Effect.catch(() => spawner.exitCode(ChildProcess.make("podman", args, options)))); + .pipe( + Effect.catch(() => + spawner + .exitCode(ChildProcess.make("podman", podmanArgs ?? args, options)) + .pipe( + Effect.catch(() => + Effect.fail( + new LegacyContainerRuntimeNotFoundError({ message: RUNTIME_NOT_FOUND_MESSAGE }), + ), + ), + ), + ), + ); + +function collectDockerCliText(stream: Stream.Stream) { + const decoder = new TextDecoder(); + return Stream.runFold( + stream, + () => "", + (text, chunk) => text + decoder.decode(chunk, { stream: true }), + ).pipe(Effect.map((text) => text + decoder.decode())); +} + +/** + * Mirrors Go's `versions.GreaterThanOrEqualTo` (`docker/api/types/versions`, + * used by `apps/cli-go/internal/utils/docker.go:128`): splits each version on + * `.` and compares the parts numerically, component by component — not a + * naive string/float compare, which would misorder e.g. `"1.9"` vs `"1.10"`. + */ +function isDockerApiVersionAtLeast(version: string, minVersion: string): boolean { + const parts = version.split(".").map((part) => Number.parseInt(part, 10)); + const minParts = minVersion.split(".").map((part) => Number.parseInt(part, 10)); + for (let index = 0; index < Math.max(parts.length, minParts.length); index++) { + const part = parts[index] ?? 0; + const minPart = minParts[index] ?? 0; + if (part !== minPart) return part > minPart; + } + return true; +} + +/** + * Docker CLI's own `volume prune --all` flag is annotated `version: "1.42"` + * (vendored `docker/cli@v28.5.2` `cli/command/volume/prune.go:53`) and + * enforced by Cobra's `Args` validator *before* `RunE` runs + * (`cmd/docker/docker.go:659-660`): against a daemon with a lower negotiated + * API version, `docker volume prune --all ...` exits nonzero without pruning + * anything at all, rather than degrading gracefully. Go avoids ever hitting + * that by gating the equivalent `all=true` filter on + * `Docker.ClientVersion() >= "1.42"` (`docker.go:126-133`); there is no + * persistent Engine API client here to ask, so this asks the `docker` CLI + * itself via `docker version`. + * + * Deliberately does not fall back to Podman like {@link containerCliExitCode} + * does: Podman's `volume prune` never has an `--all` flag to gate in the + * first place (callers already omit it from their Podman argv + * unconditionally), so this check is meaningless on that path. Resolves to + * `false` (omit `--all`, matching how a pre-1.42 daemon's own `volume prune` + * already prunes every unused volume without it) whenever `docker` can't be + * spawned, its `version` call fails, or the reported version can't be read — + * the side that can never turn into a hard failure of the prune call itself. + */ +export const legacyDockerSupportsVolumePruneAllFlag = (spawner: Spawner) => + Effect.scoped( + Effect.gen(function* () { + const child = yield* spawner.spawn( + ChildProcess.make("docker", ["version", "--format", "{{.Server.APIVersion}}"], { + stdin: "ignore", + stdout: "pipe", + stderr: "ignore", + }), + ); + const [exitCode, stdout] = yield* Effect.all( + [child.exitCode.pipe(Effect.map(Number)), collectDockerCliText(child.stdout)], + { concurrency: "unbounded" }, + ); + if (exitCode !== 0) return false; + const version = stdout.trim(); + return version.length > 0 && isDockerApiVersionAtLeast(version, "1.42"); + }), + ).pipe(Effect.orElseSucceed(() => false)); diff --git a/apps/cli/src/legacy/shared/legacy-container-cli.unit.test.ts b/apps/cli/src/legacy/shared/legacy-container-cli.unit.test.ts index 7c6f97aeca..e80dbf5ae8 100644 --- a/apps/cli/src/legacy/shared/legacy-container-cli.unit.test.ts +++ b/apps/cli/src/legacy/shared/legacy-container-cli.unit.test.ts @@ -2,9 +2,21 @@ import { describe, expect, it } from "@effect/vitest"; import { Deferred, Effect, PlatformError, Sink, Stream } from "effect"; import { ChildProcessSpawner } from "effect/unstable/process"; -import { containerCliExitCode, spawnContainerCli } from "./legacy-container-cli.ts"; +import { + containerCliExitCode, + legacyDescribeContainerCliFailure, + legacyDockerSupportsVolumePruneAllFlag, + spawnContainerCli, +} from "./legacy-container-cli.ts"; -function mockSpawner(opts: { readonly dockerMissing?: boolean; readonly exitCode?: number } = {}) { +function mockSpawner( + opts: { + readonly dockerMissing?: boolean; + readonly bothMissing?: boolean; + readonly exitCode?: number; + readonly stdout?: string; + } = {}, +) { const spawned: Array<{ readonly command: string; readonly args: ReadonlyArray }> = []; const spawner = ChildProcessSpawner.make((command) => @@ -13,13 +25,13 @@ function mockSpawner(opts: { readonly dockerMissing?: boolean; readonly exitCode const args = command._tag === "StandardCommand" ? command.args : []; spawned.push({ command: cmd, args }); - if (opts.dockerMissing && cmd === "docker") { + if ((opts.dockerMissing && cmd === "docker") || opts.bothMissing === true) { return yield* Effect.fail( PlatformError.systemError({ _tag: "NotFound", module: "ChildProcess", method: "spawn", - description: "docker not found", + description: `${cmd} not found`, }), ); } @@ -29,7 +41,10 @@ function mockSpawner(opts: { readonly dockerMissing?: boolean; readonly exitCode return ChildProcessSpawner.makeHandle({ pid: ChildProcessSpawner.ProcessId(1), - stdout: Stream.empty, + stdout: + opts.stdout !== undefined + ? Stream.fromIterable([new TextEncoder().encode(opts.stdout)]) + : Stream.empty, stderr: Stream.empty, all: Stream.empty, exitCode: Deferred.await(exitDeferred), @@ -98,4 +113,108 @@ describe("containerCliExitCode", () => { }), ); }); + + it.live("fails with a clear message when neither docker nor podman can be spawned", () => { + const mock = mockSpawner({ bothMissing: true }); + return containerCliExitCode(mock.spawner, ["image", "inspect", "img"]).pipe( + Effect.flip, + Effect.map((error) => { + expect(legacyDescribeContainerCliFailure(error)).toBe( + "docker: command not found (podman also not found) — install Docker Desktop or Podman and ensure it is on PATH", + ); + }), + ); + }); +}); + +describe("legacyDockerSupportsVolumePruneAllFlag", () => { + it.live("returns true when the daemon reports an API version at or above 1.42", () => { + const mock = mockSpawner({ stdout: "1.42" }); + return legacyDockerSupportsVolumePruneAllFlag(mock.spawner).pipe( + Effect.map((supportsAll) => { + expect(supportsAll).toBe(true); + expect(mock.spawned).toEqual([ + { command: "docker", args: ["version", "--format", "{{.Server.APIVersion}}"] }, + ]); + }), + ); + }); + + it.live("returns true for a version comfortably above 1.42", () => { + const mock = mockSpawner({ stdout: "1.51" }); + return legacyDockerSupportsVolumePruneAllFlag(mock.spawner).pipe( + Effect.map((supportsAll) => { + expect(supportsAll).toBe(true); + }), + ); + }); + + it.live("returns false when the daemon reports an API version below 1.42", () => { + const mock = mockSpawner({ stdout: "1.41" }); + return legacyDockerSupportsVolumePruneAllFlag(mock.spawner).pipe( + Effect.map((supportsAll) => { + expect(supportsAll).toBe(false); + }), + ); + }); + + it.live("compares version components numerically, not lexicographically", () => { + // A naive string compare would misorder "1.9" as greater than "1.42" — this + // guards the numeric, component-by-component comparison instead. + const mock = mockSpawner({ stdout: "1.9" }); + return legacyDockerSupportsVolumePruneAllFlag(mock.spawner).pipe( + Effect.map((supportsAll) => { + expect(supportsAll).toBe(false); + }), + ); + }); + + it.live("returns false when the version command exits non-zero", () => { + const mock = mockSpawner({ exitCode: 1, stdout: "1.51" }); + return legacyDockerSupportsVolumePruneAllFlag(mock.spawner).pipe( + Effect.map((supportsAll) => { + expect(supportsAll).toBe(false); + }), + ); + }); + + it.live("returns false when the reported version is empty", () => { + const mock = mockSpawner({ stdout: "" }); + return legacyDockerSupportsVolumePruneAllFlag(mock.spawner).pipe( + Effect.map((supportsAll) => { + expect(supportsAll).toBe(false); + }), + ); + }); + + it.live("returns false without falling back to podman when docker cannot be spawned", () => { + const mock = mockSpawner({ dockerMissing: true }); + return legacyDockerSupportsVolumePruneAllFlag(mock.spawner).pipe( + Effect.map((supportsAll) => { + expect(supportsAll).toBe(false); + expect(mock.spawned.map((entry) => entry.command)).toEqual(["docker"]); + }), + ); + }); +}); + +describe("legacyDescribeContainerCliFailure", () => { + it.live("describes a both-runtimes-missing failure with its clear message", () => { + const mock = mockSpawner({ bothMissing: true }); + return containerCliExitCode(mock.spawner, ["ps"]).pipe( + Effect.flip, + Effect.map((error) => { + expect(legacyDescribeContainerCliFailure(error)).toContain("docker: command not found"); + }), + ); + }); + + it("falls back to an Error instance's own message", () => { + expect(legacyDescribeContainerCliFailure(new Error("boom"))).toBe("boom"); + }); + + it("stringifies a non-Error cause", () => { + expect(legacyDescribeContainerCliFailure("boom")).toBe("boom"); + expect(legacyDescribeContainerCliFailure(42)).toBe("42"); + }); }); diff --git a/apps/cli/src/legacy/shared/legacy-db-config.toml-read.ts b/apps/cli/src/legacy/shared/legacy-db-config.toml-read.ts index 71b989aaeb..639c1169ca 100644 --- a/apps/cli/src/legacy/shared/legacy-db-config.toml-read.ts +++ b/apps/cli/src/legacy/shared/legacy-db-config.toml-read.ts @@ -1,5 +1,26 @@ import { Effect, type FileSystem, Option, type Path } from "effect"; import * as SmolToml from "smol-toml"; +import { + LEGACY_PROJECT_REF_PATTERN, + type LegacyAnalyticsInput, + type LegacyAuthInput, + type LegacyCaptchaInput, + type LegacyConfigValidationInput, + type LegacyDbInput, + legacyEmailContentPathReadErrorMessage, + type LegacyExperimentalInput, + type LegacyHookInput, + type LegacyMfaFactorInput, + legacyParseGoBool, + type LegacyPasskeyInput, + legacyResolveEmailTemplateContentPath, + legacyResolveSigningKeysPath, + legacySigningKeysDecodeErrorMessage, + legacySigningKeysReadErrorMessage, + type LegacySmtpInput, + type LegacyThirdPartyInput, + legacyValidateResolvedConfig, +} from "./legacy-config-validate.ts"; import { LegacyDbConfigLoadError } from "./legacy-db-config.errors.ts"; import { parseDotEnv } from "./legacy-dotenv.ts"; import { @@ -348,23 +369,6 @@ function findDuplicateRemoteProjectId( return undefined; } -// Go's project-ref pattern (`apps/cli-go/pkg/config/config.go:470`): exactly 20 -// lowercase ASCII letters. -const LEGACY_PROJECT_REF_PATTERN = /^[a-z]{20}$/; - -// Go's storage bucket-name pattern (`apps/cli-go/pkg/config/config.go:1382`). -// `config.Validate` runs `ValidateBucketName` over every `[storage.buckets.*]` key -// during config load (`config.go:898-903`), aborting before any db command when a -// name does not match. The source string is reused verbatim in the error message via -// `.source` so it byte-matches Go's `bucketNamePattern.String()`. -const LEGACY_BUCKET_NAME_PATTERN = /^(\w|!|-|\.|\*|'|\(|\)| |&|\$|@|=|;|:|\+|,|\?)*$/; - -// Go's function-slug pattern (`apps/cli-go/pkg/config/config.go:1372`). `config.Validate` -// runs `ValidateFunctionSlug` over every `[functions.*]` key during config load -// (`config.go:993-998`), rejecting the config before any db command. `.source` is reused -// in the message so it byte-matches Go's `funcSlugPattern.String()`. -const LEGACY_FUNCTION_SLUG_PATTERN = /^[A-Za-z][A-Za-z0-9_-]*$/; - /** * Go's `config.Validate` rejects any `[remotes.]` whose `project_id` is not a * valid project ref (`config.go:832-836`), on every config load — so a malformed or @@ -623,33 +627,6 @@ function nonEmptyString(value: unknown): Option.Option { return typeof value === "string" && value.length > 0 ? Option.some(value) : Option.none(); } -/** Go's `json.Valid` (`encoding/json`): reports whether the string is well-formed JSON. */ -function legacyIsValidJson(value: string): boolean { - try { - JSON.parse(value); - return true; - } catch { - return false; - } -} - -// Go's `strconv.ParseBool` accepted forms (`go-viper/mapstructure` `decodeBool` under -// viper's forced `WeaklyTypedInput`): a string decodes to bool via ParseBool, an empty -// string is `false`, and any other value is a parse error. -const GO_BOOL_TRUE = new Set(["1", "t", "T", "TRUE", "true", "True"]); -const GO_BOOL_FALSE = new Set(["0", "f", "F", "FALSE", "false", "False", ""]); - -/** - * Parse a config bool the way Go does (`strconv.ParseBool` via mapstructure's weakly - * typed decode). Returns the bool, or `undefined` for a malformed value (which Go - * surfaces as a `failed to parse config` error). - */ -function legacyParseGoBool(value: string): boolean | undefined { - if (GO_BOOL_TRUE.has(value)) return true; - if (GO_BOOL_FALSE.has(value)) return false; - return undefined; -} - /** * Resolve a `[section] enabled` style bool. Go decodes a TOML bool natively and a * string (incl. an `env(VAR)` reference) via `strconv.ParseBool` — so `"1"`/`"t"`/etc. @@ -864,405 +841,6 @@ const legacyAssertDecryptableSecrets = ( // Go merges the template default before Validate (`templates/config.toml`), so an absent // `auth.site_url` is non-empty; only an explicit empty string fails A1 (`config.go:1037`). const DEFAULT_AUTH_SITE_URL = "http://127.0.0.1:3000"; -// Go's `hookSecretPattern` (`pkg/config/config.go:1436`). -const LEGACY_HOOK_SECRET_PATTERN = /^v1,whsec_[A-Za-z0-9+/=]{32,88}$/u; -// Go's `clerkDomainPattern` (`pkg/config/config.go:1553`). -const LEGACY_CLERK_DOMAIN_PATTERN = - /^(clerk([.][a-z0-9-]+){2,}|([a-z0-9-]+[.])+clerk[.]accounts[.]dev)$/u; - -/** - * Ports the FATAL validations Go runs inside `if c.Auth.Enabled { … }` during - * `config.Validate` (`apps/cli-go/pkg/config/config.go:1036-1102` + the nested - * `.validate()` methods at 1242-1632), in Go's first-failure-wins order, so a db/migration - * command aborts on an invalid auth config exactly like the Go CLI (the reviewer's case: - * `migration down --local` must stop before any destructive work). Every check below mirrors - * a Go `return errors.New(...)` site with the byte-exact message. - * - * Deliberately NOT ported: the `assertEnvLoaded` WARN lines (Go's `config.go:1143-1148` only - * prints a stderr warning and always returns nil — never fatal), and the non-fatal mutations - * (the SMS "no provider → disable phone login" WARN, the linkedin/slack deprecation WARN); - * those affect neither the exit code nor any value this subset reader exposes. The - * linkedin/slack providers are still skipped (Go deletes them before validating). - */ -const legacyValidateAuthConfig = Effect.fnUntraced(function* ( - authRaw: RawDoc, - fs: FileSystem.FileSystem, - path: Path.Path, - workdir: string, - lookup: EnvLookup, -) { - const fail = (message: string) => Effect.fail(new LegacyDbConfigLoadError({ message })); - const supabaseDir = path.join(workdir, "supabase"); - // Env-expanded string of `rec[key]` ("" when absent/non-string). An unresolved `env(VAR)` - // stays literal (non-empty), matching Go's LoadEnvHook + the Secret decode hook. - const str = (rec: RawDoc | undefined, key: string): string => { - const value = rec?.[key]; - return typeof value === "string" ? legacyExpandEnv(value, lookup) : ""; - }; - // Weak-bool decode (Go mapstructure): boolean | nonzero number | strconv.ParseBool string. - // A malformed string ABORTS the load like Go's decode (it does NOT coerce to false), using - // the reader's `failed to parse config: invalid .` shape (the same simplification the - // db.* bools use; Go's verbose mapstructure string is not reproduced byte-for-byte there - // either). Absent / non-string → false (the default for every auth enable-flag). - const gate = (rec: RawDoc | undefined, key: string, field: string) => - Effect.gen(function* () { - const value = rec?.[key]; - if (typeof value === "boolean") return value; - if (typeof value === "number") return value !== 0; - if (typeof value !== "string") return false; - const parsed = legacyParseGoBool(legacyExpandEnv(value, lookup)); - if (parsed === undefined) return yield* fail(`failed to parse config: invalid ${field}.`); - return parsed; - }); - // Resolve a config file path: absolute → verbatim (Go opens it from the OS root after chdir); - // relative → joined under `base` (`filepath.IsAbs` guards, `config.go:854-878`). - const resolvePath = (p: string, base: string): string => - path.isAbsolute(p) ? p : path.join(base, p); - - // A1: site_url required (`config.go:1037-1039`). - const siteUrl = - authRaw["site_url"] === undefined ? DEFAULT_AUTH_SITE_URL : str(authRaw, "site_url"); - if (siteUrl.length === 0) return yield* fail("Missing required field in config: auth.site_url"); - - // A4: [auth.captcha]. The provider enum is a decode-time check (`CaptchaProvider.UnmarshalText`, - // `auth.go:58-71`) that fires whenever `provider` is set, regardless of `enabled`; the - // required-field checks run only when enabled (`config.go:1048-1058`). - const captcha = asRecord(authRaw["captcha"]); - if (captcha !== undefined) { - const provider = str(captcha, "provider"); - if (provider.length > 0 && provider !== "hcaptcha" && provider !== "turnstile") - return yield* fail( - "failed to parse config: decoding failed due to the following error(s):\n\n'auth.captcha.provider' must be one of [hcaptcha turnstile]", - ); - if (yield* gate(captcha, "enabled", "auth.captcha.enabled")) { - if (provider.length === 0) - return yield* fail("Missing required field in config: auth.captcha.provider"); - if (str(captcha, "secret").length === 0) - return yield* fail("Missing required field in config: auth.captcha.secret"); - } - } - - // A5: signing keys file load (`config.go:1059-1065`) — read + parse as a JSON array. A - // relative path resolves under the supabase dir (`config.go:877-878`); absolute is verbatim. - const signingKeysPath = str(authRaw, "signing_keys_path"); - if (signingKeysPath.length > 0) { - const keysJson = yield* fs.readFileString(resolvePath(signingKeysPath, supabaseDir)).pipe( - Effect.mapError( - (cause) => - new LegacyDbConfigLoadError({ - message: `failed to read signing keys: ${cause.message}`, - }), - ), - ); - yield* Effect.try({ - try: () => { - const parsed: unknown = JSON.parse(keysJson); - if (!Array.isArray(parsed)) throw new Error("signing keys must be a JSON array of JWKs"); - return parsed; - }, - catch: (cause) => - new LegacyDbConfigLoadError({ - message: `failed to decode signing keys: ${cause instanceof Error ? cause.message : String(cause)}`, - }), - }); - } - - // A6: passkey/webauthn when passkey enabled (`config.go:1066-1084`). - const passkey = asRecord(authRaw["passkey"]); - if (passkey !== undefined && (yield* gate(passkey, "enabled", "auth.passkey.enabled"))) { - const webauthn = asRecord(authRaw["webauthn"]); - if (webauthn === undefined) - return yield* fail( - "Missing required config section: auth.webauthn (required when auth.passkey.enabled is true)", - ); - if (str(webauthn, "rp_id").length === 0) - return yield* fail("Missing required field in config: auth.webauthn.rp_id"); - const rpOrigins = webauthn["rp_origins"]; - if (!Array.isArray(rpOrigins) || rpOrigins.length === 0) - return yield* fail("Missing required field in config: auth.webauthn.rp_origins"); - } - - // B1: hooks — each enabled hook (`config.go:1402-1470`). - const hook = asRecord(authRaw["hook"]); - if (hook !== undefined) { - const hookTypes = [ - "mfa_verification_attempt", - "password_verification_attempt", - "custom_access_token", - "send_sms", - "send_email", - "before_user_created", - ] as const; - for (const hookType of hookTypes) { - const h = asRecord(hook[hookType]); - if (h === undefined) continue; - if (!(yield* gate(h, "enabled", `auth.hook.${hookType}.enabled`))) continue; - const uri = str(h, "uri"); - if (uri.length === 0) - return yield* fail(`Missing required field in config: auth.hook.${hookType}.uri`); - // Go uses net/url.Parse, which (unlike `new URL`) does not throw on a missing scheme; - // extract the scheme the same lenient way so a no-scheme/other URI hits the default - // branch rather than erroring (the rare url.Parse error case is not separately ported). - const scheme = (/^([a-zA-Z][a-zA-Z0-9+.-]*):/u.exec(uri)?.[1] ?? "").toLowerCase(); - const secrets = str(h, "secrets"); - if (scheme === "http" || scheme === "https") { - if (secrets.length === 0) - return yield* fail(`Missing required field in config: auth.hook.${hookType}.secrets`); - for (const secret of secrets.split("|")) { - if (!LEGACY_HOOK_SECRET_PATTERN.test(secret)) - return yield* fail( - `Invalid hook config: auth.hook.${hookType}.secrets must be formatted as "v1,whsec_" with a minimum length of 32 characters.`, - ); - } - } else if (scheme === "pg-functions") { - if (secrets.length > 0) - return yield* fail( - `Invalid hook config: auth.hook.${hookType}.secrets is unsupported for pg-functions URI`, - ); - } else { - return yield* fail( - `Invalid hook config: auth.hook.${hookType}.uri should be a HTTP, HTTPS, or pg-functions URI`, - ); - } - } - } - - // B2: mfa — enroll requires verify (`config.go:1472-1483`). - const mfa = asRecord(authRaw["mfa"]); - if (mfa !== undefined) { - for (const [key, label] of [ - ["totp", "totp"], - ["phone", "phone"], - ["web_authn", "web_authn"], - ] as const) { - const factor = asRecord(mfa[key]); - if (factor === undefined) continue; - const enroll = yield* gate(factor, "enroll_enabled", `auth.mfa.${label}.enroll_enabled`); - const verify = yield* gate(factor, "verify_enabled", `auth.mfa.${label}.verify_enabled`); - if (enroll && !verify) - return yield* fail( - `Invalid MFA config: auth.mfa.${label}.enroll_enabled requires verify_enabled`, - ); - } - } - - // B3: email (`config.go:1242-1295`). - const email = asRecord(authRaw["email"]); - if (email !== undefined) { - // Go resolves a relative `content_path` differently per section: email TEMPLATE paths are - // relative to the PROJECT ROOT (`config.go:854-856`, the `// FIXME` there), while - // NOTIFICATION paths are relative to the supabase dir (`config.go:861-862`); absolute → as-is. - const validateTemplate = ( - section: "template" | "notification", - name: string, - tmpl: RawDoc, - base: string, - ) => - Effect.gen(function* () { - const contentPath = str(tmpl, "content_path"); - if (contentPath.length === 0) { - if (tmpl["content"] !== undefined) - return yield* fail( - `Invalid config for auth.email.${section}.${name}.content: please use content_path instead`, - ); - return; - } - yield* fs.readFileString(resolvePath(contentPath, base)).pipe( - Effect.mapError( - (cause) => - new LegacyDbConfigLoadError({ - message: `Invalid config for auth.email.${section}.${name}.content_path: ${cause.message}`, - }), - ), - ); - }); - const templates = asRecord(email["template"]); - if (templates !== undefined) { - for (const name of Object.keys(templates)) { - const tmpl = asRecord(templates[name]); - if (tmpl !== undefined) yield* validateTemplate("template", name, tmpl, workdir); - } - } - const notifications = asRecord(email["notification"]); - if (notifications !== undefined) { - for (const name of Object.keys(notifications)) { - const tmpl = asRecord(notifications[name]); - if ( - tmpl !== undefined && - (yield* gate(tmpl, "enabled", `auth.email.notification.${name}.enabled`)) - ) - yield* validateTemplate("notification", name, tmpl, supabaseDir); - } - } - // Go defaults `auth.email.smtp.enabled = true` when the `[auth.email.smtp]` table is present - // but omits `enabled` (`config.go:692-696`), so a present table validates unless explicitly - // disabled. - const smtp = asRecord(email["smtp"]); - const smtpEnabled = - smtp !== undefined && - (smtp["enabled"] === undefined - ? true - : yield* gate(smtp, "enabled", "auth.email.smtp.enabled")); - if (smtp !== undefined && smtpEnabled) { - if (str(smtp, "host").length === 0) - return yield* fail("Missing required field in config: auth.email.smtp.host"); - const portRaw = smtp["port"]; - const port = - typeof portRaw === "number" - ? portRaw - : typeof portRaw === "string" - ? Number(legacyExpandEnv(portRaw, lookup)) - : 0; - if (!port) return yield* fail("Missing required field in config: auth.email.smtp.port"); - if (str(smtp, "user").length === 0) - return yield* fail("Missing required field in config: auth.email.smtp.user"); - if (str(smtp, "pass").length === 0) - return yield* fail("Missing required field in config: auth.email.smtp.pass"); - if (str(smtp, "admin_email").length === 0) - return yield* fail("Missing required field in config: auth.email.smtp.admin_email"); - } - } - - // B4: sms — only the FIRST enabled provider is validated (Go's switch, `config.go:1297-1364`). - const sms = asRecord(authRaw["sms"]); - if (sms !== undefined) { - const twilio = asRecord(sms["twilio"]); - const twilioVerify = asRecord(sms["twilio_verify"]); - const messagebird = asRecord(sms["messagebird"]); - const textlocal = asRecord(sms["textlocal"]); - const vonage = asRecord(sms["vonage"]); - // Resolve every provider's enable-flag (a malformed bool aborts like Go's decode); Go's - // switch then validates only the FIRST enabled provider. - const twilioEnabled = yield* gate(twilio, "enabled", "auth.sms.twilio.enabled"); - const twilioVerifyEnabled = yield* gate( - twilioVerify, - "enabled", - "auth.sms.twilio_verify.enabled", - ); - const messagebirdEnabled = yield* gate(messagebird, "enabled", "auth.sms.messagebird.enabled"); - const textlocalEnabled = yield* gate(textlocal, "enabled", "auth.sms.textlocal.enabled"); - const vonageEnabled = yield* gate(vonage, "enabled", "auth.sms.vonage.enabled"); - if (twilioEnabled) { - if (str(twilio, "account_sid").length === 0) - return yield* fail("Missing required field in config: auth.sms.twilio.account_sid"); - if (str(twilio, "message_service_sid").length === 0) - return yield* fail("Missing required field in config: auth.sms.twilio.message_service_sid"); - if (str(twilio, "auth_token").length === 0) - return yield* fail("Missing required field in config: auth.sms.twilio.auth_token"); - } else if (twilioVerifyEnabled) { - if (str(twilioVerify, "account_sid").length === 0) - return yield* fail("Missing required field in config: auth.sms.twilio_verify.account_sid"); - if (str(twilioVerify, "message_service_sid").length === 0) - return yield* fail( - "Missing required field in config: auth.sms.twilio_verify.message_service_sid", - ); - if (str(twilioVerify, "auth_token").length === 0) - return yield* fail("Missing required field in config: auth.sms.twilio_verify.auth_token"); - } else if (messagebirdEnabled) { - if (str(messagebird, "originator").length === 0) - return yield* fail("Missing required field in config: auth.sms.messagebird.originator"); - if (str(messagebird, "access_key").length === 0) - return yield* fail("Missing required field in config: auth.sms.messagebird.access_key"); - } else if (textlocalEnabled) { - if (str(textlocal, "sender").length === 0) - return yield* fail("Missing required field in config: auth.sms.textlocal.sender"); - if (str(textlocal, "api_key").length === 0) - return yield* fail("Missing required field in config: auth.sms.textlocal.api_key"); - } else if (vonageEnabled) { - if (str(vonage, "from").length === 0) - return yield* fail("Missing required field in config: auth.sms.vonage.from"); - if (str(vonage, "api_key").length === 0) - return yield* fail("Missing required field in config: auth.sms.vonage.api_key"); - if (str(vonage, "api_secret").length === 0) - return yield* fail("Missing required field in config: auth.sms.vonage.api_secret"); - } - } - - // B5: external providers (`config.go:1368-1398`). linkedin/slack are deprecated and deleted - // before validation, so they are never validated here. - const external = asRecord(authRaw["external"]); - if (external !== undefined) { - for (const name of Object.keys(external)) { - if (name === "linkedin" || name === "slack") continue; - const provider = asRecord(external[name]); - if (provider === undefined) continue; - if (!(yield* gate(provider, "enabled", `auth.external.${name}.enabled`))) continue; - if (str(provider, "client_id").length === 0) - return yield* fail(`Missing required field in config: auth.external.${name}.client_id`); - if (name !== "apple" && name !== "google" && str(provider, "secret").length === 0) - return yield* fail(`Missing required field in config: auth.external.${name}.secret`); - } - } - - // B6: third_party — validate each enabled provider in order, then mutual exclusivity - // (`config.go:1584-1632`). Note `aws_cognito`'s messages say `cognito` (Go's wording). - const thirdParty = asRecord(authRaw["third_party"]); - if (thirdParty !== undefined) { - let enabledCount = 0; - const firebase = asRecord(thirdParty["firebase"]); - if ( - firebase !== undefined && - (yield* gate(firebase, "enabled", "auth.third_party.firebase.enabled")) - ) { - enabledCount += 1; - if (str(firebase, "project_id").length === 0) - return yield* fail( - "Invalid config: auth.third_party.firebase is enabled but without a project_id.", - ); - } - const auth0 = asRecord(thirdParty["auth0"]); - if (auth0 !== undefined && (yield* gate(auth0, "enabled", "auth.third_party.auth0.enabled"))) { - enabledCount += 1; - if (str(auth0, "tenant").length === 0) - return yield* fail( - "Invalid config: auth.third_party.auth0 is enabled but without a tenant.", - ); - } - const cognito = asRecord(thirdParty["aws_cognito"]); - if ( - cognito !== undefined && - (yield* gate(cognito, "enabled", "auth.third_party.aws_cognito.enabled")) - ) { - enabledCount += 1; - if (str(cognito, "user_pool_id").length === 0) - return yield* fail( - "Invalid config: auth.third_party.cognito is enabled but without a user_pool_id.", - ); - if (str(cognito, "user_pool_region").length === 0) - return yield* fail( - "Invalid config: auth.third_party.cognito is enabled but without a user_pool_region.", - ); - } - const clerk = asRecord(thirdParty["clerk"]); - if (clerk !== undefined && (yield* gate(clerk, "enabled", "auth.third_party.clerk.enabled"))) { - enabledCount += 1; - const domain = str(clerk, "domain"); - if (domain.length === 0) - return yield* fail( - "Invalid config: auth.third_party.clerk is enabled but without a domain.", - ); - if (!LEGACY_CLERK_DOMAIN_PATTERN.test(domain)) - return yield* fail( - "Invalid config: auth.third_party.clerk has invalid domain, it usually is like clerk.example.com or example.clerk.accounts.dev. Check https://clerk.com/setup/supabase on how to find the correct value.", - ); - } - const workos = asRecord(thirdParty["workos"]); - if ( - workos !== undefined && - (yield* gate(workos, "enabled", "auth.third_party.workos.enabled")) - ) { - enabledCount += 1; - if (str(workos, "issuer_url").length === 0) - return yield* fail( - "Invalid config: auth.third_party.workos is enabled but without a issuer_url.", - ); - } - if (enabledCount > 1) - return yield* fail( - "Invalid config: Only one third_party provider allowed to be enabled at a time.", - ); - } -}); /** * Reads `/supabase/config.toml` (db subtree + project id) and the linked @@ -1534,22 +1112,10 @@ const readDbTomlCore = Effect.fnUntraced(function* ( }), ); } - // Reject unsupported major versions like Go's config.Validate ({13,14,15,17}; - // `apps/cli-go/pkg/config/config.go:869-897`) before any image/container runs. An - // absent value falls through to the default (Go's zero-then-default). - if ( - typeof majorVersionResolved === "number" && - ![13, 14, 15, 17].includes(majorVersionResolved) - ) { - return yield* Effect.fail( - new LegacyDbConfigLoadError({ - message: - majorVersionResolved === 12 - ? "Postgres version 12.x is unsupported. To use the CLI, either start a new project or follow project migration steps here: https://supabase.com/docs/guides/database#migrating-between-projects." - : `Failed reading config: Invalid db.major_version: ${majorVersionResolved}.`, - }), - ); - } + // Rejecting an unsupported major version ({13,14,15,17}; `apps/cli-go/pkg/config/config.go: + // 869-897`) is now `legacyValidateResolvedConfig`'s `db.major_version` switch (called once, + // below) — an absent value falls through to the default (Go's zero-then-default) and a present + // one (including `0`) flows into `input.db.majorVersion` for that switch to check. const majorVersion = typeof majorVersionResolved === "number" ? majorVersionResolved : DEFAULT_MAJOR_VERSION; @@ -1602,25 +1168,10 @@ const readDbTomlCore = Effect.fnUntraced(function* ( }), ); } - // Go's config.Validate rejects a present-but-invalid deno_version before pg-delta - // runs (`config.go:999-1008`): 0 → missing-required, anything other than 1/2 → - // invalid. An absent key falls through to the default (Go merges deno_version=2). - if (typeof denoVersionResolved === "number") { - if (denoVersionResolved === 0) { - return yield* Effect.fail( - new LegacyDbConfigLoadError({ - message: "Missing required field in config: edge_runtime.deno_version", - }), - ); - } - if (denoVersionResolved !== 1 && denoVersionResolved !== 2) { - return yield* Effect.fail( - new LegacyDbConfigLoadError({ - message: `Failed reading config: Invalid edge_runtime.deno_version: ${denoVersionResolved}.`, - }), - ); - } - } + // Rejecting a present-but-invalid deno_version (`config.go:999-1008`: 0 → missing-required, + // anything other than 1/2 → invalid) is now `legacyValidateResolvedConfig`'s + // `edgeRuntimeDenoVersion` switch (called once, below). An absent key falls through to the + // default (Go merges deno_version=2). const denoVersion = typeof denoVersionResolved === "number" ? denoVersionResolved : DEFAULT_DENO_VERSION; @@ -1704,60 +1255,19 @@ const readDbTomlCore = Effect.fnUntraced(function* ( (typeof formatOptionsRaw === "string" ? formatOptionsRaw : ""), lookup, ); - // Go's config.Validate aborts config load when a non-empty format_options is not - // valid JSON (`apps/cli-go/pkg/config/config.go:1685-1686`), before any shadow / - // catalog container runs. Fail here with Go's exact message so the user gets the - // actionable error up front rather than a later `JSON.parse` failure in the script. - if (formatOptionsExpanded.length > 0 && !legacyIsValidJson(formatOptionsExpanded)) { - return yield* Effect.fail( - new LegacyDbConfigLoadError({ - message: "Invalid config for experimental.pgdelta.format_options: must be valid JSON", - }), - ); - } + // Rejecting a non-empty, non-JSON `format_options` (`apps/cli-go/pkg/config/config.go: + // 1685-1686`) is now `legacyValidateResolvedConfig`'s `experimental.pgdeltaFormatOptions` + // check (called once, below). const formatOptions = nonEmptyString(formatOptionsExpanded); - // Go's config.Validate runs `ValidateBucketName` over every `[storage.buckets.*]` - // key on load (`apps/cli-go/pkg/config/config.go:898-903`), rejecting the config - // before any db command when a bucket name does not match `bucketNamePattern`. - // The reader otherwise drops `storage.buckets`, so port the check here with Go's - // exact message (the trailing `(%s)` is the regex source, `config.go:1386`). + // Bucket-name/function-slug validation (`config.go:898-903`/`993-998`) now lives in + // `legacyValidateResolvedConfig` (called once, below); only the pure extraction stays here. const bucketsRaw = asRecord(storageRaw?.["buckets"]); - if (bucketsRaw !== undefined) { - for (const name of Object.keys(bucketsRaw)) { - if (!LEGACY_BUCKET_NAME_PATTERN.test(name)) { - return yield* Effect.fail( - new LegacyDbConfigLoadError({ - message: `Invalid Bucket name: ${name}. Only lowercase letters, numbers, dots, hyphens, and spaces are allowed. (${LEGACY_BUCKET_NAME_PATTERN.source})`, - }), - ); - } - } - } - // Go's config.Validate runs `ValidateFunctionSlug` over every `[functions.*]` key on - // load (`apps/cli-go/pkg/config/config.go:993-998`, immediately after the bucket loop), - // rejecting the config before any db command when a slug does not match - // `funcSlugPattern`. The reader otherwise drops `functions`, so port the check here - // with Go's exact message (the trailing `(%s)` is the regex source, `config.go:1376`). - if (functionsRaw !== undefined) { - for (const name of Object.keys(functionsRaw)) { - if (!LEGACY_FUNCTION_SLUG_PATTERN.test(name)) { - return yield* Effect.fail( - new LegacyDbConfigLoadError({ - message: `Invalid Function name: ${name}. Must start with at least one letter, and only include alphanumeric characters, underscores, and hyphens. (${LEGACY_FUNCTION_SLUG_PATTERN.source})`, - }), - ); - } - } - } - - // Go's config.Validate runs the full `if c.Auth.Enabled` block (`config.go:1036-1102`) - // after the bucket/function checks — port its fatal validations so db/migration commands - // abort on an invalid auth config exactly like Go (e.g. an enabled passkey without a valid - // [auth.webauthn], or two third_party providers). Gated on `auth.enabled` (default true). - // Go's viper AutomaticEnv binds `auth.enabled` to `SUPABASE_AUTH_ENABLED` before Validate - // (`config.go:529-535`), so the env override decides whether the auth block is validated. + // Go's config.Validate runs the full `if c.Auth.Enabled` block (`config.go:1036-1102`) after + // the bucket/function checks. Gated on `auth.enabled` (default true); Go's viper AutomaticEnv + // binds `auth.enabled` to `SUPABASE_AUTH_ENABLED` before Validate (`config.go:529-535`), so the + // env override decides whether the auth block is validated. const authEnabled = yield* resolveBoolOrFail( "auth.enabled", authRaw?.["enabled"], @@ -1765,41 +1275,300 @@ const readDbTomlCore = Effect.fnUntraced(function* ( lookup, envOverride("SUPABASE_AUTH_ENABLED"), ); + + // Local helpers mirroring the deleted `legacyValidateAuthConfig`'s closures — its Go-parity + // CHECKS now live in `legacyValidateResolvedConfig`; `str`/`gate`/`fail` are still needed here + // to build that call's `LegacyAuthInput`, and by the D-only sms/external checks below (never + // part of the shared validator — see `legacy-config-validate.ts`'s module header). + const fail = (message: string) => Effect.fail(new LegacyDbConfigLoadError({ message })); + // Env-expanded string of `rec[key]` ("" when absent/non-string). An unresolved `env(VAR)` + // stays literal (non-empty), matching Go's LoadEnvHook + the Secret decode hook. + const str = (rec: RawDoc | undefined, key: string): string => { + const value = rec?.[key]; + return typeof value === "string" ? legacyExpandEnv(value, lookup) : ""; + }; + // Weak-bool decode (Go mapstructure): boolean | nonzero number | strconv.ParseBool string. A + // malformed string ABORTS the load like Go's decode (it does NOT coerce to false). Absent / + // non-string → false (the default for every auth enable-flag). + const gate = (rec: RawDoc | undefined, key: string, field: string) => + Effect.gen(function* () { + const value = rec?.[key]; + if (typeof value === "boolean") return value; + if (typeof value === "number") return value !== 0; + if (typeof value !== "string") return false; + const parsed = legacyParseGoBool(legacyExpandEnv(value, lookup)); + if (parsed === undefined) return yield* fail(`failed to parse config: invalid ${field}.`); + return parsed; + }); + + const authRawResolved = authRaw ?? {}; + let authInput: LegacyAuthInput | undefined; if (authEnabled) { - yield* legacyValidateAuthConfig(authRaw ?? {}, fs, path, workdir, lookup); + // A1: site_url required (`config.go:1037-1039`). + const siteUrl = + authRawResolved["site_url"] === undefined + ? DEFAULT_AUTH_SITE_URL + : str(authRawResolved, "site_url"); + + // A4: [auth.captcha]. The provider enum is a decode-time check (`CaptchaProvider. + // UnmarshalText`, `auth.go:58-71`); the required-field checks are `enabled`-gated + // (`config.go:1048-1058`) — both now live in `legacyValidateResolvedConfig`. + const captchaRaw = asRecord(authRawResolved["captcha"]); + let captchaInput: LegacyCaptchaInput | undefined; + if (captchaRaw !== undefined) { + const provider = str(captchaRaw, "provider"); + const secret = str(captchaRaw, "secret"); + captchaInput = { + enabled: yield* gate(captchaRaw, "enabled", "auth.captcha.enabled"), + // `str()` returns `""` for an absent key, but the shared validator's + // `provider === undefined` check needs a real `undefined` to fire correctly for an + // enabled captcha with no provider set. + provider: provider.length > 0 ? provider : undefined, + secret: secret.length > 0 ? secret : undefined, + }; + } + + // A5: signing keys file load (`config.go:1059-1065`) — I/O, stays in D. A relative path + // resolves under the supabase dir; absolute is verbatim. + const signingKeysPath = str(authRawResolved, "signing_keys_path"); + if (signingKeysPath.length > 0) { + const keysJson = yield* fs + .readFileString(legacyResolveSigningKeysPath(workdir, signingKeysPath)) + .pipe( + Effect.mapError( + (cause) => + new LegacyDbConfigLoadError({ message: legacySigningKeysReadErrorMessage(cause) }), + ), + ); + yield* Effect.try({ + try: () => { + const parsed: unknown = JSON.parse(keysJson); + if (!Array.isArray(parsed)) { + throw new Error("signing keys must be a JSON array of JWKs"); + } + return parsed; + }, + catch: (cause) => + new LegacyDbConfigLoadError({ message: legacySigningKeysDecodeErrorMessage(cause) }), + }); + } + + // A6: passkey/webauthn when passkey enabled (`config.go:1066-1084`). + const passkeyRaw = asRecord(authRawResolved["passkey"]); + let passkeyInput: LegacyPasskeyInput | undefined; + if (passkeyRaw !== undefined && (yield* gate(passkeyRaw, "enabled", "auth.passkey.enabled"))) { + const webauthnRaw = asRecord(authRawResolved["webauthn"]); + const rpOrigins = webauthnRaw?.["rp_origins"]; + passkeyInput = { + webauthnPresent: webauthnRaw !== undefined, + rpId: str(webauthnRaw, "rp_id"), + rpOrigins: Array.isArray(rpOrigins) ? rpOrigins : undefined, + }; + } + + // B1: hooks — each enabled hook, Go's fixed iteration order (`config.go:1402-1470`). + const hookRaw = asRecord(authRawResolved["hook"]); + const hookTypes = [ + "mfa_verification_attempt", + "password_verification_attempt", + "custom_access_token", + "send_sms", + "send_email", + "before_user_created", + ] as const; + const hooks: Array = []; + for (const hookType of hookTypes) { + const h = asRecord(hookRaw?.[hookType]); + if (h !== undefined && (yield* gate(h, "enabled", `auth.hook.${hookType}.enabled`))) { + hooks.push({ type: hookType, uri: str(h, "uri"), secrets: str(h, "secrets") }); + } + } + + // B2: mfa — enroll requires verify (`config.go:1472-1483`), fixed totp/phone/web_authn order. + const mfaRaw = asRecord(authRawResolved["mfa"]); + const mfa: Array = []; + for (const label of ["totp", "phone", "web_authn"] as const) { + const factor = asRecord(mfaRaw?.[label]); + mfa.push({ + label, + enrollEnabled: yield* gate(factor, "enroll_enabled", `auth.mfa.${label}.enroll_enabled`), + verifyEnabled: yield* gate(factor, "verify_enabled", `auth.mfa.${label}.verify_enabled`), + }); + } + + // B3: email (`config.go:1242-1295`) — template/notification content is I/O, stays in D. Go + // resolves a relative `content_path` differently per section: TEMPLATE paths are relative to + // the PROJECT ROOT (`config.go:854-856`, the `// FIXME` there), NOTIFICATION paths are + // relative to the supabase dir (`config.go:861-862`); absolute → as-is. + const emailRaw = asRecord(authRawResolved["email"]); + const templatesRaw = asRecord(emailRaw?.["template"]); + if (templatesRaw !== undefined) { + for (const name of Object.keys(templatesRaw)) { + const tmpl = asRecord(templatesRaw[name]); + if (tmpl === undefined) continue; + const contentPath = yield* Effect.try({ + try: () => + legacyResolveEmailTemplateContentPath({ + section: "template", + name, + contentPath: str(tmpl, "content_path"), + contentPresent: tmpl["content"] !== undefined, + base: workdir, + }), + catch: (cause) => + new LegacyDbConfigLoadError({ + message: cause instanceof Error ? cause.message : String(cause), + }), + }); + if (contentPath === undefined) continue; + yield* fs.readFileString(contentPath).pipe( + Effect.mapError( + (cause) => + new LegacyDbConfigLoadError({ + message: legacyEmailContentPathReadErrorMessage("template", name, cause), + }), + ), + ); + } + } + const notificationsRaw = asRecord(emailRaw?.["notification"]); + if (notificationsRaw !== undefined) { + for (const name of Object.keys(notificationsRaw)) { + const tmpl = asRecord(notificationsRaw[name]); + if ( + tmpl === undefined || + !(yield* gate(tmpl, "enabled", `auth.email.notification.${name}.enabled`)) + ) { + continue; + } + const contentPath = yield* Effect.try({ + try: () => + legacyResolveEmailTemplateContentPath({ + section: "notification", + name, + contentPath: str(tmpl, "content_path"), + contentPresent: tmpl["content"] !== undefined, + base: supabaseDir, + }), + catch: (cause) => + new LegacyDbConfigLoadError({ + message: cause instanceof Error ? cause.message : String(cause), + }), + }); + if (contentPath === undefined) continue; + yield* fs.readFileString(contentPath).pipe( + Effect.mapError( + (cause) => + new LegacyDbConfigLoadError({ + message: legacyEmailContentPathReadErrorMessage("notification", name, cause), + }), + ), + ); + } + } + // Go defaults `auth.email.smtp.enabled = true` when the `[auth.email.smtp]` table is present + // but omits `enabled` (`config.go:692-696`), so a present table validates unless explicitly + // disabled. + const smtpRaw = asRecord(emailRaw?.["smtp"]); + let smtpInput: LegacySmtpInput | undefined; + if (smtpRaw !== undefined) { + const smtpPortRaw = smtpRaw["port"]; + // The shared validator's required-field check is `port === 0` (Go decodes `port` into a + // numeric type at the config-decode step, so it can never observe a non-numeric value here). + // D reads the raw TOML/env string directly, so a non-numeric `port` (or an unresolved + // `env(VAR)`) parses to `NaN` via `Number(...)` — normalize that to `0` so it still trips + // the "missing required field" check instead of silently passing config load. + const smtpPortNumeric = + typeof smtpPortRaw === "number" + ? smtpPortRaw + : typeof smtpPortRaw === "string" + ? Number(legacyExpandEnv(smtpPortRaw, lookup)) + : 0; + smtpInput = { + enabled: + smtpRaw["enabled"] === undefined + ? true + : yield* gate(smtpRaw, "enabled", "auth.email.smtp.enabled"), + host: str(smtpRaw, "host"), + port: Number.isNaN(smtpPortNumeric) ? 0 : smtpPortNumeric, + user: str(smtpRaw, "user"), + pass: str(smtpRaw, "pass"), + adminEmail: str(smtpRaw, "admin_email"), + }; + } + + // B6: third_party — each enabled provider, Go's fixed order (`config.go:1584-1632`). Note + // `aws_cognito`'s messages say `cognito` (Go's wording). + const thirdPartyRaw = asRecord(authRawResolved["third_party"]); + const thirdParty: Array = []; + const firebaseRaw = asRecord(thirdPartyRaw?.["firebase"]); + if ( + firebaseRaw !== undefined && + (yield* gate(firebaseRaw, "enabled", "auth.third_party.firebase.enabled")) + ) { + thirdParty.push({ provider: "firebase", requiredField: str(firebaseRaw, "project_id") }); + } + const auth0Raw = asRecord(thirdPartyRaw?.["auth0"]); + if ( + auth0Raw !== undefined && + (yield* gate(auth0Raw, "enabled", "auth.third_party.auth0.enabled")) + ) { + thirdParty.push({ provider: "auth0", requiredField: str(auth0Raw, "tenant") }); + } + const cognitoRaw = asRecord(thirdPartyRaw?.["aws_cognito"]); + if ( + cognitoRaw !== undefined && + (yield* gate(cognitoRaw, "enabled", "auth.third_party.aws_cognito.enabled")) + ) { + thirdParty.push({ + provider: "cognito", + requiredField: str(cognitoRaw, "user_pool_id"), + cognitoUserPoolRegion: str(cognitoRaw, "user_pool_region"), + }); + } + const clerkRaw = asRecord(thirdPartyRaw?.["clerk"]); + if ( + clerkRaw !== undefined && + (yield* gate(clerkRaw, "enabled", "auth.third_party.clerk.enabled")) + ) { + thirdParty.push({ provider: "clerk", requiredField: str(clerkRaw, "domain") }); + } + const workosRaw = asRecord(thirdPartyRaw?.["workos"]); + if ( + workosRaw !== undefined && + (yield* gate(workosRaw, "enabled", "auth.third_party.workos.enabled")) + ) { + thirdParty.push({ provider: "workos", requiredField: str(workosRaw, "issuer_url") }); + } + + authInput = { + siteUrl, + captcha: captchaInput, + passkey: passkeyInput, + hooks, + mfa, + smtp: smtpInput, + thirdParty, + }; } // Go's config.Validate validates `[analytics]` after the auth block (`config.go:1123-1135`). - // Two fatal checks run on the db/migration path: - // 1. `LogflareBackend.UnmarshalText` (`config.go:60-66`) is a decode-time enum that rejects - // any `backend` other than `postgres`/`bigquery` — regardless of `enabled` (it fires - // during UnmarshalExact, like the captcha provider enum), so it gates here too. - // 2. When analytics is enabled with the BigQuery backend, the three GCP fields are required - // (`config.go:1124-1134`), in order, with byte-exact messages. - // Go merges the template defaults `enabled = true`, `backend = "postgres"` before Validate - // (`templates/config.toml:388-392`), so an absent `[analytics]` section is enabled+postgres and - // passes (an empty backend never equals `bigquery`, so the GCP block is skipped). viper - // AutomaticEnv binds `SUPABASE_ANALYTICS_*`; a matched remote block makes those keys env-immune, - // same as every other `LEGACY_ENV_OVERRIDABLE_KEYS` field above. + // Computed here (after the auth block, before the single shared call) rather than pure-derived + // earlier: `analyticsEnabled` resolves through `resolveBoolOrFail`, which can itself FAIL on a + // malformed `SUPABASE_ANALYTICS_ENABLED`/`analytics.enabled` bool — positioning that failure + // point ahead of the auth block would report it before an auth-block error even for a config + // that's ALSO broken there, reversing Go's real order. Go merges the template defaults + // `enabled = true`, `backend = "postgres"` before Validate (`templates/config.toml:388-392`), so + // an absent `[analytics]` section is enabled+postgres and passes (an empty backend never equals + // `bigquery`, so the GCP block is skipped). viper AutomaticEnv binds `SUPABASE_ANALYTICS_*`; a + // matched remote block makes those keys env-immune, same as every other + // `LEGACY_ENV_OVERRIDABLE_KEYS` field above. const analyticsString = (key: string, envName: string): string => { const fromEnv = remoteOverrideKeys.has(`analytics.${key}`) ? undefined : envOverride(envName); const raw = fromEnv ?? analyticsRaw?.[key]; return typeof raw === "string" ? legacyExpandEnv(raw, lookup) : ""; }; const analyticsBackend = analyticsString("backend", "SUPABASE_ANALYTICS_BACKEND"); - if ( - analyticsBackend.length > 0 && - analyticsBackend !== "postgres" && - analyticsBackend !== "bigquery" - ) { - // Mirror the captcha enum's mapstructure envelope (`%v` of the allowed `[]LogflareBackend`). - return yield* Effect.fail( - new LegacyDbConfigLoadError({ - message: - "failed to parse config: decoding failed due to the following error(s):\n\n'analytics.backend' must be one of [postgres bigquery]", - }), - ); - } const analyticsEnabled = yield* resolveBoolOrFail( "analytics.enabled", analyticsRaw?.["enabled"], @@ -1809,32 +1578,128 @@ const readDbTomlCore = Effect.fnUntraced(function* ( ? undefined : envOverride("SUPABASE_ANALYTICS_ENABLED"), ); - if (analyticsEnabled && analyticsBackend === "bigquery") { - // Each GCP value is env-expanded (Go's LoadEnvHook), so an unresolved `env(VAR)` stays - // non-empty and passes the `len(...) == 0` check, exactly like Go. - if (analyticsString("gcp_project_id", "SUPABASE_ANALYTICS_GCP_PROJECT_ID").length === 0) { - return yield* Effect.fail( - new LegacyDbConfigLoadError({ - message: "Missing required field in config: analytics.gcp_project_id", - }), + // Each GCP value is env-expanded (Go's LoadEnvHook), so an unresolved `env(VAR)` stays + // non-empty and passes the shared validator's `length === 0` check, exactly like Go. + const gcpProjectId = analyticsString("gcp_project_id", "SUPABASE_ANALYTICS_GCP_PROJECT_ID"); + const gcpProjectNumber = analyticsString( + "gcp_project_number", + "SUPABASE_ANALYTICS_GCP_PROJECT_NUMBER", + ); + const gcpJwtPath = analyticsString("gcp_jwt_path", "SUPABASE_ANALYTICS_GCP_JWT_PATH"); + + // Every PURE Config.Validate check this module/legacy-config-validate.ts jointly own (db.port + // is checked earlier, above, and stays there — see the comment at that check) is deferred to + // this single call, in Go's exact relative order. D's sms/external checks (D-only, never part + // of the shared validator) run AFTER this call succeeds, still gated on `authEnabled` — see + // `legacy-config-validate.ts`'s module header for the accepted ordering tradeoff this + // introduces against third_party. + const dbInput: LegacyDbInput = { port, majorVersion }; + const analyticsInput: LegacyAnalyticsInput = { + enabled: analyticsEnabled, + backend: analyticsBackend.length > 0 ? analyticsBackend : undefined, + gcpProjectId, + gcpProjectNumber, + gcpJwtPath, + }; + const experimentalInput: LegacyExperimentalInput = { + pgdeltaFormatOptions: formatOptionsExpanded, + }; + const validationInput: LegacyConfigValidationInput = { + db: dbInput, + storageBucketNames: bucketsRaw !== undefined ? Object.keys(bucketsRaw) : [], + functionSlugs: functionsRaw !== undefined ? Object.keys(functionsRaw) : [], + auth: authInput, + edgeRuntimeDenoVersion: denoVersion, + analytics: analyticsInput, + experimental: experimentalInput, + }; + yield* Effect.try({ + try: () => legacyValidateResolvedConfig(validationInput), + catch: (cause) => + new LegacyDbConfigLoadError({ + message: cause instanceof Error ? cause.message : String(cause), + }), + }); + + if (authEnabled) { + // B4: sms — D-only (`config.go:1145-1147`/`1348-1417`, never part of the shared validator, + // see `legacy-config-validate.ts`'s module header). Only the FIRST enabled provider is + // validated (Go's switch). + const sms = asRecord(authRawResolved["sms"]); + if (sms !== undefined) { + const twilio = asRecord(sms["twilio"]); + const twilioVerify = asRecord(sms["twilio_verify"]); + const messagebird = asRecord(sms["messagebird"]); + const textlocal = asRecord(sms["textlocal"]); + const vonage = asRecord(sms["vonage"]); + const twilioEnabled = yield* gate(twilio, "enabled", "auth.sms.twilio.enabled"); + const twilioVerifyEnabled = yield* gate( + twilioVerify, + "enabled", + "auth.sms.twilio_verify.enabled", ); - } - if ( - analyticsString("gcp_project_number", "SUPABASE_ANALYTICS_GCP_PROJECT_NUMBER").length === 0 - ) { - return yield* Effect.fail( - new LegacyDbConfigLoadError({ - message: "Missing required field in config: analytics.gcp_project_number", - }), + const messagebirdEnabled = yield* gate( + messagebird, + "enabled", + "auth.sms.messagebird.enabled", ); + const textlocalEnabled = yield* gate(textlocal, "enabled", "auth.sms.textlocal.enabled"); + const vonageEnabled = yield* gate(vonage, "enabled", "auth.sms.vonage.enabled"); + if (twilioEnabled) { + if (str(twilio, "account_sid").length === 0) + return yield* fail("Missing required field in config: auth.sms.twilio.account_sid"); + if (str(twilio, "message_service_sid").length === 0) + return yield* fail( + "Missing required field in config: auth.sms.twilio.message_service_sid", + ); + if (str(twilio, "auth_token").length === 0) + return yield* fail("Missing required field in config: auth.sms.twilio.auth_token"); + } else if (twilioVerifyEnabled) { + if (str(twilioVerify, "account_sid").length === 0) + return yield* fail( + "Missing required field in config: auth.sms.twilio_verify.account_sid", + ); + if (str(twilioVerify, "message_service_sid").length === 0) + return yield* fail( + "Missing required field in config: auth.sms.twilio_verify.message_service_sid", + ); + if (str(twilioVerify, "auth_token").length === 0) + return yield* fail("Missing required field in config: auth.sms.twilio_verify.auth_token"); + } else if (messagebirdEnabled) { + if (str(messagebird, "originator").length === 0) + return yield* fail("Missing required field in config: auth.sms.messagebird.originator"); + if (str(messagebird, "access_key").length === 0) + return yield* fail("Missing required field in config: auth.sms.messagebird.access_key"); + } else if (textlocalEnabled) { + if (str(textlocal, "sender").length === 0) + return yield* fail("Missing required field in config: auth.sms.textlocal.sender"); + if (str(textlocal, "api_key").length === 0) + return yield* fail("Missing required field in config: auth.sms.textlocal.api_key"); + } else if (vonageEnabled) { + if (str(vonage, "from").length === 0) + return yield* fail("Missing required field in config: auth.sms.vonage.from"); + if (str(vonage, "api_key").length === 0) + return yield* fail("Missing required field in config: auth.sms.vonage.api_key"); + if (str(vonage, "api_secret").length === 0) + return yield* fail("Missing required field in config: auth.sms.vonage.api_secret"); + } } - if (analyticsString("gcp_jwt_path", "SUPABASE_ANALYTICS_GCP_JWT_PATH").length === 0) { - return yield* Effect.fail( - new LegacyDbConfigLoadError({ - message: - "Path to GCP Service Account Key must be provided in config, relative to config.toml: analytics.gcp_jwt_path", - }), - ); + + // B5: external providers — D-only (`config.go:1148-1150`/`1368-1398`, never part of the + // shared validator). linkedin/slack are deprecated and deleted before validation, so they are + // never validated here. + const external = asRecord(authRawResolved["external"]); + if (external !== undefined) { + for (const name of Object.keys(external)) { + if (name === "linkedin" || name === "slack") continue; + const provider = asRecord(external[name]); + if (provider === undefined) continue; + if (!(yield* gate(provider, "enabled", `auth.external.${name}.enabled`))) continue; + if (str(provider, "client_id").length === 0) + return yield* fail(`Missing required field in config: auth.external.${name}.client_id`); + if (name !== "apple" && name !== "google" && str(provider, "secret").length === 0) + return yield* fail(`Missing required field in config: auth.external.${name}.secret`); + } } } diff --git a/apps/cli/src/legacy/shared/legacy-db-config.toml-read.unit.test.ts b/apps/cli/src/legacy/shared/legacy-db-config.toml-read.unit.test.ts index 3fd179eaf8..673fd52d41 100644 --- a/apps/cli/src/legacy/shared/legacy-db-config.toml-read.unit.test.ts +++ b/apps/cli/src/legacy/shared/legacy-db-config.toml-read.unit.test.ts @@ -1530,6 +1530,28 @@ describe("legacyReadDbToml", () => { ); }); + it.effect("rejects db.major_version = 0 with Go's missing-required message", () => { + // Divergence #1 fix: this used to fall through to the generic + // "Failed reading config: Invalid db.major_version: 0." message (the same branch that + // catches an unsupported value like 16) — `legacyValidateResolvedConfig`'s dedicated `0` case + // now matches Go's `Missing required field in config: db.major_version`. + const dir = withConfig(["[db]", "major_version = 0", ""].join("\n")); + return read(dir).pipe( + Effect.exit, + Effect.tap((exit) => + Effect.sync(() => { + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(JSON.stringify(exit.cause)).toContain( + "Missing required field in config: db.major_version", + ); + } + rmSync(dir, { recursive: true, force: true }); + }), + ), + ); + }); + it.effect("rejects db.major_version = 12 with Go's 12.x message", () => { const dir = withConfig(["[db]", "major_version = 12", ""].join("\n")); return read(dir).pipe( diff --git a/apps/cli/src/legacy/shared/legacy-docker-ids.ts b/apps/cli/src/legacy/shared/legacy-docker-ids.ts index 41a5e74b14..93ac55723b 100644 --- a/apps/cli/src/legacy/shared/legacy-docker-ids.ts +++ b/apps/cli/src/legacy/shared/legacy-docker-ids.ts @@ -32,15 +32,30 @@ function truncateText(text: string, maxLength: number) { return text.length > maxLength ? text.slice(0, maxLength) : text; } -/** Go's `GetId` sanitisation: replace invalid runs with `_`, strip leading - * `_.-`, and cap at 40 chars. */ -function sanitizeProjectId(src: string) { +/** + * Go's `GetId` sanitisation: replace invalid runs with `_`, strip leading + * `_.-`, and cap at 40 chars. + * + * Exported because it is not only a container-*naming* concern: Go's + * `Config.Validate` (`pkg/config/config.go:938-944`) rewrites `c.ProjectId` + * to this same sanitized form **in place, once, at config-load time** (every + * `flags.LoadConfig` call ends in `Load` -> `Validate`), and every later use + * of `Config.ProjectId` — including the Docker LABEL value written by `start` + * (`internal/utils/docker.go:375`: `config.Labels[CliProjectLabel] = + * Config.ProjectId`) — reads that already-sanitized singleton. `GetId` itself + * performs no sanitisation of its own; it just reads the pre-sanitized value. + * So on the config/env-derived (non-`--project-id`) path, callers building a + * Docker label FILTER must sanitize too, or a `project_id` like `"my app"` + * filters on the raw string while `start` labeled the sanitized one and never + * matches anything (see `legacyCliProjectFilterValue`'s doc comment). + */ +export function legacySanitizeProjectId(src: string) { const sanitized = src.replaceAll(INVALID_PROJECT_ID, "_").replace(/^[_.-]+/, ""); return truncateText(sanitized, MAX_PROJECT_ID_LENGTH); } function localDockerId(name: string, projectId: string) { - return `supabase_${name}_${sanitizeProjectId(projectId)}`; + return `supabase_${name}_${legacySanitizeProjectId(projectId)}`; } /** `utils.DbId` — the local Postgres container name. */ @@ -52,3 +67,57 @@ export function localDbContainerId(projectId: string) { export function localNetworkId(projectId: string) { return localDockerId("network", projectId); } + +/** Go's `utils.CliProjectLabel` (`apps/cli-go/internal/utils/docker.go:59`) — the + * Docker label every container/volume/network created by `supabase start` carries. */ +export const LEGACY_CLI_PROJECT_LABEL = "com.supabase.cli.project"; + +/** + * Go's `utils.GetDockerIds()` (`apps/cli-go/internal/utils/config.go:82-98`) — the + * 13 service container ids (excludes `db`, `network`, and the `differ` shadow + * container, which are not part of the "expected running services" set). Order and + * alias-name strings are taken verbatim from `config.go:36-49,61-79`. + */ +export function legacyServiceContainerIds(projectId: string): ReadonlyArray { + return [ + localDockerId("kong", projectId), + localDockerId("auth", projectId), + localDockerId("inbucket", projectId), + localDockerId("realtime", projectId), + localDockerId("rest", projectId), + localDockerId("storage", projectId), + localDockerId("imgproxy", projectId), + localDockerId("pg_meta", projectId), + localDockerId("studio", projectId), + localDockerId("edge_runtime", projectId), + localDockerId("analytics", projectId), + localDockerId("vector", projectId), + localDockerId("pooler", projectId), + ]; +} + +/** + * Go's `utils.CliProjectFilter` (`apps/cli-go/internal/utils/docker.go:148-156`) — + * the value that follows `--filter label=` on the `docker`/`podman` CLI. An empty + * `projectId` (Go's `--all` path) filters on the bare label across every project. + * + * This function itself does not sanitize — by design, it's a pure pass-through. + * The caller is responsible for sanitizing `projectId` with + * {@link legacySanitizeProjectId} on the config/env-derived (default) path + * BEFORE calling this, matching Go's `Config.Validate` sanitizing the + * `Config.ProjectId` singleton once at config-load time so every later + * reader — including the Docker LABEL `start` writes — sees the same + * sanitized string. An explicit `--project-id ` (where one exists, + * e.g. `stop`) is Go's one exception: it assigns straight to + * `Config.ProjectId` without going through `Validate` + * (`apps/cli-go/internal/stop/stop.go:19-20`), so that path must stay raw/ + * unsanitized to match. There is also no injection risk either way: this + * value is always passed as a single argv element to a spawned process + * (never through a shell), so a malformed value can only make Docker's own + * filter parsing reject it or match nothing — it cannot break out into + * another command. + */ +export function legacyCliProjectFilterValue(projectId: string): string { + if (projectId.length === 0) return LEGACY_CLI_PROJECT_LABEL; + return `${LEGACY_CLI_PROJECT_LABEL}=${projectId}`; +} diff --git a/apps/cli/src/legacy/shared/legacy-docker-ids.unit.test.ts b/apps/cli/src/legacy/shared/legacy-docker-ids.unit.test.ts index ff967f18b8..9c07805652 100644 --- a/apps/cli/src/legacy/shared/legacy-docker-ids.unit.test.ts +++ b/apps/cli/src/legacy/shared/legacy-docker-ids.unit.test.ts @@ -1,6 +1,13 @@ import { describe, expect, it } from "vitest"; -import { legacyResolveLocalProjectId, localDbContainerId } from "./legacy-docker-ids.ts"; +import { + LEGACY_CLI_PROJECT_LABEL, + legacyCliProjectFilterValue, + legacyResolveLocalProjectId, + legacySanitizeProjectId, + legacyServiceContainerIds, + localDbContainerId, +} from "./legacy-docker-ids.ts"; describe("legacyResolveLocalProjectId", () => { it("prefers SUPABASE_PROJECT_ID (env) over config.toml and the basename", () => { @@ -23,3 +30,72 @@ describe("legacyResolveLocalProjectId", () => { expect(localDbContainerId(id)).toBe("supabase_db_env-id"); }); }); + +describe("legacyServiceContainerIds", () => { + it("returns the 13 service container ids in Go's GetDockerIds() order", () => { + // apps/cli-go/internal/utils/config.go:82-98 — kong, auth, inbucket, realtime, + // rest, storage, imgproxy, pg_meta, studio, edge_runtime, analytics, vector, pooler. + expect(legacyServiceContainerIds("my-app")).toEqual([ + "supabase_kong_my-app", + "supabase_auth_my-app", + "supabase_inbucket_my-app", + "supabase_realtime_my-app", + "supabase_rest_my-app", + "supabase_storage_my-app", + "supabase_imgproxy_my-app", + "supabase_pg_meta_my-app", + "supabase_studio_my-app", + "supabase_edge_runtime_my-app", + "supabase_analytics_my-app", + "supabase_vector_my-app", + "supabase_pooler_my-app", + ]); + }); + + it("sanitizes the project id the same way as localDbContainerId", () => { + const ids = legacyServiceContainerIds("My App!!"); + expect(ids[0]).toBe("supabase_kong_My_App_"); + }); +}); + +describe("legacyCliProjectFilterValue", () => { + it("returns the bare label when the project id is empty (Go's --all path)", () => { + expect(legacyCliProjectFilterValue("")).toBe(LEGACY_CLI_PROJECT_LABEL); + }); + + it("returns label=projectId when a project id is given", () => { + expect(legacyCliProjectFilterValue("my-app")).toBe(`${LEGACY_CLI_PROJECT_LABEL}=my-app`); + }); + + it("must be sanitized by the caller for the label to match what start wrote", () => { + // This function is a pure pass-through by design (see its doc comment) — a + // dirty config/env-derived id must be sanitized by the caller BEFORE being + // passed here, matching Go's Config.Validate sanitizing Config.ProjectId + // once at config-load time so every reader (including the Docker label + // `start` writes) sees the same string. + const dirty = "My App!!"; + expect(legacyCliProjectFilterValue(dirty)).toBe(`${LEGACY_CLI_PROJECT_LABEL}=My App!!`); + expect(legacyCliProjectFilterValue(legacySanitizeProjectId(dirty))).toBe( + `${LEGACY_CLI_PROJECT_LABEL}=My_App_`, + ); + }); +}); + +describe("legacySanitizeProjectId", () => { + it("replaces invalid character runs with a single underscore", () => { + expect(legacySanitizeProjectId("My App!!")).toBe("My_App_"); + }); + + it("strips leading underscore/dot/dash runs", () => { + expect(legacySanitizeProjectId("...hidden-app")).toBe("hidden-app"); + }); + + it("caps the result at 40 characters", () => { + const long = "a".repeat(50); + expect(legacySanitizeProjectId(long)).toBe("a".repeat(40)); + }); + + it("leaves an already-clean id unchanged", () => { + expect(legacySanitizeProjectId("my-app_123")).toBe("my-app_123"); + }); +}); diff --git a/apps/cli/src/legacy/shared/legacy-docker-lifecycle.ts b/apps/cli/src/legacy/shared/legacy-docker-lifecycle.ts new file mode 100644 index 0000000000..f8884a700a --- /dev/null +++ b/apps/cli/src/legacy/shared/legacy-docker-lifecycle.ts @@ -0,0 +1,259 @@ +import { Data, Effect, Stream } from "effect"; +import type { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner"; + +import { legacyDescribeContainerCliFailure, spawnContainerCli } from "./legacy-container-cli.ts"; + +type Spawner = ChildProcessSpawner["Service"]; + +/** + * Listing containers or volumes by Docker label failed. Wraps Go's + * `Docker.ContainerList`/`Docker.VolumeList` errors (`docker.go:99-104`, + * `docker.go:334-336` — see `checkServiceHealth`/`DockerRemoveAll`), which Go + * wraps as `"failed to list containers: %w"` / equivalent. + */ +export class LegacyDockerLifecycleListError extends Data.TaggedError( + "LegacyDockerLifecycleListError", +)<{ + readonly message: string; +}> {} + +/** Inspecting a single container's state failed for a reason other than "not found". */ +export class LegacyDockerLifecycleInspectError extends Data.TaggedError( + "LegacyDockerLifecycleInspectError", +)<{ + readonly message: string; +}> {} + +function collectByteStream(stream: Stream.Stream) { + const decoder = new TextDecoder(); + return Stream.runFold( + stream, + () => "", + (text, chunk) => text + decoder.decode(chunk, { stream: true }), + ).pipe(Effect.map((text) => text + decoder.decode())); +} + +function splitNonEmptyLines(text: string): ReadonlyArray { + return text + .split(/\r?\n/u) + .map((line) => line.trim()) + .filter((line) => line.length > 0); +} + +/** + * Go's `Docker.ContainerList(ctx, container.ListOptions{All, Filters})` + * (`docker.go:99-104`, `status.go:126-131`) via `docker ps --filter + * label=`. `all: false` mirrors `status`'s running-only list; + * `all: true` mirrors `stop`'s "every container regardless of state" list. + */ +export const legacyListContainersByLabel = ( + spawner: Spawner, + opts: { + readonly projectIdFilter: string; + readonly all: boolean; + readonly format: "id" | "names"; + }, +) => + Effect.scoped( + Effect.gen(function* () { + const formatArg = opts.format === "names" ? "{{.Names}}" : "{{.ID}}"; + const args = [ + "ps", + "--filter", + `label=${opts.projectIdFilter}`, + ...(opts.all ? ["--all"] : []), + "--format", + formatArg, + ]; + const child = yield* spawnContainerCli(spawner, args, { + stdin: "ignore", + stdout: "pipe", + stderr: "pipe", + }).pipe( + Effect.mapError( + (cause) => + new LegacyDockerLifecycleListError({ + message: `failed to list containers: ${legacyDescribeContainerCliFailure(cause)}`, + }), + ), + ); + // Concurrency is required, not cosmetic: sequential `Effect.all` would + // await `exitCode` (resolved by Node's "exit" event) before subscribing + // to `stdout`/`stderr` at all. Node's "exit" can fire before a fast + // process's stdio pipes are drained, so a late subscriber sees an + // already-ended, empty stream instead of the buffered bytes. + const [exitCode, stdout, stderr] = yield* Effect.all( + [ + child.exitCode.pipe(Effect.map(Number)), + collectByteStream(child.stdout), + collectByteStream(child.stderr), + ], + { concurrency: "unbounded" }, + ).pipe( + Effect.mapError( + () => new LegacyDockerLifecycleListError({ message: "failed to list containers" }), + ), + ); + if (exitCode !== 0) { + const message = stderr.trim(); + return yield* Effect.fail( + new LegacyDockerLifecycleListError({ + message: + message.length > 0 + ? `failed to list containers: ${message}` + : "failed to list containers", + }), + ); + } + return splitNonEmptyLines(stdout); + }), + ); + +/** + * Go's `Docker.ContainerInspect(ctx, containerId)` (`docker.go:148`, + * `status.go:148-155`) via `docker container inspect --format + * {{json .State}}`. Go's `assertContainerHealthy` does not special-case a + * missing container — it wraps whatever error `ContainerInspect` returns + * (`status.go:148-149`), so every non-zero exit, including "no such + * container", propagates as `LegacyDockerLifecycleInspectError` carrying the + * real Docker stderr text. + */ +export const legacyInspectContainerState = (spawner: Spawner, containerId: string) => + Effect.scoped( + Effect.gen(function* () { + const child = yield* spawnContainerCli( + spawner, + ["container", "inspect", containerId, "--format", "{{json .State}}"], + { + stdin: "ignore", + stdout: "pipe", + stderr: "pipe", + }, + ).pipe( + Effect.mapError( + (cause) => + new LegacyDockerLifecycleInspectError({ + message: `failed to inspect container health: ${legacyDescribeContainerCliFailure(cause)}`, + }), + ), + ); + // Concurrency is required, not cosmetic — see the matching comment in + // `legacyListContainersByLabel` above. + const [exitCode, stdout, stderr] = yield* Effect.all( + [ + child.exitCode.pipe(Effect.map(Number)), + collectByteStream(child.stdout), + collectByteStream(child.stderr), + ], + { concurrency: "unbounded" }, + ).pipe( + Effect.mapError( + () => + new LegacyDockerLifecycleInspectError({ + message: "failed to inspect container health", + }), + ), + ); + if (exitCode !== 0) { + const message = stderr.trim(); + return yield* Effect.fail( + new LegacyDockerLifecycleInspectError({ + message: + message.length > 0 + ? `failed to inspect container health: ${message}` + : "failed to inspect container health", + }), + ); + } + return parseContainerState(stdout); + }), + ); + +function parseContainerState(stdout: string): { + readonly running: boolean; + readonly status: string; + readonly health?: string; +} { + const trimmed = stdout.trim(); + let parsed: unknown; + try { + parsed = trimmed.length > 0 ? JSON.parse(trimmed) : {}; + } catch { + parsed = {}; + } + const state = isJsonRecord(parsed) ? parsed : {}; + // Go's `assertContainerHealthy` (`internal/status/status.go:147-156`) gates + // on the boolean `resp.State.Running`, not the status string — Docker's + // inspect `State` struct exposes both independently, and a paused or + // restarting container reports `Running: true` alongside a non-"running" + // `Status` (`"paused"`/`"restarting"`). `status` is kept as-is for the + // "container is not running: " message text (`status.go:151`), + // which still reads the string, but the gate itself must read the boolean. + const status = typeof state["Status"] === "string" ? state["Status"] : ""; + const running = state["Running"] === true; + const health = state["Health"]; + const healthStatus = + isJsonRecord(health) && typeof health["Status"] === "string" ? health["Status"] : undefined; + return healthStatus !== undefined + ? { running, status, health: healthStatus } + : { running, status }; +} + +function isJsonRecord(value: unknown): value is { readonly [key: string]: unknown } { + return typeof value === "object" && value !== null; +} + +/** + * Go's `Docker.VolumeList(ctx, volume.ListOptions{Filters})` + * (`docker.go` — used by the `stop` post-run volume-suggestion check) via + * `docker volume ls --filter label=`. + */ +export const legacyListVolumesByLabel = (spawner: Spawner, projectIdFilter: string) => + Effect.scoped( + Effect.gen(function* () { + const args = [ + "volume", + "ls", + "--filter", + `label=${projectIdFilter}`, + "--format", + "{{.Name}}", + ]; + const child = yield* spawnContainerCli(spawner, args, { + stdin: "ignore", + stdout: "pipe", + stderr: "pipe", + }).pipe( + Effect.mapError( + (cause) => + new LegacyDockerLifecycleListError({ + message: `failed to list volumes: ${legacyDescribeContainerCliFailure(cause)}`, + }), + ), + ); + // Concurrency is required, not cosmetic — see the matching comment in + // `legacyListContainersByLabel` above. + const [exitCode, stdout, stderr] = yield* Effect.all( + [ + child.exitCode.pipe(Effect.map(Number)), + collectByteStream(child.stdout), + collectByteStream(child.stderr), + ], + { concurrency: "unbounded" }, + ).pipe( + Effect.mapError( + () => new LegacyDockerLifecycleListError({ message: "failed to list volumes" }), + ), + ); + if (exitCode !== 0) { + const message = stderr.trim(); + return yield* Effect.fail( + new LegacyDockerLifecycleListError({ + message: + message.length > 0 ? `failed to list volumes: ${message}` : "failed to list volumes", + }), + ); + } + return splitNonEmptyLines(stdout); + }), + ); diff --git a/apps/cli/src/legacy/shared/legacy-docker-lifecycle.unit.test.ts b/apps/cli/src/legacy/shared/legacy-docker-lifecycle.unit.test.ts new file mode 100644 index 0000000000..0ecc41b372 --- /dev/null +++ b/apps/cli/src/legacy/shared/legacy-docker-lifecycle.unit.test.ts @@ -0,0 +1,344 @@ +import { describe, expect, it } from "@effect/vitest"; +import { Deferred, Effect, Sink, Stream } from "effect"; +import { ChildProcessSpawner } from "effect/unstable/process"; + +import { + LegacyDockerLifecycleInspectError, + LegacyDockerLifecycleListError, + legacyInspectContainerState, + legacyListContainersByLabel, + legacyListVolumesByLabel, +} from "./legacy-docker-lifecycle.ts"; + +function mockSpawner( + opts: { + readonly exitCode?: number; + readonly stdout?: string; + readonly stderr?: string; + } = {}, +) { + const encoder = new TextEncoder(); + const spawned: Array<{ readonly command: string; readonly args: ReadonlyArray }> = []; + + const spawner = ChildProcessSpawner.make((command) => + Effect.gen(function* () { + const cmd = command._tag === "StandardCommand" ? command.command : ""; + const args = command._tag === "StandardCommand" ? command.args : []; + spawned.push({ command: cmd, args }); + + const exitDeferred = yield* Deferred.make(); + yield* Deferred.succeed(exitDeferred, ChildProcessSpawner.ExitCode(opts.exitCode ?? 0)); + + return ChildProcessSpawner.makeHandle({ + pid: ChildProcessSpawner.ProcessId(1), + stdout: Stream.fromIterable(opts.stdout !== undefined ? [encoder.encode(opts.stdout)] : []), + stderr: Stream.fromIterable(opts.stderr !== undefined ? [encoder.encode(opts.stderr)] : []), + all: Stream.empty, + exitCode: Deferred.await(exitDeferred), + isRunning: Effect.succeed(false), + stdin: Sink.drain, + kill: () => Effect.void, + unref: Effect.succeed(Effect.void), + getInputFd: () => Sink.drain, + getOutputFd: () => Stream.empty, + }); + }), + ); + + return { + spawner, + get spawned() { + return spawned; + }, + }; +} + +describe("legacyListContainersByLabel", () => { + it.live("returns container ids for a successful listing", () => { + const mock = mockSpawner({ stdout: "abc123\ndef456\n" }); + return legacyListContainersByLabel(mock.spawner, { + projectIdFilter: "com.supabase.cli.project=my-app", + all: false, + format: "id", + }).pipe( + Effect.map((ids) => { + expect(ids).toEqual(["abc123", "def456"]); + expect(mock.spawned).toEqual([ + { + command: "docker", + args: [ + "ps", + "--filter", + "label=com.supabase.cli.project=my-app", + "--format", + "{{.ID}}", + ], + }, + ]); + }), + ); + }); + + it.live("passes --all and requests names when configured", () => { + const mock = mockSpawner({ stdout: "supabase_db_my-app\n" }); + return legacyListContainersByLabel(mock.spawner, { + projectIdFilter: "com.supabase.cli.project", + all: true, + format: "names", + }).pipe( + Effect.map((names) => { + expect(names).toEqual(["supabase_db_my-app"]); + expect(mock.spawned).toEqual([ + { + command: "docker", + args: [ + "ps", + "--filter", + "label=com.supabase.cli.project", + "--all", + "--format", + "{{.Names}}", + ], + }, + ]); + }), + ); + }); + + it.live("returns an empty array when no containers match", () => { + const mock = mockSpawner({ stdout: "" }); + return legacyListContainersByLabel(mock.spawner, { + projectIdFilter: "com.supabase.cli.project", + all: true, + format: "id", + }).pipe( + Effect.map((ids) => { + expect(ids).toEqual([]); + }), + ); + }); + + it.live("filters out blank lines from the trimmed output", () => { + const mock = mockSpawner({ stdout: "abc123\n\n \ndef456\n" }); + return legacyListContainersByLabel(mock.spawner, { + projectIdFilter: "com.supabase.cli.project", + all: false, + format: "id", + }).pipe( + Effect.map((ids) => { + expect(ids).toEqual(["abc123", "def456"]); + }), + ); + }); + + it.live("fails with LegacyDockerLifecycleListError on a non-zero exit", () => { + const mock = mockSpawner({ exitCode: 1, stderr: "Cannot connect to the Docker daemon\n" }); + return legacyListContainersByLabel(mock.spawner, { + projectIdFilter: "com.supabase.cli.project", + all: false, + format: "id", + }).pipe( + Effect.flip, + Effect.map((error) => { + expect(error).toBeInstanceOf(LegacyDockerLifecycleListError); + expect(error.message).toBe( + "failed to list containers: Cannot connect to the Docker daemon", + ); + }), + ); + }); + + it.live("fails with a generic message when stderr is empty", () => { + const mock = mockSpawner({ exitCode: 1, stderr: "" }); + return legacyListContainersByLabel(mock.spawner, { + projectIdFilter: "com.supabase.cli.project", + all: false, + format: "id", + }).pipe( + Effect.flip, + Effect.map((error) => { + expect(error).toBeInstanceOf(LegacyDockerLifecycleListError); + expect(error.message).toBe("failed to list containers"); + }), + ); + }); +}); + +describe("legacyInspectContainerState", () => { + it.live("parses a running, healthy container's state", () => { + const mock = mockSpawner({ + stdout: JSON.stringify({ + Status: "running", + Running: true, + Health: { Status: "healthy" }, + }), + }); + return legacyInspectContainerState(mock.spawner, "supabase_db_my-app").pipe( + Effect.map((state) => { + expect(state).toEqual({ running: true, status: "running", health: "healthy" }); + expect(mock.spawned).toEqual([ + { + command: "docker", + args: ["container", "inspect", "supabase_db_my-app", "--format", "{{json .State}}"], + }, + ]); + }), + ); + }); + + it.live("parses a running container with no health check configured", () => { + const mock = mockSpawner({ stdout: JSON.stringify({ Status: "running", Running: true }) }); + return legacyInspectContainerState(mock.spawner, "supabase_kong_my-app").pipe( + Effect.map((state) => { + expect(state).toEqual({ running: true, status: "running" }); + }), + ); + }); + + it.live("parses a stopped/exited container", () => { + const mock = mockSpawner({ stdout: JSON.stringify({ Status: "exited", Running: false }) }); + return legacyInspectContainerState(mock.spawner, "supabase_kong_my-app").pipe( + Effect.map((state) => { + expect(state).toEqual({ running: false, status: "exited" }); + }), + ); + }); + + it.live( + "treats a paused/restarting container as running, matching Go's boolean-based gate", + () => { + // Go's `assertContainerHealthy` (`status.go:150`) checks `resp.State.Running`, + // not `resp.State.Status` — a paused or restarting container reports + // `Running: true` alongside a non-"running" status string, and Go + // continues past the not-running branch in that case. + const mock = mockSpawner({ stdout: JSON.stringify({ Status: "paused", Running: true }) }); + return legacyInspectContainerState(mock.spawner, "supabase_db_my-app").pipe( + Effect.map((state) => { + expect(state).toEqual({ running: true, status: "paused" }); + }), + ); + }, + ); + + it.live( + "fails with LegacyDockerLifecycleInspectError, preserving the real stderr, when the container does not exist", + () => { + // Go's `assertContainerHealthy` never special-cases "not found" — it + // wraps whatever `ContainerInspect` returns (`status.go:148-149`), so a + // missing container is just another non-zero exit here too. + const mock = mockSpawner({ + exitCode: 1, + stderr: "Error response from daemon: No such container: supabase_db_my-app\n", + }); + return legacyInspectContainerState(mock.spawner, "supabase_db_my-app").pipe( + Effect.flip, + Effect.map((error) => { + expect(error).toBeInstanceOf(LegacyDockerLifecycleInspectError); + expect(error.message).toBe( + "failed to inspect container health: Error response from daemon: No such container: supabase_db_my-app", + ); + }), + ); + }, + ); + + it.live("fails with LegacyDockerLifecycleInspectError on any other inspect failure", () => { + const mock = mockSpawner({ exitCode: 1, stderr: "Cannot connect to the Docker daemon\n" }); + return legacyInspectContainerState(mock.spawner, "supabase_db_my-app").pipe( + Effect.flip, + Effect.map((error) => { + expect(error).toBeInstanceOf(LegacyDockerLifecycleInspectError); + expect(error.message).toBe( + "failed to inspect container health: Cannot connect to the Docker daemon", + ); + }), + ); + }); + + it.live( + "fails with LegacyDockerLifecycleInspectError with a generic message when stderr is empty", + () => { + const mock = mockSpawner({ exitCode: 1, stderr: "" }); + return legacyInspectContainerState(mock.spawner, "supabase_db_my-app").pipe( + Effect.flip, + Effect.map((error) => { + expect(error).toBeInstanceOf(LegacyDockerLifecycleInspectError); + expect(error.message).toBe("failed to inspect container health"); + }), + ); + }, + ); + + it.live("treats empty inspect output as an unknown, not-running state", () => { + const mock = mockSpawner({ stdout: "" }); + return legacyInspectContainerState(mock.spawner, "supabase_db_my-app").pipe( + Effect.map((state) => { + expect(state).toEqual({ running: false, status: "" }); + }), + ); + }); + + it.live("treats non-object inspect JSON as an unknown, not-running state", () => { + const mock = mockSpawner({ stdout: "null" }); + return legacyInspectContainerState(mock.spawner, "supabase_db_my-app").pipe( + Effect.map((state) => { + expect(state).toEqual({ running: false, status: "" }); + }), + ); + }); +}); + +describe("legacyListVolumesByLabel", () => { + it.live("returns volume names for a successful listing", () => { + const mock = mockSpawner({ stdout: "supabase_db_my-app\n" }); + return legacyListVolumesByLabel(mock.spawner, "com.supabase.cli.project=my-app").pipe( + Effect.map((names) => { + expect(names).toEqual(["supabase_db_my-app"]); + expect(mock.spawned).toEqual([ + { + command: "docker", + args: [ + "volume", + "ls", + "--filter", + "label=com.supabase.cli.project=my-app", + "--format", + "{{.Name}}", + ], + }, + ]); + }), + ); + }); + + it.live("returns an empty array when no volumes remain", () => { + const mock = mockSpawner({ stdout: "" }); + return legacyListVolumesByLabel(mock.spawner, "com.supabase.cli.project").pipe( + Effect.map((names) => { + expect(names).toEqual([]); + }), + ); + }); + + it.live("fails with LegacyDockerLifecycleListError on a non-zero exit", () => { + const mock = mockSpawner({ exitCode: 1, stderr: "boom\n" }); + return legacyListVolumesByLabel(mock.spawner, "com.supabase.cli.project").pipe( + Effect.flip, + Effect.map((error) => { + expect(error).toBeInstanceOf(LegacyDockerLifecycleListError); + expect(error.message).toBe("failed to list volumes: boom"); + }), + ); + }); + + it.live("fails with a generic message when stderr is empty", () => { + const mock = mockSpawner({ exitCode: 1, stderr: "" }); + return legacyListVolumesByLabel(mock.spawner, "com.supabase.cli.project").pipe( + Effect.flip, + Effect.map((error) => { + expect(error).toBeInstanceOf(LegacyDockerLifecycleListError); + expect(error.message).toBe("failed to list volumes"); + }), + ); + }); +}); diff --git a/apps/cli/src/legacy/shared/legacy-go-jwt.ts b/apps/cli/src/legacy/shared/legacy-go-jwt.ts new file mode 100644 index 0000000000..a7b2768edf --- /dev/null +++ b/apps/cli/src/legacy/shared/legacy-go-jwt.ts @@ -0,0 +1,197 @@ +import { createHmac, createPrivateKey, createSign } from "node:crypto"; + +/** + * RFC 7517 JWK fields Go's `JWK` struct round-trips (`pkg/config/auth.go:88-108`, + * `toml`/`json` tags `kty`, `kid`, `alg`, `n`, `e`, `d`, `p`, `q`, `dp`, `dq`, + * `qi`, `crv`, `x`, `y`) — field names match exactly, so a signing-keys file can + * be parsed straight into this shape. A superset of Node's own + * `crypto.webcrypto.JsonWebKey` (which omits `kid`), so it's still assignable + * wherever that type is expected (e.g. `createPrivateKey`'s `format: "jwk"` input). + */ +export interface LegacyJwk { + readonly kty: string; + readonly kid?: string; + readonly alg?: string; + readonly n?: string; + readonly e?: string; + readonly d?: string; + readonly p?: string; + readonly q?: string; + readonly dp?: string; + readonly dq?: string; + readonly qi?: string; + readonly crv?: string; + readonly x?: string; + readonly y?: string; +} + +/** + * Go-byte-exact HS256 signer for the default local-dev `anon`/`service_role` + * keys, ported from `CustomClaims`/`generateJWT` (`apps/cli-go/pkg/config/apikeys.go:23-40,75-86`). + * {@link legacyGenerateAsymmetricGoJwt} below covers the RS256/ES256 branch of + * the same Go function, taken when `auth.signing_keys_path` is configured. + * + * This intentionally does NOT reuse `@supabase/stack`'s `generateJwt` + * (`packages/stack/src/JwtGenerator.ts`) — that helper uses `iss:"supabase"`, + * a dynamic `iat`/10-year `exp`, and a different claim order, none of which + * byte-match what Go prints for `supabase status`. Go's claims, in + * declaration order (the outer `CustomClaims.Issuer` field shadows the + * embedded `jwt.RegisteredClaims.Issuer`, so only one `iss` key is emitted): + * + * iss (fixed "supabase-demo"), ref (omitempty), role, is_anonymous (omitempty), + * then the remaining `jwt.RegisteredClaims` fields (sub, aud, exp, nbf, iat, jti), + * all `omitempty` except `exp`, which Go always sets to the fixed + * `defaultJwtExpiry = 1983812996` unix timestamp (never computed from "now"). + * + * `status` never sets `ref`/`is_anonymous`, so for this signer's two roles the + * payload always serializes to exactly `{"iss":...,"role":...,"exp":...}`. + */ + +const GO_JWT_ISSUER = "supabase-demo"; +const GO_JWT_FIXED_EXP = 1983812996; + +function base64UrlEncode(input: string): string { + return Buffer.from(input).toString("base64url"); +} + +export function legacyGenerateGoJwt(secret: string, role: "anon" | "service_role"): string { + const header = base64UrlEncode(JSON.stringify({ alg: "HS256", typ: "JWT" })); + const payload = base64UrlEncode( + JSON.stringify({ iss: GO_JWT_ISSUER, role, exp: GO_JWT_FIXED_EXP }), + ); + const data = `${header}.${payload}`; + const signature = createHmac("sha256", secret).update(data).digest("base64url"); + return `${data}.${signature}`; +} + +/** Go's asymmetric-JWT expiry: `time.Now().Add(time.Hour * 24 * 365 * 10)` (10 years). */ +const GO_JWT_ASYMMETRIC_EXPIRY_SECONDS = 60 * 60 * 24 * 365 * 10; + +function base64UrlToBigInt(value: string): bigint { + const hex = Buffer.from(value, "base64url").toString("hex"); + return hex.length === 0 ? 0n : BigInt(`0x${hex}`); +} + +function bigIntToBase64Url(value: bigint): string { + let hex = value.toString(16); + if (hex.length % 2 === 1) hex = `0${hex}`; + return Buffer.from(hex, "hex").toString("base64url"); +} + +/** Modular inverse of `a` mod `m` via the extended Euclidean algorithm (`a`/`m` coprime, as `q`/`p` always are for a valid RSA key). */ +function modInverse(a: bigint, m: bigint): bigint { + let [oldR, r] = [a, m]; + let [oldS, s] = [1n, 0n]; + while (r !== 0n) { + const quotient = oldR / r; + [oldR, r] = [r, oldR - quotient * r]; + [oldS, s] = [s, oldS - quotient * s]; + } + return ((oldS % m) + m) % m; +} + +/** + * Backfills the RSA CRT parameters (`dp`, `dq`, `qi`) Go's `jwkToRSAPrivateKey` + * (`apps/cli-go/pkg/config/apikeys.go:132-168`) never reads — it constructs + * `rsa.PrivateKey{N, E, D, Primes: [p, q]}` from `n`/`e`/`d`/`p`/`q` alone, and + * Go's stdlib `crypto/rsa` (`SignPKCS1v15` -> `precompute()`) lazily derives + * `Dp`/`Dq`/`Qinv` from `p`/`q`/`d` itself when they're absent, so a JWK + * missing them still signs successfully in Go. Node's + * `createPrivateKey({ format: "jwk" })` has no such fallback — it hard-rejects + * an RSA JWK without `dp`/`dq`/`qi` (`The "key.dp" property must be of type + * string`) — so this reproduces Go's derivation before handing the key to + * Node: `dp = d mod (p-1)`, `dq = d mod (q-1)`, `qi = q^-1 mod p` (RFC 7517 + * section 6.3.2 / RFC 3447 section 3.2). A key that already has all three (the common case + * for a Node/openssl-generated JWK) is returned unchanged; one missing + * `d`/`p`/`q` themselves is also returned unchanged — that's a genuinely + * invalid key in Go too, and `createPrivateKey` will raise its own error. + */ +function ensureRsaCrtParams(jwk: LegacyJwk): LegacyJwk { + if (jwk.dp !== undefined && jwk.dq !== undefined && jwk.qi !== undefined) { + return jwk; + } + if (jwk.d === undefined || jwk.p === undefined || jwk.q === undefined) { + return jwk; + } + const d = base64UrlToBigInt(jwk.d); + const p = base64UrlToBigInt(jwk.p); + const q = base64UrlToBigInt(jwk.q); + return { + ...jwk, + dp: bigIntToBase64Url(d % (p - 1n)), + dq: bigIntToBase64Url(d % (q - 1n)), + qi: bigIntToBase64Url(modInverse(q, p)), + }; +} + +/** + * Go's `GenerateAsymmetricJWT` (`pkg/config/apikeys.go:88-113`), reached from + * `generateJWT` only when `auth.signing_keys_path` resolves to a non-empty JWK + * array (`pkg/config/apikeys.go:76-80`) — the first key in the file signs both + * the anon and service_role tokens. Same claim shape as {@link legacyGenerateGoJwt} + * (`iss`/`role`/`exp`), except the expiry is 10 years from now rather than Go's + * fixed HMAC-path timestamp, since `generateJWT` sets `claims.ExpiresAt` + * explicitly before calling this function instead of falling through to + * `CustomClaims.NewToken()`'s fixed default. + * + * Only `RS256`/`ES256` are supported, matching Go's `jwkToPrivateKey` + * (RSA/EC key types) + this function's own switch on `jwk.alg`. `kty`/`alg` + * are cross-validated (RS256 requires `kty: "RSA"`, ES256 requires + * `kty: "EC"` and `crv: "P-256"`) — matching Go's `jwkToRSAPrivateKey` / + * `jwkToECDSAPrivateKey`, which reject any other combination rather than + * signing with a mismatched key or curve (Node's `createPrivateKey`/`createSign` + * do not themselves catch this: an EC key signed as RS256, or a non-P-256 + * curve signed as ES256, both "succeed" and produce a spec-invalid token that + * silently fails verification instead of raising an error). The header key + * order (`alg`, `kid`, `typ`) matches Go's `encoding/json` alphabetically + * sorting `map[string]interface{}` keys — `kid` is only present when set on + * the JWK, matching Go's `if len(jwk.KeyID) > 0` guard. + * + * `dsaEncoding: "ieee-p1363"` is required for ES256: Node's default ECDSA + * signature output is DER-encoded, which is not the raw (r‖s) format JWS + * requires — verified by round-tripping through `jose`'s `jwtVerify`. + */ +export function legacyGenerateAsymmetricGoJwt( + jwk: LegacyJwk, + role: "anon" | "service_role", +): string { + const algorithm = jwk.alg; + if (algorithm !== "RS256" && algorithm !== "ES256") { + throw new Error(`unsupported algorithm: ${algorithm ?? ""}`); + } + if (algorithm === "RS256" && jwk.kty !== "RSA") { + throw new Error(`unsupported key type: ${jwk.kty}`); + } + if (algorithm === "ES256") { + if (jwk.kty !== "EC") { + throw new Error(`unsupported key type: ${jwk.kty}`); + } + if (jwk.crv !== "P-256") { + throw new Error(`unsupported curve: ${jwk.crv ?? ""}`); + } + } + const header = + jwk.kid !== undefined && jwk.kid.length > 0 + ? { alg: algorithm, kid: jwk.kid, typ: "JWT" } + : { alg: algorithm, typ: "JWT" }; + const expiresAt = Math.floor(Date.now() / 1000) + GO_JWT_ASYMMETRIC_EXPIRY_SECONDS; + const headerEncoded = base64UrlEncode(JSON.stringify(header)); + const payloadEncoded = base64UrlEncode( + JSON.stringify({ iss: GO_JWT_ISSUER, role, exp: expiresAt }), + ); + const data = `${headerEncoded}.${payloadEncoded}`; + + const privateKey = createPrivateKey({ + key: algorithm === "RS256" ? ensureRsaCrtParams(jwk) : jwk, + format: "jwk", + }); + const signature = + algorithm === "RS256" + ? createSign("RSA-SHA256").update(data).end().sign(privateKey) + : createSign("sha256") + .update(data) + .end() + .sign({ key: privateKey, dsaEncoding: "ieee-p1363" }); + + return `${data}.${signature.toString("base64url")}`; +} diff --git a/apps/cli/src/legacy/shared/legacy-go-jwt.unit.test.ts b/apps/cli/src/legacy/shared/legacy-go-jwt.unit.test.ts new file mode 100644 index 0000000000..5bfeabfe70 --- /dev/null +++ b/apps/cli/src/legacy/shared/legacy-go-jwt.unit.test.ts @@ -0,0 +1,179 @@ +import { createHmac, generateKeyPairSync } from "node:crypto"; +import { importJWK, jwtVerify } from "jose"; +import { describe, expect, it } from "vitest"; + +import { + legacyGenerateAsymmetricGoJwt, + legacyGenerateGoJwt, + type LegacyJwk, +} from "./legacy-go-jwt.ts"; + +const SECRET = "super-secret-jwt-token-with-at-least-32-characters-long"; + +function generateRsaJwk(kid?: string): LegacyJwk { + const { privateKey } = generateKeyPairSync("rsa", { modulusLength: 2048 }); + const jwk = privateKey.export({ format: "jwk" }); + return { ...jwk, kty: "RSA", alg: "RS256", kid }; +} + +function generateEcJwk(kid?: string): LegacyJwk { + const { privateKey } = generateKeyPairSync("ec", { namedCurve: "P-256" }); + const jwk = privateKey.export({ format: "jwk" }); + return { ...jwk, kty: "EC", alg: "ES256", kid }; +} + +function publicJwkOf(jwk: LegacyJwk): LegacyJwk { + const { d: _d, p: _p, q: _q, dp: _dp, dq: _dq, qi: _qi, ...publicJwk } = jwk; + return publicJwk; +} + +function decodeSegment(segment: string): string { + return Buffer.from(segment, "base64url").toString("utf8"); +} + +describe("legacyGenerateGoJwt", () => { + it("emits Go's exact JWT header (no extra fields, alg before typ)", () => { + const token = legacyGenerateGoJwt(SECRET, "anon"); + const [header] = token.split("."); + expect(header).toBeDefined(); + // Go's jwt.NewWithClaims builds Header as map[string]any{"typ":..,"alg":..}; + // encoding/json marshals map keys in sorted order, so "alg" sorts before "typ". + expect(decodeSegment(header ?? "")).toBe('{"alg":"HS256","typ":"JWT"}'); + }); + + it("emits the anon payload with Go's exact key order and fixed claims", () => { + const token = legacyGenerateGoJwt(SECRET, "anon"); + const [, payload] = token.split("."); + expect(payload).toBeDefined(); + const raw = decodeSegment(payload ?? ""); + // Byte-exact key order: iss, role, exp — ref/is_anonymous/iat are omitted + // entirely (Go's `omitempty`), matching status's no-ref, non-anonymous use. + expect(raw).toBe('{"iss":"supabase-demo","role":"anon","exp":1983812996}'); + + const parsed = JSON.parse(raw) as Record; + expect(parsed).toEqual({ iss: "supabase-demo", role: "anon", exp: 1983812996 }); + expect(Object.keys(parsed)).not.toContain("iat"); + expect(Object.keys(parsed)).not.toContain("ref"); + expect(Object.keys(parsed)).not.toContain("is_anonymous"); + }); + + it("emits the service_role payload with Go's exact key order and fixed claims", () => { + const token = legacyGenerateGoJwt(SECRET, "service_role"); + const [, payload] = token.split("."); + const raw = decodeSegment(payload ?? ""); + expect(raw).toBe('{"iss":"supabase-demo","role":"service_role","exp":1983812996}'); + }); + + it("signs with plain HMAC-SHA256 over the base64url header.payload, base64url-encoded", () => { + const token = legacyGenerateGoJwt(SECRET, "anon"); + const [header, payload, signature] = token.split("."); + const expectedSignature = createHmac("sha256", SECRET) + .update(`${header}.${payload}`) + .digest("base64url"); + expect(signature).toBe(expectedSignature); + }); + + it("is deterministic across calls (no timestamp derived from Date.now())", () => { + const first = legacyGenerateGoJwt(SECRET, "anon"); + const second = legacyGenerateGoJwt(SECRET, "anon"); + expect(first).toBe(second); + }); + + it("produces different tokens for different secrets", () => { + const a = legacyGenerateGoJwt(SECRET, "anon"); + const b = legacyGenerateGoJwt("a-different-secret-value-1234567", "anon"); + expect(a).not.toBe(b); + }); +}); + +describe("legacyGenerateAsymmetricGoJwt", () => { + it("signs and verifies an RS256 token from an RSA JWK", async () => { + const jwk = generateRsaJwk("rsa-kid"); + const token = legacyGenerateAsymmetricGoJwt(jwk, "anon"); + const publicKey = await importJWK(publicJwkOf(jwk), "RS256"); + const { payload, protectedHeader } = await jwtVerify(token, publicKey); + expect(payload).toMatchObject({ iss: "supabase-demo", role: "anon" }); + expect(protectedHeader).toEqual({ alg: "RS256", kid: "rsa-kid", typ: "JWT" }); + }); + + it("signs an RS256 token from an RSA JWK missing CRT exponents (dp/dq/qi), matching Go", async () => { + // Go's `jwkToRSAPrivateKey` (`apps/cli-go/pkg/config/apikeys.go:132-168`) + // never reads `dp`/`dq`/`qi` — it builds the key from `n`/`e`/`d`/`p`/`q` + // alone, and Go's stdlib derives the CRT params itself when absent. A + // hand-authored signing-keys file that omits them (common — RFC 7517 marks + // them optional) must still sign successfully here. + const jwk = generateRsaJwk("rsa-kid"); + const { dp: _dp, dq: _dq, qi: _qi, ...jwkWithoutCrtParams } = jwk; + const token = legacyGenerateAsymmetricGoJwt(jwkWithoutCrtParams, "anon"); + const publicKey = await importJWK(publicJwkOf(jwk), "RS256"); + const { payload, protectedHeader } = await jwtVerify(token, publicKey); + expect(payload).toMatchObject({ iss: "supabase-demo", role: "anon" }); + expect(protectedHeader).toEqual({ alg: "RS256", kid: "rsa-kid", typ: "JWT" }); + }); + + it("signs and verifies an ES256 token from an EC JWK", async () => { + const jwk = generateEcJwk("ec-kid"); + const token = legacyGenerateAsymmetricGoJwt(jwk, "service_role"); + const publicKey = await importJWK(publicJwkOf(jwk), "ES256"); + const { payload, protectedHeader } = await jwtVerify(token, publicKey); + expect(payload).toMatchObject({ iss: "supabase-demo", role: "service_role" }); + expect(protectedHeader).toEqual({ alg: "ES256", kid: "ec-kid", typ: "JWT" }); + }); + + it("omits the kid header entirely when the JWK has no kid", () => { + const jwk = generateRsaJwk(); + const token = legacyGenerateAsymmetricGoJwt(jwk, "anon"); + const [header] = token.split("."); + const decoded = JSON.parse(Buffer.from(header ?? "", "base64url").toString()); + expect(decoded).toEqual({ alg: "RS256", typ: "JWT" }); + }); + + it("sets a ~10-year expiry computed from the current time, not a fixed timestamp", () => { + const jwk = generateRsaJwk(); + const before = Math.floor(Date.now() / 1000); + const token = legacyGenerateAsymmetricGoJwt(jwk, "anon"); + const [, payload] = token.split("."); + const decoded = JSON.parse(Buffer.from(payload ?? "", "base64url").toString()); + const tenYearsSeconds = 60 * 60 * 24 * 365 * 10; + expect(decoded.exp).toBeGreaterThanOrEqual(before + tenYearsSeconds); + expect(decoded.exp).toBeLessThan(before + tenYearsSeconds + 10); + }); + + it("rejects an unsupported algorithm", () => { + const jwk = { ...generateRsaJwk(), alg: "RS512" }; + expect(() => legacyGenerateAsymmetricGoJwt(jwk, "anon")).toThrow( + "unsupported algorithm: RS512", + ); + }); + + it("rejects a JWK with no algorithm", () => { + const { alg: _alg, ...jwkWithoutAlg } = generateRsaJwk(); + expect(() => legacyGenerateAsymmetricGoJwt(jwkWithoutAlg, "anon")).toThrow( + "unsupported algorithm: ", + ); + }); + + it("rejects an EC key forged with alg: RS256 instead of signing garbage", () => { + const jwk = { ...generateEcJwk(), alg: "RS256" }; + expect(() => legacyGenerateAsymmetricGoJwt(jwk, "anon")).toThrow("unsupported key type: EC"); + }); + + it("rejects an RSA key forged with alg: ES256 instead of signing garbage", () => { + const jwk = { ...generateRsaJwk(), alg: "ES256" }; + expect(() => legacyGenerateAsymmetricGoJwt(jwk, "anon")).toThrow("unsupported key type: RSA"); + }); + + it("rejects an ES256 EC key whose curve is not P-256", () => { + const { privateKey } = generateKeyPairSync("ec", { namedCurve: "P-384" }); + const jwk = { ...privateKey.export({ format: "jwk" }), kty: "EC", alg: "ES256" }; + expect(() => legacyGenerateAsymmetricGoJwt(jwk, "anon")).toThrow("unsupported curve: P-384"); + }); + + it("rejects an ES256 EC key with no curve at all", () => { + const jwk = generateEcJwk(); + const { crv: _crv, ...jwkWithoutCurve } = jwk; + expect(() => legacyGenerateAsymmetricGoJwt(jwkWithoutCurve, "anon")).toThrow( + "unsupported curve: ", + ); + }); +}); diff --git a/apps/cli/src/legacy/shared/legacy-hostname.ts b/apps/cli/src/legacy/shared/legacy-hostname.ts index 087363c20b..d2c6d719cd 100644 --- a/apps/cli/src/legacy/shared/legacy-hostname.ts +++ b/apps/cli/src/legacy/shared/legacy-hostname.ts @@ -1,17 +1,128 @@ +import { createHash } from "node:crypto"; +import { readFileSync } from "node:fs"; +import { homedir } from "node:os"; +import { join } from "node:path"; + const LOCAL_HOST = "127.0.0.1"; +/** Docker CLI's reserved "no context store entry" name (`docker/cli` `cli/command/cli.go`'s `DefaultContextName`). */ +const DEFAULT_CONTEXT_NAME = "default"; + +/** + * Docker CLI's config directory: `$DOCKER_CONFIG` or `~/.docker` + * (`docker/cli` `cliconfig.Dir()`), read from here rather than + * `client.Client`'s own resolution since this module never spawns a real + * Docker client — it only needs the same two on-disk files that resolution + * reads. + */ +function dockerConfigDir(): string { + const override = process.env["DOCKER_CONFIG"]; + return override !== undefined && override.length > 0 ? override : join(homedir(), ".docker"); +} + +/** + * Go's `cli.CurrentContext()` name resolution (`docker/cli` + * `cli/command/cli.go`'s `resolveContextName`): `DOCKER_CONTEXT` env, else the + * config file's `currentContext`, else `"default"`. Only reached when + * `DOCKER_HOST` is unset — `resolveContextName` itself forces `"default"` + * when `DOCKER_HOST`/`--host` is set, which {@link legacyGetHostname} below + * already handles as its own, earlier branch. + */ +function currentDockerContextName(): string { + const fromEnv = process.env["DOCKER_CONTEXT"]; + if (fromEnv !== undefined && fromEnv.length > 0) { + return fromEnv; + } + try { + const config = JSON.parse(readFileSync(join(dockerConfigDir(), "config.json"), "utf8")) as { + currentContext?: unknown; + }; + if (typeof config.currentContext === "string" && config.currentContext.length > 0) { + return config.currentContext; + } + } catch { + // Missing/malformed config.json → the default context, same as Go's own + // silent fallback when it can't load the config file here. + } + return DEFAULT_CONTEXT_NAME; +} + +/** + * Reads a non-default context's daemon endpoint from Docker CLI's context + * store: `/contexts/meta//meta.json`'s + * `Endpoints.docker.Host` (`docker/cli` `cli/context/store/metadatastore.go`). + * The `"default"` context has no store entry (it's Go's synthetic + * always-available context, resolved without a client, see + * `cli.Initialize`), so it's never looked up here — matching the earlier + * `"default"` short-circuit in {@link currentDockerContextName}'s caller. + */ +function dockerContextEndpointHost(contextName: string): string | undefined { + if (contextName === DEFAULT_CONTEXT_NAME) { + return undefined; + } + try { + const contextId = createHash("sha256").update(contextName).digest("hex"); + const metaPath = join(dockerConfigDir(), "contexts", "meta", contextId, "meta.json"); + const meta = JSON.parse(readFileSync(metaPath, "utf8")) as { + readonly Endpoints?: { readonly docker?: { readonly Host?: unknown } }; + }; + const host = meta.Endpoints?.docker?.Host; + return typeof host === "string" && host.length > 0 ? host : undefined; + } catch { + // Missing/malformed context store entry → treat as unresolvable, same as + // Go silently falling back to the loopback default below. + return undefined; + } +} + +/** + * Extracts the bare host from a `tcp://host:port` daemon endpoint, mirroring + * Go's `client.ParseHostURL` + `net.SplitHostPort` (`misc.go:307`). Returns + * `undefined` for a non-`tcp://` endpoint (e.g. `unix://`, `npipe://`) or an + * unparseable one, in which case the caller falls back to the loopback + * default, matching Go's `net.SplitHostPort` failure/non-TCP handling. + */ +function hostFromTcpEndpoint(endpoint: string): string | undefined { + try { + const url = new URL(endpoint); + if (url.protocol !== "tcp:" || url.hostname.length === 0) { + return undefined; + } + // WHATWG `URL.hostname` returns an IPv6 host bracketed (`[::1]`), but Go's + // `net.SplitHostPort` (`misc.go:307`) returns the bare host (`::1`). Strip a + // single surrounding bracket pair so local-stack probes dial/compare the + // same host Go does; IPv4 and named hosts are returned unchanged. + const host = url.hostname; + return host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host; + } catch { + return undefined; + } +} + /** * Resolves the hostname used for local Supabase service connections, mirroring - * Go's `utils.GetHostname` (`apps/cli-go/internal/utils/misc.go:298`): + * Go's `utils.GetHostname` (`apps/cli-go/internal/utils/misc.go:298-311`): * * 1. `SUPABASE_SERVICES_HOSTNAME` env override — set in dev containers or when * the Docker daemon is not reachable on the container's own loopback. * 2. The Docker daemon host when `DOCKER_HOST` is a `tcp://host:port` endpoint * (Go's `Docker.DaemonHost()` + `client.ParseHostURL` + `net.SplitHostPort`). - * 3. `127.0.0.1` otherwise (the default unix-socket daemon). + * 3. Otherwise, the ACTIVE DOCKER CONTEXT's daemon endpoint, when it's a + * `tcp://` one — Go's `Docker.DaemonHost()` comes from a client built via + * `command.NewDockerCli()` + `cli.Initialize()` (`apps/cli-go/internal/ + * utils/docker.go:41-54`), whose endpoint resolution walks `DOCKER_HOST` -> + * `DOCKER_CONTEXT` -> the config file's `currentContext` -> the context + * store (`docker/cli` `cli/command/cli.go`'s `getDockerEndPoint`/ + * `resolveContextName`) — not just `DOCKER_HOST`. The `docker`/`podman` + * binary this module's callers shell out to for `ps`/`inspect` already + * resolves the same active context itself, so without this step `status` + * could correctly inspect a remote daemon while printing unusable + * `127.0.0.1` API/DB/Studio URLs for it. + * 4. `127.0.0.1` otherwise (the default unix-socket daemon, or an + * unresolvable/malformed context). * * Shared across legacy commands that connect to the local stack (`gen types`, - * `test db`, and later `db reset` / `db dump`). + * `test db`, `status`, `stop`, and later `db reset` / `db dump`). */ export function legacyGetHostname(): string { const override = process.env["SUPABASE_SERVICES_HOSTNAME"]; @@ -20,18 +131,13 @@ export function legacyGetHostname(): string { } const dockerHost = process.env["DOCKER_HOST"]; if (dockerHost !== undefined && dockerHost.length > 0) { - try { - const url = new URL(dockerHost); - if (url.protocol === "tcp:" && url.hostname.length > 0) { - // WHATWG `URL.hostname` returns an IPv6 host bracketed (`[::1]`), but Go's - // `net.SplitHostPort` (`misc.go:307`) returns the bare host (`::1`). Strip a - // single surrounding bracket pair so local-stack probes dial/compare the - // same host Go does; IPv4 and named hosts are returned unchanged. - const host = url.hostname; - return host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host; - } - } catch { - // Unparseable DOCKER_HOST → fall through to the loopback default. + return hostFromTcpEndpoint(dockerHost) ?? LOCAL_HOST; + } + const contextEndpoint = dockerContextEndpointHost(currentDockerContextName()); + if (contextEndpoint !== undefined) { + const host = hostFromTcpEndpoint(contextEndpoint); + if (host !== undefined) { + return host; } } return LOCAL_HOST; diff --git a/apps/cli/src/legacy/shared/legacy-hostname.unit.test.ts b/apps/cli/src/legacy/shared/legacy-hostname.unit.test.ts index 9706f5f002..0f1c351d0e 100644 --- a/apps/cli/src/legacy/shared/legacy-hostname.unit.test.ts +++ b/apps/cli/src/legacy/shared/legacy-hostname.unit.test.ts @@ -1,4 +1,8 @@ -import { describe, expect, it } from "vitest"; +import { createHash } from "node:crypto"; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; import { legacyGetHostname } from "./legacy-hostname.ts"; @@ -19,6 +23,30 @@ function withEnv(entries: Record, run: () => T): } } +/** Writes a Docker CLI-shaped `$DOCKER_CONFIG` directory (`config.json` + a context store entry). */ +function writeDockerConfigDir(options: { + readonly currentContext?: string; + readonly contexts?: Readonly>; // context name -> docker.Host endpoint +}): string { + const dir = mkdtempSync(join(tmpdir(), "legacy-hostname-docker-config-")); + if (options.currentContext !== undefined) { + writeFileSync( + join(dir, "config.json"), + JSON.stringify({ currentContext: options.currentContext }), + ); + } + for (const [name, host] of Object.entries(options.contexts ?? {})) { + const contextId = createHash("sha256").update(name).digest("hex"); + const metaDir = join(dir, "contexts", "meta", contextId); + mkdirSync(metaDir, { recursive: true }); + writeFileSync( + join(metaDir, "meta.json"), + JSON.stringify({ Endpoints: { docker: { Host: host } } }), + ); + } + return dir; +} + describe("legacyGetHostname", () => { it("prefers SUPABASE_SERVICES_HOSTNAME over everything else", () => { expect( @@ -63,4 +91,98 @@ describe("legacyGetHostname", () => { withEnv({ SUPABASE_SERVICES_HOSTNAME: undefined, DOCKER_HOST: undefined }, legacyGetHostname), ).toBe("127.0.0.1"); }); + + describe("active Docker context resolution (Go's Docker.DaemonHost() parity)", () => { + let configDirs: Array = []; + + afterEach(() => { + for (const dir of configDirs) rmSync(dir, { recursive: true, force: true }); + configDirs = []; + }); + + function withDockerConfig( + options: Parameters[0], + env: Record, + run: () => T, + ): T { + const dir = writeDockerConfigDir(options); + configDirs.push(dir); + return withEnv( + { + SUPABASE_SERVICES_HOSTNAME: undefined, + DOCKER_HOST: undefined, + DOCKER_CONFIG: dir, + ...env, + }, + run, + ); + } + + it("resolves the host from the active context's tcp:// endpoint via config.json's currentContext", () => { + const result = withDockerConfig( + { currentContext: "remote", contexts: { remote: "tcp://remote-host:2375" } }, + {}, + legacyGetHostname, + ); + expect(result).toBe("remote-host"); + }); + + it("prefers DOCKER_CONTEXT over config.json's currentContext", () => { + const result = withDockerConfig( + { + currentContext: "other", + contexts: { envctx: "tcp://envctx-host:2375", other: "tcp://other-host:2375" }, + }, + { DOCKER_CONTEXT: "envctx" }, + legacyGetHostname, + ); + expect(result).toBe("envctx-host"); + }); + + it("strips brackets from an IPv6 context endpoint (net.SplitHostPort parity)", () => { + const result = withDockerConfig( + { currentContext: "remote", contexts: { remote: "tcp://[::1]:2375" } }, + {}, + legacyGetHostname, + ); + expect(result).toBe("::1"); + }); + + it("falls back to 127.0.0.1 when the active context's endpoint is not tcp://", () => { + const result = withDockerConfig( + { currentContext: "remote", contexts: { remote: "unix:///var/run/docker.sock" } }, + {}, + legacyGetHostname, + ); + expect(result).toBe("127.0.0.1"); + }); + + it("falls back to 127.0.0.1 when the context store entry is missing", () => { + const result = withDockerConfig({ currentContext: "ghost" }, {}, legacyGetHostname); + expect(result).toBe("127.0.0.1"); + }); + + it("falls back to 127.0.0.1 when config.json is missing entirely (default context)", () => { + const result = withDockerConfig({}, {}, legacyGetHostname); + expect(result).toBe("127.0.0.1"); + }); + + it("never consults the context store for the default context", () => { + const result = withDockerConfig( + { currentContext: "default", contexts: { default: "tcp://should-never-be-read:2375" } }, + {}, + legacyGetHostname, + ); + expect(result).toBe("127.0.0.1"); + }); + + it("DOCKER_HOST still takes precedence over an active non-default context", () => { + const result = withDockerConfig( + { currentContext: "remote", contexts: { remote: "tcp://context-host:2375" } }, + { DOCKER_HOST: "tcp://direct-host:2375" }, + legacyGetHostname, + ); + expect(result).toBe("direct-host"); + }); + }); }); diff --git a/apps/cli/src/legacy/shared/legacy-local-config-values.ts b/apps/cli/src/legacy/shared/legacy-local-config-values.ts new file mode 100644 index 0000000000..a851d5a670 --- /dev/null +++ b/apps/cli/src/legacy/shared/legacy-local-config-values.ts @@ -0,0 +1,1573 @@ +import { readFileSync } from "node:fs"; +import { basename, join } from "node:path"; + +import { ENV_CAPTURE_REGEX, type ProjectConfig } from "@supabase/config"; +import { defaultJwtSecret, defaultPublishableKey, defaultSecretKey } from "@supabase/stack/effect"; +import { Schema } from "effect"; + +import { legacyResolveApiExternalUrl } from "./legacy-api-url.ts"; +import { legacySanitizeProjectId } from "./legacy-docker-ids.ts"; +import { + legacyApiTlsCertReadErrorMessage, + legacyApiTlsKeyReadErrorMessage, + type LegacyAnalyticsInput, + type LegacyApiInput, + type LegacyAuthInput, + type LegacyCaptchaInput, + LegacyConfigValidateError, + type LegacyConfigValidationInput, + type LegacyDbInput, + legacyEmailContentPathReadErrorMessage, + type LegacyExperimentalInput, + type LegacyHookInput, + type LegacyLocalSmtpInput, + type LegacyMfaFactorInput, + legacyParseGoBool, + type LegacyPasskeyInput, + legacyResolveApiTlsPath, + legacyResolveEmailTemplateContentPath, + legacyResolveSigningKeysPath, + legacySigningKeysDecodeErrorMessage, + legacySigningKeysReadErrorMessage, + type LegacySmtpInput, + type LegacyStudioInput, + type LegacyThirdPartyInput, + legacyValidateResolvedConfig, +} from "./legacy-config-validate.ts"; +import { + legacyGenerateAsymmetricGoJwt, + legacyGenerateGoJwt, + type LegacyJwk, +} from "./legacy-go-jwt.ts"; +import { + legacyCollectDotenvPrivateKeys, + legacyDecryptSecret, + legacyIsEncryptedSecret, +} from "./legacy-vault-decrypt.ts"; + +/** + * Go-parity derived local-dev config values, ported from `utils.Config`'s + * post-load defaulting (`pkg/config/config.go:406-441,748-758`) and + * `utils.GetApiUrl`/status's `toValues()` (`internal/utils/config.go:255-268`, + * `internal/status/status.go:52-95`). `@supabase/config`'s schema has no field for + * a handful of Go constants (`db.password`, the S3 credential triple) — those are + * Go-hardcoded literals, reproduced here rather than added to the shared schema + * (`pkg/config/config.go:408,437-441`). + * + * Kept generic (no `status`-specific shaping) so a future native `start`/`restart` + * port can reuse it instead of re-deriving these values — see the plan's + * "Files to create" note. Do not fold this into `legacy-storage-credentials.ts`; + * that module resolves credentials through a different (HTTP/tenant-aware) path + * for the remote-project branch, which this pure resolver does not need (the + * shared `://:` derivation itself lives in + * `legacy-api-url.ts`, used by both). + */ + +/** Go's `Db.Password` default (`pkg/config/config.go:408`) — never present in config.toml. */ +const DEFAULT_DB_PASSWORD = "postgres"; + +/** Go's hardcoded local S3 credentials (`pkg/config/config.go:437-441`). */ +const DEFAULT_S3_ACCESS_KEY_ID = "625729a08b95bf1b7ff351a663f3a23c"; +const DEFAULT_S3_SECRET_ACCESS_KEY = + "850181e4652dd023b7a98c58ae0d2d34bd487ee0cc3254aed6eda37307425907"; +const DEFAULT_S3_REGION = "local"; + +export interface LegacyLocalConfigValues { + readonly apiUrl: string; + readonly restUrl: string; + readonly graphqlUrl: string; + readonly functionsUrl: string; + readonly mcpUrl: string; + readonly studioUrl: string; + readonly mailpitUrl: string; + readonly dbUrl: string; + readonly publishableKey: string; + readonly secretKey: string; + readonly jwtSecret: string; + readonly anonKey: string; + readonly serviceRoleKey: string; + readonly storageS3Url: string; + readonly storageS3AccessKeyId: string; + readonly storageS3SecretAccessKey: string; + readonly storageS3Region: string; +} + +/** + * Go's `utils.GetApiUrl(path)` (`internal/utils/config.go:255-268`): appends + * `path` to the resolved external URL. Go's own fallback branch (building a bare + * `http://host:port` when `Config.Api.ExternalUrl` is empty) is unreachable in + * practice because `config.Load` already defaults `ExternalUrl` before `status` + * runs — `resolveApiExternalUrl` reproduces that same default, so `apiExternalUrl` + * passed in here is never empty. + */ +function apiUrlWithPath(apiExternalUrl: string, path: string): string { + return `${apiExternalUrl}${path}`; +} + +/** + * Thrown by {@link legacyResolveLocalConfigValues} when `auth.jwt_secret` is + * configured but too short to sign with, mirroring Go's `Config.Validate` + * (`pkg/config/apikeys.go:45-47`) — that check runs at config-load time, before + * any command renders output, so no local dev stack can even start with a + * short secret. + */ +export class LegacyInvalidJwtSecretError extends Error { + constructor() { + super("Invalid config for auth.jwt_secret. Must be at least 16 characters"); + this.name = "LegacyInvalidJwtSecretError"; + } +} + +/** Go's minimum `auth.jwt_secret` length (`pkg/config/apikeys.go:46`). */ +const MIN_JWT_SECRET_LENGTH = 16; + +/** + * Thrown by {@link envOverridePort} when a `SUPABASE_*_PORT` env/dotenv + * override doesn't parse as a valid port, mirroring Go's `Config.Load` + * (`pkg/config/config.go:749-756`): `v.UnmarshalExact` decodes with + * `WeaklyTypedInput` on (viper's `defaultDecoderConfig`, never reset by our + * decoder options), so mapstructure's `decodeUint` runs `strconv.ParseUint` + * on the override string and hard-fails config loading on a bad value — + * there is no Go code path that reaches `status`/`stop` with a malformed + * port override. The message text isn't a byte-match for mapstructure's + * internal error (that's viper/mapstructure library text, not a Go-authored + * string), but the parity-relevant part — hard-fail, same field name — is. + */ +export class LegacyInvalidPortEnvOverrideError extends Error { + constructor(dottedFieldPath: string, value: string) { + super(`Invalid config for ${dottedFieldPath}: cannot parse "${value}" as a port`); + this.name = "LegacyInvalidPortEnvOverrideError"; + } +} + +/** Go's `uint16` port fields' valid range (`pkg/config/db.go:84`, `pkg/config/api.go:29`, etc). */ +const MAX_PORT = 65535; + +/** + * Port-flavored sibling of {@link envOverride}/{@link legacyEnvOverrideBool} + * for `SUPABASE_*_PORT` fields Go decodes as `uint16` rather than a plain + * string. Unlike the boolean sibling — which intentionally falls back to + * `configured` on a malformed override — a bad port override is a genuine + * Go-parity hard failure (see {@link LegacyInvalidPortEnvOverrideError}), not + * a leniency case: Go never proceeds with the pre-override value on a decode + * error, it fails config loading outright. + */ +function envOverridePort( + name: string, + configuredPort: number, + dottedFieldPath: string, + projectEnvValues: Readonly> | undefined, +): number { + const value = envOverride(name, undefined, projectEnvValues); + if (value === undefined) return configuredPort; + if (!/^\d+$/.test(value)) { + throw new LegacyInvalidPortEnvOverrideError(dottedFieldPath, value); + } + const port = Number(value); + if (port > MAX_PORT) { + throw new LegacyInvalidPortEnvOverrideError(dottedFieldPath, value); + } + return port; +} + +/** + * Go's `Config.Load` binds Viper with `SetEnvPrefix("SUPABASE")` + + * `AutomaticEnv()` + a `.`→`_` key replacer (`pkg/config/config.go:529-535`), + * so ANY config field can be overridden by a `SUPABASE_` env var, + * generically across the whole struct — not just auth fields + * (`config_test.go:351,1061` exercise this against `auth.site_url`, and + * `internal/status/status.go:52-95`'s `toValues()` reads `utils.Config.*` + * directly, so every already-overridden field is automatically reflected in + * `status`'s output). This resolves it for every field this module derives a + * URL/port from, at the same higher-than-config.toml precedence Viper gives + * env vars. An empty env var is treated as unset, matching Viper's default + * (`AllowEmptyEnv` is never enabled in `config.go`). + * + * Viper's `AutomaticEnv` binding runs AFTER `Config.Load`'s `loadNestedEnv` + * (`config.go:735-738`), which loads `supabase/.env`(.local) and project-root + * dotenv files into the process env before any `SUPABASE_*` var is read + * (`config.go:1169-1207`) — so a value that lives only in one of those files, + * not the ambient shell, must still be visible here. `projectEnvValues` is + * that already-resolved map (see `legacyResolveProjectEnvironmentValues`); + * falling back to `process.env` covers the "no `supabase/` project found" + * case, where `projectEnvValues` is `undefined`. + * + * The resolved override string itself can be a further `env(VAR)` indirection + * (e.g. `SUPABASE_API_ENABLED=env(API_ENABLED)`) — Go's `LoadEnvHook` + * (`decode_hooks.go:15-23`) is the first mapstructure decode hook composed + * into `v.UnmarshalExact` (`config.go:749-753,769-772`), so it resolves + * `env(...)` on every string mapstructure decodes into the struct, regardless + * of whether Viper sourced that string from `config.toml` or a `SUPABASE_*` + * `AutomaticEnv` override (`config.go:582-586`) — Viper's `Get()` just returns + * a string; the hook chain doesn't know or care where it came from. Resolved + * with the same `projectEnvValues ?? process.env` precedence and non-empty + * gate as the outer lookup (mirroring `decode_hooks.go:19-24`'s `len(env) > 0` + * check); an unresolved/empty indirection leaves the `env(VAR)` literal + * untouched, same as Go. + */ +function envOverride( + name: string, + configured: string | undefined, + projectEnvValues: Readonly> | undefined, +): string | undefined { + const value = projectEnvValues?.[name] ?? process.env[name]; + if (value === undefined || value.length === 0) return configured; + const indirection = ENV_CAPTURE_REGEX.exec(value)?.[1]; + if (indirection === undefined) return value; + const resolved = projectEnvValues?.[indirection] ?? process.env[indirection]; + return resolved !== undefined && resolved.length > 0 ? resolved : value; +} + +/** + * Thrown by {@link legacyEnvOverrideBool} when a `SUPABASE_*_ENABLED` (or other + * bool-typed) env/dotenv override doesn't parse as one of Go's accepted bool + * spellings, mirroring Go's `Config.Load` (`pkg/config/config.go:749-756`): + * `v.UnmarshalExact` decodes with `WeaklyTypedInput` on (viper's + * `defaultDecoderConfig`, never reset by our decoder options — same mechanism + * as {@link LegacyInvalidPortEnvOverrideError}), so mapstructure's `decodeBool` + * runs `strconv.ParseBool` on the override string and hard-fails config + * loading on a bad value — there is no Go code path that reaches `status`/ + * `stop` with a malformed bool override. + */ +export class LegacyInvalidBoolEnvOverrideError extends Error { + constructor(dottedFieldPath: string, value: string) { + super(`Invalid config for ${dottedFieldPath}: cannot parse "${value}" as a bool`); + this.name = "LegacyInvalidBoolEnvOverrideError"; + } +} + +/** + * Boolean-flavored sibling of {@link envOverride} for `SUPABASE_*` fields Go + * decodes as a native bool (`api.tls.enabled`, `auth.enabled`, and every other + * `
.enabled` gate `status`/`stop` read — see `status.values.ts`) + * rather than a string/number — those are bound by the same generic Viper + * mechanism (`ExperimentalBindStruct` + `SetEnvPrefix("SUPABASE")` + + * `AutomaticEnv()`, `pkg/config/config.go:582-586`), but the override string + * must be decoded with Go's own `strconv.ParseBool` acceptance set + * ({@link legacyParseGoBool}) instead of used verbatim. Unlike a plain string + * override — where an unparsed value has no Go-observable failure mode — a + * malformed bool override is a genuine Go-parity hard failure (see + * {@link LegacyInvalidBoolEnvOverrideError}), same as + * {@link LegacyInvalidPortEnvOverrideError} for ports: Go never proceeds with + * the pre-override value on a decode error, it fails config loading outright. + * + * Exported (not just used internally) because `status.values.ts`'s own + * `
.enabled` gates need this same override treatment — Go's + * `status.toValues()` reads `utils.Config.*.Enabled` post-Viper-override for + * every gated service, not only auth. + */ +export function legacyEnvOverrideBool( + name: string, + configured: boolean, + dottedFieldPath: string, + projectEnvValues: Readonly> | undefined, +): boolean { + const value = envOverride(name, undefined, projectEnvValues); + if (value === undefined) return configured; + const parsed = legacyParseGoBool(value); + if (parsed === undefined) { + throw new LegacyInvalidBoolEnvOverrideError(dottedFieldPath, value); + } + return parsed; +} + +/** + * Thrown by {@link envOverrideAnalyticsBackend} when `SUPABASE_ANALYTICS_BACKEND` + * doesn't match one of Go's `LogflareBackend` values. `Analytics.Backend` is + * typed `LogflareBackend` (`pkg/config/config.go:303`), and + * `LogflareBackend.UnmarshalText` (`config.go:60-65`) hard-rejects anything + * outside `{postgres, bigquery}` — that runs inside the same + * `v.UnmarshalExact` decode call (`config.go:749-756`) every other + * `SUPABASE_*` override goes through, so a malformed override fails config + * loading outright, same mechanism as {@link LegacyInvalidPortEnvOverrideError}/ + * {@link LegacyInvalidBoolEnvOverrideError}. + */ +export class LegacyInvalidAnalyticsBackendEnvOverrideError extends Error { + constructor(dottedFieldPath: string, value: string) { + super( + `Invalid config for ${dottedFieldPath}: cannot parse "${value}" as one of "postgres", "bigquery"`, + ); + this.name = "LegacyInvalidAnalyticsBackendEnvOverrideError"; + } +} + +/** + * `analytics.backend`-flavored sibling of {@link envOverridePort}/ + * {@link legacyEnvOverrideBool} for the one `SUPABASE_*` override this file + * decodes as a Go text-unmarshalled enum rather than a string/number/bool — + * see {@link LegacyInvalidAnalyticsBackendEnvOverrideError}. Validates the + * override-or-configured value with a SINGLE check (rather than only + * validating the override, trusting the schema for the configured value), + * matching Go more closely: viper merges the config.toml value and any env + * override into one string BEFORE `UnmarshalExact` calls `UnmarshalText` + * exactly once on the resolved value (`config.go:749-756`), not once per + * source. `@supabase/config`'s `stringEnum` (`packages/config/src/ + * analytics.ts:31-39`) already guards the `config.toml`-sourced value at + * decode time, so this is belt-and-suspenders for that source and the sole + * guard for the env-override one, which bypasses that schema entirely. + */ +function envOverrideAnalyticsBackend( + configured: string, + projectEnvValues: Readonly> | undefined, +): "postgres" | "bigquery" { + const value = + envOverride("SUPABASE_ANALYTICS_BACKEND", undefined, projectEnvValues) ?? configured; + if (value !== "postgres" && value !== "bigquery") { + throw new LegacyInvalidAnalyticsBackendEnvOverrideError("analytics.backend", value); + } + return value; +} + +/** + * Decrypts a resolved auth identity-key field (`jwt_secret`, `publishable_key`, + * `secret_key`, `anon_key`, `service_role_key`) when it's a dotenvx `encrypted:` + * value, mirroring Go's `DecryptSecretHookFunc` (`pkg/config/secret.go:30-73`), + * which Go runs unconditionally during `UnmarshalExact` for every + * `config.Secret`-typed field (`pkg/config/auth.go:181-185` types these five as + * `Secret`) — an undecryptable value aborts config loading with + * `failed to parse config: ` (`config.go:704`) before `status`/`stop` + * continue. `@supabase/config`'s schema only tags these fields for later + * `Redacted` wrapping (`packages/config/src/lib/env.ts`) and never decrypts, so + * without this step a valid `encrypted:` secret would be used as literal (wrong) + * key material and a malformed one would silently pass through instead of + * failing like Go does. + * + * Applied AFTER {@link envOverride}, matching Go: an env-sourced override lands + * on the same `config.Secret` field and goes through the same decode hook as a + * TOML-sourced value, so `SUPABASE_AUTH_JWT_SECRET=encrypted:...` is decrypted + * too, not just the config.toml value. + */ +function decryptAuthSecret( + value: string | undefined, + projectEnvValues: Readonly> | undefined, +): string | undefined { + if (value === undefined || !legacyIsEncryptedSecret(value)) return value; + const dotenvPrivateKeys = legacyCollectDotenvPrivateKeys({ ...projectEnvValues, ...process.env }); + const decrypted = legacyDecryptSecret(value, dotenvPrivateKeys); + if (!decrypted.ok) { + throw new LegacyConfigValidateError(`failed to parse config: ${decrypted.error}`); + } + return decrypted.value; +} + +/** Go's `(a *auth) generateAPIKeys` (`pkg/config/apikeys.go:43-73`). */ +function resolveJwtSecret(configured: string | undefined): string { + if (configured === undefined || configured.length === 0) return defaultJwtSecret; + if (configured.length < MIN_JWT_SECRET_LENGTH) { + throw new LegacyInvalidJwtSecretError(); + } + return configured; +} + +function resolveOpaqueKey(configured: string | undefined, fallback: string): string { + return configured !== undefined && configured.length > 0 ? configured : fallback; +} + +function resolveSignedKey( + configured: string | undefined, + jwtSecret: string, + signingKey: LegacyJwk | undefined, + role: "anon" | "service_role", +): string { + if (configured !== undefined && configured.length > 0) return configured; + return signingKey !== undefined + ? legacyGenerateAsymmetricGoJwt(signingKey, role) + : legacyGenerateGoJwt(jwtSecret, role); +} + +/** Matches Go's `JWK` struct fields (`pkg/config/auth.go:88-108`) — see `LegacyJwk`. */ +const LegacyJwkSchema = Schema.Struct({ + kty: Schema.String, + kid: Schema.optionalKey(Schema.String), + alg: Schema.optionalKey(Schema.String), + n: Schema.optionalKey(Schema.String), + e: Schema.optionalKey(Schema.String), + d: Schema.optionalKey(Schema.String), + p: Schema.optionalKey(Schema.String), + q: Schema.optionalKey(Schema.String), + dp: Schema.optionalKey(Schema.String), + dq: Schema.optionalKey(Schema.String), + qi: Schema.optionalKey(Schema.String), + crv: Schema.optionalKey(Schema.String), + x: Schema.optionalKey(Schema.String), + y: Schema.optionalKey(Schema.String), +}); +const decodeLegacyJwks = Schema.decodeUnknownSync(Schema.Array(LegacyJwkSchema)); + +/** + * Go's `Config.Validate` (`pkg/config/config.go:877-878,1059-1062`): a relative + * `signing_keys_path` resolves against `/supabase`, then the file is + * read and JSON-decoded into `[]JWK`. Only the first key is ever used + * ({@link resolveSignedKey}), matching `generateJWT`'s `a.SigningKeys[0]`. + * + * Uses `node:fs` directly (not the `FileSystem` Effect service other Go-parity + * resolvers in `legacy/` use for file reads) so this function — and its large + * existing test surface — can stay a plain synchronous resolver; this is an + * optional, rarely-configured field, not worth threading Effect dependencies + * through `legacyStatusValues`/`status.handler.ts` for. + * + * Error wording matches Go's two `Validate` failure branches exactly + * (`"failed to read signing keys: %w"` for an open failure, `"failed to decode + * signing keys: %w"` for a parse failure) rather than letting `readFileSync`/ + * `JSON.parse`'s raw Node error text through unwrapped. + * + * Callers must only invoke this when auth is enabled (the `SUPABASE_AUTH_ENABLED`- + * overridden value, not necessarily raw `config.auth.enabled` — see + * {@link legacyEnvOverrideBool}) — Go's `Validate` nests the entire signing-keys read + * inside `if c.Auth.Enabled` (`pkg/config/config.go:1036,1059-1065`), reading + * that same post-override value, so a disabled auth section never touches + * `signing_keys_path`, however stale or missing that file is. + */ +function loadFirstSigningKey(workdir: string, signingKeysPath: string): LegacyJwk | undefined { + const absolutePath = legacyResolveSigningKeysPath(workdir, signingKeysPath); + + let contents: string; + try { + contents = readFileSync(absolutePath, "utf8"); + } catch (cause) { + throw new LegacyConfigValidateError(legacySigningKeysReadErrorMessage(cause)); + } + + let jwks: ReadonlyArray; + try { + jwks = decodeLegacyJwks(JSON.parse(contents)); + } catch (cause) { + throw new LegacyConfigValidateError(legacySigningKeysDecodeErrorMessage(cause)); + } + return jwks[0]; +} + +/** + * Go's `Config.Validate` TLS branch (`pkg/config/config.go:1006-1027`) file reads: gated on + * `api.enabled && api.tls.enabled` same as the caller, each configured path is read to confirm + * it's actually reachable, matching Go's `fs.ReadFile` calls (Go caches the bytes for `start` to + * serve as `CertContent`/`KeyContent` — `status`/`stop` have no use for the bytes, only the same + * validation outcome, so they're discarded here). The "exactly one of cert/key set" presence + * check now lives in `legacyValidateResolvedConfig`'s `api.tls` step + * (`legacy-config-validate.ts`) — this function only runs the reads, and only when BOTH paths + * are actually present: neither path set, or only one, never reaches a `fs.ReadFile` call here, + * since the presence check (run later, as part of the single consolidated validation call) owns + * rejecting the one-but-not-the-other case. + * + * Go joins both paths unconditionally with the `supabase/` dir — no `filepath.IsAbs` guard + * (`config.go:961-965` uses `path.Join`, which absorbs a leading `/`) — unlike + * {@link loadFirstSigningKey}'s `signing_keys_path`, which Go does guard with `filepath.IsAbs` + * (`config.go:928-929`). See `legacyResolveApiTlsPath`. Matches the identical Kong-side + * validation already ported for `seed buckets`/`storage` in + * `legacy-storage-credentials.ts`'s `validateLocalKongTls`. + * + * Uses `node:fs` directly for the same reason as {@link loadFirstSigningKey}: this stays a plain + * synchronous resolver rather than threading the Effect `FileSystem` service through + * `legacyStatusValues`/`status.handler.ts`. + */ +function readApiTlsFiles( + workdir: string, + certPath: string | undefined, + keyPath: string | undefined, +): void { + if (certPath === undefined || certPath.length === 0) return; + if (keyPath === undefined || keyPath.length === 0) return; + + try { + readFileSync(legacyResolveApiTlsPath(workdir, certPath), "utf8"); + } catch (cause) { + throw new LegacyConfigValidateError(legacyApiTlsCertReadErrorMessage(cause)); + } + try { + readFileSync(legacyResolveApiTlsPath(workdir, keyPath), "utf8"); + } catch (cause) { + throw new LegacyConfigValidateError(legacyApiTlsKeyReadErrorMessage(cause)); + } +} + +/** + * Go's `(e *email) validate(fsys)` template/notification content read (`pkg/config/ + * config.go:1293-1313`), called from `Config.Validate` right after `Auth.MFA.validate()`, still + * inside `if c.Auth.Enabled` (`config.go:1142`). Every template is checked unconditionally; a + * notification only when that notification is itself enabled (`config.go:1308`). Uses the same + * `readFileSync`-based pattern as {@link loadFirstSigningKey}/`readApiTlsFiles` in this file, + * not an Effect `FileSystem` service. + * + * The `content`-vs-`content_path` exclusivity decision and path resolution (including the + * TEMPLATE-vs-`workdir`/NOTIFICATION-vs-`/supabase` base asymmetry, per Go's `(c + * *baseConfig) resolve` (`config.go:900-916`) — this asymmetry is real, intentional Go behavior + * to match, not a bug to fix) now live in `legacyResolveEmailTemplateContentPath` + * (`legacy-config-validate.ts`); this function only feeds it `contentPresent` (computed from the + * raw `document`, since `@supabase/config`'s `template`/`notification` schema + * (`packages/config/src/auth/email.ts`) has no `content` field to see) and performs the read + * when a path comes back. + * + * `auth.email.template..*`/`auth.email.notification..*` are Viper-bound like every + * other nested field once `[auth.email.template.]`/`[auth.email.notification.]` are + * present in config.toml (`ExperimentalBindStruct`/`AutomaticEnv`, `config.go:581-586`), so + * `SUPABASE_AUTH_EMAIL_TEMPLATE__CONTENT_PATH`/`SUPABASE_AUTH_EMAIL_NOTIFICATION__ + * ENABLED`/`_CONTENT_PATH` overrides apply before this read runs. Unlike the hook/passkey/smtp + * presence gates elsewhere in this file, no extra raw-document presence check is needed here to + * decide WHETHER to apply an override: `email.template`/`email.notification` are `Schema.Record`s + * (`packages/config/src/auth/email.ts`), which — unlike a fixed-shape struct with + * `withDecodingDefaultKey` — only ever contain a key when the TOML section was actually present, + * so `Object.entries` already reflects presence. + */ +function readAuthEmailTemplateContent( + email: ProjectConfig["auth"]["email"], + workdir: string, + authDocument: Record | undefined, + projectEnvValues: Readonly> | undefined, +): void { + const emailDoc = asRecord(authDocument?.["email"]); + const templatesDoc = asRecord(emailDoc?.["template"]); + const notificationsDoc = asRecord(emailDoc?.["notification"]); + + for (const [name, tmpl] of Object.entries(email.template)) { + const contentPath = + envOverride( + `SUPABASE_AUTH_EMAIL_TEMPLATE_${name.toUpperCase()}_CONTENT_PATH`, + tmpl.content_path, + projectEnvValues, + ) ?? tmpl.content_path; + const path = legacyResolveEmailTemplateContentPath({ + section: "template", + name, + contentPath, + contentPresent: asRecord(templatesDoc?.[name])?.["content"] !== undefined, + base: workdir, + }); + if (path === undefined) continue; + try { + readFileSync(path, "utf8"); + } catch (cause) { + throw new LegacyConfigValidateError( + legacyEmailContentPathReadErrorMessage("template", name, cause), + ); + } + } + for (const [name, tmpl] of Object.entries(email.notification)) { + const envPrefix = `SUPABASE_AUTH_EMAIL_NOTIFICATION_${name.toUpperCase()}`; + const enabled = legacyEnvOverrideBool( + `${envPrefix}_ENABLED`, + tmpl.enabled, + `auth.email.notification.${name}.enabled`, + projectEnvValues, + ); + if (!enabled) continue; + const contentPath = + envOverride(`${envPrefix}_CONTENT_PATH`, tmpl.content_path, projectEnvValues) ?? + tmpl.content_path; + const path = legacyResolveEmailTemplateContentPath({ + section: "notification", + name, + contentPath, + contentPresent: asRecord(notificationsDoc?.[name])?.["content"] !== undefined, + base: join(workdir, "supabase"), + }); + if (path === undefined) continue; + try { + readFileSync(path, "utf8"); + } catch (cause) { + throw new LegacyConfigValidateError( + legacyEmailContentPathReadErrorMessage("notification", name, cause), + ); + } + } +} + +/** + * `SUPABASE_DB_MAJOR_VERSION` sibling of {@link envOverridePort} for the one + * numeric field Go decodes as `uint` rather than `uint16` (`pkg/config/db.go:87`) + * — same generic Viper `AutomaticEnv` binding (`config.go:576-586`), same + * mapstructure hard-fail-on-bad-value semantics as the port/bool overrides, but + * with no upper-bound cap. A non-digit override folds into the same generic + * "Invalid db.major_version" message `legacyValidateResolvedConfig` produces for + * an out-of-set numeric value, since Go's own decode failure and `Validate` + * failure for this field aren't independently distinguishable from the CLI's + * output the way ports/bools are. + */ +function envOverrideMajorVersion( + configured: number, + projectEnvValues: Readonly> | undefined, +): number { + const value = envOverride("SUPABASE_DB_MAJOR_VERSION", undefined, projectEnvValues); + if (value === undefined) return configured; + if (!/^\d+$/.test(value)) { + throw new Error(`Failed reading config: Invalid db.major_version: ${value}.`); + } + return Number(value); +} + +/** + * `SUPABASE_EDGE_RUNTIME_DENO_VERSION` sibling of {@link envOverrideMajorVersion} + * — same generic Viper `AutomaticEnv` binding, same mapstructure + * hard-fail-on-bad-value semantics, no upper-bound cap. A non-digit override + * folds into the same generic "Invalid edge_runtime.deno_version" message + * `legacyValidateResolvedConfig` produces for an out-of-set numeric value. + */ +function envOverrideDenoVersion( + configured: number, + projectEnvValues: Readonly> | undefined, +): number { + const value = envOverride("SUPABASE_EDGE_RUNTIME_DENO_VERSION", undefined, projectEnvValues); + if (value === undefined) return configured; + if (!/^\d+$/.test(value)) { + throw new Error(`Failed reading config: Invalid edge_runtime.deno_version: ${value}.`); + } + return Number(value); +} + +/** Narrows an unknown value to a plain object, mirroring `legacy-db-config.toml-read.ts`'s `asRecord`. */ +function asRecord(value: unknown): Record | undefined { + return typeof value === "object" && value !== null && !Array.isArray(value) + ? (value as Record) + : undefined; +} + +/** Go's `hook.validate()` hook-type iteration order (`pkg/config/config.go:1453-1485`), used + * only to build {@link legacyResolveLocalConfigValues}'s `hooks` input in the right order — + * the actual per-hook validation now lives in `legacyValidateResolvedConfig`. */ +const LEGACY_HOOK_TYPE_ORDER = [ + "mfa_verification_attempt", + "password_verification_attempt", + "custom_access_token", + "send_sms", + "send_email", + "before_user_created", +] as const; + +/** Go's `(s *sms) validate()` fixed provider priority (`pkg/config/config.go:1348-1410`) — a + * `switch` that validates ONLY the first enabled provider in this order. */ +const LEGACY_SMS_PROVIDER_ORDER = [ + "twilio", + "twilio_verify", + "messagebird", + "textlocal", + "vonage", +] as const; + +/** Required fields per SMS provider, in the order Go checks them (`config.go:1349-1403`). */ +const LEGACY_SMS_REQUIRED_FIELDS: Record< + (typeof LEGACY_SMS_PROVIDER_ORDER)[number], + ReadonlyArray +> = { + twilio: ["account_sid", "message_service_sid", "auth_token"], + twilio_verify: ["account_sid", "message_service_sid", "auth_token"], + messagebird: ["originator", "access_key"], + textlocal: ["sender", "api_key"], + vonage: ["from", "api_key", "api_secret"], +}; + +/** + * Go's `(s *sms) validate()` (`pkg/config/config.go:1348-1410`): a boolean `switch` that inspects + * providers in the FIXED priority order above and validates ONLY the first one whose `enabled` is + * true — a later enabled-but-incomplete provider is never even looked at. `@supabase/config`'s + * `sms` schema (`packages/config/src/auth/sms.ts`) already implements this exact switch for the + * schema-decoded (pre-env-override) TOML value, which is Go-parity-correct for a config with no + * relevant `SUPABASE_AUTH_SMS_*` env override — but `@supabase/config`'s decode pipeline never + * resolves `SUPABASE_*` overrides at all (only this legacy-shell layer does, post-decode), so a + * `SUPABASE_AUTH_SMS__ENABLED` override that flips a section's enabled state after + * decode is invisible to it. This re-runs the same switch against the RAW `authDocument` with + * env overrides applied first — same document-based, post-override pattern as + * {@link validateAuthExternalProviders} below, and the same "duplicate D's/the schema's check for + * the env-override-aware L path" tradeoff already accepted for that function. + * + * `auth.sms..*` is Viper-bound like every other nested field once + * `[auth.sms.]` is present in config.toml (`ExperimentalBindStruct`/`AutomaticEnv`, + * `config.go:581-586`), so `SUPABASE_AUTH_SMS__ENABLED`/`_` overrides apply + * before this validation runs, gated on the raw provider section already being present, matching + * `AutomaticEnv` (which only intercepts keys already present in the merged config). + */ +function validateAuthSmsProviders( + authDocument: Record | undefined, + projectEnvValues: Readonly> | undefined, +): void { + const smsDoc = asRecord(authDocument?.["sms"]); + if (smsDoc === undefined) return; + for (const providerName of LEGACY_SMS_PROVIDER_ORDER) { + const providerDoc = asRecord(smsDoc[providerName]); + if (providerDoc === undefined) continue; + const envPrefix = `SUPABASE_AUTH_SMS_${providerName.toUpperCase()}`; + const enabled = legacyEnvOverrideBool( + `${envPrefix}_ENABLED`, + providerDoc["enabled"] === true, + `auth.sms.${providerName}.enabled`, + projectEnvValues, + ); + if (!enabled) continue; + for (const field of LEGACY_SMS_REQUIRED_FIELDS[providerName]) { + const value = envOverride( + `${envPrefix}_${field.toUpperCase()}`, + typeof providerDoc[field] === "string" ? providerDoc[field] : undefined, + projectEnvValues, + ); + if (value === undefined || value.length === 0) { + throw new LegacyConfigValidateError( + `Missing required field in config: auth.sms.${providerName}.${field}`, + ); + } + } + // Go's switch stops at the first enabled provider — later providers are never inspected. + return; + } +} + +/** Go's `external.validate()` deprecated-provider skip (`config.go:1419-1423`) — `linkedin`/ + * `slack` are deleted (and warned on, if enabled) before the required-field loop runs, so they + * are never validated here. Mirrors `legacy-db-config.toml-read.ts`'s identical "B5: external + * providers" skip list. */ +const DEPRECATED_EXTERNAL_PROVIDERS = new Set(["linkedin", "slack"]); + +/** + * Go's `(e external) validate()` (`pkg/config/config.go:1419-1451`) — D-only per + * `legacy-config-validate.ts`'s module header ("`auth.external` ... stays 100% inline in D"), so + * this ports the identical inline block D already has (`legacy-db-config.toml-read.ts`'s "B5: + * external providers") to close the same gap for L. `auth.external` is a genuine Go + * `map[string]provider` (`apps/cli-go/pkg/config/auth.go:190`), so an arbitrary/unmodeled + * provider name (e.g. `[auth.external.custom]`) is a legitimate config shape — Go validates + * every enabled entry regardless of name. `@supabase/config`'s `external` schema only models the + * ~20 known provider ids and silently drops anything else at decode time + * (`packages/config/src/auth/providers.ts`), so an unmodeled provider's required-field check + * must run against the RAW `authDocument` instead of the decoded `ProjectConfig` — same + * document-based approach as {@link readAuthEmailTemplateContent}/the passkey/smtp checks above. + * Known providers are already covered by the schema's own `requiredWhenEnabled` check at decode + * time, so in practice this only ever fires for a name the schema doesn't model, but it runs + * over every raw key unconditionally, matching Go's own map iteration rather than special-casing + * "unknown" a different way. `authDocument`'s values are already post-`env()`-interpolation (see + * `LoadedProjectConfig.document`), so no `legacyExpandEnv`-style resolution is needed here, + * unlike D's raw pre-interpolation document. + * + * `auth.external..*` is Viper-bound like every other nested field once + * `[auth.external.]` is present in config.toml (`ExperimentalBindStruct`/`AutomaticEnv`, + * `config.go:581-586`), so `SUPABASE_AUTH_EXTERNAL__ENABLED`/`_CLIENT_ID`/`_SECRET` + * overrides apply before this validation runs — same gap this schema's own `requiredWhenEnabled` + * check has for KNOWN providers too (that check only sees the decoded, pre-override TOML value), + * so this now covers both known and unmodeled provider names uniformly, matching Go not + * distinguishing between them either. + */ +function validateAuthExternalProviders( + authDocument: Record | undefined, + projectEnvValues: Readonly> | undefined, +): void { + const external = asRecord(authDocument?.["external"]); + if (external === undefined) return; + for (const name of Object.keys(external)) { + if (DEPRECATED_EXTERNAL_PROVIDERS.has(name)) continue; + const provider = asRecord(external[name]); + if (provider === undefined) continue; + const envPrefix = `SUPABASE_AUTH_EXTERNAL_${name.toUpperCase()}`; + const enabled = legacyEnvOverrideBool( + `${envPrefix}_ENABLED`, + provider["enabled"] === true, + `auth.external.${name}.enabled`, + projectEnvValues, + ); + if (!enabled) continue; + const clientId = envOverride( + `${envPrefix}_CLIENT_ID`, + typeof provider["client_id"] === "string" ? provider["client_id"] : undefined, + projectEnvValues, + ); + if (clientId === undefined || clientId.length === 0) { + throw new LegacyConfigValidateError( + `Missing required field in config: auth.external.${name}.client_id`, + ); + } + const secret = envOverride( + `${envPrefix}_SECRET`, + typeof provider["secret"] === "string" ? provider["secret"] : undefined, + projectEnvValues, + ); + if (name !== "apple" && name !== "google" && (secret === undefined || secret.length === 0)) { + throw new LegacyConfigValidateError( + `Missing required field in config: auth.external.${name}.secret`, + ); + } + } +} + +/** + * @throws when `project_id` (post-override, post-workdir-basename-fallback) is + * an explicit empty string. Go's `Config.Validate` checks this FIRST, before + * every other field (`pkg/config/config.go:990-991`): `mergeDefaultValues` + * merges `sanitizeProjectId(filepath.Base(cwd))` in as a viper DEFAULT value + * BEFORE `config.toml` is merged (`config.go:690-699`, via `Eject`, + * `config.go:561-570`) — so `c.ProjectId` is NEVER Go's zero value by the time + * `Validate` runs; it's always at least this sanitized basename. A workdir + * whose basename sanitizes to the empty string (e.g. `!!!`) therefore fails + * config loading in Go even with NO `project_id` key in the file at all. An + * explicit `project_id = ""` IN the file overwrites that default with the + * literal empty string the same way (rather than being treated as absent) — + * Go fails outright rather than falling back to the basename either way. + * `legacySanitizeProjectId` is only applied to the BASENAME fallback here, + * matching `Eject`'s pre-sanitized default — an explicit non-empty + * `config.project_id`/`SUPABASE_PROJECT_ID` value is intentionally NOT + * re-sanitized at this point, matching Go's `Validate` "auto-fix" branch + * (`config.go:992-996`) being a WARN-only rewrite with no throwing + * equivalent, same precedent as this module's other WARN-only omissions + * (`auth.captcha.secret`/`assertEnvLoaded`, SMS's `EnableSignup` case). + * @throws {LegacyInvalidJwtSecretError} when `auth.jwt_secret` is set but too short. + * @throws {LegacyInvalidPortEnvOverrideError} when a `SUPABASE_*_PORT` env/dotenv + * override doesn't parse as a valid port. + * @throws {LegacyInvalidBoolEnvOverrideError} when a `SUPABASE_*_ENABLED` env/dotenv + * override doesn't parse as a valid bool. + * @throws when a configured `api.tls` cert/key file can't be read — see + * {@link readApiTlsFiles}. The "exactly one of cert/key set" presence check + * runs later, as part of {@link legacyValidateResolvedConfig}. + * @throws when `auth.signing_keys_path` is set but the file is missing, malformed, + * or its first key uses an unsupported algorithm — see {@link loadFirstSigningKey} + * and {@link legacyGenerateAsymmetricGoJwt}. + * @throws when an email template's `content` is present without `content_path`, or a + * configured `content_path` file can't be read — see {@link readAuthEmailTemplateContent}. + * @throws {LegacyInvalidAnalyticsBackendEnvOverrideError} when `SUPABASE_ANALYTICS_BACKEND` + * doesn't parse as one of Go's `LogflareBackend` values. + * @throws {LegacyConfigValidateError} for every other `Config.Validate` branch this module + * and `legacy-config-validate.ts` jointly own — project_id emptiness aside (checked above, + * inline, since the value is also needed for the throw's own message-free early-exit shape), + * every REMAINING pure check (api.port/tls presence, db.port/major_version, storage bucket + * names, studio, local_smtp, auth.site_url/captcha/passkey/hooks/mfa/smtp/third_party, + * function slugs, edge_runtime.deno_version, analytics.gcp_*, experimental.*) is deferred to a + * SINGLE call to {@link legacyValidateResolvedConfig} at the end of this function, in Go's exact + * relative order — see that module's header for the full table and the accepted ordering + * tradeoff this introduces against the I/O checks listed above (which keep running at their + * original position, per-caller, rather than being folded into that single call). + */ +export function legacyResolveLocalConfigValues( + config: ProjectConfig, + hostname: string, + workdir: string, + projectEnvValues: Readonly> | undefined = undefined, + /** + * `LoadedProjectConfig.document` (`packages/config/src/io.ts`) — the raw, + * pre-schema-default TOML document `config` was decoded from. Lets checks + * that hinge on TOML-section PRESENCE (not the decoded, always-defaulted + * value) inspect the file directly — see `legacyValidateResolvedConfig`'s + * `experimental.webhooks`/`auth.passkey`/`auth.email.smtp` steps. + * `undefined` for callers that haven't threaded it through yet (e.g. most + * existing unit tests); those checks are then simply skipped rather than + * guessed at. + */ + document: Readonly> | undefined = undefined, +): LegacyLocalConfigValues { + // Go's `Config.Validate` checks `ProjectId` FIRST, before every other field + // (`pkg/config/config.go:990-991`) — see this function's `@throws` doc above + // for why a workdir basename that sanitizes to `""` fails here even when + // `project_id` is absent from the file entirely. `config.project_id` is + // `undefined` only when the key is genuinely absent (`optionalKey`, see + // `packages/config/src/base.ts`) — that's the ONE case where Go's own + // sanitized-basename viper default shows through instead of a file value, + // so the fallback belongs here, not as a third branch after `envOverride`. + // `SUPABASE_PROJECT_ID` is checked via the same `envOverride` precedence + // every other field here uses, since Viper's `AutomaticEnv` binds it too + // (`config.go:529-535`) and it can turn an explicit-empty file value (or an + // unsanitizable basename fallback) back into a valid override. + const resolvedProjectId = envOverride( + "SUPABASE_PROJECT_ID", + config.project_id ?? legacySanitizeProjectId(basename(workdir)), + projectEnvValues, + ); + + // Go's `status` reads `utils.Config.Api.Port`/`ExternalUrl`/`Tls.Enabled` + // after Viper's AutomaticEnv has already applied any `SUPABASE_API_PORT`/ + // `SUPABASE_API_EXTERNAL_URL`/`SUPABASE_API_TLS_ENABLED` override + // (`config.go:529-535,799-809`), so the values fed into + // `legacyResolveApiExternalUrl`'s own `external_url`-wins-else- + // `scheme://host:port` derivation (which picks `https` vs `http` from + // `tls.enabled`) must be the overridden ones too. + const apiTlsEnabled = legacyEnvOverrideBool( + "SUPABASE_API_TLS_ENABLED", + config.api.tls.enabled, + "api.tls.enabled", + projectEnvValues, + ); + // Go's TLS cert/key validation nests entirely inside `if c.Api.Enabled` + // (`config.go:1006,1010`) — mirroring `authEnabled` below, gate on the + // POST-`SUPABASE_API_ENABLED`-override value, not raw `config.api.enabled`. + const apiEnabled = legacyEnvOverrideBool( + "SUPABASE_API_ENABLED", + config.api.enabled, + "api.enabled", + projectEnvValues, + ); + const apiTlsCertPath = envOverride( + "SUPABASE_API_TLS_CERT_PATH", + config.api.tls.cert_path, + projectEnvValues, + ); + const apiTlsKeyPath = envOverride( + "SUPABASE_API_TLS_KEY_PATH", + config.api.tls.key_path, + projectEnvValues, + ); + if (apiEnabled && apiTlsEnabled) { + readApiTlsFiles(workdir, apiTlsCertPath, apiTlsKeyPath); + } + // Go's `Config.Validate` rejects `api.port === 0`/`SUPABASE_API_PORT=0` ONLY + // when `api.enabled` (`pkg/config/config.go:1006-1008`) — unlike `db.port` + // below, which has no `enabled` gate. Resolved once into a named const so the + // check and the URL derivation below share the same overridden value instead + // of calling `envOverridePort` twice. + const apiPort = envOverridePort( + "SUPABASE_API_PORT", + config.api.port, + "api.port", + projectEnvValues, + ); + const apiExternalUrl = legacyResolveApiExternalUrl( + { + external_url: envOverride( + "SUPABASE_API_EXTERNAL_URL", + config.api.external_url, + projectEnvValues, + ), + port: apiPort, + tls: { enabled: apiTlsEnabled }, + }, + hostname, + ); + // Unlike `api.port`/`studio.port`/`local_smtp.port` below, `db.port` has no + // `enabled` gate in Go's `Config.Validate` — it's unconditionally required, + // and a decoded `0` (e.g. `SUPABASE_DB_PORT=0`) fails validation with this + // exact message (`pkg/config/config.go:1031-1032`) before `status`/`stop` + // render anything, same wording already used for the `db query`/`test db` + // path (`legacy-db-config.toml-read.ts:1380`). + const dbPort = envOverridePort("SUPABASE_DB_PORT", config.db.port, "db.port", projectEnvValues); + // Go's `Config.Validate` checks `db.major_version` right after `db.port` + // (`pkg/config/config.go:1034-1061`), unconditionally (no `enabled` gate). + const majorVersion = envOverrideMajorVersion(config.db.major_version, projectEnvValues); + // Go's `Config.Validate` runs `ValidateBucketName` over every `[storage.buckets.*]` + // key right after `db.major_version`, unconditionally. + const storageBucketNames = + config.storage.buckets !== undefined ? Object.keys(config.storage.buckets) : []; + // Go's `Config.Validate` rejects `studio.port === 0`/`SUPABASE_STUDIO_PORT=0` + // ONLY when `studio.enabled` (`pkg/config/config.go:1070-1073`) — same + // enabled-gated pattern as `api.port` above. + const studioEnabled = legacyEnvOverrideBool( + "SUPABASE_STUDIO_ENABLED", + config.studio.enabled, + "studio.enabled", + projectEnvValues, + ); + const studioPort = envOverridePort( + "SUPABASE_STUDIO_PORT", + config.studio.port, + "studio.port", + projectEnvValues, + ); + // Go's `Config.Validate` parses `studio.api_url` with `net/url.Parse` right + // after the port check, still inside `if c.Studio.Enabled` + // (`pkg/config/config.go:1074-1078`). `config.studio.api_url` is a required + // (defaulted) field, so `envOverride` can only return `undefined` here if + // that default itself were somehow undefined — the `??` fallback just + // satisfies that generic signature. + const studioApiUrl = + envOverride("SUPABASE_STUDIO_API_URL", config.studio.api_url, projectEnvValues) ?? + config.studio.api_url; + // Go's `Config.Validate` rejects `local_smtp.port === 0`/ + // `SUPABASE_LOCAL_SMTP_PORT=0` ONLY when `local_smtp.enabled` — Go's struct + // field is still named `Inbucket` for the `[local_smtp]` TOML section + // (`pkg/config/config.go:235,1081-1083`), so `local_smtp.enabled` and the + // deprecated `inbucket.enabled` alias are the same underlying flag, not two + // independent ones. + const mailpitEnabled = legacyEnvOverrideBool( + "SUPABASE_LOCAL_SMTP_ENABLED", + config.local_smtp.enabled, + "local_smtp.enabled", + projectEnvValues, + ); + const mailpitPort = envOverridePort( + "SUPABASE_LOCAL_SMTP_PORT", + config.local_smtp.port, + "local_smtp.port", + projectEnvValues, + ); + const jwtSecret = resolveJwtSecret( + decryptAuthSecret( + envOverride("SUPABASE_AUTH_JWT_SECRET", config.auth.jwt_secret, projectEnvValues), + projectEnvValues, + ), + ); + const signingKeysPath = envOverride( + "SUPABASE_AUTH_SIGNING_KEYS_PATH", + config.auth.signing_keys_path, + projectEnvValues, + ); + // Gated on `auth.enabled` to match Go's `Validate` (`pkg/config/config.go:1036,1059-1065`): + // the signing-keys file read lives entirely inside `if c.Auth.Enabled`, so a + // disabled auth section never opens/parses `signing_keys_path`, even a stale + // or missing one. JWT-secret validation and anon/service_role key generation + // (`generateAPIKeys`, `apikeys.go:43-73`) run unconditionally either way, so + // only this file read is gated. `c.Auth.Enabled` is itself Viper-bound like + // any other field (`config.go:582-586`), so `Validate`'s gate reads the + // POST-`SUPABASE_AUTH_ENABLED`-override value, not the raw TOML one — hence + // `legacyEnvOverrideBool` here instead of `config.auth.enabled` directly. + const authEnabled = legacyEnvOverrideBool( + "SUPABASE_AUTH_ENABLED", + config.auth.enabled, + "auth.enabled", + projectEnvValues, + ); + // Go's `Config.Validate` checks `auth.site_url` first inside `if c.Auth.Enabled` + // (`pkg/config/config.go:1086-1090`), before the signing-keys read below — + // `@supabase/config`'s schema only defaults `site_url` when the key is ABSENT + // (`Schema.withDecodingDefaultKey`), so an explicit `site_url = ""` decodes as + // `""` with no schema-level error, same gap as `db.port === 0` above. + const siteUrl = envOverride("SUPABASE_AUTH_SITE_URL", config.auth.site_url, projectEnvValues); + // `LoadedProjectConfig.document` (the raw, pre-schema-default TOML `config` was decoded from) — + // hoisted here (rather than inside the `authEnabled` block below, where it used to live) because + // the captcha presence check right below needs it too. `undefined` for callers that haven't + // threaded `document` through yet, in which case presence-based checks are simply skipped. + const authDocument = asRecord(document?.["auth"]); + // Go's `Config.Validate` checks `auth.captcha` right after `auth.site_url`, + // still inside `if c.Auth.Enabled` (`pkg/config/config.go:1099-1109`): an + // enabled CAPTCHA section requires both `provider` and `secret`. `auth.captcha.*` + // is Viper-bound like every other nested field once `[auth.captcha]` is present + // in config.toml (`ExperimentalBindStruct`/`AutomaticEnv`, `config.go:581-586`), + // so `SUPABASE_AUTH_CAPTCHA_ENABLED`/`_PROVIDER`/`_SECRET` overrides apply before + // this validation runs. Unlike the flat `auth.site_url` field, `config.auth.captcha` + // does NOT decode to `undefined` when `[auth.captcha]` is absent from config.toml — + // `captcha.ts`'s own `withDecodingDefaultKey` fills in `{ enabled: false }` even + // through the outer `Schema.optionalKey` wrapper (`packages/config/src/auth/index.ts`), + // confirmed empirically; there is no schema-level presence signal here, unlike + // `auth.passkey`/`auth.webauthn` below. So, like those, presence is read from the raw + // `authDocument` instead — matching Go's `AutomaticEnv` (which only intercepts keys + // already present in the merged config), an absent `[auth.captcha]` section never + // picks up an env override alone. + const captchaDoc = asRecord(authDocument?.["captcha"]); + const captchaInput: LegacyCaptchaInput | undefined = config.auth.captcha + ? { + enabled: + captchaDoc !== undefined + ? legacyEnvOverrideBool( + "SUPABASE_AUTH_CAPTCHA_ENABLED", + config.auth.captcha.enabled ?? false, + "auth.captcha.enabled", + projectEnvValues, + ) + : (config.auth.captcha.enabled ?? false), + provider: + captchaDoc !== undefined + ? envOverride( + "SUPABASE_AUTH_CAPTCHA_PROVIDER", + config.auth.captcha.provider, + projectEnvValues, + ) + : config.auth.captcha.provider, + secret: + captchaDoc !== undefined + ? envOverride( + "SUPABASE_AUTH_CAPTCHA_SECRET", + config.auth.captcha.secret, + projectEnvValues, + ) + : config.auth.captcha.secret, + } + : undefined; + const signingKey = + authEnabled && signingKeysPath !== undefined && signingKeysPath.length > 0 + ? loadFirstSigningKey(workdir, signingKeysPath) + : undefined; + // Go's `Config.Validate` runs passkey/webauthn validation, then + // `Auth.Hook.validate()`, then `Auth.MFA.validate()`, then + // `Auth.Email.validate()`, then `Auth.Sms.validate()`/`Auth.ThirdParty.validate()` (skipping + // the D-only `external` step, ported separately below), all right after the signing-keys read + // and still inside `if c.Auth.Enabled` (`pkg/config/config.go:1117-1153`). Sms + // (`config.go:1145-1147`/`1348-1417`) is enforced at decode time by `@supabase/config`'s `sms` + // schema (`packages/config/src/auth/sms.ts`'s provider-switch check) for the TOML-only case, + // AND re-checked here post-env-override by {@link validateAuthSmsProviders} (called alongside + // {@link validateAuthExternalProviders}, after the single `legacyValidateResolvedConfig` call + // below) — see that function's doc comment for why both are needed. External + // (`config.go:1148-1150`/`1419-1451`) is D-only per `legacy-config-validate.ts`'s module + // header; {@link validateAuthExternalProviders} ports D's identical inline check. This block + // only ACCUMULATES the inputs those checks need — the checks themselves run once, later, as + // part of the single `legacyValidateResolvedConfig` call below. + let authInput: LegacyAuthInput | undefined; + if (authEnabled) { + // `@supabase/config`'s auth schema has no `passkey`/`webauthn` fields at all (see + // `config-sync/auth.sync.ts`'s "not in `@supabase/config` schema" note), so passkey/webauthn + // are read from the RAW, post-`env()`-interpolation TOML document (`authDocument`, hoisted + // above) instead of the decoded `ProjectConfig` — same document-based approach already used + // on the `db`/migration config-load path (`legacy-db-config.toml-read.ts`'s + // `legacyValidateAuthConfig`, section A6). `authDocument` is `undefined` when a caller hasn't + // threaded `document` through yet, in which case passkey/smtp presence-based checks are + // simply skipped rather than guessed at. + const passkeyDoc = asRecord(authDocument?.["passkey"]); + const webauthnDoc = asRecord(authDocument?.["webauthn"]); + // `auth.passkey.enabled`/`auth.webauthn.*` are Viper-bound like every other nested field once + // `[auth.passkey]`/`[auth.webauthn]` are present in config.toml (`ExperimentalBindStruct`/ + // `AutomaticEnv`, `config.go:581-586`), so `SUPABASE_AUTH_PASSKEY_ENABLED` and + // `SUPABASE_AUTH_WEBAUTHN_RP_ID`/`_RP_ORIGINS` overrides apply before `Auth.Passkey`/ + // `Auth.Webauthn` validation runs (`config.go:1117-1134`). Gated on the raw section already + // being present (`passkeyDoc`/`webauthnDoc !== undefined`), matching Go's `AutomaticEnv` + // (which only intercepts keys already present in the merged config) — an absent + // `[auth.passkey]`/`[auth.webauthn]` section is never synthesized from an env override alone. + const passkeyEnabled = + passkeyDoc !== undefined + ? legacyEnvOverrideBool( + "SUPABASE_AUTH_PASSKEY_ENABLED", + passkeyDoc["enabled"] === true, + "auth.passkey.enabled", + projectEnvValues, + ) + : false; + const rpId = + webauthnDoc !== undefined + ? envOverride( + "SUPABASE_AUTH_WEBAUTHN_RP_ID", + typeof webauthnDoc["rp_id"] === "string" ? webauthnDoc["rp_id"] : undefined, + projectEnvValues, + ) + : undefined; + // Go decodes `rp_origins` (a `[]string`) through the same `StringToSliceHookFunc(",")` + // mapstructure hook as every other Go string-slice field (`config.go:775-784`), so a + // `SUPABASE_AUTH_WEBAUTHN_RP_ORIGINS` override is comma-split the same way. + const rpOriginsOverride = + webauthnDoc !== undefined + ? envOverride("SUPABASE_AUTH_WEBAUTHN_RP_ORIGINS", undefined, projectEnvValues) + : undefined; + const rpOrigins = + rpOriginsOverride !== undefined + ? rpOriginsOverride.split(",") + : Array.isArray(webauthnDoc?.["rp_origins"]) + ? webauthnDoc["rp_origins"] + : undefined; + const passkey: LegacyPasskeyInput | undefined = passkeyEnabled + ? { webauthnPresent: webauthnDoc !== undefined, rpId, rpOrigins } + : undefined; + + // Go's `hook.validate()` fixed iteration order (`pkg/config/config.go:1453-1485`) — only + // enabled hooks are forwarded, in that order. `auth.hook..*` is Viper-bound like every + // other nested field (`ExperimentalBindStruct`/`AutomaticEnv`, `config.go:581-586`), so + // `SUPABASE_AUTH_HOOK__ENABLED`/`_URI`/`_SECRETS` overrides apply before this + // validation runs. `@supabase/config`'s hook schema always decodes a `{ enabled: false }` + // default per type regardless of file presence (`packages/config/src/auth/hooks.ts`'s + // `withDecodingDefaultKey`), which erases the presence signal Go's `AutomaticEnv` needs (it + // only intercepts keys already present in the merged config) — so, like the passkey/webauthn + // overrides above, this reads the raw `[auth.hook.]` document instead to gate the + // override on the section actually being present. + const hookDocument = asRecord(authDocument?.["hook"]); + const hooks: Array = []; + for (const hookType of LEGACY_HOOK_TYPE_ORDER) { + const hook = config.auth.hook[hookType]; + const hookSectionPresent = asRecord(hookDocument?.[hookType]) !== undefined; + const envPrefix = `SUPABASE_AUTH_HOOK_${hookType.toUpperCase()}`; + const hookEnabled = hookSectionPresent + ? legacyEnvOverrideBool( + `${envPrefix}_ENABLED`, + hook.enabled, + `auth.hook.${hookType}.enabled`, + projectEnvValues, + ) + : hook.enabled; + if (hookEnabled) { + hooks.push({ + type: hookType, + uri: + (hookSectionPresent + ? envOverride(`${envPrefix}_URI`, hook.uri, projectEnvValues) + : hook.uri) ?? "", + secrets: + (hookSectionPresent + ? envOverride(`${envPrefix}_SECRETS`, hook.secrets, projectEnvValues) + : hook.secrets) ?? "", + }); + } + } + + // Go's `Auth.MFA` factor fields (`TOTP`/`Phone`/`WebAuthn`) are value-typed structs + // (`pkg/config/auth.go:317-320`), never `nil` — unlike `Auth.Hook`'s pointer-typed fields + // above, `ExperimentalBindStruct` always recurses into them (vendored Viper's + // `decodeStructKeys`/`flattenAndMergeMap`) regardless of whether `[auth.mfa.]` is + // present in config.toml (the default template even leaves `[auth.mfa.web_authn]` commented + // out and it's still overridable), so `SUPABASE_AUTH_MFA__{ENROLL,VERIFY}_ENABLED` + // overrides always apply before `Auth.MFA.validate()` runs (`config.go:1523-1534`) — no + // raw-document presence gate needed here, unlike hooks/smtp above. + const mfa: ReadonlyArray = [ + { + label: "totp", + enrollEnabled: legacyEnvOverrideBool( + "SUPABASE_AUTH_MFA_TOTP_ENROLL_ENABLED", + config.auth.mfa.totp.enroll_enabled, + "auth.mfa.totp.enroll_enabled", + projectEnvValues, + ), + verifyEnabled: legacyEnvOverrideBool( + "SUPABASE_AUTH_MFA_TOTP_VERIFY_ENABLED", + config.auth.mfa.totp.verify_enabled, + "auth.mfa.totp.verify_enabled", + projectEnvValues, + ), + }, + { + label: "phone", + enrollEnabled: legacyEnvOverrideBool( + "SUPABASE_AUTH_MFA_PHONE_ENROLL_ENABLED", + config.auth.mfa.phone.enroll_enabled, + "auth.mfa.phone.enroll_enabled", + projectEnvValues, + ), + verifyEnabled: legacyEnvOverrideBool( + "SUPABASE_AUTH_MFA_PHONE_VERIFY_ENABLED", + config.auth.mfa.phone.verify_enabled, + "auth.mfa.phone.verify_enabled", + projectEnvValues, + ), + }, + { + label: "web_authn", + enrollEnabled: legacyEnvOverrideBool( + "SUPABASE_AUTH_MFA_WEB_AUTHN_ENROLL_ENABLED", + config.auth.mfa.web_authn.enroll_enabled, + "auth.mfa.web_authn.enroll_enabled", + projectEnvValues, + ), + verifyEnabled: legacyEnvOverrideBool( + "SUPABASE_AUTH_MFA_WEB_AUTHN_VERIFY_ENABLED", + config.auth.mfa.web_authn.verify_enabled, + "auth.mfa.web_authn.verify_enabled", + projectEnvValues, + ), + }, + ]; + + // Go's `Config.Validate` runs the email template/notification content read right after + // `Auth.MFA.validate()`, still inside `if c.Auth.Enabled` (`config.go:1142`) — this I/O read + // stays at this exact textual position (see this function's `@throws` doc for why). + readAuthEmailTemplateContent(config.auth.email, workdir, authDocument, projectEnvValues); + + // Go's `[auth.email.smtp]` presence-based `enabled` default (`pkg/config/config.go:743-748`): + // when the TOML table is present but omits `enabled`, Go treats it as `true` — a genuinely + // presence-based default `@supabase/config`'s schema can't see (it always decodes + // `smtp.enabled` to `false` when the key is absent), so this reads the raw `document` too. + // `auth.email.smtp.*` is Viper-bound like every other nested field once `[auth.email.smtp]` + // is present in config.toml (`ExperimentalBindStruct`/`AutomaticEnv`, `config.go:581-586`), + // so `SUPABASE_AUTH_EMAIL_SMTP_ENABLED`/`_HOST`/`_PORT`/`_USER`/`_PASS`/`_ADMIN_EMAIL` + // overrides apply before `Auth.Email.validate` runs (`config.go:1325-1344`) — layered on top + // of the presence-aware raw-document read above, same `envOverride`/`envOverridePort` + // precedent as every other field in this file. + const smtpDoc = asRecord(asRecord(authDocument?.["email"])?.["smtp"]); + const smtp: LegacySmtpInput | undefined = + smtpDoc !== undefined + ? { + enabled: legacyEnvOverrideBool( + "SUPABASE_AUTH_EMAIL_SMTP_ENABLED", + smtpDoc["enabled"] === undefined ? true : smtpDoc["enabled"] === true, + "auth.email.smtp.enabled", + projectEnvValues, + ), + host: + envOverride( + "SUPABASE_AUTH_EMAIL_SMTP_HOST", + typeof smtpDoc["host"] === "string" ? smtpDoc["host"] : "", + projectEnvValues, + ) ?? "", + port: envOverridePort( + "SUPABASE_AUTH_EMAIL_SMTP_PORT", + typeof smtpDoc["port"] === "number" ? smtpDoc["port"] : 0, + "auth.email.smtp.port", + projectEnvValues, + ), + user: + envOverride( + "SUPABASE_AUTH_EMAIL_SMTP_USER", + typeof smtpDoc["user"] === "string" ? smtpDoc["user"] : "", + projectEnvValues, + ) ?? "", + pass: + envOverride( + "SUPABASE_AUTH_EMAIL_SMTP_PASS", + typeof smtpDoc["pass"] === "string" ? smtpDoc["pass"] : "", + projectEnvValues, + ) ?? "", + adminEmail: + envOverride( + "SUPABASE_AUTH_EMAIL_SMTP_ADMIN_EMAIL", + typeof smtpDoc["admin_email"] === "string" ? smtpDoc["admin_email"] : "", + projectEnvValues, + ) ?? "", + } + : undefined; + + // Go's `(tpa *thirdParty) validate()` fixed provider order (`pkg/config/config.go:1635-1683`) + // — only enabled providers are forwarded, in that order. Like `Auth.MFA` above, each provider + // struct (`tpaFirebase`/`tpaAuth0`/`tpaCognito`/`tpaClerk`/`tpaWorkOs`, `auth.go:191-198`) is + // value-typed, so `SUPABASE_AUTH_THIRD_PARTY__*` overrides always apply — including + // `workos`, whose default template omits `[auth.third_party.workos]` entirely — before + // `Auth.ThirdParty.validate()` runs; no raw-document presence gate needed. + const thirdParty: Array = []; + if ( + legacyEnvOverrideBool( + "SUPABASE_AUTH_THIRD_PARTY_FIREBASE_ENABLED", + config.auth.third_party.firebase.enabled, + "auth.third_party.firebase.enabled", + projectEnvValues, + ) + ) { + thirdParty.push({ + provider: "firebase", + requiredField: + envOverride( + "SUPABASE_AUTH_THIRD_PARTY_FIREBASE_PROJECT_ID", + config.auth.third_party.firebase.project_id, + projectEnvValues, + ) ?? "", + }); + } + if ( + legacyEnvOverrideBool( + "SUPABASE_AUTH_THIRD_PARTY_AUTH0_ENABLED", + config.auth.third_party.auth0.enabled, + "auth.third_party.auth0.enabled", + projectEnvValues, + ) + ) { + thirdParty.push({ + provider: "auth0", + requiredField: + envOverride( + "SUPABASE_AUTH_THIRD_PARTY_AUTH0_TENANT", + config.auth.third_party.auth0.tenant, + projectEnvValues, + ) ?? "", + }); + } + if ( + legacyEnvOverrideBool( + "SUPABASE_AUTH_THIRD_PARTY_AWS_COGNITO_ENABLED", + config.auth.third_party.aws_cognito.enabled, + "auth.third_party.aws_cognito.enabled", + projectEnvValues, + ) + ) { + thirdParty.push({ + provider: "cognito", + requiredField: + envOverride( + "SUPABASE_AUTH_THIRD_PARTY_AWS_COGNITO_USER_POOL_ID", + config.auth.third_party.aws_cognito.user_pool_id, + projectEnvValues, + ) ?? "", + cognitoUserPoolRegion: envOverride( + "SUPABASE_AUTH_THIRD_PARTY_AWS_COGNITO_USER_POOL_REGION", + config.auth.third_party.aws_cognito.user_pool_region, + projectEnvValues, + ), + }); + } + if ( + legacyEnvOverrideBool( + "SUPABASE_AUTH_THIRD_PARTY_CLERK_ENABLED", + config.auth.third_party.clerk.enabled, + "auth.third_party.clerk.enabled", + projectEnvValues, + ) + ) { + thirdParty.push({ + provider: "clerk", + requiredField: + envOverride( + "SUPABASE_AUTH_THIRD_PARTY_CLERK_DOMAIN", + config.auth.third_party.clerk.domain, + projectEnvValues, + ) ?? "", + }); + } + if ( + legacyEnvOverrideBool( + "SUPABASE_AUTH_THIRD_PARTY_WORKOS_ENABLED", + config.auth.third_party.workos.enabled, + "auth.third_party.workos.enabled", + projectEnvValues, + ) + ) { + thirdParty.push({ + provider: "workos", + requiredField: + envOverride( + "SUPABASE_AUTH_THIRD_PARTY_WORKOS_ISSUER_URL", + config.auth.third_party.workos.issuer_url, + projectEnvValues, + ) ?? "", + }); + } + + authInput = { + siteUrl: siteUrl ?? "", + captcha: captchaInput, + passkey, + hooks, + mfa, + smtp, + thirdParty, + }; + } + // Go's `Config.Validate` runs `ValidateFunctionSlug` over every `[functions.*]` + // key right after the auth block/`generateAPIKeys`, unconditionally. + const functionSlugs = Object.keys(config.functions); + // Go's `Config.Validate` checks `edge_runtime.deno_version` after the auth + // block and the functions loop (`pkg/config/config.go:1158-1173`), and — + // unlike `studio.port`/`local_smtp.port` above — unconditionally, with no + // `edge_runtime.enabled` gate. + const denoVersion = envOverrideDenoVersion(config.edge_runtime.deno_version, projectEnvValues); + + // Go's `Config.Validate` validates `[analytics]` right after + // `edge_runtime.deno_version` (`pkg/config/config.go:1174-1187`): when + // `analytics.enabled` and `analytics.backend == "bigquery"`, all three GCP + // fields are required, checked in that order, each with its own message. + // Backend-enum validation (rejecting a non-postgres/bigquery value) is + // covered at decode time for the `config.toml`-sourced value by + // `@supabase/config`'s `stringEnum` (`packages/config/src/analytics.ts:17-41`), + // but that schema doesn't see the `SUPABASE_ANALYTICS_BACKEND` env-override + // path — see {@link envOverrideAnalyticsBackend} for that case. + const analyticsEnabled = legacyEnvOverrideBool( + "SUPABASE_ANALYTICS_ENABLED", + config.analytics.enabled, + "analytics.enabled", + projectEnvValues, + ); + const analyticsBackend = envOverrideAnalyticsBackend(config.analytics.backend, projectEnvValues); + const gcpProjectId = envOverride( + "SUPABASE_ANALYTICS_GCP_PROJECT_ID", + config.analytics.gcp_project_id, + projectEnvValues, + ); + const gcpProjectNumber = envOverride( + "SUPABASE_ANALYTICS_GCP_PROJECT_NUMBER", + config.analytics.gcp_project_number, + projectEnvValues, + ); + const gcpJwtPath = envOverride( + "SUPABASE_ANALYTICS_GCP_JWT_PATH", + config.analytics.gcp_jwt_path, + projectEnvValues, + ); + + // Go's `Config.Validate` calls `c.Experimental.validate()` right after the + // analytics/bigquery block and right before returning. The webhooks check is NOT "the user + // disabled a feature" — Go's bool zero-value is `false`, so `e.Webhooks != nil && + // !e.Webhooks.Enabled` rejects ANY present `[experimental.webhooks]` section whose `enabled` + // isn't explicitly `true`, including one where the key is simply omitted; the section exists + // only so it can be turned on, never explicitly off. This hinges on PRESENCE of the TOML + // section, not the decoded `enabled` value — `@supabase/config`'s decode-time default + // (`packages/config/src/experimental.ts`'s `withDecodingDefaultKey(Effect.succeed({}))`) fills + // in `experimental.webhooks = { enabled: false }` on the DECODED `ProjectConfig` even when the + // TOML section is entirely absent — verified empirically, this default-fill erases exactly the + // presence signal this check needs. So this reads `LoadedProjectConfig.document` (the raw, + // pre-default TOML) instead, same as the passkey/smtp checks above. + const experimentalDocument = asRecord(document?.["experimental"]); + const webhooksPresent = asRecord(experimentalDocument?.["webhooks"]) !== undefined; + const webhooksEnabled = config.experimental.webhooks?.enabled === true; + const pgdeltaFormatOptions = config.experimental.pgdelta?.format_options ?? ""; + + // Every PURE Config.Validate check this module/legacy-config-validate.ts jointly own is + // deferred to this single call, positioned here (where the last of those checks ran until + // this commit), in Go's exact relative order against every OTHER pure check. This means a + // config broken in TWO OR MORE independent pure-section ways reports whichever Go considers + // first among the ones broken — unchanged from before. The only real reordering risk is + // between a pure check and one of this function's 3 I/O reads (signing keys, api.tls + // cert/key, email template/notification content) that in THIS function's source sits between + // two pure sections (e.g. the signing-keys read sits between the captcha check above and the + // passkey/hooks/mfa/email/smtp/third_party checks folded into `authInput` above) — that I/O + // read now effectively runs BEFORE those later pure checks rather than interleaved at its + // original relative position. This is the same narrow, accepted, documented tradeoff recorded + // in `legacy-config-validate.ts`'s module header; every existing test constructs exactly one + // validation failure at a time, so it has zero effect on any real test. + const apiInput: LegacyApiInput = { + enabled: apiEnabled, + port: apiPort, + tls: { enabled: apiTlsEnabled, certPath: apiTlsCertPath, keyPath: apiTlsKeyPath }, + }; + const dbInput: LegacyDbInput = { port: dbPort, majorVersion }; + const studioInput: LegacyStudioInput = { + enabled: studioEnabled, + port: studioPort, + apiUrl: studioApiUrl, + }; + const localSmtpInput: LegacyLocalSmtpInput = { enabled: mailpitEnabled, port: mailpitPort }; + const analyticsInput: LegacyAnalyticsInput = { + enabled: analyticsEnabled, + backend: analyticsBackend, + gcpProjectId: gcpProjectId ?? "", + gcpProjectNumber: gcpProjectNumber ?? "", + gcpJwtPath: gcpJwtPath ?? "", + }; + const experimentalInput: LegacyExperimentalInput = { + webhooksPresent, + webhooksEnabled, + pgdeltaFormatOptions, + }; + + const input: LegacyConfigValidationInput = { + projectId: resolvedProjectId, + api: apiInput, + db: dbInput, + storageBucketNames, + studio: studioInput, + localSmtp: localSmtpInput, + auth: authInput, + functionSlugs, + edgeRuntimeDenoVersion: denoVersion, + analytics: analyticsInput, + experimental: experimentalInput, + }; + legacyValidateResolvedConfig(input); + // Both run after the single shared `legacyValidateResolvedConfig` call per the module's + // documented sms/external-vs-third_party ordering tradeoff (third_party is checked inside that + // call; sms/external run after it here) — in Go's own relative sms-then-external order + // (`config.go:1145-1150`). `validateAuthSmsProviders` re-runs `@supabase/config`'s schema-level + // switch with env overrides applied (see its doc comment); `validateAuthExternalProviders` is + // D-only per `legacy-config-validate.ts`'s module header ("auth.external ... stays 100% inline + // in D") — this is L's port of D's identical inline block. + if (authEnabled) { + validateAuthSmsProviders(authDocument, projectEnvValues); + validateAuthExternalProviders(authDocument, projectEnvValues); + } + + return { + apiUrl: apiExternalUrl, + restUrl: apiUrlWithPath(apiExternalUrl, "/rest/v1"), + graphqlUrl: apiUrlWithPath(apiExternalUrl, "/graphql/v1"), + functionsUrl: apiUrlWithPath(apiExternalUrl, "/functions/v1"), + mcpUrl: apiUrlWithPath(apiExternalUrl, "/mcp"), + studioUrl: `http://${hostname}:${studioPort}`, + mailpitUrl: `http://${hostname}:${mailpitPort}`, + dbUrl: `postgresql://postgres:${DEFAULT_DB_PASSWORD}@${hostname}:${dbPort}/postgres`, + publishableKey: resolveOpaqueKey( + decryptAuthSecret( + envOverride("SUPABASE_AUTH_PUBLISHABLE_KEY", config.auth.publishable_key, projectEnvValues), + projectEnvValues, + ), + defaultPublishableKey, + ), + secretKey: resolveOpaqueKey( + decryptAuthSecret( + envOverride("SUPABASE_AUTH_SECRET_KEY", config.auth.secret_key, projectEnvValues), + projectEnvValues, + ), + defaultSecretKey, + ), + jwtSecret, + anonKey: resolveSignedKey( + decryptAuthSecret( + envOverride("SUPABASE_AUTH_ANON_KEY", config.auth.anon_key, projectEnvValues), + projectEnvValues, + ), + jwtSecret, + signingKey, + "anon", + ), + serviceRoleKey: resolveSignedKey( + decryptAuthSecret( + envOverride( + "SUPABASE_AUTH_SERVICE_ROLE_KEY", + config.auth.service_role_key, + projectEnvValues, + ), + projectEnvValues, + ), + jwtSecret, + signingKey, + "service_role", + ), + storageS3Url: apiUrlWithPath(apiExternalUrl, "/storage/v1/s3"), + storageS3AccessKeyId: DEFAULT_S3_ACCESS_KEY_ID, + storageS3SecretAccessKey: DEFAULT_S3_SECRET_ACCESS_KEY, + storageS3Region: DEFAULT_S3_REGION, + }; +} diff --git a/apps/cli/src/legacy/shared/legacy-local-config-values.unit.test.ts b/apps/cli/src/legacy/shared/legacy-local-config-values.unit.test.ts new file mode 100644 index 0000000000..d3ca495d7c --- /dev/null +++ b/apps/cli/src/legacy/shared/legacy-local-config-values.unit.test.ts @@ -0,0 +1,1716 @@ +import { generateKeyPairSync } from "node:crypto"; +import { mkdirSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; + +import { ProjectConfigSchema, type ProjectConfig } from "@supabase/config"; +import { Schema } from "effect"; +import { importJWK, jwtVerify } from "jose"; +import { afterEach, describe, expect, it } from "vitest"; + +import { useLegacyTempWorkdir } from "../../../tests/helpers/legacy-mocks.ts"; +import { + LegacyInvalidAnalyticsBackendEnvOverrideError, + LegacyInvalidBoolEnvOverrideError, + LegacyInvalidJwtSecretError, + LegacyInvalidPortEnvOverrideError, + legacyResolveLocalConfigValues, +} from "./legacy-local-config-values.ts"; + +const decodeConfig = Schema.decodeUnknownSync(ProjectConfigSchema); +const WORKDIR = "/tmp/legacy-local-config-values-test"; + +function baseConfig(overrides: Record = {}): ProjectConfig { + return decodeConfig({ project_id: "test", ...overrides }); +} + +/** RSA JWK matching Go's `JWK` struct field names (kty/n/e/d/p/q/dp/dq/qi). */ +function generateRsaJwk(): Record { + const { privateKey } = generateKeyPairSync("rsa", { modulusLength: 2048 }); + const jwk = privateKey.export({ format: "jwk" }); + return { ...jwk, alg: "RS256", kid: "test-rsa-kid" }; +} + +function writeSigningKeys(workdir: string, jwks: ReadonlyArray>) { + const supabaseDir = join(workdir, "supabase"); + mkdirSync(supabaseDir, { recursive: true }); + writeFileSync(join(supabaseDir, "signing_keys.json"), JSON.stringify(jwks)); +} + +describe("legacyResolveLocalConfigValues", () => { + it("derives every URL from api.external_url when unset", () => { + const config = baseConfig(); + const values = legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + + expect(values.apiUrl).toBe("http://127.0.0.1:54321"); + expect(values.restUrl).toBe("http://127.0.0.1:54321/rest/v1"); + expect(values.graphqlUrl).toBe("http://127.0.0.1:54321/graphql/v1"); + expect(values.functionsUrl).toBe("http://127.0.0.1:54321/functions/v1"); + expect(values.mcpUrl).toBe("http://127.0.0.1:54321/mcp"); + expect(values.storageS3Url).toBe("http://127.0.0.1:54321/storage/v1/s3"); + expect(values.studioUrl).toBe("http://127.0.0.1:54323"); + expect(values.mailpitUrl).toBe("http://127.0.0.1:54324"); + }); + + it("uses https and the configured port when api.tls.enabled", () => { + const config = baseConfig({ api: { tls: { enabled: true }, port: 54321 } }); + const values = legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + expect(values.apiUrl).toBe("https://127.0.0.1:54321"); + }); + + it("uses api.external_url verbatim when configured", () => { + const config = baseConfig({ api: { external_url: "https://example.test" } }); + const values = legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + expect(values.apiUrl).toBe("https://example.test"); + expect(values.restUrl).toBe("https://example.test/rest/v1"); + }); + + it("brackets an IPv6 hostname when building host:port", () => { + const config = baseConfig(); + const values = legacyResolveLocalConfigValues(config, "::1", WORKDIR); + expect(values.apiUrl).toBe("http://[::1]:54321"); + }); + + it("builds the db URL with the hardcoded postgres password", () => { + const config = baseConfig({ db: { port: 54322 } }); + const values = legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + expect(values.dbUrl).toBe("postgresql://postgres:postgres@127.0.0.1:54322/postgres"); + }); + + it("falls back to the default JWT secret and opaque keys when unset", () => { + const config = baseConfig(); + const values = legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + expect(values.jwtSecret).toBe("super-secret-jwt-token-with-at-least-32-characters-long"); + expect(values.publishableKey).toBe("sb_publishable_ACJWlzQHlZjBrEguHvfOxg_3BJgxAaH"); + expect(values.secretKey).toBe("sb_secret_N7UND0UgjKTVK-Uodkm0Hg_xSvEMPvz"); + }); + + it("uses configured opaque keys verbatim when set", () => { + const config = baseConfig({ + auth: { publishable_key: "sb_publishable_custom", secret_key: "sb_secret_custom" }, + }); + const values = legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + expect(values.publishableKey).toBe("sb_publishable_custom"); + expect(values.secretKey).toBe("sb_secret_custom"); + }); + + it("signs the default anon/service_role JWTs from the resolved secret", () => { + const config = baseConfig(); + const values = legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + // Byte-exact Go-parity shape is covered by legacy-go-jwt.unit.test.ts; here we + // only assert the resolver wires the default secret through to both roles. + const [, anonPayload] = values.anonKey.split("."); + const [, serviceRolePayload] = values.serviceRoleKey.split("."); + expect(JSON.parse(Buffer.from(anonPayload ?? "", "base64url").toString())).toMatchObject({ + role: "anon", + }); + expect(JSON.parse(Buffer.from(serviceRolePayload ?? "", "base64url").toString())).toMatchObject( + { role: "service_role" }, + ); + }); + + it("uses configured anon/service_role keys verbatim when set", () => { + const config = baseConfig({ + auth: { anon_key: "configured-anon", service_role_key: "configured-service-role" }, + }); + const values = legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + expect(values.anonKey).toBe("configured-anon"); + expect(values.serviceRoleKey).toBe("configured-service-role"); + }); + + it("signs anon/service_role JWTs from a configured jwt_secret", () => { + const config = baseConfig({ auth: { jwt_secret: "a".repeat(32) } }); + const values = legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + expect(values.jwtSecret).toBe("a".repeat(32)); + expect(values.anonKey).not.toBe(""); + }); + + it("rejects a configured jwt_secret shorter than 16 characters", () => { + // Go's Config.Validate fails this at config-load time, before any command + // can render output (pkg/config/apikeys.go:45-47) — reproduced as a thrown + // error here rather than silently signing with the too-short secret. + const config = baseConfig({ auth: { jwt_secret: "a".repeat(15) } }); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + LegacyInvalidJwtSecretError, + ); + }); + + describe("encrypted auth secrets", () => { + // Go's test vector (`apps/cli-go/pkg/config/secret_test.go`): this ciphertext + // decrypts to "value" under the keypair below. + const VAULT_PRIVATE_KEY = "7fd7210cef8f331ee8c55897996aaaafd853a2b20a4dc73d6d75759f65d2a7eb"; + const VAULT_ENCRYPTED = + "encrypted:BKiXH15AyRzeohGyUrmB6cGjSklCrrBjdesQlX1VcXo/Xp20Bi2gGZ3AlIqxPQDmjVAALnhZamKnuY73l8Dz1P+BYiZUgxTSLzdCvdYUyVbNekj2UudbdUizBViERtZkuQwZHIv/"; + + afterEach(() => { + delete process.env["DOTENV_PRIVATE_KEY"]; + }); + + it("decrypts an encrypted: jwt_secret when DOTENV_PRIVATE_KEY is set", () => { + // "value" is only 5 characters, shorter than Go's minimum JWT secret length, + // so pad it out the way a real deployment's decrypted secret would be sized. + process.env["DOTENV_PRIVATE_KEY"] = VAULT_PRIVATE_KEY; + const config = baseConfig({ auth: { jwt_secret: VAULT_ENCRYPTED } }); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + LegacyInvalidJwtSecretError, + ); + }); + + it("decrypts an encrypted: publishable_key when DOTENV_PRIVATE_KEY is set", () => { + process.env["DOTENV_PRIVATE_KEY"] = VAULT_PRIVATE_KEY; + const config = baseConfig({ auth: { publishable_key: VAULT_ENCRYPTED } }); + const values = legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + expect(values.publishableKey).toBe("value"); + }); + + it("fails config loading for an encrypted: secret with no private key, matching Go", () => { + // Go aborts the whole command with `failed to parse config: ` rather + // than silently using the ciphertext as literal key material + // (`secret.go:30-73`, `config.go:704`). + const config = baseConfig({ auth: { publishable_key: VAULT_ENCRYPTED } }); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + "failed to parse config: missing private key", + ); + }); + + it("decrypts an encrypted: SUPABASE_AUTH_* env override, not just the config.toml value", () => { + // Go's decrypt hook runs on whatever value reaches the config.Secret field, + // whether it was sourced from config.toml or a Viper env override. + process.env["DOTENV_PRIVATE_KEY"] = VAULT_PRIVATE_KEY; + const config = baseConfig(); + const values = legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR, { + SUPABASE_AUTH_SECRET_KEY: VAULT_ENCRYPTED, + }); + expect(values.secretKey).toBe("value"); + delete process.env["DOTENV_PRIVATE_KEY"]; + }); + }); + + it("rejects an explicit empty project_id, matching Go's Config.Validate", () => { + // Go's Config.Validate checks ProjectId first, before any other field + // (pkg/config/config.go:990-991). The workdir-basename default is merged + // in as a viper default BEFORE config.toml is merged, so an explicit + // `project_id = ""` in the file overwrites that default with the literal + // empty string rather than being treated as absent — Go fails outright. + const config = baseConfig({ project_id: "" }); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + "Missing required field in config: project_id", + ); + }); + + it("does not reject an absent project_id when the workdir basename sanitizes to a non-empty value", () => { + const config = Schema.decodeUnknownSync(ProjectConfigSchema)({}); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).not.toThrow(); + }); + + it("rejects an absent project_id when the workdir basename sanitizes to empty, matching Go", () => { + // Go's `mergeDefaultValues` merges `sanitizeProjectId(filepath.Base(cwd))` in as a viper + // DEFAULT before config.toml is merged (config.go:690-699, via Eject at config.go:561-570) — + // so `c.ProjectId` is never Go's zero value by the time `Validate` runs. A workdir whose + // basename sanitizes to `""` (every character invalid, e.g. `!!!`) therefore still fails + // config loading in Go even with no `project_id` key in the file at all. + const config = Schema.decodeUnknownSync(ProjectConfigSchema)({}); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", "/tmp/!!!")).toThrow( + "Missing required field in config: project_id", + ); + }); + + it("lets SUPABASE_PROJECT_ID override an absent project_id whose basename sanitizes to empty", () => { + const config = Schema.decodeUnknownSync(ProjectConfigSchema)({}); + expect(() => + legacyResolveLocalConfigValues(config, "127.0.0.1", "/tmp/!!!", { + SUPABASE_PROJECT_ID: "env-project", + }), + ).not.toThrow(); + }); + + it("lets SUPABASE_PROJECT_ID override an explicit empty project_id", () => { + // Viper's AutomaticEnv binds SUPABASE_PROJECT_ID with higher precedence + // than config.toml (config.go:529-535), so a non-empty env override must + // win even when the file's project_id is explicitly empty. + const config = baseConfig({ project_id: "" }); + expect(() => + legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR, { + SUPABASE_PROJECT_ID: "env-project", + }), + ).not.toThrow(); + }); + + it("hardcodes the Go-parity local S3 credentials", () => { + const config = baseConfig(); + const values = legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + expect(values.storageS3AccessKeyId).toBe("625729a08b95bf1b7ff351a663f3a23c"); + expect(values.storageS3SecretAccessKey).toBe( + "850181e4652dd023b7a98c58ae0d2d34bd487ee0cc3254aed6eda37307425907", + ); + expect(values.storageS3Region).toBe("local"); + }); + + describe("SUPABASE_AUTH_* env overrides", () => { + const tempRoot = useLegacyTempWorkdir("supabase-signing-keys-env-override-test-"); + + // Go's Config.Load binds Viper with SetEnvPrefix("SUPABASE") + AutomaticEnv() + // (pkg/config/config.go:529-535) — env vars take precedence over config.toml. + const ENV_KEYS = [ + "SUPABASE_AUTH_JWT_SECRET", + "SUPABASE_AUTH_PUBLISHABLE_KEY", + "SUPABASE_AUTH_SECRET_KEY", + "SUPABASE_AUTH_ANON_KEY", + "SUPABASE_AUTH_SERVICE_ROLE_KEY", + "SUPABASE_AUTH_SIGNING_KEYS_PATH", + ] as const; + + afterEach(() => { + for (const key of ENV_KEYS) delete process.env[key]; + }); + + it("overrides jwt_secret even when config.toml sets one", () => { + process.env["SUPABASE_AUTH_JWT_SECRET"] = "b".repeat(32); + const config = baseConfig({ auth: { jwt_secret: "a".repeat(32) } }); + const values = legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + expect(values.jwtSecret).toBe("b".repeat(32)); + }); + + it("overrides publishable_key/secret_key", () => { + process.env["SUPABASE_AUTH_PUBLISHABLE_KEY"] = "env-publishable"; + process.env["SUPABASE_AUTH_SECRET_KEY"] = "env-secret"; + const config = baseConfig({ + auth: { publishable_key: "config-publishable", secret_key: "config-secret" }, + }); + const values = legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + expect(values.publishableKey).toBe("env-publishable"); + expect(values.secretKey).toBe("env-secret"); + }); + + it("overrides anon_key/service_role_key", () => { + process.env["SUPABASE_AUTH_ANON_KEY"] = "env-anon"; + process.env["SUPABASE_AUTH_SERVICE_ROLE_KEY"] = "env-service-role"; + const config = baseConfig({ + auth: { anon_key: "config-anon", service_role_key: "config-service-role" }, + }); + const values = legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + expect(values.anonKey).toBe("env-anon"); + expect(values.serviceRoleKey).toBe("env-service-role"); + }); + + it("treats an empty env var as unset, matching Viper's default", () => { + process.env["SUPABASE_AUTH_JWT_SECRET"] = ""; + const config = baseConfig({ auth: { jwt_secret: "a".repeat(32) } }); + const values = legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + expect(values.jwtSecret).toBe("a".repeat(32)); + }); + + it("still applies the short-secret validation to an env-provided jwt_secret", () => { + process.env["SUPABASE_AUTH_JWT_SECRET"] = "too-short"; + const config = baseConfig(); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + LegacyInvalidJwtSecretError, + ); + }); + + it("overrides signing_keys_path even when config.toml doesn't set one", async () => { + const jwk = generateRsaJwk(); + writeSigningKeys(tempRoot.current, [jwk]); + process.env["SUPABASE_AUTH_SIGNING_KEYS_PATH"] = "signing_keys.json"; + const config = baseConfig(); + const values = legacyResolveLocalConfigValues(config, "127.0.0.1", tempRoot.current); + + const publicJwk = { ...jwk, d: undefined, p: undefined, q: undefined, dp: undefined }; + const publicKey = await importJWK(publicJwk, "RS256"); + const { protectedHeader } = await jwtVerify(values.anonKey, publicKey); + expect(protectedHeader).toMatchObject({ alg: "RS256", kid: "test-rsa-kid" }); + }); + + it("prefers an env-provided signing_keys_path over config.toml's", () => { + const envJwk = { ...generateRsaJwk(), kid: "env-kid" }; + const configJwk = { ...generateRsaJwk(), kid: "config-kid" }; + writeSigningKeys(tempRoot.current, [envJwk]); + const supabaseDir = join(tempRoot.current, "supabase"); + writeFileSync(join(supabaseDir, "other_keys.json"), JSON.stringify([configJwk])); + process.env["SUPABASE_AUTH_SIGNING_KEYS_PATH"] = "signing_keys.json"; + const config = baseConfig({ auth: { signing_keys_path: "other_keys.json" } }); + const values = legacyResolveLocalConfigValues(config, "127.0.0.1", tempRoot.current); + const [header] = values.anonKey.split("."); + expect(JSON.parse(Buffer.from(header ?? "", "base64url").toString())).toMatchObject({ + kid: "env-kid", + }); + }); + }); + + describe("SUPABASE_* env(VAR) indirection (Go's LoadEnvHook)", () => { + // Go's `LoadEnvHook` (`apps/cli-go/pkg/config/decode_hooks.go:15-23`) is + // the first mapstructure decode hook composed into `v.UnmarshalExact` + // (`config.go:749-753,769-772`), so it resolves a nested `env(VAR)` + // reference on ANY string mapstructure decodes into the struct — including + // a `SUPABASE_*` env-override value itself, not just a `config.toml` + // literal. `envOverride`'s callers (string/port/bool fields) must all see + // that same resolution. + const ENV_KEYS = ["SUPABASE_AUTH_JWT_SECRET", "SUPABASE_DB_PORT", "SUPABASE_API_ENABLED"]; + + afterEach(() => { + for (const key of ENV_KEYS) delete process.env[key]; + delete process.env["INDIRECT_JWT_SECRET"]; + delete process.env["INDIRECT_DB_PORT"]; + delete process.env["INDIRECT_API_ENABLED"]; + }); + + it("resolves a string override's env(VAR) indirection", () => { + process.env["SUPABASE_AUTH_JWT_SECRET"] = "env(INDIRECT_JWT_SECRET)"; + process.env["INDIRECT_JWT_SECRET"] = "c".repeat(32); + const config = baseConfig({ auth: { jwt_secret: "a".repeat(32) } }); + const values = legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + expect(values.jwtSecret).toBe("c".repeat(32)); + }); + + it("resolves a port override's env(VAR) indirection", () => { + process.env["SUPABASE_DB_PORT"] = "env(INDIRECT_DB_PORT)"; + process.env["INDIRECT_DB_PORT"] = "54329"; + const config = baseConfig({ db: { port: 54322 } }); + const values = legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + expect(values.dbUrl).toBe("postgresql://postgres:postgres@127.0.0.1:54329/postgres"); + }); + + it("resolves a bool override's env(VAR) indirection", () => { + process.env["SUPABASE_API_ENABLED"] = "env(INDIRECT_API_ENABLED)"; + process.env["INDIRECT_API_ENABLED"] = "false"; + const config = baseConfig({ + api: { enabled: true, tls: { enabled: true, cert_path: "missing-cert.pem" } }, + }); + // If the bool override weren't resolved through the indirection, the + // literal "env(INDIRECT_API_ENABLED)" string would fail Go's + // strconv.ParseBool acceptance set and throw LegacyInvalidBoolEnvOverrideError; + // resolving it to "false" disables api.enabled and skips the TLS check + // that would otherwise throw on the missing cert file. + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).not.toThrow(); + }); + + it("preserves the env(VAR) literal when the indirected var is unset, matching Go", () => { + process.env["SUPABASE_AUTH_JWT_SECRET"] = "env(INDIRECT_JWT_SECRET)"; + const config = baseConfig({ auth: { jwt_secret: "a".repeat(32) } }); + const values = legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + // Go's LoadEnvHook only substitutes when the target var is non-empty + // (`decode_hooks.go:19-24`) — an unset indirection leaves the literal + // `env(VAR)` string, same as an unresolved config.toml-level reference. + expect(values.jwtSecret).toBe("env(INDIRECT_JWT_SECRET)"); + }); + }); + + describe("non-auth SUPABASE_* env overrides", () => { + // Go's Config.Load binds Viper with SetEnvPrefix("SUPABASE") + AutomaticEnv() + // generically across the whole config struct (pkg/config/config.go:529-535), + // not just auth fields — config_test.go:351,1061 exercise this against + // auth.site_url, and status.go's toValues() reads the already-overridden + // utils.Config.* directly, so every port/URL status derives must honor the + // same override. + const ENV_KEYS = [ + "SUPABASE_DB_PORT", + "SUPABASE_STUDIO_PORT", + "SUPABASE_LOCAL_SMTP_PORT", + "SUPABASE_API_PORT", + "SUPABASE_API_EXTERNAL_URL", + "SUPABASE_STUDIO_API_URL", + ] as const; + + afterEach(() => { + for (const key of ENV_KEYS) delete process.env[key]; + }); + + it("overrides db.port for the derived DB URL", () => { + process.env["SUPABASE_DB_PORT"] = "54329"; + const config = baseConfig({ db: { port: 54322 } }); + const values = legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + expect(values.dbUrl).toBe("postgresql://postgres:postgres@127.0.0.1:54329/postgres"); + }); + + it("overrides studio.port for the derived Studio URL", () => { + process.env["SUPABASE_STUDIO_PORT"] = "54330"; + const config = baseConfig({ studio: { port: 54323 } }); + const values = legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + expect(values.studioUrl).toBe("http://127.0.0.1:54330"); + }); + + it("overrides local_smtp.port for the derived Mailpit URL", () => { + process.env["SUPABASE_LOCAL_SMTP_PORT"] = "54331"; + const config = baseConfig({ local_smtp: { port: 54324 } }); + const values = legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + expect(values.mailpitUrl).toBe("http://127.0.0.1:54331"); + }); + + it("overrides api.port for every API-derived URL", () => { + process.env["SUPABASE_API_PORT"] = "54332"; + const config = baseConfig({ api: { port: 54321 } }); + const values = legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + expect(values.apiUrl).toBe("http://127.0.0.1:54332"); + expect(values.restUrl).toBe("http://127.0.0.1:54332/rest/v1"); + }); + + it("overrides api.external_url even when config.toml sets one", () => { + process.env["SUPABASE_API_EXTERNAL_URL"] = "https://env-override.example"; + const config = baseConfig({ api: { external_url: "https://config.example" } }); + const values = legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + expect(values.apiUrl).toBe("https://env-override.example"); + }); + + it("treats an empty non-auth env var as unset, matching Viper's default", () => { + process.env["SUPABASE_DB_PORT"] = ""; + const config = baseConfig({ db: { port: 54322 } }); + const values = legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + expect(values.dbUrl).toBe("postgresql://postgres:postgres@127.0.0.1:54322/postgres"); + }); + + // Go's Config.Load decodes `SUPABASE_*_PORT` overrides as `uint16` via + // Viper's UnmarshalExact (pkg/config/config.go:749-756, WeaklyTypedInput + // decodes the override string with strconv.ParseUint and hard-fails on a + // malformed value) rather than silently producing a `NaN`-laced URL. + it.each([ + "SUPABASE_DB_PORT", + "SUPABASE_STUDIO_PORT", + "SUPABASE_LOCAL_SMTP_PORT", + "SUPABASE_API_PORT", + ] as const)("rejects a malformed %s override instead of producing NaN", (envKey) => { + process.env[envKey] = "abc"; + const config = baseConfig(); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + LegacyInvalidPortEnvOverrideError, + ); + }); + + it("rejects a SUPABASE_DB_PORT override above the uint16 range", () => { + process.env["SUPABASE_DB_PORT"] = "99999"; + const config = baseConfig(); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + LegacyInvalidPortEnvOverrideError, + ); + }); + + // Unlike the malformed/out-of-range cases above (a decode-time hard-fail, + // uniform across all four SUPABASE_*_PORT fields), db.port=0 is a + // Config.Validate-time hard-fail specific to db.port: it has no `enabled` + // gate in Go, unlike api.port/studio.port/local_smtp.port + // (pkg/config/config.go:1006-1009,1031-1032,1070-1073,1081-1084). + it("rejects a zero SUPABASE_DB_PORT override, matching Go's required-field check", () => { + process.env["SUPABASE_DB_PORT"] = "0"; + const config = baseConfig(); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + "Missing required field in config: db.port", + ); + }); + + // Unlike db.port, Go gates the api.port===0 rejection on api.enabled + // (pkg/config/config.go:1006-1008) — api.enabled defaults to true, so a + // configured or env-overridden zero port is rejected by default. + it("rejects a configured api.port of 0 when api is enabled", () => { + const config = baseConfig({ api: { port: 0 } }); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + "Missing required field in config: api.port", + ); + }); + + it("rejects a zero SUPABASE_API_PORT override when api is enabled", () => { + process.env["SUPABASE_API_PORT"] = "0"; + const config = baseConfig(); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + "Missing required field in config: api.port", + ); + }); + + it("does not reject a zero api.port when api is disabled", () => { + const config = baseConfig({ api: { enabled: false, port: 0 } }); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).not.toThrow(); + }); + + // Go gates the studio.port===0 rejection on studio.enabled + // (pkg/config/config.go:1070-1073), same pattern as api.port above. + // studio.enabled defaults to true, so a configured or env-overridden zero + // port is rejected by default. + it("rejects a configured studio.port of 0 when studio is enabled", () => { + const config = baseConfig({ studio: { port: 0 } }); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + "Missing required field in config: studio.port", + ); + }); + + it("rejects a zero SUPABASE_STUDIO_PORT override when studio is enabled", () => { + process.env["SUPABASE_STUDIO_PORT"] = "0"; + const config = baseConfig(); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + "Missing required field in config: studio.port", + ); + }); + + it("does not reject a zero studio.port when studio is disabled", () => { + const config = baseConfig({ studio: { enabled: false, port: 0 } }); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).not.toThrow(); + }); + + // Go's Config.Validate parses studio.api_url with net/url.Parse right + // after the port check, still inside `if c.Studio.Enabled` + // (pkg/config/config.go:1074-1078). + it("rejects a malformed studio.api_url (unterminated IPv6 literal) when studio is enabled", () => { + const config = baseConfig({ studio: { api_url: "http://[::1" } }); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + `Invalid config for studio.api_url: parse "http://[::1": missing ']' in host`, + ); + }); + + it("does not reject a malformed studio.api_url when studio is disabled", () => { + const config = baseConfig({ studio: { enabled: false, api_url: "http://[::1" } }); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).not.toThrow(); + }); + + it("does not throw for the default studio.api_url", () => { + const config = baseConfig(); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).not.toThrow(); + }); + + it("rejects a malformed SUPABASE_STUDIO_API_URL override", () => { + process.env["SUPABASE_STUDIO_API_URL"] = "http://[::1"; + const config = baseConfig(); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + `Invalid config for studio.api_url: parse "http://[::1": missing ']' in host`, + ); + }); + + // Go gates the local_smtp.port===0 rejection on local_smtp.enabled (Go's + // struct field is still named `Inbucket` for the `[local_smtp]` TOML + // section, pkg/config/config.go:235,1081-1083), same pattern as api.port/ + // studio.port above. local_smtp.enabled defaults to true, so a configured + // or env-overridden zero port is rejected by default. + it("rejects a configured local_smtp.port of 0 when local_smtp is enabled", () => { + const config = baseConfig({ local_smtp: { port: 0 } }); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + "Missing required field in config: local_smtp.port", + ); + }); + + it("rejects a zero SUPABASE_LOCAL_SMTP_PORT override when local_smtp is enabled", () => { + process.env["SUPABASE_LOCAL_SMTP_PORT"] = "0"; + const config = baseConfig(); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + "Missing required field in config: local_smtp.port", + ); + }); + + it("does not reject a zero local_smtp.port when local_smtp is disabled", () => { + const config = baseConfig({ local_smtp: { enabled: false, port: 0 } }); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).not.toThrow(); + }); + }); + + describe("db.major_version (required field in config)", () => { + // The pure 0/12/13-17/generic-invalid assertions moved to + // `legacy-config-validate.unit.test.ts` (direct `legacyValidateResolvedConfig` calls) — + // only the SUPABASE_DB_MAJOR_VERSION env-override mechanics stay here. + afterEach(() => { + delete process.env["SUPABASE_DB_MAJOR_VERSION"]; + }); + + it("overrides a valid configured major_version via SUPABASE_DB_MAJOR_VERSION", () => { + process.env["SUPABASE_DB_MAJOR_VERSION"] = "15"; + const config = baseConfig({ db: { major_version: 17 } }); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).not.toThrow(); + }); + + it("rejects an unsupported SUPABASE_DB_MAJOR_VERSION override", () => { + process.env["SUPABASE_DB_MAJOR_VERSION"] = "16"; + const config = baseConfig({ db: { major_version: 17 } }); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + "Failed reading config: Invalid db.major_version: 16.", + ); + }); + + it("rejects a non-numeric SUPABASE_DB_MAJOR_VERSION override", () => { + process.env["SUPABASE_DB_MAJOR_VERSION"] = "abc"; + const config = baseConfig(); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + "Failed reading config: Invalid db.major_version: abc.", + ); + }); + + it("treats an empty SUPABASE_DB_MAJOR_VERSION override as unset, matching Viper's default", () => { + process.env["SUPABASE_DB_MAJOR_VERSION"] = ""; + const config = baseConfig({ db: { major_version: 17 } }); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).not.toThrow(); + }); + }); + + // Go's Config.Validate runs ValidateBucketName over every [storage.buckets.*] + // key right after db.major_version, unconditionally — there is no + // storage.enabled-style gate (pkg/config/config.go:1063-1068). + // + // Moved to `legacy-config-validate.unit.test.ts` (direct `legacyValidateResolvedConfig` + // calls) — this section has no L-specific derivation or env-override mechanics of its own. + + // Go's Config.Validate rejects an invalid edge_runtime.deno_version + // unconditionally — NOT gated on edge_runtime.enabled + // (pkg/config/config.go:1164-1173). + describe("edge_runtime.deno_version (required field in config)", () => { + // The pure 0/1/2/generic-invalid/disabled assertions moved to + // `legacy-config-validate.unit.test.ts` (direct `legacyValidateResolvedConfig` calls) — + // only the SUPABASE_EDGE_RUNTIME_DENO_VERSION env-override mechanics stay here. + afterEach(() => { + delete process.env["SUPABASE_EDGE_RUNTIME_DENO_VERSION"]; + }); + + it("rejects a zero SUPABASE_EDGE_RUNTIME_DENO_VERSION override", () => { + process.env["SUPABASE_EDGE_RUNTIME_DENO_VERSION"] = "0"; + const config = baseConfig({ edge_runtime: { deno_version: 2 } }); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + "Missing required field in config: edge_runtime.deno_version", + ); + }); + + it("rejects an unsupported SUPABASE_EDGE_RUNTIME_DENO_VERSION override", () => { + process.env["SUPABASE_EDGE_RUNTIME_DENO_VERSION"] = "3"; + const config = baseConfig({ edge_runtime: { deno_version: 2 } }); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + "Failed reading config: Invalid edge_runtime.deno_version: 3.", + ); + }); + + it("rejects a non-numeric SUPABASE_EDGE_RUNTIME_DENO_VERSION override", () => { + process.env["SUPABASE_EDGE_RUNTIME_DENO_VERSION"] = "abc"; + const config = baseConfig(); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + "Failed reading config: Invalid edge_runtime.deno_version: abc.", + ); + }); + + it("treats an empty SUPABASE_EDGE_RUNTIME_DENO_VERSION override as unset, matching Viper's default", () => { + process.env["SUPABASE_EDGE_RUNTIME_DENO_VERSION"] = ""; + const config = baseConfig({ edge_runtime: { deno_version: 2 } }); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).not.toThrow(); + }); + }); + + describe("analytics (BigQuery backend required fields)", () => { + // Go's `Config.Validate` validates `[analytics]` right after + // `edge_runtime.deno_version` (`pkg/config/config.go:1174-1187`): when + // `analytics.enabled` and `analytics.backend == "bigquery"`, all three GCP + // fields are required, checked in that order. + // + // The pure required-field/complete/disabled assertions moved to + // `legacy-config-validate.unit.test.ts` (direct `legacyValidateResolvedConfig` calls) — + // only the SUPABASE_ANALYTICS_* env-override mechanics stay here. + afterEach(() => { + delete process.env["SUPABASE_ANALYTICS_ENABLED"]; + delete process.env["SUPABASE_ANALYTICS_BACKEND"]; + delete process.env["SUPABASE_ANALYTICS_GCP_PROJECT_ID"]; + delete process.env["SUPABASE_ANALYTICS_GCP_PROJECT_NUMBER"]; + delete process.env["SUPABASE_ANALYTICS_GCP_JWT_PATH"]; + }); + + it("rejects a bigquery backend enabled only via SUPABASE_ANALYTICS_ENABLED", () => { + process.env["SUPABASE_ANALYTICS_ENABLED"] = "true"; + const config = baseConfig({ analytics: { enabled: false, backend: "bigquery" } }); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + "Missing required field in config: analytics.gcp_project_id", + ); + }); + + it("rejects a bigquery backend selected only via SUPABASE_ANALYTICS_BACKEND", () => { + process.env["SUPABASE_ANALYTICS_BACKEND"] = "bigquery"; + const config = baseConfig({ analytics: { enabled: true, backend: "postgres" } }); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + "Missing required field in config: analytics.gcp_project_id", + ); + }); + + it("accepts env-provided GCP fields overriding empty config.toml values", () => { + process.env["SUPABASE_ANALYTICS_GCP_PROJECT_ID"] = "proj"; + process.env["SUPABASE_ANALYTICS_GCP_PROJECT_NUMBER"] = "123"; + process.env["SUPABASE_ANALYTICS_GCP_JWT_PATH"] = "gcp.json"; + const config = baseConfig({ analytics: { enabled: true, backend: "bigquery" } }); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).not.toThrow(); + }); + + // Go's `LogflareBackend.UnmarshalText` (`config.go:60-65`) hard-rejects any + // `analytics.backend` value outside `postgres`/`bigquery` during the same + // `UnmarshalExact` decode every `SUPABASE_*` override goes through + // (`config.go:749-756`) — a malformed `SUPABASE_ANALYTICS_BACKEND` fails + // config loading outright, same mechanism as the port/bool overrides below. + it("rejects an invalid SUPABASE_ANALYTICS_BACKEND override", () => { + process.env["SUPABASE_ANALYTICS_BACKEND"] = "mysql"; + const config = baseConfig({ analytics: { enabled: true, backend: "postgres" } }); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + LegacyInvalidAnalyticsBackendEnvOverrideError, + ); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + 'Invalid config for analytics.backend: cannot parse "mysql" as one of "postgres", "bigquery"', + ); + }); + }); + + describe("experimental.* (experimental.validate())", () => { + // Go's `(e *experimental) validate()` (`pkg/config/config.go:1846-1854`), + // called right after the analytics/bigquery block and right before + // `Config.Validate` returns — unconditionally, no `enabled` gate of its own. + // + // Every webhooks-presence/enabled combination and the pgdelta format_options JSON checks + // moved to `legacy-config-validate.unit.test.ts` (direct `legacyValidateResolvedConfig` + // calls, setting `experimental.webhooksPresent`/`webhooksEnabled` directly instead of + // deriving them from a raw `document`) — only this document-THREADING-specific case stays + // here, since it exercises this function's own "no document provided" fallback rather than + // a check `legacyValidateResolvedConfig` itself owns. + it("does not throw a present [experimental.webhooks] section without enabled when no document is provided", () => { + // No `document` (5th param) at all — e.g. a caller that hasn't threaded + // `LoadedProjectConfig.document` through yet. The presence-only check + // can't run without it, so it's skipped rather than guessed at; this + // also covers every pre-existing call site/test in this file that + // doesn't pass a 5th argument. + const config = baseConfig({ experimental: { webhooks: {} } }); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).not.toThrow(); + }); + }); + + describe("SUPABASE_API_TLS_ENABLED env override", () => { + // Go applies the Viper-bound `api.tls.enabled` override (config.go:582-586) + // BEFORE deriving the default `api.external_url` scheme (config.go:799-809), + // so an ambient/dotenv override flips http/https even when config.toml says + // otherwise. + afterEach(() => { + delete process.env["SUPABASE_API_TLS_ENABLED"]; + }); + + it("overrides api.tls.enabled from false to true", () => { + process.env["SUPABASE_API_TLS_ENABLED"] = "true"; + const config = baseConfig({ api: { tls: { enabled: false }, port: 54321 } }); + const values = legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + expect(values.apiUrl).toBe("https://127.0.0.1:54321"); + }); + + it("overrides api.tls.enabled from true to false", () => { + process.env["SUPABASE_API_TLS_ENABLED"] = "false"; + const config = baseConfig({ api: { tls: { enabled: true }, port: 54321 } }); + const values = legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + expect(values.apiUrl).toBe("http://127.0.0.1:54321"); + }); + + it("does not override api.tls.enabled once api.external_url is set", () => { + process.env["SUPABASE_API_TLS_ENABLED"] = "true"; + const config = baseConfig({ api: { external_url: "http://config.example" } }); + const values = legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + expect(values.apiUrl).toBe("http://config.example"); + }); + + it("rejects a malformed override instead of falling back to the configured value", () => { + process.env["SUPABASE_API_TLS_ENABLED"] = "not-a-bool"; + const config = baseConfig({ api: { tls: { enabled: true }, port: 54321 } }); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + LegacyInvalidBoolEnvOverrideError, + ); + }); + + it("treats an empty override as unset, matching Viper's default", () => { + process.env["SUPABASE_API_TLS_ENABLED"] = ""; + const config = baseConfig({ api: { tls: { enabled: true }, port: 54321 } }); + const values = legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + expect(values.apiUrl).toBe("https://127.0.0.1:54321"); + }); + }); + + describe("auth.signing_keys_path (asymmetric JWT signing)", () => { + const tempRoot = useLegacyTempWorkdir("supabase-signing-keys-test-"); + + it("signs anon/service_role with the first RS256 key in the file", async () => { + const jwk = generateRsaJwk(); + writeSigningKeys(tempRoot.current, [jwk]); + const config = baseConfig({ auth: { signing_keys_path: "signing_keys.json" } }); + const values = legacyResolveLocalConfigValues(config, "127.0.0.1", tempRoot.current); + + const publicJwk = { ...jwk, d: undefined, p: undefined, q: undefined, dp: undefined }; + const publicKey = await importJWK(publicJwk, "RS256"); + const { payload, protectedHeader } = await jwtVerify(values.anonKey, publicKey); + expect(payload).toMatchObject({ iss: "supabase-demo", role: "anon" }); + expect(protectedHeader).toMatchObject({ alg: "RS256", kid: "test-rsa-kid" }); + + const serviceRole = await jwtVerify(values.serviceRoleKey, publicKey); + expect(serviceRole.payload).toMatchObject({ role: "service_role" }); + }); + + it("resolves a relative signing_keys_path against /supabase", async () => { + const jwk = generateRsaJwk(); + writeSigningKeys(tempRoot.current, [jwk]); + const config = baseConfig({ auth: { signing_keys_path: "./signing_keys.json" } }); + const values = legacyResolveLocalConfigValues(config, "127.0.0.1", tempRoot.current); + expect(values.anonKey.split(".")).toHaveLength(3); + }); + + it("uses an absolute signing_keys_path as-is, without joining the workdir", async () => { + const jwk = generateRsaJwk(); + writeSigningKeys(tempRoot.current, [jwk]); + const absolutePath = join(tempRoot.current, "supabase", "signing_keys.json"); + const config = baseConfig({ auth: { signing_keys_path: absolutePath } }); + const values = legacyResolveLocalConfigValues(config, "127.0.0.1", "/some/unrelated/workdir"); + expect(values.anonKey.split(".")).toHaveLength(3); + }); + + it("still prefers an explicit anon_key/service_role_key over signing keys", () => { + writeSigningKeys(tempRoot.current, [generateRsaJwk()]); + const config = baseConfig({ + auth: { + signing_keys_path: "signing_keys.json", + anon_key: "configured-anon", + service_role_key: "configured-service-role", + }, + }); + const values = legacyResolveLocalConfigValues(config, "127.0.0.1", tempRoot.current); + expect(values.anonKey).toBe("configured-anon"); + expect(values.serviceRoleKey).toBe("configured-service-role"); + }); + + it("falls back to HMAC signing when signing_keys_path resolves to an empty array", () => { + writeSigningKeys(tempRoot.current, []); + const config = baseConfig({ auth: { signing_keys_path: "signing_keys.json" } }); + const values = legacyResolveLocalConfigValues(config, "127.0.0.1", tempRoot.current); + const [, payload] = values.anonKey.split("."); + expect(JSON.parse(Buffer.from(payload ?? "", "base64url").toString())).toMatchObject({ + iss: "supabase-demo", + }); + }); + + it("throws a Go-worded error when the signing keys file does not exist", () => { + const config = baseConfig({ auth: { signing_keys_path: "missing.json" } }); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", tempRoot.current)).toThrow( + "failed to read signing keys: ", + ); + }); + + it("throws a Go-worded error when the signing keys file is malformed JSON", () => { + const supabaseDir = join(tempRoot.current, "supabase"); + mkdirSync(supabaseDir, { recursive: true }); + writeFileSync(join(supabaseDir, "signing_keys.json"), "not valid json"); + const config = baseConfig({ auth: { signing_keys_path: "signing_keys.json" } }); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", tempRoot.current)).toThrow( + "failed to decode signing keys: ", + ); + }); + + it("throws when the first key uses an unsupported algorithm", () => { + writeSigningKeys(tempRoot.current, [{ ...generateRsaJwk(), alg: "RS512" }]); + const config = baseConfig({ auth: { signing_keys_path: "signing_keys.json" } }); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", tempRoot.current)).toThrow( + "unsupported algorithm: RS512", + ); + }); + + // Go's `Validate` only opens/parses `signing_keys_path` inside + // `if c.Auth.Enabled` (`pkg/config/config.go:1036,1059-1065`) — a disabled + // auth section never touches the file, however stale or missing it is. + it("skips reading a missing signing_keys_path when auth is disabled", () => { + const config = baseConfig({ + auth: { enabled: false, signing_keys_path: "missing.json" }, + }); + expect(() => + legacyResolveLocalConfigValues(config, "127.0.0.1", tempRoot.current), + ).not.toThrow(); + }); + + it("skips reading a malformed signing_keys_path when auth is disabled", () => { + const supabaseDir = join(tempRoot.current, "supabase"); + mkdirSync(supabaseDir, { recursive: true }); + writeFileSync(join(supabaseDir, "signing_keys.json"), "not valid json"); + const config = baseConfig({ + auth: { enabled: false, signing_keys_path: "signing_keys.json" }, + }); + const values = legacyResolveLocalConfigValues(config, "127.0.0.1", tempRoot.current); + // Falls back to HMAC signing, matching an absent signing key. + const [, payload] = values.anonKey.split("."); + expect(JSON.parse(Buffer.from(payload ?? "", "base64url").toString())).toMatchObject({ + iss: "supabase-demo", + }); + }); + + describe("SUPABASE_AUTH_ENABLED env override", () => { + // `c.Auth.Enabled` is Viper-bound like any other field + // (config.go:582-586), so `Validate`'s `if c.Auth.Enabled` gate + // (config.go:1036,1059-1065) reads the POST-override value, not raw + // TOML — a stale/missing signing_keys_path must be skipped when auth is + // disabled only via env/dotenv, and read when auth is enabled only via + // env/dotenv despite TOML saying otherwise. + afterEach(() => { + delete process.env["SUPABASE_AUTH_ENABLED"]; + }); + + it("skips reading a missing signing_keys_path when auth is disabled only via env", () => { + process.env["SUPABASE_AUTH_ENABLED"] = "false"; + const config = baseConfig({ + auth: { enabled: true, signing_keys_path: "missing.json" }, + }); + expect(() => + legacyResolveLocalConfigValues(config, "127.0.0.1", tempRoot.current), + ).not.toThrow(); + }); + + it("reads signing_keys_path when auth is enabled only via env despite TOML saying disabled", async () => { + process.env["SUPABASE_AUTH_ENABLED"] = "true"; + const jwk = generateRsaJwk(); + writeSigningKeys(tempRoot.current, [jwk]); + const config = baseConfig({ + auth: { enabled: false, signing_keys_path: "signing_keys.json" }, + }); + const values = legacyResolveLocalConfigValues(config, "127.0.0.1", tempRoot.current); + expect(values.anonKey.split(".")).toHaveLength(3); + }); + + it("rejects a malformed override instead of falling back to the configured value", () => { + process.env["SUPABASE_AUTH_ENABLED"] = "not-a-bool"; + const config = baseConfig({ + auth: { enabled: false, signing_keys_path: "missing.json" }, + }); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", tempRoot.current)).toThrow( + LegacyInvalidBoolEnvOverrideError, + ); + }); + }); + }); + + describe("auth.site_url (required field in config)", () => { + // The pure empty/set/disabled assertions moved to `legacy-config-validate.unit.test.ts` + // (direct `legacyValidateResolvedConfig` calls) — only the SUPABASE_AUTH_ENABLED / + // SUPABASE_AUTH_SITE_URL env-override mechanics stay here. + describe("SUPABASE_AUTH_ENABLED / SUPABASE_AUTH_SITE_URL env overrides", () => { + afterEach(() => { + delete process.env["SUPABASE_AUTH_ENABLED"]; + delete process.env["SUPABASE_AUTH_SITE_URL"]; + }); + + it("rejects an empty site_url when auth is enabled only via env", () => { + process.env["SUPABASE_AUTH_ENABLED"] = "true"; + const config = baseConfig({ auth: { enabled: false, site_url: "" } }); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + "Missing required field in config: auth.site_url", + ); + }); + + it("does not throw when auth is disabled only via env, however empty site_url is", () => { + process.env["SUPABASE_AUTH_ENABLED"] = "false"; + const config = baseConfig({ auth: { enabled: true, site_url: "" } }); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).not.toThrow(); + }); + + it("accepts an env-provided site_url overriding an empty config.toml value", () => { + process.env["SUPABASE_AUTH_SITE_URL"] = "http://localhost:4000"; + const config = baseConfig({ auth: { enabled: true, site_url: "" } }); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).not.toThrow(); + }); + }); + }); + + // auth.captcha/passkey/webauthn/hook/smtp REQUIRED-FIELD checks (the actual `enabled` ⇒ + // provider/secret/uri/host/etc. logic) live entirely in `legacy-config-validate.unit.test.ts` + // (direct `legacyValidateResolvedConfig` calls). Only the SUPABASE_*-env-override MECHANICS + // this resolver owns — layering an env/dotenv value on top of the TOML-decoded or + // raw-document-derived value before that validation ever runs — are tested here, same split as + // `auth.site_url` above. + + describe("auth.captcha env overrides", () => { + // `auth.captcha.*` is Viper-bound like any other nested field once `[auth.captcha]` is + // present in config.toml (`ExperimentalBindStruct`/`AutomaticEnv`, `config.go:581-586`). + afterEach(() => { + delete process.env["SUPABASE_AUTH_CAPTCHA_ENABLED"]; + delete process.env["SUPABASE_AUTH_CAPTCHA_PROVIDER"]; + delete process.env["SUPABASE_AUTH_CAPTCHA_SECRET"]; + }); + + it("rejects a captcha section enabled only via env with no provider", () => { + process.env["SUPABASE_AUTH_CAPTCHA_ENABLED"] = "true"; + const config = baseConfig({ auth: { captcha: { enabled: false } } }); + const document = { auth: { captcha: { enabled: false } } }; + expect(() => + legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document), + ).toThrow("Missing required field in config: auth.captcha.provider"); + }); + + it("does not throw when an incomplete enabled captcha section is disabled only via env", () => { + process.env["SUPABASE_AUTH_CAPTCHA_ENABLED"] = "false"; + const config = baseConfig({ auth: { captcha: { enabled: true } } }); + const document = { auth: { captcha: { enabled: true } } }; + expect(() => + legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document), + ).not.toThrow(); + }); + + it("accepts env-provided provider/secret overriding an enabled captcha section", () => { + process.env["SUPABASE_AUTH_CAPTCHA_PROVIDER"] = "hcaptcha"; + process.env["SUPABASE_AUTH_CAPTCHA_SECRET"] = "shh"; + const config = baseConfig({ auth: { captcha: { enabled: true } } }); + const document = { auth: { captcha: { enabled: true } } }; + expect(() => + legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document), + ).not.toThrow(); + }); + + it("does not synthesize a captcha section purely from an env override when [auth.captcha] is absent", () => { + process.env["SUPABASE_AUTH_CAPTCHA_ENABLED"] = "true"; + const config = baseConfig(); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).not.toThrow(); + }); + }); + + describe("auth.passkey / auth.webauthn env overrides", () => { + // `auth.passkey.enabled`/`auth.webauthn.*` are Viper-bound like any other nested field once + // `[auth.passkey]`/`[auth.webauthn]` are present in config.toml. Both are read from the raw + // `document` (5th param), same as the presence-based defaulting above, so these tests thread + // a `document` object through explicitly instead of relying on `baseConfig`'s decoded schema + // (which has no `passkey`/`webauthn` fields at all). + afterEach(() => { + delete process.env["SUPABASE_AUTH_PASSKEY_ENABLED"]; + delete process.env["SUPABASE_AUTH_WEBAUTHN_RP_ID"]; + delete process.env["SUPABASE_AUTH_WEBAUTHN_RP_ORIGINS"]; + }); + + it("rejects a passkey section enabled only via env with no [auth.webauthn] section", () => { + process.env["SUPABASE_AUTH_PASSKEY_ENABLED"] = "true"; + const config = baseConfig(); + const document = { auth: { passkey: { enabled: false } } }; + expect(() => + legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document), + ).toThrow( + "Missing required config section: auth.webauthn (required when auth.passkey.enabled is true)", + ); + }); + + it("accepts env-provided rp_id/rp_origins overriding an incomplete [auth.webauthn] section", () => { + process.env["SUPABASE_AUTH_PASSKEY_ENABLED"] = "true"; + process.env["SUPABASE_AUTH_WEBAUTHN_RP_ID"] = "localhost"; + process.env["SUPABASE_AUTH_WEBAUTHN_RP_ORIGINS"] = + "http://localhost:3000,http://localhost:3001"; + const config = baseConfig(); + const document = { auth: { passkey: { enabled: false }, webauthn: {} } }; + expect(() => + legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document), + ).not.toThrow(); + }); + + it("does not synthesize a passkey section purely from an env override when [auth.passkey] is absent from the document", () => { + process.env["SUPABASE_AUTH_PASSKEY_ENABLED"] = "true"; + const config = baseConfig(); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).not.toThrow(); + }); + }); + + describe("auth.hook.* env overrides", () => { + // `auth.hook..*` is Viper-bound like any other nested field once `[auth.hook.]` + // is present in config.toml. `@supabase/config`'s hook schema always decodes a default + // `{ enabled: false }` regardless of file presence, so — like passkey/webauthn above — the + // presence gate is read from the raw `document`, not the decoded `config`. + afterEach(() => { + delete process.env["SUPABASE_AUTH_HOOK_SEND_EMAIL_ENABLED"]; + delete process.env["SUPABASE_AUTH_HOOK_SEND_EMAIL_URI"]; + delete process.env["SUPABASE_AUTH_HOOK_SEND_EMAIL_SECRETS"]; + }); + + it("rejects a hook section enabled only via env with no uri", () => { + process.env["SUPABASE_AUTH_HOOK_SEND_EMAIL_ENABLED"] = "true"; + const config = baseConfig(); + const document = { auth: { hook: { send_email: { enabled: false } } } }; + expect(() => + legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document), + ).toThrow("Missing required field in config: auth.hook.send_email.uri"); + }); + + it("accepts an env-provided uri overriding a TOML-enabled hook missing its uri", () => { + process.env["SUPABASE_AUTH_HOOK_SEND_EMAIL_URI"] = "pg-functions://postgres/auth/hook"; + const config = baseConfig({ auth: { hook: { send_email: { enabled: true } } } }); + const document = { auth: { hook: { send_email: { enabled: true } } } }; + expect(() => + legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document), + ).not.toThrow(); + }); + + it("does not synthesize a hook enablement purely from an env override when the section is absent from the document", () => { + process.env["SUPABASE_AUTH_HOOK_SEND_EMAIL_ENABLED"] = "true"; + const config = baseConfig(); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).not.toThrow(); + }); + }); + + describe("auth.email.smtp env overrides", () => { + // `auth.email.smtp.*` is Viper-bound like any other nested field once `[auth.email.smtp]` + // is present in config.toml — layered on top of the presence-aware raw-document read that + // already exists here for Go's presence-based `enabled` default. + afterEach(() => { + delete process.env["SUPABASE_AUTH_EMAIL_SMTP_ENABLED"]; + delete process.env["SUPABASE_AUTH_EMAIL_SMTP_HOST"]; + delete process.env["SUPABASE_AUTH_EMAIL_SMTP_PORT"]; + delete process.env["SUPABASE_AUTH_EMAIL_SMTP_USER"]; + delete process.env["SUPABASE_AUTH_EMAIL_SMTP_PASS"]; + delete process.env["SUPABASE_AUTH_EMAIL_SMTP_ADMIN_EMAIL"]; + }); + + it("rejects an smtp section enabled only via env with no host", () => { + process.env["SUPABASE_AUTH_EMAIL_SMTP_ENABLED"] = "true"; + const config = baseConfig(); + const document = { auth: { email: { smtp: { enabled: false } } } }; + expect(() => + legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document), + ).toThrow("Missing required field in config: auth.email.smtp.host"); + }); + + it("accepts env-provided host/port/user/pass/admin_email overriding an enabled-but-incomplete smtp section", () => { + process.env["SUPABASE_AUTH_EMAIL_SMTP_HOST"] = "smtp.example.com"; + process.env["SUPABASE_AUTH_EMAIL_SMTP_PORT"] = "587"; + process.env["SUPABASE_AUTH_EMAIL_SMTP_USER"] = "user"; + process.env["SUPABASE_AUTH_EMAIL_SMTP_PASS"] = "pass"; + process.env["SUPABASE_AUTH_EMAIL_SMTP_ADMIN_EMAIL"] = "admin@example.com"; + const config = baseConfig(); + const document = { auth: { email: { smtp: { enabled: true } } } }; + expect(() => + legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document), + ).not.toThrow(); + }); + + it("rejects an invalid SUPABASE_AUTH_EMAIL_SMTP_PORT override", () => { + process.env["SUPABASE_AUTH_EMAIL_SMTP_HOST"] = "smtp.example.com"; + process.env["SUPABASE_AUTH_EMAIL_SMTP_PORT"] = "not-a-port"; + process.env["SUPABASE_AUTH_EMAIL_SMTP_USER"] = "user"; + process.env["SUPABASE_AUTH_EMAIL_SMTP_PASS"] = "pass"; + process.env["SUPABASE_AUTH_EMAIL_SMTP_ADMIN_EMAIL"] = "admin@example.com"; + const config = baseConfig(); + const document = { auth: { email: { smtp: { enabled: true } } } }; + expect(() => + legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document), + ).toThrow(LegacyInvalidPortEnvOverrideError); + }); + + it("does not synthesize an smtp section purely from an env override when [auth.email.smtp] is absent from the document", () => { + process.env["SUPABASE_AUTH_EMAIL_SMTP_ENABLED"] = "true"; + const config = baseConfig(); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).not.toThrow(); + }); + }); + + describe("auth.mfa env overrides", () => { + // `auth.mfa..*` is Viper-bound unconditionally (value-typed struct fields, never + // `nil`) — unlike hooks/smtp above, no raw-document presence gate is needed; see the block + // comment above the `mfa` array in legacy-local-config-values.ts. + afterEach(() => { + delete process.env["SUPABASE_AUTH_MFA_TOTP_ENROLL_ENABLED"]; + delete process.env["SUPABASE_AUTH_MFA_TOTP_VERIFY_ENABLED"]; + }); + + it("rejects an env-enabled enroll factor left at its TOML-decoded verify default", () => { + process.env["SUPABASE_AUTH_MFA_TOTP_ENROLL_ENABLED"] = "true"; + const config = baseConfig(); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + "Invalid MFA config: auth.mfa.totp.enroll_enabled requires verify_enabled", + ); + }); + + it("accepts an env-enabled enroll factor when verify is also env-enabled", () => { + process.env["SUPABASE_AUTH_MFA_TOTP_ENROLL_ENABLED"] = "true"; + process.env["SUPABASE_AUTH_MFA_TOTP_VERIFY_ENABLED"] = "true"; + const config = baseConfig(); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).not.toThrow(); + }); + + it("rejects a malformed SUPABASE_AUTH_MFA_TOTP_ENROLL_ENABLED override", () => { + process.env["SUPABASE_AUTH_MFA_TOTP_ENROLL_ENABLED"] = "not-a-bool"; + const config = baseConfig(); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + LegacyInvalidBoolEnvOverrideError, + ); + }); + }); + + describe("auth.third_party env overrides", () => { + // Same value-typed-struct reasoning as auth.mfa above — including `workos`, whose default + // template omits `[auth.third_party.workos]` entirely yet is still unconditionally overridable. + afterEach(() => { + delete process.env["SUPABASE_AUTH_THIRD_PARTY_FIREBASE_ENABLED"]; + delete process.env["SUPABASE_AUTH_THIRD_PARTY_FIREBASE_PROJECT_ID"]; + }); + + it("rejects a third-party provider enabled only via env with no required field configured", () => { + process.env["SUPABASE_AUTH_THIRD_PARTY_FIREBASE_ENABLED"] = "true"; + const config = baseConfig(); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + "Invalid config: auth.third_party.firebase is enabled but without a project_id.", + ); + }); + + it("accepts an env-provided project_id overriding a TOML-enabled firebase provider", () => { + process.env["SUPABASE_AUTH_THIRD_PARTY_FIREBASE_PROJECT_ID"] = "my-project"; + const config = baseConfig({ auth: { third_party: { firebase: { enabled: true } } } }); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).not.toThrow(); + }); + + it("does not enable a third-party provider purely from a required-field env override", () => { + process.env["SUPABASE_AUTH_THIRD_PARTY_FIREBASE_PROJECT_ID"] = "my-project"; + const config = baseConfig(); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).not.toThrow(); + }); + }); + + describe("auth.email.template/notification (content_path validation)", () => { + // Go's `(e *email) validate(fsys)` (`pkg/config/config.go:1293-1313`), + // called right after `Auth.MFA.validate()`, still inside `if c.Auth.Enabled`. + const tempRoot = useLegacyTempWorkdir("supabase-email-templates-test-"); + + it("rejects a template content_path pointing at a missing file", () => { + const config = baseConfig({ + auth: { + enabled: true, + site_url: "http://localhost:3000", + email: { template: { invite: { content_path: "missing-invite.html" } } }, + }, + }); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", tempRoot.current)).toThrow( + "Invalid config for auth.email.template.invite.content_path: ", + ); + }); + + it("resolves a relative template content_path against the workdir itself, not /supabase", () => { + writeFileSync(join(tempRoot.current, "invite.html"), ""); + const config = baseConfig({ + auth: { + enabled: true, + site_url: "http://localhost:3000", + email: { template: { invite: { content_path: "invite.html" } } }, + }, + }); + expect(() => + legacyResolveLocalConfigValues(config, "127.0.0.1", tempRoot.current), + ).not.toThrow(); + }); + + it("does not throw a template with no content_path configured", () => { + const config = baseConfig({ + auth: { + enabled: true, + site_url: "http://localhost:3000", + email: { template: { invite: {} } }, + }, + }); + expect(() => + legacyResolveLocalConfigValues(config, "127.0.0.1", tempRoot.current), + ).not.toThrow(); + }); + + it("rejects an enabled notification content_path pointing at a missing file", () => { + const config = baseConfig({ + auth: { + enabled: true, + site_url: "http://localhost:3000", + email: { + notification: { password_changed: { enabled: true, content_path: "missing.html" } }, + }, + }, + }); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", tempRoot.current)).toThrow( + "Invalid config for auth.email.notification.password_changed.content_path: ", + ); + }); + + it("resolves a relative notification content_path against /supabase", () => { + const supabaseDir = join(tempRoot.current, "supabase"); + mkdirSync(supabaseDir, { recursive: true }); + writeFileSync(join(supabaseDir, "pw-changed.html"), ""); + const config = baseConfig({ + auth: { + enabled: true, + site_url: "http://localhost:3000", + email: { + notification: { + password_changed: { enabled: true, content_path: "pw-changed.html" }, + }, + }, + }, + }); + expect(() => + legacyResolveLocalConfigValues(config, "127.0.0.1", tempRoot.current), + ).not.toThrow(); + }); + + it("does not throw a disabled notification's missing content_path", () => { + const config = baseConfig({ + auth: { + enabled: true, + site_url: "http://localhost:3000", + email: { + notification: { + password_changed: { enabled: false, content_path: "missing.html" }, + }, + }, + }, + }); + expect(() => + legacyResolveLocalConfigValues(config, "127.0.0.1", tempRoot.current), + ).not.toThrow(); + }); + + it("does not throw a missing template content_path when auth is disabled", () => { + const config = baseConfig({ + auth: { enabled: false, email: { template: { invite: { content_path: "missing.html" } } } }, + }); + expect(() => + legacyResolveLocalConfigValues(config, "127.0.0.1", tempRoot.current), + ).not.toThrow(); + }); + + // Divergence #2 (see `legacy-config-validate.ts`'s port-plan notes): Go's asymmetric + // content-vs-content_path exclusivity (`config.go:1293-1313`) — a raw `content` key present + // with no `content_path` is an error, not a silent no-op. `@supabase/config`'s schema has no + // `content` field to see, so this only fires when the raw `document` (5th param) carries it. + it("rejects a template content key present without content_path", () => { + const config = baseConfig({ + auth: { + enabled: true, + site_url: "http://localhost:3000", + email: { template: { invite: {} } }, + }, + }); + expect(() => + legacyResolveLocalConfigValues(config, "127.0.0.1", tempRoot.current, undefined, { + auth: { email: { template: { invite: { content: "Hi" } } } }, + }), + ).toThrow( + "Invalid config for auth.email.template.invite.content: please use content_path instead", + ); + }); + }); + + describe("auth.email.template/notification env overrides", () => { + // `auth.email.template..*`/`auth.email.notification..*` are Viper-bound like any + // other nested field once the section is present in config.toml. Unlike hook/passkey, no + // extra raw-document presence gate is needed: `email.template`/`email.notification` are + // `Schema.Record`s, so `Object.entries` on the decoded config already reflects presence. + const tempRoot = useLegacyTempWorkdir("supabase-email-template-env-test-"); + + afterEach(() => { + delete process.env["SUPABASE_AUTH_EMAIL_TEMPLATE_INVITE_CONTENT_PATH"]; + delete process.env["SUPABASE_AUTH_EMAIL_NOTIFICATION_PASSWORD_CHANGED_ENABLED"]; + delete process.env["SUPABASE_AUTH_EMAIL_NOTIFICATION_PASSWORD_CHANGED_CONTENT_PATH"]; + }); + + it("lets an env-provided template content_path override a missing TOML content_path", () => { + writeFileSync(join(tempRoot.current, "invite.html"), ""); + process.env["SUPABASE_AUTH_EMAIL_TEMPLATE_INVITE_CONTENT_PATH"] = "invite.html"; + const config = baseConfig({ + auth: { + enabled: true, + site_url: "http://localhost:3000", + email: { template: { invite: {} } }, + }, + }); + expect(() => + legacyResolveLocalConfigValues(config, "127.0.0.1", tempRoot.current), + ).not.toThrow(); + }); + + it("rejects a notification enabled only via env with a missing content_path file", () => { + // Go applies SUPABASE_AUTH_EMAIL_NOTIFICATION_PASSWORD_CHANGED_ENABLED before + // Auth.Email.validate() decides whether to read content_path — a notification disabled + // in TOML but enabled by env must still be checked. + process.env["SUPABASE_AUTH_EMAIL_NOTIFICATION_PASSWORD_CHANGED_ENABLED"] = "true"; + const config = baseConfig({ + auth: { + enabled: true, + site_url: "http://localhost:3000", + email: { + notification: { password_changed: { enabled: false, content_path: "missing.html" } }, + }, + }, + }); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", tempRoot.current)).toThrow( + "Invalid config for auth.email.notification.password_changed.content_path: ", + ); + }); + + it("does not validate a notification disabled only via env despite a TOML-enabled section", () => { + process.env["SUPABASE_AUTH_EMAIL_NOTIFICATION_PASSWORD_CHANGED_ENABLED"] = "false"; + const config = baseConfig({ + auth: { + enabled: true, + site_url: "http://localhost:3000", + email: { + notification: { password_changed: { enabled: true, content_path: "missing.html" } }, + }, + }, + }); + expect(() => + legacyResolveLocalConfigValues(config, "127.0.0.1", tempRoot.current), + ).not.toThrow(); + }); + + it("lets an env-provided notification content_path override a missing TOML content_path", () => { + const supabaseDir = join(tempRoot.current, "supabase"); + mkdirSync(supabaseDir, { recursive: true }); + writeFileSync(join(supabaseDir, "pw-changed.html"), ""); + process.env["SUPABASE_AUTH_EMAIL_NOTIFICATION_PASSWORD_CHANGED_CONTENT_PATH"] = + "pw-changed.html"; + const config = baseConfig({ + auth: { + enabled: true, + site_url: "http://localhost:3000", + email: { notification: { password_changed: { enabled: true } } }, + }, + }); + expect(() => + legacyResolveLocalConfigValues(config, "127.0.0.1", tempRoot.current), + ).not.toThrow(); + }); + }); + + // auth.third_party.* (thirdParty.validate()) and functions.* (function-slug validation) + // moved entirely to `legacy-config-validate.unit.test.ts` (direct `legacyValidateResolvedConfig` + // calls) — L pre-filters to enabled-only third_party providers and derives function slugs + // directly off `config.functions` with no env-override mechanics of its own for these checks. + + describe("auth.external (external.validate(), D-only, ported to L)", () => { + // `auth.external` is a genuine Go `map[string]provider`, so an unmodeled/arbitrary provider + // name is a legitimate config shape `@supabase/config`'s schema silently drops at decode — + // this check reads the raw `document` (5th param) instead, same as passkey/hook above. + it("rejects an enabled unmodeled external provider missing client_id", () => { + const config = baseConfig(); + const document = { auth: { external: { custom: { enabled: true } } } }; + expect(() => + legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document), + ).toThrow("Missing required field in config: auth.external.custom.client_id"); + }); + + it("rejects an enabled unmodeled external provider missing secret", () => { + const config = baseConfig(); + const document = { + auth: { external: { custom: { enabled: true, client_id: "abc" } } }, + }; + expect(() => + legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document), + ).toThrow("Missing required field in config: auth.external.custom.secret"); + }); + + it("does not require a secret for apple/google providers", () => { + const config = baseConfig(); + const document = { + auth: { external: { apple: { enabled: true, client_id: "abc" } } }, + }; + expect(() => + legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document), + ).not.toThrow(); + }); + + it("skips deprecated linkedin/slack providers", () => { + const config = baseConfig(); + const document = { auth: { external: { slack: { enabled: true } } } }; + expect(() => + legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document), + ).not.toThrow(); + }); + + it("does not validate a disabled unmodeled external provider", () => { + const config = baseConfig(); + const document = { auth: { external: { custom: { enabled: false } } } }; + expect(() => + legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document), + ).not.toThrow(); + }); + + it("skips the check entirely when no document is threaded through", () => { + const config = baseConfig(); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).not.toThrow(); + }); + }); + + describe("auth.external env overrides", () => { + // `auth.external..*` is Viper-bound like any other nested field once + // `[auth.external.]` is present in config.toml — same gap the schema's own + // `requiredWhenEnabled` check has for KNOWN providers too. + afterEach(() => { + delete process.env["SUPABASE_AUTH_EXTERNAL_CUSTOM_ENABLED"]; + delete process.env["SUPABASE_AUTH_EXTERNAL_CUSTOM_CLIENT_ID"]; + delete process.env["SUPABASE_AUTH_EXTERNAL_CUSTOM_SECRET"]; + }); + + it("rejects a provider enabled only via env with no client_id", () => { + process.env["SUPABASE_AUTH_EXTERNAL_CUSTOM_ENABLED"] = "true"; + const config = baseConfig(); + const document = { auth: { external: { custom: { enabled: false } } } }; + expect(() => + legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document), + ).toThrow("Missing required field in config: auth.external.custom.client_id"); + }); + + it("accepts env-provided client_id/secret overriding a TOML-enabled provider missing both", () => { + process.env["SUPABASE_AUTH_EXTERNAL_CUSTOM_CLIENT_ID"] = "abc"; + process.env["SUPABASE_AUTH_EXTERNAL_CUSTOM_SECRET"] = "shh"; + const config = baseConfig(); + const document = { auth: { external: { custom: { enabled: true } } } }; + expect(() => + legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document), + ).not.toThrow(); + }); + + it("does not synthesize a provider purely from an env override when the section is absent from the document", () => { + process.env["SUPABASE_AUTH_EXTERNAL_CUSTOM_ENABLED"] = "true"; + const config = baseConfig(); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).not.toThrow(); + }); + }); + + describe("auth.sms env overrides (provider switch)", () => { + // Go's `(s *sms) validate()` (`pkg/config/config.go:1348-1410`) is a `switch` that validates + // ONLY the first enabled provider in a fixed priority order (twilio, twilio_verify, + // messagebird, textlocal, vonage). `@supabase/config`'s schema already implements this switch + // for the schema-decoded (pre-env-override) TOML value; this re-runs it against the raw + // document with `SUPABASE_AUTH_SMS_*` overrides applied, since the schema never sees them. + afterEach(() => { + delete process.env["SUPABASE_AUTH_SMS_TWILIO_ENABLED"]; + delete process.env["SUPABASE_AUTH_SMS_TWILIO_ACCOUNT_SID"]; + delete process.env["SUPABASE_AUTH_SMS_TWILIO_MESSAGE_SERVICE_SID"]; + delete process.env["SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN"]; + delete process.env["SUPABASE_AUTH_SMS_MESSAGEBIRD_ENABLED"]; + }); + + it("rejects a provider enabled only via env with missing required fields", () => { + process.env["SUPABASE_AUTH_SMS_TWILIO_ENABLED"] = "true"; + const config = baseConfig(); + const document = { auth: { sms: { twilio: { enabled: false } } } }; + expect(() => + legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document), + ).toThrow("Missing required field in config: auth.sms.twilio.account_sid"); + }); + + it("accepts env-provided credentials overriding a TOML-enabled provider missing them", () => { + process.env["SUPABASE_AUTH_SMS_TWILIO_ACCOUNT_SID"] = "AC123"; + process.env["SUPABASE_AUTH_SMS_TWILIO_MESSAGE_SERVICE_SID"] = "MG123"; + process.env["SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN"] = "tok"; + const config = baseConfig(); + const document = { auth: { sms: { twilio: { enabled: true } } } }; + expect(() => + legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document), + ).not.toThrow(); + }); + + it("only validates the first enabled provider in Go's fixed priority order", () => { + // twilio is disabled via env; messagebird becomes the switch winner and is missing its + // required fields — twilio's own (still-missing) fields must never be inspected. + process.env["SUPABASE_AUTH_SMS_TWILIO_ENABLED"] = "false"; + process.env["SUPABASE_AUTH_SMS_MESSAGEBIRD_ENABLED"] = "true"; + const config = baseConfig(); + const document = { + auth: { sms: { twilio: { enabled: true }, messagebird: { enabled: false } } }, + }; + expect(() => + legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document), + ).toThrow("Missing required field in config: auth.sms.messagebird.originator"); + }); + + it("does not synthesize a provider purely from an env override when the section is absent from the document", () => { + process.env["SUPABASE_AUTH_SMS_TWILIO_ENABLED"] = "true"; + const config = baseConfig(); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).not.toThrow(); + }); + }); + + describe("api.tls (cert/key validation)", () => { + const tempRoot = useLegacyTempWorkdir("supabase-api-tls-test-"); + + function writeTlsFile(workdir: string, name: string, contents = "dummy") { + const supabaseDir = join(workdir, "supabase"); + mkdirSync(supabaseDir, { recursive: true }); + writeFileSync(join(supabaseDir, name), contents); + } + + it("does not throw when tls.enabled with neither cert_path nor key_path set", () => { + // Go's Validate only rejects the "exactly one set" case (config.go:1010-1027); + // tls.enabled with nothing configured still loads. + const config = baseConfig({ api: { tls: { enabled: true } } }); + expect(() => + legacyResolveLocalConfigValues(config, "127.0.0.1", tempRoot.current), + ).not.toThrow(); + }); + + // The "exactly one of cert/key set" presence-only assertions moved to + // `legacy-config-validate.unit.test.ts` (direct `legacyValidateResolvedConfig` calls) — + // the actual file reads below stay here, since I/O is per-caller. + + it("throws a Go-worded error when the configured cert file does not exist", () => { + writeTlsFile(tempRoot.current, "key.pem"); + const config = baseConfig({ + api: { tls: { enabled: true, cert_path: "missing-cert.pem", key_path: "key.pem" } }, + }); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", tempRoot.current)).toThrow( + "failed to read TLS cert: ", + ); + }); + + it("throws a Go-worded error when the configured key file does not exist", () => { + writeTlsFile(tempRoot.current, "cert.pem"); + const config = baseConfig({ + api: { tls: { enabled: true, cert_path: "cert.pem", key_path: "missing-key.pem" } }, + }); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", tempRoot.current)).toThrow( + "failed to read TLS key: ", + ); + }); + + it("succeeds when both cert_path and key_path are readable", () => { + writeTlsFile(tempRoot.current, "cert.pem"); + writeTlsFile(tempRoot.current, "key.pem"); + const config = baseConfig({ + api: { tls: { enabled: true, cert_path: "cert.pem", key_path: "key.pem" } }, + }); + expect(() => + legacyResolveLocalConfigValues(config, "127.0.0.1", tempRoot.current), + ).not.toThrow(); + }); + + it("resolves cert_path/key_path against /supabase unconditionally, no isAbsolute guard", () => { + // Go's `path.Join` (config.go:961-965) absorbs a leading "/" — unlike + // signing_keys_path, which Go DOES guard with filepath.IsAbs. + writeTlsFile(tempRoot.current, "cert.pem"); + writeTlsFile(tempRoot.current, "key.pem"); + const config = baseConfig({ + api: { + tls: { + enabled: true, + cert_path: "/cert.pem", + key_path: "/key.pem", + }, + }, + }); + expect(() => + legacyResolveLocalConfigValues(config, "127.0.0.1", tempRoot.current), + ).not.toThrow(); + }); + + // Go's `Validate` nests the whole TLS branch inside `if c.Api.Enabled` + // (config.go:1006,1010) — a disabled api section never validates cert/key, + // however invalid the pairing. + it("skips TLS validation entirely when api is disabled", () => { + const config = baseConfig({ + api: { enabled: false, tls: { enabled: true, cert_path: "missing-cert.pem" } }, + }); + expect(() => + legacyResolveLocalConfigValues(config, "127.0.0.1", tempRoot.current), + ).not.toThrow(); + }); + + describe("SUPABASE_API_ENABLED / SUPABASE_API_TLS_ENABLED env overrides", () => { + afterEach(() => { + delete process.env["SUPABASE_API_ENABLED"]; + delete process.env["SUPABASE_API_TLS_ENABLED"]; + }); + + it("skips TLS validation when api is disabled only via env", () => { + process.env["SUPABASE_API_ENABLED"] = "false"; + const config = baseConfig({ + api: { enabled: true, tls: { enabled: true, cert_path: "missing-cert.pem" } }, + }); + expect(() => + legacyResolveLocalConfigValues(config, "127.0.0.1", tempRoot.current), + ).not.toThrow(); + }); + + it("validates TLS when enabled only via env despite TOML saying tls.enabled = false", () => { + process.env["SUPABASE_API_TLS_ENABLED"] = "true"; + const config = baseConfig({ + api: { tls: { enabled: false, cert_path: "missing-cert.pem" } }, + }); + expect(() => legacyResolveLocalConfigValues(config, "127.0.0.1", tempRoot.current)).toThrow( + "Missing required field in config: api.tls.key_path", + ); + }); + }); + }); +}); diff --git a/apps/cli/src/legacy/shared/legacy-project-environment.ts b/apps/cli/src/legacy/shared/legacy-project-environment.ts new file mode 100644 index 0000000000..fe1ad08291 --- /dev/null +++ b/apps/cli/src/legacy/shared/legacy-project-environment.ts @@ -0,0 +1,141 @@ +import { existsSync, readFileSync } from "node:fs"; +import { join } from "node:path"; + +import type { ProjectEnvironment } from "@supabase/config"; + +import { parseDotEnv } from "./legacy-dotenv.ts"; + +/** + * Fills the gap between `@supabase/config`'s `loadProjectEnvironment` and Go's + * `loadNestedEnv` (`apps/cli-go/pkg/config/config.go:1169-1190`). Go's version + * walks not just `supabase/` but one directory further, up to the project + * root/workdir (the loop stops once `cwd == filepath.Dir(repoDir)`, i.e. after + * exactly two directories: `supabase/`, then its parent), and at each + * directory calls `loadDefaultEnv` (`config.go:1192-1207`), which loads dotenv + * files chosen by `SUPABASE_ENV` (empty/unset defaults to `"development"`, + * `config.go:1193-1195`): `.env..local`, `.env.local` (skipped when + * `env === "test"`), `.env.`, `.env` — via `godotenv.Load`, which only + * sets a key if it isn't already present in the process environment + * (`godotenv@v1.5.1/godotenv.go:184-204`, `overload: false`). Because + * `godotenv.Load` writes straight into the process env as it goes, the net + * precedence (highest first) is: ambient shell env > `supabase/`-dir dotenv + * files (`.local` variant before non-local, env-specific before bare `.env`) + * > project-root dotenv files (same internal order). + * + * `loadProjectEnvironment` only implements the `supabase/`-dir, plain + * `.env`/`.env.local` half of this (no project-root pass, no `SUPABASE_ENV` + * filename selection) — and it's shared infrastructure used well beyond + * `legacy/` (the `next/` command tree, `secrets set`), so extending its + * file-resolution semantics is out of scope for a `stop`/`status` port. + * Instead, this fills in the missing project-root + `SUPABASE_ENV`-selected + * files locally: `loadProjectEnvironment`'s already-resolved `values` (its + * ambient-wins-over-`supabase/.env`(.local) result) always takes precedence + * over anything discovered here, since it's already correct for the keys it + * knows about. + */ +function candidateDotenvFilenames(env: string): ReadonlyArray { + return [`.env.${env}.local`, ...(env === "test" ? [] : [".env.local"]), `.env.${env}`, ".env"]; +} + +/** + * Minimal dotenv reader for the project-root and `SUPABASE_ENV`-selected extra + * files this module resolves, intentionally not reusing `@supabase/config`'s + * Effect-based `FileSystem` parser: this module stays a plain synchronous + * helper (like `legacy-local-config-values.ts`'s `loadFirstSigningKey`) since + * it only needs a handful of extra files read once per `stop`/`status` + * invocation. Delegates to {@link parseDotEnv} — the same `godotenv`-faithful, + * cursor-based parser `bootstrap`/`legacyReadDbToml` already use — rather than + * a hand-rolled line-by-line scan, so a quoted value spanning physical lines + * (a PEM/private key) parses correctly instead of aborting on what looks like + * a malformed continuation line. + * + * @throws on a line that isn't blank, a comment, or a `KEY=VALUE`/`KEY: VALUE` + * assignment — matching Go's `loadEnvIfExists` (`pkg/config/config.go:1209-1234`), + * which propagates `godotenv.Load`'s parse error up through `loadNestedEnv` and + * fails `Config.Load` before `stop`/`status` touch Docker, rather than silently + * skipping the bad line. + */ +function readDotEnvFile(path: string): Record | undefined { + if (!existsSync(path)) return undefined; + + const contents = readFileSync(path, "utf8"); + try { + return parseDotEnv(contents); + } catch (cause) { + throw new Error( + `failed to parse environment file: ${path} (${cause instanceof Error ? cause.message : String(cause)})`, + ); + } +} + +/** + * Returns the merged env-var map `stop`/`status` should read `SUPABASE_*` + * overrides (project id, auth fields) from — the project-root and + * `SUPABASE_ENV`-selected files `loadProjectEnvironment` doesn't cover, layered + * under only the truly ambient-sourced entries of `projectEnv.values`. + * + * Only `projectEnv`'s AMBIENT entries outrank `merged`: `projectEnv.values` + * also carries plain `supabase/.env`/`.env.local` values it read itself, and + * those are not necessarily higher Go precedence than an env-specific file + * (`.env..local`/`.env.`) `merged` resolved — `loadProjectEnvironment` + * has no notion of `SUPABASE_ENV`-selected filenames, so it can't tell the two + * apart itself. `merged`'s own walk below already re-derives the full file + * precedence, including `supabase/.env`(.local), so only ambient needs to be + * layered back on top (`projectEnv.sources[key] === "ambient"` marks exactly + * those entries — see `loadProjectEnvironment`'s `ProjectEnvironment` shape). + * + * `projectEnv` is `null` whenever `@supabase/config` found no + * `supabase/config.toml`/`config.json` (searching ancestors, or at exactly + * `workdir` when the caller passed `search: false`) — but Go's dotenv loading + * doesn't share that precondition: `Config.Load` calls + * `loadNestedEnv(builder.SupabaseDirPath)` BEFORE it ever opens `config.toml` + * (`pkg/config/config.go:786-793`), and `SupabaseDirPath` is a pure string + * join with no existence check (`NewPathBuilder`, `pkg/config/utils.go:43-48`). + * So a missing/absent config file must not skip dotenv loading — fall back to + * deriving the same two directories directly from `workdir` + * (`/supabase` and `workdir` itself) and read `process.env` itself as + * the ambient layer, since there's no `loadProjectEnvironment` result to + * consult for it in this branch. + */ +export function legacyResolveProjectEnvironmentValues( + projectEnv: ProjectEnvironment | null, + workdir: string, +): Record { + const env = process.env["SUPABASE_ENV"] || "development"; + const filenames = candidateDotenvFilenames(env); + const merged: Record = {}; + + const supabaseDir = projectEnv?.paths.supabaseDir ?? join(workdir, "supabase"); + const projectRoot = projectEnv?.paths.projectRoot ?? workdir; + + // supabase/ dir first, then its parent (the project root) — matching Go's + // directory walk order. Within a directory, `godotenv.Load`'s "never + // override an already-set var" means first-processed-wins, so the plain + // merge below (skip keys already present) reproduces both orderings at once. + for (const dir of [supabaseDir, projectRoot]) { + for (const filename of filenames) { + const parsed = readDotEnvFile(join(dir, filename)); + if (parsed === undefined) continue; + for (const [key, value] of Object.entries(parsed)) { + if (!(key in merged)) merged[key] = value; + } + } + } + + const ambientOverrides: Record = {}; + if (projectEnv !== null) { + for (const [key, value] of Object.entries(projectEnv.values)) { + if (projectEnv.sources[key] === "ambient") { + ambientOverrides[key] = value; + } + } + } else { + for (const [key, value] of Object.entries(process.env)) { + if (value !== undefined) { + ambientOverrides[key] = value; + } + } + } + + return { ...merged, ...ambientOverrides }; +} diff --git a/apps/cli/src/legacy/shared/legacy-project-environment.unit.test.ts b/apps/cli/src/legacy/shared/legacy-project-environment.unit.test.ts new file mode 100644 index 0000000000..8e7e71b826 --- /dev/null +++ b/apps/cli/src/legacy/shared/legacy-project-environment.unit.test.ts @@ -0,0 +1,300 @@ +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import type { ProjectEnvironment } from "@supabase/config"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; + +import { legacyResolveProjectEnvironmentValues } from "./legacy-project-environment.ts"; + +let root: string; +let supabaseDir: string; + +beforeEach(() => { + root = mkdtempSync(join(tmpdir(), "supabase-legacy-project-env-")); + supabaseDir = join(root, "supabase"); + mkdirSync(supabaseDir, { recursive: true }); +}); + +afterEach(() => { + rmSync(root, { recursive: true, force: true }); + delete process.env["SUPABASE_ENV"]; + delete process.env["SUPABASE_PROJECT_ID"]; +}); + +function fakeProjectEnv( + values: Record = {}, + sources: Record = {}, +): ProjectEnvironment { + return { + paths: { + projectRoot: root, + supabaseDir, + configPath: join(supabaseDir, "config.toml"), + envPath: join(supabaseDir, ".env"), + envLocalPath: join(supabaseDir, ".env.local"), + }, + values, + loadedPaths: [], + // Default every given value to "ambient" unless the caller says otherwise — + // matches how most tests use this helper (representing an already-resolved, + // highest-precedence value) without forcing every call site to spell it out. + sources: Object.fromEntries(Object.keys(values).map((key) => [key, sources[key] ?? "ambient"])), + }; +} + +describe("legacyResolveProjectEnvironmentValues", () => { + it("returns just the already-loaded values when no extra dotenv files exist", () => { + const projectEnv = fakeProjectEnv({ SUPABASE_PROJECT_ID: "from-loader" }); + expect(legacyResolveProjectEnvironmentValues(projectEnv, root)).toEqual({ + SUPABASE_PROJECT_ID: "from-loader", + }); + }); + + it("fills in a value from a project-root .env file Go's loadNestedEnv would load", () => { + writeFileSync(join(root, ".env"), "SUPABASE_PROJECT_ID=root-env-project\n"); + const merged = legacyResolveProjectEnvironmentValues(fakeProjectEnv(), root); + expect(merged["SUPABASE_PROJECT_ID"]).toBe("root-env-project"); + }); + + it("prefers a supabase/-dir dotenv file over the same key in a project-root file", () => { + writeFileSync(join(supabaseDir, ".env"), "SUPABASE_PROJECT_ID=supabase-dir-project\n"); + writeFileSync(join(root, ".env"), "SUPABASE_PROJECT_ID=root-dir-project\n"); + const merged = legacyResolveProjectEnvironmentValues(fakeProjectEnv(), root); + expect(merged["SUPABASE_PROJECT_ID"]).toBe("supabase-dir-project"); + }); + + it("lets already-resolved projectEnv.values win over anything discovered locally", () => { + // `projectEnv.values` already reflects loadProjectEnvironment's correct + // ambient-wins-over-supabase/.env(.local) result; a redundant root .env + // entry for the same key must never override it. + writeFileSync(join(root, ".env"), "SUPABASE_PROJECT_ID=root-env-project\n"); + const projectEnv = fakeProjectEnv({ SUPABASE_PROJECT_ID: "ambient-project" }); + const merged = legacyResolveProjectEnvironmentValues(projectEnv, root); + expect(merged["SUPABASE_PROJECT_ID"]).toBe("ambient-project"); + }); + + it("defaults SUPABASE_ENV to development when unset", () => { + writeFileSync(join(root, ".env.development"), "SUPABASE_PROJECT_ID=dev-project\n"); + const merged = legacyResolveProjectEnvironmentValues(fakeProjectEnv(), root); + expect(merged["SUPABASE_PROJECT_ID"]).toBe("dev-project"); + }); + + it("selects the SUPABASE_ENV-named file over the bare .env file", () => { + process.env["SUPABASE_ENV"] = "production"; + writeFileSync(join(root, ".env"), "SUPABASE_PROJECT_ID=bare-env-project\n"); + writeFileSync(join(root, ".env.production"), "SUPABASE_PROJECT_ID=prod-project\n"); + const merged = legacyResolveProjectEnvironmentValues(fakeProjectEnv(), root); + expect(merged["SUPABASE_PROJECT_ID"]).toBe("prod-project"); + }); + + it("prefers the .local variant of the SUPABASE_ENV file over the non-local one", () => { + process.env["SUPABASE_ENV"] = "production"; + writeFileSync(join(root, ".env.production"), "SUPABASE_PROJECT_ID=prod-project\n"); + writeFileSync(join(root, ".env.production.local"), "SUPABASE_PROJECT_ID=prod-local-project\n"); + const merged = legacyResolveProjectEnvironmentValues(fakeProjectEnv(), root); + expect(merged["SUPABASE_PROJECT_ID"]).toBe("prod-local-project"); + }); + + it("skips .env.local when SUPABASE_ENV=test, matching Go's loadDefaultEnv", () => { + process.env["SUPABASE_ENV"] = "test"; + writeFileSync(join(root, ".env.local"), "SUPABASE_PROJECT_ID=local-project\n"); + writeFileSync(join(root, ".env.test"), "SUPABASE_PROJECT_ID=test-project\n"); + const merged = legacyResolveProjectEnvironmentValues(fakeProjectEnv(), root); + expect(merged["SUPABASE_PROJECT_ID"]).toBe("test-project"); + }); + + it("strips quotes the same way the shared dotenv parser does", () => { + writeFileSync(join(root, ".env"), 'SUPABASE_AUTH_JWT_SECRET="a quoted value"\n'); + const merged = legacyResolveProjectEnvironmentValues(fakeProjectEnv(), root); + expect(merged["SUPABASE_AUTH_JWT_SECRET"]).toBe("a quoted value"); + }); + + it("ignores blank lines and comments", () => { + writeFileSync(root + "/.env", "\n# a comment\nSUPABASE_PROJECT_ID=commented-project\n"); + const merged = legacyResolveProjectEnvironmentValues(fakeProjectEnv(), root); + expect(merged["SUPABASE_PROJECT_ID"]).toBe("commented-project"); + }); + + it("preserves a literal # in an unquoted value with no leading whitespace, matching godotenv", () => { + // godotenv only starts an inline comment at a `#` preceded by whitespace + // (`godotenv@v1.5.1/parser.go:144-153`); `foo#bar` keeps the `#` verbatim. + writeFileSync(root + "/.env", "SUPABASE_AUTH_JWT_SECRET=long#secret\n"); + const merged = legacyResolveProjectEnvironmentValues(fakeProjectEnv(), root); + expect(merged["SUPABASE_AUTH_JWT_SECRET"]).toBe("long#secret"); + }); + + it("still truncates an unquoted value at a whitespace-preceded inline comment", () => { + writeFileSync(root + "/.env", "SUPABASE_PROJECT_ID=54323 # local\n"); + const merged = legacyResolveProjectEnvironmentValues(fakeProjectEnv(), root); + expect(merged["SUPABASE_PROJECT_ID"]).toBe("54323"); + }); + + it("strips a trailing comment after a quoted value, matching godotenv", () => { + // godotenv's `extractVarValue` locates the quoted span by scanning forward for the + // closing quote (`godotenv@v1.5.1/parser.go:160-180`) and discards anything after + // it as a comment — the value is `demo`, not the literal `"demo"` a check that + // requires the whole trimmed remainder to end with a quote would produce. + writeFileSync(root + "/.env", 'SUPABASE_PROJECT_ID="demo" # local\n'); + const merged = legacyResolveProjectEnvironmentValues(fakeProjectEnv(), root); + expect(merged["SUPABASE_PROJECT_ID"]).toBe("demo"); + }); + + it("accepts a colon-separated assignment, matching godotenv's YAML-style key/value form", () => { + // godotenv's `locateKeyName` treats `=` and `:` as interchangeable separators + // (`godotenv@v1.5.1/parser.go:90-95`), and the repo's other dotenv parser + // (`packages/config/src/project.ts`'s `parseDotEnv`) already accepts both. + writeFileSync(root + "/.env", "SUPABASE_PROJECT_ID: colon-project\n"); + const merged = legacyResolveProjectEnvironmentValues(fakeProjectEnv(), root); + expect(merged["SUPABASE_PROJECT_ID"]).toBe("colon-project"); + }); + + it("prefers an env-specific file over a same-key value projectEnv.values sourced from a bare .env file", () => { + // `projectEnv.values` has no notion of SUPABASE_ENV-selected filenames, so + // a key it resolved from a plain supabase/.env file is NOT necessarily + // higher Go precedence than a same-named key from `.env..local` — + // only an "ambient" source outranks the file precedence computed locally. + process.env["SUPABASE_ENV"] = "development"; + writeFileSync( + join(supabaseDir, ".env.development.local"), + "SUPABASE_PROJECT_ID=env-specific-project\n", + ); + const projectEnv = fakeProjectEnv( + { SUPABASE_PROJECT_ID: "bare-dotenv-project" }, + { SUPABASE_PROJECT_ID: ".env" }, + ); + const merged = legacyResolveProjectEnvironmentValues(projectEnv, root); + expect(merged["SUPABASE_PROJECT_ID"]).toBe("env-specific-project"); + }); + + it("still lets a truly ambient-sourced value win over any file", () => { + process.env["SUPABASE_ENV"] = "development"; + writeFileSync( + join(supabaseDir, ".env.development.local"), + "SUPABASE_PROJECT_ID=env-specific-project\n", + ); + const projectEnv = fakeProjectEnv( + { SUPABASE_PROJECT_ID: "ambient-project" }, + { SUPABASE_PROJECT_ID: "ambient" }, + ); + const merged = legacyResolveProjectEnvironmentValues(projectEnv, root); + expect(merged["SUPABASE_PROJECT_ID"]).toBe("ambient-project"); + }); + + it("throws on a malformed line, matching Go's loadEnvIfExists propagating godotenv's parse error", () => { + writeFileSync(join(root, ".env"), "not a valid line\n"); + expect(() => legacyResolveProjectEnvironmentValues(fakeProjectEnv(), root)).toThrow( + /failed to parse environment file/, + ); + }); + + it("expands an unquoted $VAR reference to an earlier value in the same file", () => { + // godotenv expands unquoted/double-quoted references while loading + // (`godotenv@v1.5.1/parser.go:157`), so a later key can reuse an earlier one. + writeFileSync(join(root, ".env"), "BASE=demo\nSUPABASE_PROJECT_ID=$BASE\n"); + const merged = legacyResolveProjectEnvironmentValues(fakeProjectEnv(), root); + expect(merged["SUPABASE_PROJECT_ID"]).toBe("demo"); + }); + + it("expands a braced ${VAR} reference in a double-quoted value", () => { + writeFileSync(join(root, ".env"), 'SECRET=shh\nSUPABASE_AUTH_JWT_SECRET="${SECRET}"\n'); + const merged = legacyResolveProjectEnvironmentValues(fakeProjectEnv(), root); + expect(merged["SUPABASE_AUTH_JWT_SECRET"]).toBe("shh"); + }); + + it("does not expand variable references inside single-quoted values", () => { + // godotenv never calls expandVariables for single-quoted values + // (`parser.go:172-173`) — they stay byte-literal. + writeFileSync(join(root, ".env"), "BASE=demo\nSUPABASE_PROJECT_ID='$BASE'\n"); + const merged = legacyResolveProjectEnvironmentValues(fakeProjectEnv(), root); + expect(merged["SUPABASE_PROJECT_ID"]).toBe("$BASE"); + }); + + it("expands an unresolved bare reference to an empty string, matching Go's map zero-value", () => { + writeFileSync(join(root, ".env"), "SUPABASE_PROJECT_ID=$NOPE\n"); + const merged = legacyResolveProjectEnvironmentValues(fakeProjectEnv(), root); + expect(merged["SUPABASE_PROJECT_ID"]).toBe(""); + }); + + it("expands an unresolved braced reference to an empty string, matching Go's map zero-value", () => { + writeFileSync(join(root, ".env"), 'SUPABASE_AUTH_JWT_SECRET="${NOPE}"\n'); + const merged = legacyResolveProjectEnvironmentValues(fakeProjectEnv(), root); + expect(merged["SUPABASE_AUTH_JWT_SECRET"]).toBe(""); + }); + + it("preserves a backslash-escaped $VAR reference as a literal, matching godotenv's escape rule", () => { + // godotenv's expandVarRegex captures a leading backslash and strips ONLY + // that backslash, returning the rest of the match verbatim instead of + // doing a lookup (`godotenv@v1.5.1/parser.go:253,264-265`) — even when + // BASE is defined, `demo\$BASE` must stay `demo$BASE`, not become + // `demodemo`. + writeFileSync(join(root, ".env"), "BASE=demo\nSUPABASE_PROJECT_ID=demo\\$BASE\n"); + const merged = legacyResolveProjectEnvironmentValues(fakeProjectEnv(), root); + expect(merged["SUPABASE_PROJECT_ID"]).toBe("demo$BASE"); + }); + + it("preserves a backslash-escaped ${VAR} reference in a double-quoted value", () => { + writeFileSync(join(root, ".env"), 'BASE=demo\nSUPABASE_PROJECT_ID="demo\\${BASE}"\n'); + const merged = legacyResolveProjectEnvironmentValues(fakeProjectEnv(), root); + expect(merged["SUPABASE_PROJECT_ID"]).toBe("demo${BASE}"); + }); + + it("treats a bare trailing $ with no variable name as a literal", () => { + writeFileSync(join(root, ".env"), "SUPABASE_PROJECT_ID=demo$\n"); + const merged = legacyResolveProjectEnvironmentValues(fakeProjectEnv(), root); + expect(merged["SUPABASE_PROJECT_ID"]).toBe("demo$"); + }); + + it("preserves a multiline quoted value alongside an unrelated SUPABASE_* key (godotenv parity)", () => { + // godotenv's parser scans the whole buffer with a cursor, not line-by-line + // (`godotenv@v1.5.1/parser.go:20-45`), so a quoted value spanning physical + // lines — e.g. a pasted PEM private key — doesn't break parsing of the rest + // of the file. A naive line-by-line reader would see the continuation line + // as malformed and abort before SUPABASE_PROJECT_ID is ever read. + const pem = "-----BEGIN PRIVATE KEY-----\nMIIBogIBAAJ\n-----END PRIVATE KEY-----"; + writeFileSync( + join(root, ".env"), + `PRIVATE_KEY="${pem}"\nSUPABASE_PROJECT_ID=multiline-safe-project\n`, + ); + const merged = legacyResolveProjectEnvironmentValues(fakeProjectEnv(), root); + expect(merged["SUPABASE_PROJECT_ID"]).toBe("multiline-safe-project"); + }); + + describe("when no project was found (projectEnv is null)", () => { + // Go's `loadNestedEnv` runs unconditionally before `config.toml` is ever + // opened (`pkg/config/config.go:786-793`), so a missing config file must + // not skip dotenv loading — these cover the local fallback that derives + // `/supabase`/`workdir` directly instead of giving up. + + it("still reads a supabase/-dir dotenv file directly under workdir", () => { + writeFileSync(join(supabaseDir, ".env"), "SUPABASE_PROJECT_ID=fallback-project\n"); + const merged = legacyResolveProjectEnvironmentValues(null, root); + expect(merged["SUPABASE_PROJECT_ID"]).toBe("fallback-project"); + }); + + it("still reads a project-root dotenv file directly under workdir", () => { + writeFileSync(join(root, ".env"), "SUPABASE_PROJECT_ID=root-fallback-project\n"); + const merged = legacyResolveProjectEnvironmentValues(null, root); + expect(merged["SUPABASE_PROJECT_ID"]).toBe("root-fallback-project"); + }); + + it("prefers the supabase/-dir file over the project-root file, same as the non-null case", () => { + writeFileSync(join(supabaseDir, ".env"), "SUPABASE_PROJECT_ID=supabase-dir-project\n"); + writeFileSync(join(root, ".env"), "SUPABASE_PROJECT_ID=root-dir-project\n"); + const merged = legacyResolveProjectEnvironmentValues(null, root); + expect(merged["SUPABASE_PROJECT_ID"]).toBe("supabase-dir-project"); + }); + + it("lets an ambient shell var win over a dotenv value, using process.env directly", () => { + process.env["SUPABASE_PROJECT_ID"] = "ambient-fallback-project"; + writeFileSync(join(supabaseDir, ".env"), "SUPABASE_PROJECT_ID=dotenv-fallback-project\n"); + const merged = legacyResolveProjectEnvironmentValues(null, root); + expect(merged["SUPABASE_PROJECT_ID"]).toBe("ambient-fallback-project"); + }); + + it("returns an empty object when workdir has no dotenv files and no ambient value", () => { + const merged = legacyResolveProjectEnvironmentValues(null, root); + expect(merged["SUPABASE_PROJECT_ID"]).toBeUndefined(); + }); + }); +}); diff --git a/apps/cli/src/legacy/shared/legacy-seed-buckets.ts b/apps/cli/src/legacy/shared/legacy-seed-buckets.ts index 382d16e6b1..0ec17002b5 100644 --- a/apps/cli/src/legacy/shared/legacy-seed-buckets.ts +++ b/apps/cli/src/legacy/shared/legacy-seed-buckets.ts @@ -144,8 +144,8 @@ export const legacySeedBucketsRun = Effect.fnUntraced(function* (opts: { // Load config.toml, passing projectRef so `[remotes.*]` overrides are merged for // --linked. A parse failure aborts before any network call. - const loadOptions: LoadProjectConfigOptions | undefined = - projectRef !== "" ? { projectRef } : undefined; + const loadOptions: LoadProjectConfigOptions = + projectRef !== "" ? { projectRef, goViperCompat: true } : { goViperCompat: true }; const loaded = yield* loadProjectConfig(cliConfig.workdir, loadOptions).pipe( Effect.catchTag( "ProjectConfigParseError", diff --git a/apps/cli/src/legacy/shared/legacy-storage-credentials.ts b/apps/cli/src/legacy/shared/legacy-storage-credentials.ts index 0826a7de8f..21983e8d15 100644 --- a/apps/cli/src/legacy/shared/legacy-storage-credentials.ts +++ b/apps/cli/src/legacy/shared/legacy-storage-credentials.ts @@ -4,6 +4,7 @@ import { Effect, FileSystem, Path } from "effect"; import { LegacyPlatformApiFactory } from "../auth/legacy-platform-api-factory.service.ts"; import { LegacyCliConfig } from "../config/legacy-cli-config.service.ts"; +import { legacyResolveApiExternalUrl } from "./legacy-api-url.ts"; import { legacyMapTenantApiKeysError } from "./legacy-get-tenant-api-keys.ts"; import { legacyGetHostname } from "./legacy-hostname.ts"; import { legacyExtractServiceKeys } from "./legacy-tenant-keys.ts"; @@ -122,23 +123,11 @@ export const legacyResolveStorageCredentials = Effect.fnUntraced(function* (opts }); /** - * Local API URL, mirroring Go's `config.go:634-644` + `misc.go:298`: an explicit - * `api.external_url` wins, otherwise `://:` where the scheme - * follows `api.tls.enabled`, the host is `legacyGetHostname` (Go's - * `utils.GetHostname`), and the port is `api.port`. + * Local API URL: `legacyResolveApiExternalUrl` with `legacyGetHostname` (Go's + * `utils.GetHostname`) supplying the host when `api.external_url` is unset. */ function resolveLocalBaseUrl(config: LegacyStorageConfigView): string { - if (config.api.external_url !== undefined && config.api.external_url.length > 0) { - return config.api.external_url; - } - const host = legacyGetHostname(); - const scheme = config.api.tls.enabled ? "https" : "http"; - // Go builds host:port with net.JoinHostPort (config.go:636-638), bracketing an - // IPv6 host. legacyGetHostname returns the unbracketed host, so bracket here. - const hostPort = host.includes(":") - ? `[${host}]:${config.api.port}` - : `${host}:${config.api.port}`; - return `${scheme}://${hostPort}`; + return legacyResolveApiExternalUrl(config.api, legacyGetHostname()); } /** diff --git a/apps/cli/src/legacy/shared/legacy-storage-url.ts b/apps/cli/src/legacy/shared/legacy-storage-url.ts index 4401c56387..0810c61872 100644 --- a/apps/cli/src/legacy/shared/legacy-storage-url.ts +++ b/apps/cli/src/legacy/shared/legacy-storage-url.ts @@ -155,6 +155,62 @@ function hostFromAuthority(authority: string): string { return at === -1 ? authority : authority.slice(at + 1); } +/** Go `net/url.validOptionalPort` (`url.go:761-774`): `""`, or `:` followed by only digits. */ +function isValidOptionalPort(port: string): boolean { + if (port.length === 0) return true; + if (port.charCodeAt(0) !== 0x3a /* : */) return false; + for (let i = 1; i < port.length; i++) { + if (!isDigit(port.charCodeAt(i))) return false; + } + return true; +} + +/** + * Go `net/url.parseHost`, restricted to the branches this module's callers + * can actually hit (`net/url/url.go:544-608` in the Go stdlib `apps/cli-go` + * builds against): IP-literal (`[...]`) bracket/port validation, and the bare + * `host[:port]` port-digit check. Deliberately does NOT implement IPv6 + * zone-identifier percent-decoding or `netip.ParseAddr` address validation + * (Go additionally requires a bracketed literal to parse as a valid, + * non-IPv4 IP address) — callers only need to know whether validation + * THROWS, and a config author writing a bogus-but-bracket-balanced IPv6 + * literal is exotic enough that the narrower `netip.ParseAddr` check isn't + * worth the added surface; the common bracket-mismatch/invalid-port mistakes + * Go actually raises for realistic input (e.g. `http://[::1`, a truncated + * IPv6 literal) are still reproduced byte-for-byte. Also skips Go's + * `GODEBUG=urlstrictcolons=0` legacy multi-colon opt-out (`url.go:596-604`) — + * an explicit, non-default env var this codebase's bundled Go binary doesn't + * set, so the http/https "first colon is the port separator" behavior always + * applies for those schemes; other schemes use the last colon, matching Go's + * unconditional `else` branch for non-http(s) schemes. + */ +function validateGoUrlHost(scheme: string, host: string): void { + if (host.length === 0) return; + const openBracketIdx = host.indexOf("["); + if (openBracketIdx > 0) { + throw new Error("invalid IP-literal"); + } + if (openBracketIdx === 0) { + const closeBracketIdx = host.lastIndexOf("]"); + if (closeBracketIdx < 0) { + throw new Error("missing ']' in host"); + } + const colonPort = host.slice(closeBracketIdx + 1); + if (!isValidOptionalPort(colonPort)) { + throw new Error(`invalid port ${JSON.stringify(colonPort)} after host`); + } + return; + } + const colonIdx = + scheme === "http" || scheme === "https" ? host.indexOf(":") : host.lastIndexOf(":"); + if (colonIdx !== -1) { + const colonPort = host.slice(colonIdx); + if (!isValidOptionalPort(colonPort)) { + throw new Error(`invalid port ${JSON.stringify(colonPort)} after host`); + } + } +} + /** * Port of Go's `url.Parse` restricted to `Scheme`/`Host`/`Path`. Throws * `LegacyGoUrlParseError` (Go's `*url.Error`) on the failures the storage @@ -208,6 +264,11 @@ export function legacyGoUrlParse(rawURL: string): LegacyGoUrl { const authority = slash === -1 ? afterSlashes : afterSlashes.slice(0, slash); rest = slash === -1 ? "" : afterSlashes.slice(slash); host = hostFromAuthority(authority); + try { + validateGoUrlHost(scheme, host); + } catch (cause) { + throw new LegacyGoUrlParseError(u, cause instanceof Error ? cause.message : String(cause)); + } } let path: string; diff --git a/apps/cli/src/legacy/shared/legacy-storage-url.unit.test.ts b/apps/cli/src/legacy/shared/legacy-storage-url.unit.test.ts index 5e9643f48e..6100802123 100644 --- a/apps/cli/src/legacy/shared/legacy-storage-url.unit.test.ts +++ b/apps/cli/src/legacy/shared/legacy-storage-url.unit.test.ts @@ -4,6 +4,7 @@ import { LegacyGoUrlParseError, LegacyStorageUrlPatternError, legacyDetectScheme, + legacyGoUrlParse, legacyParseStorageUrl, legacySplitBucketPrefix, legacyStorageIsDir, @@ -85,6 +86,52 @@ describe("legacyDetectScheme", () => { }); }); +// Oracle: `go run` against Go 1.25's `net/url.Parse` directly (net/url/url.go's +// `parseHost`) — used by `studio.api_url` validation, which needs the Host, not +// just Scheme/Path, so the failures below matter beyond the storage commands. +describe("legacyGoUrlParse (host validation)", () => { + it("rejects an unterminated IPv6 literal", () => { + expect(() => legacyGoUrlParse("http://[::1")).toThrow(LegacyGoUrlParseError); + expect(() => legacyGoUrlParse("http://[::1")).toThrow( + `parse "http://[::1": missing ']' in host`, + ); + }); + + it("accepts a bracketed IPv6 literal with no port", () => { + expect(legacyGoUrlParse("http://[::1]").host).toBe("[::1]"); + }); + + it("accepts a bracketed IPv6 literal with a valid port", () => { + expect(legacyGoUrlParse("http://[::1]:8080").host).toBe("[::1]:8080"); + }); + + it("rejects a bracketed IPv6 literal with a non-numeric port", () => { + expect(() => legacyGoUrlParse("http://[::1]:abc")).toThrow( + `parse "http://[::1]:abc": invalid port ":abc" after host`, + ); + }); + + it("rejects a bracket that isn't the first character of the host", () => { + expect(() => legacyGoUrlParse("http://host[::1]")).toThrow( + `parse "http://host[::1]": invalid IP-literal`, + ); + }); + + it("accepts a plain hostname with a numeric port", () => { + expect(legacyGoUrlParse("http://example.com:99999").host).toBe("example.com:99999"); + }); + + it("rejects more than one colon in an http(s) host (strict-colon default)", () => { + expect(() => legacyGoUrlParse("http://host:1:2")).toThrow( + `parse "http://host:1:2": invalid port ":1:2" after host`, + ); + }); + + it("does not validate a host for a scheme with no authority (ss:///bucket)", () => { + expect(() => legacyGoUrlParse("ss:///bucket/x")).not.toThrow(); + }); +}); + describe("legacyStorageIsDir", () => { it.each([ ["", true], diff --git a/apps/cli/src/legacy/shared/legacy-workdir-validation.ts b/apps/cli/src/legacy/shared/legacy-workdir-validation.ts new file mode 100644 index 0000000000..40e8a5976f --- /dev/null +++ b/apps/cli/src/legacy/shared/legacy-workdir-validation.ts @@ -0,0 +1,52 @@ +import { Data, Effect, FileSystem } from "effect"; + +/** + * Raised by {@link legacyValidateWorkdirIsDirectory} when the target path + * doesn't exist or isn't a directory. Callers map this into their own + * command-specific error type. + */ +export class LegacyWorkdirValidationError extends Data.TaggedError("LegacyWorkdirValidationError")<{ + readonly message: string; +}> {} + +/** + * Validates that `workdir` exists and is a directory, the way Go's + * `ChangeWorkDir` implicitly does via `os.Chdir` (`apps/cli-go/internal/utils/ + * misc.go:231-250`, called from `PersistentPreRunE`, `apps/cli-go/cmd/root.go: + * 93-105`, before any command runs): a missing path or a path that isn't a + * directory fails immediately, before config load or any Docker/API access. + * + * Callers that resolve `workdir` via `LegacyCliConfig` only need this check + * when `--workdir`/`SUPABASE_WORKDIR` was set explicitly — `legacy-cli-config. + * layer.ts`'s default walk-up-for-`supabase/config.toml` resolution always + * returns a real, already-existing directory (either one containing + * `supabase/config.toml`, or the process's own `cwd`), so it can never fail + * this check; calling it unconditionally is therefore safe and simpler than + * threading "was this explicit?" through every caller. + */ +export function legacyValidateWorkdirIsDirectory( + workdir: string, + fs: FileSystem.FileSystem, +): Effect.Effect { + return fs.stat(workdir).pipe( + Effect.matchEffect({ + onFailure: (error) => { + const reason = + error.reason._tag === "NotFound" ? "no such file or directory" : error.message; + return Effect.fail( + new LegacyWorkdirValidationError({ + message: `failed to change workdir: chdir ${workdir}: ${reason}`, + }), + ); + }, + onSuccess: (info) => + info.type === "Directory" + ? Effect.void + : Effect.fail( + new LegacyWorkdirValidationError({ + message: `failed to change workdir: chdir ${workdir}: not a directory`, + }), + ), + }), + ); +} diff --git a/apps/cli/src/next/commands/functions/deploy/deploy.handler.ts b/apps/cli/src/next/commands/functions/deploy/deploy.handler.ts index 1b61948324..acd17b7a1b 100644 --- a/apps/cli/src/next/commands/functions/deploy/deploy.handler.ts +++ b/apps/cli/src/next/commands/functions/deploy/deploy.handler.ts @@ -34,6 +34,7 @@ export const functionsDeploy = Effect.fn("functions.deploy")(function* ( projectRoot: projectHome.projectRoot, supabaseDir: projectHome.supabaseDir, dashboardUrl: cliConfig.dashboardUrl, + goViperCompat: false, yes: flags.yes, rawArgs, edgeRuntimeVersion, diff --git a/apps/cli/src/next/commands/functions/dev/functions-dev-config.unit.test.ts b/apps/cli/src/next/commands/functions/dev/functions-dev-config.unit.test.ts index b31bdd35b9..983a7a6a5e 100644 --- a/apps/cli/src/next/commands/functions/dev/functions-dev-config.unit.test.ts +++ b/apps/cli/src/next/commands/functions/dev/functions-dev-config.unit.test.ts @@ -4,7 +4,7 @@ import { mkdtempSync } from "node:fs"; import { mkdir, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { Effect, Layer, Option } from "effect"; +import { Effect, Exit, Layer, Option } from "effect"; import { ProjectHome } from "../../../config/project-home.service.ts"; import { RuntimeInfo } from "../../../../shared/runtime/runtime-info.service.ts"; import { @@ -147,4 +147,79 @@ literal = "literal-secret" Effect.provide(projectLayer(cwd)), ); }); + + it.live("does not resolve a lowercase-named env() reference in edge runtime secrets", () => { + const cwd = makeTempProject(); + + return Effect.gen(function* () { + yield* Effect.tryPromise(() => mkdir(join(cwd, "supabase"), { recursive: true })); + yield* Effect.tryPromise(() => + writeFile(join(cwd, "supabase", ".env"), "lowercase_env_var=should-not-resolve\n"), + ); + yield* Effect.tryPromise(() => + writeFile( + join(cwd, "supabase", "config.toml"), + `project_id = "test" + +[edge_runtime] +policy = "oneshot" +inspector_port = 8123 + +[edge_runtime.secrets] +lowercase_secret = "env(lowercase_env_var)" +`, + ), + ); + + const result = yield* resolveFunctionsDevEdgeRuntimeConfig(); + + expect(result.config).toEqual({ + enabled: true, + inspectorPort: 8123, + policy: "oneshot", + env: { + LOWERCASE_SECRET: "env(lowercase_env_var)", + }, + }); + }).pipe( + Effect.ensuring(Effect.tryPromise(() => rm(cwd, { recursive: true, force: true }))), + Effect.provide(projectLayer(cwd)), + ); + }); + + it.live("does not split a comma-separated string literal for an array field", () => { + const cwd = makeTempProject(); + + return Effect.gen(function* () { + yield* Effect.tryPromise(() => mkdir(join(cwd, "supabase"), { recursive: true })); + yield* Effect.tryPromise(() => + writeFile(join(cwd, "supabase", ".env"), "EDGE_API_KEY=edge-secret\n"), + ); + yield* Effect.tryPromise(() => + writeFile( + join(cwd, "supabase", "config.toml"), + `project_id = "test" + +[auth] +additional_redirect_urls = "http://a,http://b" + +[edge_runtime] +policy = "oneshot" +inspector_port = 8123 + +[edge_runtime.secrets] +api_key = "env(EDGE_API_KEY)" +literal = "literal-secret" +`, + ), + ); + + const exit = yield* resolveFunctionsDevEdgeRuntimeConfig().pipe(Effect.exit); + + expect(Exit.isFailure(exit)).toBe(true); + }).pipe( + Effect.ensuring(Effect.tryPromise(() => rm(cwd, { recursive: true, force: true }))), + Effect.provide(projectLayer(cwd)), + ); + }); }); diff --git a/apps/cli/src/next/commands/start/start.integration.test.ts b/apps/cli/src/next/commands/start/start.integration.test.ts index 7179a25000..be7b06c555 100644 --- a/apps/cli/src/next/commands/start/start.integration.test.ts +++ b/apps/cli/src/next/commands/start/start.integration.test.ts @@ -1,7 +1,12 @@ +import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; import { describe, expect, it } from "@effect/vitest"; +import { BunServices } from "@effect/platform-bun"; import { Deferred, Effect, Exit, Fiber, Layer } from "effect"; import type { StackServiceStatus } from "@supabase/stack"; import { DEFAULT_VERSIONS, stackMetadata, type StackInfo } from "@supabase/stack/effect"; +import { loadProjectConfig } from "@supabase/config"; import { start } from "./start.handler.ts"; import { StartVersionState } from "./start.command.ts"; import { startForegroundWithStopSignal } from "./flows/foreground.flow.ts"; @@ -532,3 +537,68 @@ describe("start", () => { }).pipe(Effect.provide(layer)); }); }); + +describe("next start config-load path (goViperCompat default-off regression)", () => { + // start.command.ts's `Command.provide` factory calls + // `loadProjectConfig(projectHome.projectRoot)` with no options (line ~183) — + // deeply inside `Layer.unwrap`/`StateManager`/daemon-layer construction that + // this file's `start()`-from-handler harness (StartVersionState provided + // directly) never reaches and can't be extended to reach without + // reproducing that machinery. This test instead exercises the exact + // zero-options `loadProjectConfig` call directly against a real temp + // project, pinning that `next start` still loads successfully when a + // config.toml has a duplicate or malformed `[remotes.*]` block — the + // regression that motivated gating these Go-viper checks behind + // `goViperCompat` (packages/config/src/io.unit.test.ts has the + // authoritative, broader coverage of this default-off behavior). + it("loads successfully despite a duplicate [remotes.*] project_id", async () => { + const tempDir = await mkdtemp(join(tmpdir(), "supabase-next-start-config-")); + try { + await mkdir(join(tempDir, "supabase"), { recursive: true }); + await writeFile( + join(tempDir, "supabase", "config.toml"), + `project_id = "baseref" + +[remotes.a] +project_id = "dupref" + +[remotes.b] +project_id = "dupref" +`, + ); + + const result = await Effect.runPromise( + loadProjectConfig(tempDir).pipe(Effect.provide(BunServices.layer)), + ); + + expect(result).not.toBeNull(); + expect(result?.config.project_id).toBe("baseref"); + } finally { + await rm(tempDir, { recursive: true, force: true }); + } + }); + + it("loads successfully despite a malformed [remotes.*] project_id", async () => { + const tempDir = await mkdtemp(join(tmpdir(), "supabase-next-start-config-")); + try { + await mkdir(join(tempDir, "supabase"), { recursive: true }); + await writeFile( + join(tempDir, "supabase", "config.toml"), + `project_id = "baseref" + +[remotes.bad] +project_id = "not-a-ref" +`, + ); + + const result = await Effect.runPromise( + loadProjectConfig(tempDir).pipe(Effect.provide(BunServices.layer)), + ); + + expect(result).not.toBeNull(); + expect(result?.config.project_id).toBe("baseref"); + } finally { + await rm(tempDir, { recursive: true, force: true }); + } + }); +}); diff --git a/apps/cli/src/shared/cli/hidden-flag.unit.test.ts b/apps/cli/src/shared/cli/hidden-flag.unit.test.ts index 8dc2efe194..3020fe3c1d 100644 --- a/apps/cli/src/shared/cli/hidden-flag.unit.test.ts +++ b/apps/cli/src/shared/cli/hidden-flag.unit.test.ts @@ -123,10 +123,15 @@ describe("native hidden flags", () => { Effect.scoped( Effect.gen(function* () { yield* Command.runWith(legacyTestRoot, { version: "0.0.0-test" })(["start", "--preview"]); - yield* Command.runWith(legacyTestRoot, { version: "0.0.0-test" })([ + // `stop` is natively ported (no longer a `LegacyGoProxy` forward), so it can fail for + // Docker-related reasons in this proxy-only test layer — the point here is only to + // prove the hidden `--backup` flag still parses by exact name, not that the command + // succeeds, matching the `functions deploy`/`serve` assertions below. + const stopExit = yield* Command.runWith(legacyTestRoot, { version: "0.0.0-test" })([ "stop", "--backup=false", - ]); + ]).pipe(Effect.exit); + expect(JSON.stringify(stopExit)).not.toContain("UnrecognizedFlag"); yield* Command.runWith(legacyTestRoot, { version: "0.0.0-test" })([ "functions", "download", @@ -162,7 +167,6 @@ describe("native hidden flags", () => { expect(proxy.calls).toEqual([ ["start", "--preview"], - ["stop", "--backup=false"], ["functions", "download", "hello", "--project-ref", "abcdefghijklmnopqrst", "--use-docker"], ]); }); diff --git a/apps/cli/src/shared/functions/deploy.ts b/apps/cli/src/shared/functions/deploy.ts index b5d7be72a0..51f0d9a9a8 100644 --- a/apps/cli/src/shared/functions/deploy.ts +++ b/apps/cli/src/shared/functions/deploy.ts @@ -62,6 +62,7 @@ interface DeployFunctionsDependencies { readonly projectRoot: string; readonly supabaseDir: string; readonly dashboardUrl: string; + readonly goViperCompat: boolean; readonly yes?: boolean; readonly rawArgs: ReadonlyArray; readonly edgeRuntimeVersion: string; @@ -2150,7 +2151,10 @@ export function deployFunctions( // `@supabase/config` merges the matching `[remotes.*]` block over the base // config (Go's `loadFromFile` with `Config.ProjectId` set), so the resolved // config already reflects any remote function/edge_runtime overrides. - const loadedConfig = yield* loadProjectConfig(dependencies.projectRoot, { projectRef }); + const loadedConfig = yield* loadProjectConfig(dependencies.projectRoot, { + projectRef, + goViperCompat: dependencies.goViperCompat, + }); const deployConfig = loadedConfig?.config; const edgeRuntimeVersion = yield* resolveEdgeRuntimeVersion( deployConfig?.edge_runtime.deno_version, diff --git a/apps/cli/src/shared/functions/serve.ts b/apps/cli/src/shared/functions/serve.ts index e11a1c4736..710afdd972 100644 --- a/apps/cli/src/shared/functions/serve.ts +++ b/apps/cli/src/shared/functions/serve.ts @@ -127,6 +127,7 @@ export interface FunctionsServeDependencies { readonly debug: boolean; readonly networkId: Option.Option; readonly projectIdOverride: Option.Option; + readonly goViperCompat: boolean; } interface PlainServeAuthConfig { @@ -616,6 +617,7 @@ const resolveAuthArtifacts = Effect.fnUntraced(function* ( const resolveServeConfig = Effect.fnUntraced(function* ( projectRoot: string, projectIdOverride: Option.Option, + goViperCompat: boolean, ) { const projectEnv = yield* loadServeProjectEnvironment(projectRoot); const projectRef = Option.match(projectIdOverride, { @@ -632,28 +634,35 @@ const resolveServeConfig = Effect.fnUntraced(function* ( const loadedConfig = yield* loadProjectConfig(projectRoot, { ...(projectRef === undefined ? {} : { projectRef }), ...(projectEnv === null ? {} : { projectEnv }), + goViperCompat, }); const baseConfig = loadedConfig?.config ?? defaultProjectConfig; const auth = projectEnv === null ? toPlainAuthConfig(baseConfig.auth) - : toPlainAuthConfig(yield* resolveProjectSubtree(baseConfig.auth, projectEnv, "auth")); + : toPlainAuthConfig( + yield* resolveProjectSubtree(baseConfig.auth, projectEnv, "auth", { goViperCompat }), + ); const edgeRuntime = projectEnv === null ? toPlainEdgeRuntimeConfig(baseConfig.edge_runtime) : toPlainEdgeRuntimeConfig( - yield* resolveProjectSubtree(baseConfig.edge_runtime, projectEnv, "edge_runtime"), + yield* resolveProjectSubtree(baseConfig.edge_runtime, projectEnv, "edge_runtime", { + goViperCompat, + }), ); const apiPort = projectEnv === null ? baseConfig.api.port - : (yield* resolveProjectSubtree(baseConfig.api, projectEnv, "api")).port; + : (yield* resolveProjectSubtree(baseConfig.api, projectEnv, "api", { goViperCompat })).port; const configDeclaredFunctions = projectEnv === null ? toPlainFunctionRecord(baseConfig.functions) : toPlainFunctionRecord( - yield* resolveProjectSubtree(baseConfig.functions, projectEnv, "functions"), + yield* resolveProjectSubtree(baseConfig.functions, projectEnv, "functions", { + goViperCompat, + }), ); const configForManifest: ProjectConfig = { ...baseConfig, @@ -667,7 +676,9 @@ const resolveServeConfig = Effect.fnUntraced(function* ( projectEnv === null ? (baseConfig.project_id ?? "") : (reveal( - yield* resolveProjectValue(baseConfig.project_id ?? "", projectEnv, "project_id"), + yield* resolveProjectValue(baseConfig.project_id ?? "", projectEnv, "project_id", { + goViperCompat, + }), ) ?? ""); const rawProjectId = Option.getOrElse(projectIdOverride, () => configProjectId).trim(); const fallbackProjectId = basename(resolve(projectRoot)); @@ -1296,6 +1307,7 @@ const startEdgeRuntime = Effect.fnUntraced(function* (input: { const resolved = yield* resolveServeConfig( input.dependencies.projectRoot, input.dependencies.projectIdOverride, + input.dependencies.goViperCompat, ); const projectId = resolved.projectId; const containerId = localDockerId("edge_runtime", projectId); diff --git a/apps/docs/public/cli/config.schema.json b/apps/docs/public/cli/config.schema.json index 0503269e77..c3f73abf09 100644 --- a/apps/docs/public/cli/config.schema.json +++ b/apps/docs/public/cli/config.schema.json @@ -41,19 +41,10 @@ "default": 54327 }, "backend": { - "anyOf": [ - { - "type": "string", - "enum": [ - "postgres" - ] - }, - { - "type": "string", - "enum": [ - "bigquery" - ] - } + "type": "string", + "enum": [ + "postgres", + "bigquery" ], "description": "Configure one of the supported backends:\n\n- `postgres`\n- `bigquery`", "default": "postgres" @@ -347,31 +338,12 @@ "default": 6 }, "password_requirements": { - "anyOf": [ - { - "type": "string", - "enum": [ - "" - ] - }, - { - "type": "string", - "enum": [ - "letters_digits" - ] - }, - { - "type": "string", - "enum": [ - "lower_upper_letters_digits" - ] - }, - { - "type": "string", - "enum": [ - "lower_upper_letters_digits_symbols" - ] - } + "type": "string", + "enum": [ + "", + "letters_digits", + "lower_upper_letters_digits", + "lower_upper_letters_digits_symbols" ], "description": "Password character requirements.", "default": "" @@ -600,19 +572,10 @@ "default": false }, "provider": { - "anyOf": [ - { - "type": "string", - "enum": [ - "hcaptcha" - ] - }, - { - "type": "string", - "enum": [ - "turnstile" - ] - } + "type": "string", + "enum": [ + "hcaptcha", + "turnstile" ], "description": "CAPTCHA provider to use." }, @@ -1019,30 +982,28 @@ "anyOf": [ { "type": "object", - "patternProperties": { - "^(invite|confirmation|recovery|magic_link|email_change|reauthentication)$": { - "anyOf": [ - { - "type": "object", - "properties": { - "subject": { - "type": "string", - "description": "Subject line for the email template.", - "default": "" - }, - "content_path": { - "type": "string", - "description": "Path to the HTML template.", - "default": "" - } + "additionalProperties": { + "anyOf": [ + { + "type": "object", + "properties": { + "subject": { + "type": "string", + "description": "Subject line for the email template.", + "default": "" }, - "additionalProperties": false + "content_path": { + "type": "string", + "description": "Path to the HTML template.", + "default": "" + } }, - { - "type": "null" - } - ] - } + "additionalProperties": false + }, + { + "type": "null" + } + ] }, "description": "Custom email template configuration.", "default": {} @@ -1056,35 +1017,33 @@ "anyOf": [ { "type": "object", - "patternProperties": { - "^(password_changed|email_changed|phone_changed|identity_linked|identity_unlinked|mfa_factor_enrolled|mfa_factor_unenrolled)$": { - "anyOf": [ - { - "type": "object", - "properties": { - "enabled": { - "type": "boolean", - "description": "Enable the notification email.", - "default": false - }, - "subject": { - "type": "string", - "description": "Subject line for the notification email.", - "default": "" - }, - "content_path": { - "type": "string", - "description": "Path to the HTML notification template.", - "default": "" - } + "additionalProperties": { + "anyOf": [ + { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Enable the notification email.", + "default": false }, - "additionalProperties": false + "subject": { + "type": "string", + "description": "Subject line for the notification email.", + "default": "" + }, + "content_path": { + "type": "string", + "description": "Path to the HTML notification template.", + "default": "" + } }, - { - "type": "null" - } - ] - } + "additionalProperties": false + }, + { + "type": "null" + } + ] }, "description": "Notification email configuration.", "default": {} @@ -1893,7 +1852,7 @@ }, "additionalProperties": false }, - "slack": { + "slack_oidc": { "type": "object", "properties": { "enabled": { @@ -1910,7 +1869,7 @@ "type": "string", "description": "Client secret for the Slack OAuth provider.\n\nDO NOT commit your OAuth provider secret to git. Use environment variable substitution instead.", "examples": [ - "env(SUPABASE_AUTH_EXTERNAL_SLACK_SECRET)" + "env(SUPABASE_AUTH_EXTERNAL_SLACK_OIDC_SECRET)" ] }, "url": { @@ -2334,19 +2293,10 @@ "default": 54329 }, "pool_mode": { - "anyOf": [ - { - "type": "string", - "enum": [ - "transaction" - ] - }, - { - "type": "string", - "enum": [ - "session" - ] - } + "type": "string", + "enum": [ + "transaction", + "session" ], "description": "Specifies when a server connection can be reused by other clients.", "default": "transaction" @@ -2675,25 +2625,11 @@ ] }, "session_replication_role": { - "anyOf": [ - { - "type": "string", - "enum": [ - "origin" - ] - }, - { - "type": "string", - "enum": [ - "replica" - ] - }, - { - "type": "string", - "enum": [ - "local" - ] - } + "type": "string", + "enum": [ + "origin", + "replica", + "local" ], "description": "Session replication role." }, @@ -2783,19 +2719,10 @@ "default": true }, "policy": { - "anyOf": [ - { - "type": "string", - "enum": [ - "oneshot" - ] - }, - { - "type": "string", - "enum": [ - "per_worker" - ] - } + "type": "string", + "enum": [ + "oneshot", + "per_worker" ], "description": "Configure the supported request policy.", "default": "per_worker" @@ -2903,6 +2830,22 @@ }, "description": "Static files to bundle with the function.", "default": [] + }, + "env": { + "type": "object", + "patternProperties": { + "^[A-Z_][A-Z0-9_]*$": { + "type": "string", + "allOf": [ + { + "pattern": "^env\\((.*)\\)$", + "description": "Reference to a project environment variable available to the Function." + } + ] + } + }, + "description": "Environment variables from the project environment that this Function can access.", + "default": {} } }, "additionalProperties": false @@ -3028,19 +2971,10 @@ "default": true }, "ip_version": { - "anyOf": [ - { - "type": "string", - "enum": [ - "IPv4" - ] - }, - { - "type": "string", - "enum": [ - "IPv6" - ] - } + "type": "string", + "enum": [ + "IPv4", + "IPv6" ], "description": "Bind realtime via either IPv4 or IPv6.", "default": "IPv4" @@ -3084,12 +3018,35 @@ "default": true }, "file_size_limit": { - "type": "string", - "description": "The maximum file size allowed.", - "default": "50MiB", - "examples": [ - "5MB", - "500KB" + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + } ] }, "image_transformation": { @@ -3116,12 +3073,35 @@ "default": false }, "file_size_limit": { - "type": "string", - "description": "The maximum file size allowed for the bucket.", - "default": "50MiB", - "examples": [ - "5MB", - "500KB" + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + } ] }, "allowed_mime_types": { @@ -3286,7 +3266,7 @@ "enabled": { "type": "boolean", "description": "Enable vector buckets.", - "default": false + "default": true }, "max_buckets": { "anyOf": [ @@ -3586,19 +3566,10 @@ "default": 54327 }, "backend": { - "anyOf": [ - { - "type": "string", - "enum": [ - "postgres" - ] - }, - { - "type": "string", - "enum": [ - "bigquery" - ] - } + "type": "string", + "enum": [ + "postgres", + "bigquery" ], "description": "Configure one of the supported backends:\n\n- `postgres`\n- `bigquery`", "default": "postgres" @@ -3892,31 +3863,12 @@ "default": 6 }, "password_requirements": { - "anyOf": [ - { - "type": "string", - "enum": [ - "" - ] - }, - { - "type": "string", - "enum": [ - "letters_digits" - ] - }, - { - "type": "string", - "enum": [ - "lower_upper_letters_digits" - ] - }, - { - "type": "string", - "enum": [ - "lower_upper_letters_digits_symbols" - ] - } + "type": "string", + "enum": [ + "", + "letters_digits", + "lower_upper_letters_digits", + "lower_upper_letters_digits_symbols" ], "description": "Password character requirements.", "default": "" @@ -4145,19 +4097,10 @@ "default": false }, "provider": { - "anyOf": [ - { - "type": "string", - "enum": [ - "hcaptcha" - ] - }, - { - "type": "string", - "enum": [ - "turnstile" - ] - } + "type": "string", + "enum": [ + "hcaptcha", + "turnstile" ], "description": "CAPTCHA provider to use." }, @@ -4564,30 +4507,28 @@ "anyOf": [ { "type": "object", - "patternProperties": { - "^(invite|confirmation|recovery|magic_link|email_change|reauthentication)$": { - "anyOf": [ - { - "type": "object", - "properties": { - "subject": { - "type": "string", - "description": "Subject line for the email template.", - "default": "" - }, - "content_path": { - "type": "string", - "description": "Path to the HTML template.", - "default": "" - } + "additionalProperties": { + "anyOf": [ + { + "type": "object", + "properties": { + "subject": { + "type": "string", + "description": "Subject line for the email template.", + "default": "" }, - "additionalProperties": false + "content_path": { + "type": "string", + "description": "Path to the HTML template.", + "default": "" + } }, - { - "type": "null" - } - ] - } + "additionalProperties": false + }, + { + "type": "null" + } + ] }, "description": "Custom email template configuration.", "default": {} @@ -4601,35 +4542,33 @@ "anyOf": [ { "type": "object", - "patternProperties": { - "^(password_changed|email_changed|phone_changed|identity_linked|identity_unlinked|mfa_factor_enrolled|mfa_factor_unenrolled)$": { - "anyOf": [ - { - "type": "object", - "properties": { - "enabled": { - "type": "boolean", - "description": "Enable the notification email.", - "default": false - }, - "subject": { - "type": "string", - "description": "Subject line for the notification email.", - "default": "" - }, - "content_path": { - "type": "string", - "description": "Path to the HTML notification template.", - "default": "" - } + "additionalProperties": { + "anyOf": [ + { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Enable the notification email.", + "default": false }, - "additionalProperties": false + "subject": { + "type": "string", + "description": "Subject line for the notification email.", + "default": "" + }, + "content_path": { + "type": "string", + "description": "Path to the HTML notification template.", + "default": "" + } }, - { - "type": "null" - } - ] - } + "additionalProperties": false + }, + { + "type": "null" + } + ] }, "description": "Notification email configuration.", "default": {} @@ -5438,7 +5377,7 @@ }, "additionalProperties": false }, - "slack": { + "slack_oidc": { "type": "object", "properties": { "enabled": { @@ -5455,7 +5394,7 @@ "type": "string", "description": "Client secret for the Slack OAuth provider.\n\nDO NOT commit your OAuth provider secret to git. Use environment variable substitution instead.", "examples": [ - "env(SUPABASE_AUTH_EXTERNAL_SLACK_SECRET)" + "env(SUPABASE_AUTH_EXTERNAL_SLACK_OIDC_SECRET)" ] }, "url": { @@ -5879,19 +5818,10 @@ "default": 54329 }, "pool_mode": { - "anyOf": [ - { - "type": "string", - "enum": [ - "transaction" - ] - }, - { - "type": "string", - "enum": [ - "session" - ] - } + "type": "string", + "enum": [ + "transaction", + "session" ], "description": "Specifies when a server connection can be reused by other clients.", "default": "transaction" @@ -6220,25 +6150,11 @@ ] }, "session_replication_role": { - "anyOf": [ - { - "type": "string", - "enum": [ - "origin" - ] - }, - { - "type": "string", - "enum": [ - "replica" - ] - }, - { - "type": "string", - "enum": [ - "local" - ] - } + "type": "string", + "enum": [ + "origin", + "replica", + "local" ], "description": "Session replication role." }, @@ -6328,19 +6244,10 @@ "default": true }, "policy": { - "anyOf": [ - { - "type": "string", - "enum": [ - "oneshot" - ] - }, - { - "type": "string", - "enum": [ - "per_worker" - ] - } + "type": "string", + "enum": [ + "oneshot", + "per_worker" ], "description": "Configure the supported request policy.", "default": "per_worker" @@ -6448,6 +6355,22 @@ }, "description": "Static files to bundle with the function.", "default": [] + }, + "env": { + "type": "object", + "patternProperties": { + "^[A-Z_][A-Z0-9_]*$": { + "type": "string", + "allOf": [ + { + "pattern": "^env\\((.*)\\)$", + "description": "Reference to a project environment variable available to the Function." + } + ] + } + }, + "description": "Environment variables from the project environment that this Function can access.", + "default": {} } }, "additionalProperties": false @@ -6573,19 +6496,10 @@ "default": true }, "ip_version": { - "anyOf": [ - { - "type": "string", - "enum": [ - "IPv4" - ] - }, - { - "type": "string", - "enum": [ - "IPv6" - ] - } + "type": "string", + "enum": [ + "IPv4", + "IPv6" ], "description": "Bind realtime via either IPv4 or IPv6.", "default": "IPv4" @@ -6629,12 +6543,35 @@ "default": true }, "file_size_limit": { - "type": "string", - "description": "The maximum file size allowed.", - "default": "50MiB", - "examples": [ - "5MB", - "500KB" + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + } ] }, "image_transformation": { @@ -6661,12 +6598,35 @@ "default": false }, "file_size_limit": { - "type": "string", - "description": "The maximum file size allowed for the bucket.", - "default": "50MiB", - "examples": [ - "5MB", - "500KB" + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + } ] }, "allowed_mime_types": { @@ -6831,7 +6791,7 @@ "enabled": { "type": "boolean", "description": "Enable vector buckets.", - "default": false + "default": true }, "max_buckets": { "anyOf": [ diff --git a/packages/cli-test-helpers/src/parity.ts b/packages/cli-test-helpers/src/parity.ts index 9b0aae97c1..a52401f85f 100644 --- a/packages/cli-test-helpers/src/parity.ts +++ b/packages/cli-test-helpers/src/parity.ts @@ -214,6 +214,24 @@ function snapshotChangedFiles(dir: string): FileRecord[] { // RunResult collection // --------------------------------------------------------------------------- +/** + * Docker Engine API calls carry two client-negotiation artifacts that reflect + * nothing about CLI behavior: an `HEAD /_ping` handshake the real `docker` CLI + * issues before every subprocess invocation (Go's SDK pings once per command + * via a persistent client; ts-legacy shells out to `docker` per operation, so + * it pings once per operation), and a negotiated API version segment in the + * URL path (`/v1.51/...` vs `/v1.53/...`) that reflects the installed docker + * CLI/daemon version, not anything the command controls. Strip both so parity + * comparisons reflect actual Docker operations (list, stop, prune, inspect, + * with their filters) rather than client plumbing. Mirrors the equivalent + * normalization in `apps/cli-e2e/src/server/placeholder.ts`'s + * `normalizeUrlPath`, applied here to the request-log comparison instead of + * fixture matching. + */ +function normalizeDockerRequestPath(pathname: string): string { + return pathname.replace(/\/v1\.\d+(\/|$)/g, "/__DOCKER_VERSION__$1"); +} + async function fetchRequestLog(apiUrl: string): Promise { const res = await fetch(`${apiUrl}/_ctrl/requests`); const raw = (await res.json()) as Array<{ @@ -222,12 +240,14 @@ async function fetchRequestLog(apiUrl: string): Promise { query: Record; body: unknown; }>; - return raw.map(({ method, pathname, query, body }) => ({ - method, - pathname, - query, - body, - })); + return raw + .filter(({ method, pathname }) => !(method === "HEAD" && pathname === "/_ping")) + .map(({ method, pathname, query, body }) => ({ + method, + pathname: normalizeDockerRequestPath(pathname), + query, + body, + })); } async function collectRunResult( diff --git a/packages/config/src/auth/email.ts b/packages/config/src/auth/email.ts index 536282874b..4b17fb0a6f 100644 --- a/packages/config/src/auth/email.ts +++ b/packages/config/src/auth/email.ts @@ -25,16 +25,19 @@ const defaultNotificationEnabled = false; const defaultSubject = ""; const defaultContentPath = ""; -const templateNamePattern = new RegExp( - "^(invite|confirmation|recovery|magic_link|email_change|reauthentication)$", -); - -const notificationNamePattern = new RegExp( - "^(password_changed|email_changed|phone_changed|identity_linked|identity_unlinked|mfa_factor_enrolled|mfa_factor_unenrolled)$", -); - -const templateName = Schema.String.check(Schema.isPattern(templateNamePattern)); -const notificationName = Schema.String.check(Schema.isPattern(notificationNamePattern)); +/** + * Go's `Auth.Email.Template`/`Notification` are genuine `map[string]emailTemplate`/ + * `map[string]notification` (`apps/cli-go/pkg/config/auth.go:247-248`) — open maps with no key + * restriction at all; `(e *email) validate(fsys)` (`pkg/config/config.go:1293-1313`) iterates + * every entry regardless of name. The known template/notification names below are the ones the + * Studio/docs UI and `config push`'s Management API sync recognise, but an unrecognized key like + * `[auth.email.template.custom]` is still a legitimate config shape Go accepts — it's simply + * never synced anywhere, same as an unmodeled `auth.external` provider. Record keys therefore + * accept any string, matching Go; only decode failures on a KNOWN/matching name would be a + * Go-parity bug. + */ +const templateName = Schema.String; +const notificationName = Schema.String; function requiredWhenEnabled< T extends Record & { enabled: boolean }, diff --git a/packages/config/src/auth/providers.ts b/packages/config/src/auth/providers.ts index fcb059220d..4e7691d117 100644 --- a/packages/config/src/auth/providers.ts +++ b/packages/config/src/auth/providers.ts @@ -118,6 +118,15 @@ const provider = (providerConfig: { const defaultExternal = {}; +/** + * Go's deprecated `linkedin`/`slack` provider ids (`pkg/config/config.go:1418- + * 1423`) are intentionally NOT modeled here — only their `_oidc` replacements + * (`linkedin_oidc`, `slack_oidc`) are, matching Go's `(e external) validate()`, + * which unconditionally deletes the deprecated keys before anything decodes + * them. `io.ts`'s `normalizeDeprecatedExternalProviders` strips a config's + * `linkedin`/`slack` table (warning on stderr when it was `enabled`, same as + * Go) before this schema ever sees it. + */ export const external = Schema.Struct({ apple: provider({ id: "apple", @@ -185,8 +194,8 @@ export const external = Schema.Struct({ id: "x", name: "X", }), - slack: provider({ - id: "slack", + slack_oidc: provider({ + id: "slack_oidc", name: "Slack", }), spotify: provider({ diff --git a/packages/config/src/auth/sms.ts b/packages/config/src/auth/sms.ts index 3f7b781f80..05b1570139 100644 --- a/packages/config/src/auth/sms.ts +++ b/packages/config/src/auth/sms.ts @@ -32,19 +32,114 @@ const defaultTextlocalEnabled = false; const defaultVonage = {}; const defaultVonageEnabled = false; -function requiredWhenEnabled< - T extends Record & { enabled: boolean }, ->(path: string, predicate: (value: T) => boolean, message: string) { - return Schema.makeFilter((value: T) => { - if (!value.enabled || predicate(value)) { - return undefined; - } +interface SmsProviderSwitchInput { + readonly twilio: { + readonly enabled: boolean; + readonly account_sid: string; + readonly message_service_sid: string; + readonly auth_token?: string; + }; + readonly twilio_verify: { + readonly enabled: boolean; + readonly account_sid?: string; + readonly message_service_sid?: string; + readonly auth_token?: string; + }; + readonly messagebird: { + readonly enabled: boolean; + readonly originator?: string; + readonly access_key?: string; + }; + readonly textlocal: { + readonly enabled: boolean; + readonly sender?: string; + readonly api_key?: string; + }; + readonly vonage: { + readonly enabled: boolean; + readonly from?: string; + readonly api_key?: string; + readonly api_secret?: string; + }; +} + +function missing(provider: string, field: string) { + return { + path: [provider, field], + issue: `Missing required field in config: auth.sms.${provider}.${field}`, + }; +} - return { - path: [path], - issue: message, - }; - }); +/** + * Go's `(s *sms) validate()` (`apps/cli-go/pkg/config/config.go:1348-1410`): a boolean `switch` + * that inspects providers in a FIXED priority order — twilio, twilio_verify, messagebird, + * textlocal, vonage — and validates ONLY the first one whose `enabled` is true, matching Go's + * `switch` short-circuit semantics. A later enabled-but-incomplete provider is never even looked + * at. This replaces five independent per-provider `requiredWhenEnabled` checks (one per provider + * sub-struct) that used to validate EVERY enabled provider table regardless of priority — a real + * Go-parity gap, since a stale secondary `[auth.sms.*]` block Go silently ignores could make this + * schema reject a config Go accepts. `s.EnableSignup`'s own switch case (`config.go:1408-1410`, a + * WARN-only "no SMS provider enabled" notice with no throwing equivalent) isn't reproduced here, + * matching this package's established precedent of not porting WARN-only branches (e.g. the + * `auth.captcha.secret`/`assertEnvLoaded` case). + */ +function validateSmsProviderSwitch(value: SmsProviderSwitchInput) { + if (value.twilio.enabled) { + if (value.twilio.account_sid === "") return missing("twilio", "account_sid"); + if (value.twilio.message_service_sid === "") { + return missing("twilio", "message_service_sid"); + } + if (value.twilio.auth_token === undefined || value.twilio.auth_token === "") { + return missing("twilio", "auth_token"); + } + return undefined; + } + if (value.twilio_verify.enabled) { + if (value.twilio_verify.account_sid === undefined || value.twilio_verify.account_sid === "") { + return missing("twilio_verify", "account_sid"); + } + if ( + value.twilio_verify.message_service_sid === undefined || + value.twilio_verify.message_service_sid === "" + ) { + return missing("twilio_verify", "message_service_sid"); + } + if (value.twilio_verify.auth_token === undefined || value.twilio_verify.auth_token === "") { + return missing("twilio_verify", "auth_token"); + } + return undefined; + } + if (value.messagebird.enabled) { + if (value.messagebird.originator === undefined || value.messagebird.originator === "") { + return missing("messagebird", "originator"); + } + if (value.messagebird.access_key === undefined || value.messagebird.access_key === "") { + return missing("messagebird", "access_key"); + } + return undefined; + } + if (value.textlocal.enabled) { + if (value.textlocal.sender === undefined || value.textlocal.sender === "") { + return missing("textlocal", "sender"); + } + if (value.textlocal.api_key === undefined || value.textlocal.api_key === "") { + return missing("textlocal", "api_key"); + } + return undefined; + } + if (value.vonage.enabled) { + if (value.vonage.from === undefined || value.vonage.from === "") { + return missing("vonage", "from"); + } + if (value.vonage.api_key === undefined || value.vonage.api_key === "") { + return missing("vonage", "api_key"); + } + if (value.vonage.api_secret === undefined || value.vonage.api_secret === "") { + return missing("vonage", "api_secret"); + } + return undefined; + } + return undefined; } export const sms = Schema.Struct({ @@ -100,25 +195,7 @@ export const sms = Schema.Struct({ links: [links.phoneLogin("Twilio")], }), ), - }) - .check( - requiredWhenEnabled( - "account_sid", - (value) => value.account_sid !== "", - "Missing required field in config: auth.sms.twilio.account_sid", - ), - requiredWhenEnabled( - "message_service_sid", - (value) => value.message_service_sid !== "", - "Missing required field in config: auth.sms.twilio.message_service_sid", - ), - requiredWhenEnabled( - "auth_token", - (value) => value.auth_token !== undefined && value.auth_token !== "", - "Missing required field in config: auth.sms.twilio.auth_token", - ), - ) - .pipe(Schema.withDecodingDefaultKey(Effect.succeed({ ...defaultTwilio }))), + }).pipe(Schema.withDecodingDefaultKey(Effect.succeed({ ...defaultTwilio }))), twilio_verify: Schema.Struct({ enabled: Schema.Boolean.annotate({ default: defaultTwilioVerifyEnabled, @@ -147,25 +224,7 @@ export const sms = Schema.Struct({ links: [links.phoneLogin("Twilio")], }), ), - }) - .check( - requiredWhenEnabled( - "account_sid", - (value) => value.account_sid !== undefined && value.account_sid !== "", - "Missing required field in config: auth.sms.twilio_verify.account_sid", - ), - requiredWhenEnabled( - "message_service_sid", - (value) => value.message_service_sid !== undefined && value.message_service_sid !== "", - "Missing required field in config: auth.sms.twilio_verify.message_service_sid", - ), - requiredWhenEnabled( - "auth_token", - (value) => value.auth_token !== undefined && value.auth_token !== "", - "Missing required field in config: auth.sms.twilio_verify.auth_token", - ), - ) - .pipe(Schema.withDecodingDefaultKey(Effect.succeed({ ...defaultTwilioVerify }))), + }).pipe(Schema.withDecodingDefaultKey(Effect.succeed({ ...defaultTwilioVerify }))), messagebird: Schema.Struct({ enabled: Schema.Boolean.annotate({ default: defaultMessagebirdEnabled, @@ -187,20 +246,7 @@ export const sms = Schema.Struct({ links: [links.phoneLogin("MessageBird")], }), ), - }) - .check( - requiredWhenEnabled( - "originator", - (value) => value.originator !== undefined && value.originator !== "", - "Missing required field in config: auth.sms.messagebird.originator", - ), - requiredWhenEnabled( - "access_key", - (value) => value.access_key !== undefined && value.access_key !== "", - "Missing required field in config: auth.sms.messagebird.access_key", - ), - ) - .pipe(Schema.withDecodingDefaultKey(Effect.succeed({ ...defaultMessagebird }))), + }).pipe(Schema.withDecodingDefaultKey(Effect.succeed({ ...defaultMessagebird }))), textlocal: Schema.Struct({ enabled: Schema.Boolean.annotate({ default: defaultTextlocalEnabled, @@ -222,20 +268,7 @@ export const sms = Schema.Struct({ links: [links.phoneLogin("Textlocal%2520(Community%2520Supported)")], }), ), - }) - .check( - requiredWhenEnabled( - "sender", - (value) => value.sender !== undefined && value.sender !== "", - "Missing required field in config: auth.sms.textlocal.sender", - ), - requiredWhenEnabled( - "api_key", - (value) => value.api_key !== undefined && value.api_key !== "", - "Missing required field in config: auth.sms.textlocal.api_key", - ), - ) - .pipe(Schema.withDecodingDefaultKey(Effect.succeed({ ...defaultTextlocal }))), + }).pipe(Schema.withDecodingDefaultKey(Effect.succeed({ ...defaultTextlocal }))), vonage: Schema.Struct({ enabled: Schema.Boolean.annotate({ default: defaultVonageEnabled, @@ -264,25 +297,7 @@ export const sms = Schema.Struct({ links: [links.phoneLogin("Vonage")], }), ), - }) - .check( - requiredWhenEnabled( - "from", - (value) => value.from !== undefined && value.from !== "", - "Missing required field in config: auth.sms.vonage.from", - ), - requiredWhenEnabled( - "api_key", - (value) => value.api_key !== undefined && value.api_key !== "", - "Missing required field in config: auth.sms.vonage.api_key", - ), - requiredWhenEnabled( - "api_secret", - (value) => value.api_secret !== undefined && value.api_secret !== "", - "Missing required field in config: auth.sms.vonage.api_secret", - ), - ) - .pipe(Schema.withDecodingDefaultKey(Effect.succeed({ ...defaultVonage }))), + }).pipe(Schema.withDecodingDefaultKey(Effect.succeed({ ...defaultVonage }))), test_otp: Schema.optionalKey( Schema.Record(Schema.String, Schema.String).annotate({ description: "Use pre-defined map of phone number to OTP for testing.", @@ -290,4 +305,6 @@ export const sms = Schema.Struct({ links: [links.auth], }), ), -}).pipe(Schema.withDecodingDefaultKey(Effect.succeed({ ...defaultSms }))); +}) + .check(Schema.makeFilter(validateSmsProviderSwitch)) + .pipe(Schema.withDecodingDefaultKey(Effect.succeed({ ...defaultSms }))); diff --git a/packages/config/src/base.ts b/packages/config/src/base.ts index 26e14810e0..35c727fc3b 100644 --- a/packages/config/src/base.ts +++ b/packages/config/src/base.ts @@ -55,15 +55,30 @@ const remoteProjectConfig = Schema.Struct({ experimental, }).pipe(Schema.withDecodingDefault(Effect.succeed({}))); +/** + * Exported separately (not inlined into {@link ProjectConfigSchema}) so + * `packages/config/src/io.ts` can decode it on its own with + * `disableChecks: true`. Go's `Config.Validate` only ever checks + * `remotes.*.project_id` format for every remote block + * (`apps/cli-go/pkg/config/config.go:996-1001`, "Since remote config is merged + * to base, we only need to validate the project_id field") — every other + * business-rule check (`Auth.External.validate()`, `Auth.Sms.validate()`, + * etc.) runs exactly once, against the merged effective config + * (`config.go:1136-1152`), never iterated over `c.Remotes[*]`. Decoding this + * schema normally (checks enabled) would apply those same business-rule + * `.check()`s — embedded in `auth`/`db`/etc. — to every remote regardless of + * selection, rejecting configs Go accepts (e.g. an unselected + * `[remotes.prod.auth.external.github] enabled = true` stub with no secret). + */ +export const RemotesSchema = Schema.Record(Schema.String, remoteProjectConfig).annotate({ + default: {}, + description: "Remote branch-specific project configuration.", + tags: ["general"], +}); + export const ProjectConfigSchema = Schema.Struct({ ...baseProjectConfigFields, - remotes: Schema.Record(Schema.String, remoteProjectConfig) - .annotate({ - default: {}, - description: "Remote branch-specific project configuration.", - tags: ["general"], - }) - .pipe(Schema.withDecodingDefault(Effect.succeed({}))), + remotes: RemotesSchema.pipe(Schema.withDecodingDefault(Effect.succeed({}))), }); export type ProjectConfig = typeof ProjectConfigSchema.Type; diff --git a/packages/config/src/errors.ts b/packages/config/src/errors.ts index 0644df83ce..1a3c17bc42 100644 --- a/packages/config/src/errors.ts +++ b/packages/config/src/errors.ts @@ -70,3 +70,15 @@ export class DuplicateRemoteProjectIdError extends Data.TaggedError( )<{ readonly message: string; }> {} + +/** + * A `[remotes.]` block's `project_id` is not a valid 20-lowercase-letter + * project ref. Mirrors Go's `Config.Validate` (`apps/cli-go/pkg/config/config.go: + * 558,996-1001`), which checks every remote's `project_id` against `refPattern` + * on every config load — regardless of whether that remote ends up selected — + * so this fails before Docker/API access, same as Go. `message` matches the Go + * string verbatim so callers can surface it without rewrapping. + */ +export class InvalidRemoteProjectIdError extends Data.TaggedError("InvalidRemoteProjectIdError")<{ + readonly message: string; +}> {} diff --git a/packages/config/src/index.ts b/packages/config/src/index.ts index e9a8eed763..77af2064ae 100644 --- a/packages/config/src/index.ts +++ b/packages/config/src/index.ts @@ -1,6 +1,7 @@ export { ProjectConfigSchema, type ProjectConfig, type ProjectConfigJson } from "./base.ts"; export { DuplicateRemoteProjectIdError, + InvalidRemoteProjectIdError, MissingProjectConfigValueError, ProjectConfigParseError, ProjectEnvParseError, @@ -30,6 +31,7 @@ export { type LoadProjectEnvironmentOptions, type ProjectEnvironment, type ResolvedProjectValue, + type ResolveProjectOptions, loadProjectEnvironment, resolveProjectSubtree, resolveProjectValue, @@ -39,3 +41,4 @@ export { projectConfigStoreLayer } from "./project-config.layer.ts"; export { ProjectConfigStore } from "./project-config.service.ts"; export { PROJECT_CONFIG_SCHEMA_URL } from "./schema-metadata.ts"; export { KONG_LOCAL_CA_CERT } from "./tls.ts"; +export { ENV_CAPTURE_REGEX } from "./lib/env.ts"; diff --git a/packages/config/src/io.ts b/packages/config/src/io.ts index 599833dced..151ce2a373 100644 --- a/packages/config/src/io.ts +++ b/packages/config/src/io.ts @@ -1,7 +1,11 @@ import { Console, Effect, FileSystem, Path, Redacted, Schema } from "effect"; import * as SmolToml from "smol-toml"; -import { ProjectConfigSchema, type ProjectConfig } from "./base.ts"; -import { DuplicateRemoteProjectIdError, ProjectConfigParseError } from "./errors.ts"; +import { ProjectConfigSchema, RemotesSchema, type ProjectConfig } from "./base.ts"; +import { + DuplicateRemoteProjectIdError, + InvalidRemoteProjectIdError, + ProjectConfigParseError, +} from "./errors.ts"; import { interpolateEnvReferencesAgainstSchema } from "./lib/env.ts"; import { findProjectPaths } from "./paths.ts"; import { loadProjectEnvironment, type ProjectEnvironment } from "./project.ts"; @@ -34,11 +38,18 @@ export interface LoadedProjectConfig { } /** - * When `projectRef` is set, the matching `[remotes.]` block (the one whose - * `project_id` equals it) is merged over the base config before decode, mirroring - * Go's `config.Load` with `Config.ProjectId` set - * (`apps/cli-go/pkg/config/config.go:503-562`). Omitting it loads the base config - * verbatim, so existing callers are unaffected. + * When `projectRef` is set, the matching `[remotes.]` block (the one + * whose `project_id` equals it) is merged over the base config before decode, + * mirroring Go's `config.Load` with `Config.ProjectId` set + * (`apps/cli-go/pkg/config/config.go:503-562`). Omitting it loads the base + * config verbatim (no merge), so existing callers are unaffected. Go's + * duplicate-`project_id`/project-ref-format checks across every + * `[remotes.*]` block (`config.go:594-602,996-1001`) run unconditionally on + * every config load in Go, not only when a caller ends up selecting a + * remote — but here they only run when {@link LoadProjectConfigOptions.goViperCompat} + * is `true`, regardless of whether `projectRef` is set, so non-Go-parity + * callers that never select a remote (and never opt into Go parity) aren't + * broken by an unrelated duplicate/malformed `[remotes.*]` block. */ export interface LoadProjectConfigOptions { readonly projectRef?: string; @@ -51,6 +62,37 @@ export interface LoadProjectConfigOptions { * so loading does not re-read those files or depend on `process.env` mutation. */ readonly projectEnv?: ProjectEnvironment; + /** See {@link FindProjectPathsOptions.search}. */ + readonly search?: boolean; + /** + * Skip the `config.json`-over-`config.toml` preference below and only ever + * load `config.toml`. Go's `Config.Load`/`NewPathBuilder` + * (`apps/cli-go/pkg/config/utils.go:43-48`) has no concept of a JSON project + * config file — it always resolves `supabase/config.toml` and treats a + * missing file as defaults — so Go-parity callers (the legacy `status`/`stop` + * ports) must set this to avoid picking up a stray `config.json` that Go + * would never see. + */ + readonly tomlOnly?: boolean; + /** + * Opt into the Go/viper-parity decode+validation semantics this loader + * otherwise omits, so only the Go-parity legacy shell (and shared modules + * invoked exclusively by it) pays for them. Defaults to `false` = pre-PR-#5765 + * behavior, which `next/`, `packages/stack`, and the functions manifest rely + * on. When `true`, mirrors Go's `config.Load` exactly: + * - runs the unconditional duplicate-`project_id` and project-ref-format + * checks across every `[remotes.*]` block (`config.go:594-602,996-1001`), + * even when no `projectRef` is requested; + * - warns on stderr for deprecated `auth.external.{linkedin,slack}` blocks + * (`config.go:1418-1423`) — the block is stripped from the decoded config + * either way, since the schema ignores excess properties; + * - matches `env(...)` references case-agnostically (`^env\((.*)\)$`) + * rather than the strict SCREAMING_SNAKE_CASE form; + * - splits a comma-separated string into a `[]string`-typed field (Go's + * `mapstructure.StringToSliceHookFunc(",")`, `config.go:775-784`), not + * just an `env()`-substituted one. + */ + readonly goViperCompat?: boolean; } export interface SaveProjectConfigOptions { @@ -61,6 +103,18 @@ export interface SaveProjectConfigOptions { } const decodeProjectConfig = Schema.decodeUnknownSync(ProjectConfigSchema); +/** + * Decodes the `remotes` map with `disableChecks: true` — full type/shape + * decoding, defaults, and transformations (e.g. secret redaction) still run, + * but the `.check()`-based business-rule refinements embedded in `auth`/`db`/ + * etc. (e.g. "external provider requires a secret when enabled") are skipped. + * See {@link RemotesSchema}'s doc comment for why: Go only ever applies those + * business rules to the merged effective config, never to a `[remotes.*]` + * block that wasn't selected. + */ +const decodeRemotesWithoutChecks = Schema.decodeUnknownSync(RemotesSchema, { + disableChecks: true, +}); const encodeProjectConfig = Schema.encodeSync(ProjectConfigSchema); const defaultEncodedProjectConfig = encodeProjectConfig(decodeProjectConfig({})); const defaultEncodedFunctionConfig = { @@ -124,28 +178,20 @@ function withDbSeedDisabled(document: Record): Record]` override whose `project_id` matches `projectRef` - * to `document`, mirroring Go's `loadFromFile` remote resolution - * (`config.go:503-518`). Returns the merged document (with `remotes` stripped) and - * the matched remote name. - * - * Like Go, duplicate `project_id`s are detected across *all* `[remotes.*]` blocks — - * not just the ones matching `projectRef` — before the matching override is applied. - * A missing `project_id` reads as `""` (Go's `viper.GetString`), so two remotes that + * Builds a `project_id -> "[remotes.]"` map across every `[remotes.*]` + * block, failing on the first duplicate. Mirrors Go's `loadFromFile` + * (`config.go:594-602`): that loop runs unconditionally on every config load, + * regardless of whether any remote's `project_id` ends up matching + * `Config.ProjectId`. Here, {@link applyRemoteOverride} only invokes this when + * `goViperCompat` is set, so it still runs even for callers that don't + * request a specific `projectRef` — but only under Go-parity mode. A missing + * `project_id` reads as `""` (Go's `viper.GetString`), so two remotes that * both omit it collide on the empty key and fail just as in Go. */ -const applyRemoteOverride = Effect.fnUntraced(function* ( - document: Record, - projectRef: string, +const checkDuplicateRemoteProjectIds = Effect.fnUntraced(function* ( + remotes: Record, ) { - const remotes = document["remotes"]; - if (!isObject(remotes)) { - return { document, appliedRemote: undefined as string | undefined }; - } - // Build a project_id -> "[remotes.]" map over every remote, failing on the - // first duplicate, then resolve the single block matching projectRef. const idToName = new Map(); - let name: string | undefined; for (const [remoteName, remote] of Object.entries(remotes)) { const projectId = isObject(remote) && typeof remote["project_id"] === "string" ? remote["project_id"] : ""; @@ -156,17 +202,91 @@ const applyRemoteOverride = Effect.fnUntraced(function* ( }); } idToName.set(projectId, `[remotes.${remoteName}]`); - if (projectId === projectRef) { - name = remoteName; + } +}); + +/** Go's project-ref pattern (`apps/cli-go/pkg/config/config.go:558`): exactly 20 + * lowercase ASCII letters. */ +const REMOTE_PROJECT_ID_PATTERN = /^[a-z]{20}$/; + +/** + * Rejects the first `[remotes.*]` block whose `project_id` is not a valid + * project ref, mirroring Go's `Config.Validate` (`config.go:996-1001`) — that + * loop runs unconditionally over every remote on every config load, not only + * the one that ends up selected/merged. Here, {@link applyRemoteOverride} only + * invokes this when `goViperCompat` is set. + * + * Unlike {@link checkDuplicateRemoteProjectIds}/the match below (which read + * viper's raw, pre-`LoadEnvHook` values — see {@link applyRemoteOverride}'s + * doc comment), `Config.Validate` runs entirely AFTER the struct decode + * (`config.go:882`), by which point `LoadEnvHook` has already resolved every + * `env(...)` reference (`config.go:749-753`). So this check must see the + * already-interpolated `project_id`, not the literal `env(REF)` form — an + * `[remotes.x] project_id = "env(REF)"` that resolves to a valid 20-letter ref + * passes here even though the raw string doesn't match the pattern itself. + */ +const checkRemoteProjectIdFormat = Effect.fnUntraced(function* (remotes: Record) { + for (const [remoteName, remote] of Object.entries(remotes)) { + const projectId = + isObject(remote) && typeof remote["project_id"] === "string" ? remote["project_id"] : ""; + if (!REMOTE_PROJECT_ID_PATTERN.test(projectId)) { + return yield* new InvalidRemoteProjectIdError({ + message: `Invalid config for remotes.${remoteName}.project_id. Must be like: abcdefghijklmnopqrst`, + }); } } +}); + +/** + * Applies the `[remotes.]` override whose `project_id` matches `projectRef` + * to `rawDocument`, mirroring Go's `loadFromFile` remote resolution + * (`config.go:503-518`). Returns the merged document (with `remotes` stripped, + * still pre-`env()`-interpolation — the caller re-interpolates the result) and + * the matched remote name. `projectRef` of `undefined` never matches any remote + * (including one that itself omits `project_id`, which reads as `""`) — callers + * that don't request a specific remote get the duplicate/format checks below + * without the merge, so the base document loads verbatim as before. + * + * `rawDocument`'s `remotes` block is the PRE-interpolation document: Go's + * duplicate-check/selection loop in `loadFromFile` reads directly off viper's + * raw config values (`v.GetString(fmt.Sprintf("remotes.%s.project_id", name))`, + * `config.go:596-610`) and only calls `c.load(v)` — which resolves `env(...)` + * via `LoadEnvHook` during the struct decode (`config.go:749-753`, + * `decode_hooks.go:13-26`) — afterward (`config.go:611`). So a + * `[remotes.prod] project_id = "env(REF)"` is matched/deduped against the + * LITERAL `env(REF)` string in Go, never against `REF`'s resolved value; this + * mirrors that exactly rather than matching post-interpolation, which would + * merge a remote Go itself would never select. `interpolatedRemotes` (Go's + * post-decode `c.Remotes`, mirrored here as the already-interpolated + * `remotes` subtree) is used only for {@link checkRemoteProjectIdFormat} — see + * its doc comment for why that check needs the resolved value instead. + */ +const applyRemoteOverride = Effect.fnUntraced(function* ( + rawDocument: Record, + interpolatedRemotes: Record | undefined, + projectRef: string | undefined, + goViperCompat: boolean, +) { + const remotes = rawDocument["remotes"]; + if (!isObject(remotes)) { + return { document: rawDocument, appliedRemote: undefined as string | undefined }; + } + if (goViperCompat) { + yield* checkDuplicateRemoteProjectIds(remotes); + yield* checkRemoteProjectIdFormat(interpolatedRemotes ?? remotes); + } + const name = Object.entries(remotes).find(([, remote]) => { + const projectId = + isObject(remote) && typeof remote["project_id"] === "string" ? remote["project_id"] : ""; + return projectRef !== undefined && projectId === projectRef; + })?.[0]; if (name === undefined) { - return { document, appliedRemote: undefined as string | undefined }; + return { document: rawDocument, appliedRemote: undefined as string | undefined }; } const remoteSubtree = remotes[name]; let merged = isObject(remoteSubtree) - ? mergeRemoteSubtree(document, remoteSubtree) - : { ...document }; + ? mergeRemoteSubtree(rawDocument, remoteSubtree) + : { ...rawDocument }; if (!(isObject(remoteSubtree) && remoteSetsDbSeedEnabled(remoteSubtree))) { merged = withDbSeedDisabled(merged); } @@ -317,6 +437,78 @@ function normalizeDeprecatedSMTPSections(document: unknown): NormalizedSMTPDocum return { document: normalized, deprecatedSections }; } +interface NormalizedExternalProvidersDocument { + readonly document: unknown; + /** Provider ids (`"linkedin"` | `"slack"`) whose deprecated top-level block was `enabled` — drives the WARN. */ + readonly deprecatedProviders: ReadonlyArray; +} + +const DEPRECATED_EXTERNAL_PROVIDERS = ["linkedin", "slack"] as const; + +/** + * Go's `(e external) validate()` deprecated-provider handling + * (`apps/cli-go/pkg/config/config.go:1418-1423`): `linkedin`/`slack` are + * unconditionally deleted from `auth.external` before the required-field loop + * runs, so a bare `[auth.external.slack] enabled = true` with no + * `client_id`/`secret` loads fine in Go — a warning prints to stderr only + * when the deleted provider was `enabled`, never a hard failure. + * + * Unlike {@link normalizeDeprecatedSMTPSections}'s `[inbucket]` rename — which + * Go's own `normalizeDeprecatedSMTPConfig` runs BEFORE remote selection, over + * every `[remotes.*]` entry unconditionally (`config.go:594,614-640`) — Go's + * `external.validate()` runs from `Config.Validate()`, exactly ONCE on the + * final post-remote-merge struct (`config.go:882,1148`). A non-selected + * remote's own `auth.external.slack` block is never even looked at by Go. So + * this must run on the POST-merge document (`documentForDecode`, after + * `applyRemoteOverride`), not the pre-merge one: + * - the top-level `auth.external.{linkedin,slack}` is always stripped, and + * reported (for the caller to warn on) only when it was `enabled`, + * matching Go's single `external.validate()` call. + * - any `remotes.*.auth.external.{linkedin,slack}` still present (only + * possible when no remote matched `projectRef`, so `applyRemoteOverride` + * left `remotes` in place) is also stripped, but never reported — purely + * so `remoteProjectConfig`'s eager, whole-map schema decode + * (`packages/config/src/base.ts`) doesn't reject an unselected remote's + * deprecated block over a field Go itself never struct-decodes at all for + * a remote that isn't in effect. + */ +function normalizeDeprecatedExternalProviders( + document: unknown, +): NormalizedExternalProvidersDocument { + if (!isObject(document)) { + return { document, deprecatedProviders: [] }; + } + const normalized = { ...document }; + const deprecatedProviders: Array = []; + if (isObject(normalized.auth) && isObject(normalized.auth.external)) { + const external = { ...normalized.auth.external }; + for (const ext of DEPRECATED_EXTERNAL_PROVIDERS) { + const provider = external[ext]; + if (provider === undefined) continue; + if (isObject(provider) && provider.enabled === true) { + deprecatedProviders.push(ext); + } + delete external[ext]; + } + normalized.auth = { ...normalized.auth, external }; + } + if (isObject(normalized.remotes)) { + normalized.remotes = Object.fromEntries( + Object.entries(normalized.remotes).map(([name, remote]) => { + if (!isObject(remote) || !isObject(remote.auth) || !isObject(remote.auth.external)) { + return [name, remote]; + } + const external = { ...remote.auth.external }; + for (const ext of DEPRECATED_EXTERNAL_PROVIDERS) { + delete external[ext]; + } + return [name, { ...remote, auth: { ...remote.auth, external } }]; + }), + ); + } + return { document: normalized, deprecatedProviders }; +} + /** * Wraps every `edge_runtime.secrets` value in `Redacted` before it's attached * to `ProjectConfigParseError.document`. By this point `secrets` values are @@ -382,7 +574,23 @@ function parseProjectConfig( appliedRemote: string | undefined, ): Effect.Effect { return Effect.try({ - try: () => decodeProjectConfig(document), + try: () => { + // Decode `remotes` separately, with business-rule checks disabled — see + // `decodeRemotesWithoutChecks`/`RemotesSchema`'s doc comments. Non-selected + // `[remotes.*]` blocks reach here still attached to `document` (only a + // SELECTED remote gets merged in and stripped from `remotes` by + // `applyRemoteOverride`), so decoding them through the normal, + // checks-enabled `decodeProjectConfig` below would apply Go's + // merged-config-only business rules to every remote regardless of + // selection. Structural decoding (types, defaults, transformations) + // still runs either way, matching Go's unconditional `UnmarshalExact` + // struct decode of every remote. + const rawRemotes = isObject(document) ? document.remotes : undefined; + const config = decodeProjectConfig( + isObject(document) ? { ...document, remotes: {} } : document, + ); + return { ...config, remotes: decodeRemotesWithoutChecks(rawRemotes ?? {}) }; + }, // `document` always parsed successfully by this point (raw parse failures // are caught earlier, in `loadProjectConfigFile`), so any error here is a // schema-decode failure — attach it so callers can attempt a narrower, @@ -479,25 +687,78 @@ export const loadProjectConfigFile = Effect.fnUntraced(function* ( (yield* loadProjectEnvironment({ cwd: projectRoot, baseEnv: process.env, + search: options?.search, })); - const interpolated = interpolateEnvReferencesAgainstSchema( - normalized, - projectEnv?.values ?? {}, - ProjectConfigSchema, - ); - - // Merge the matching `[remotes.*]` override over the base document before - // decode (Go's `loadFromFile` with `Config.ProjectId` set). Only requested - // when a `projectRef` is supplied, so other callers load the base verbatim. - let documentForDecode: unknown = interpolated; + const goViperCompat = options?.goViperCompat ?? false; + const interpolateDocument = (document: unknown): unknown => + interpolateEnvReferencesAgainstSchema(document, projectEnv?.values ?? {}, ProjectConfigSchema, { + goViperCompat, + }); + + // Interpolated once here purely to give `applyRemoteOverride`'s FORMAT check + // (not its match/merge — see that function's doc comment) the resolved + // `remotes.*.project_id`, matching Go's post-decode `Config.Validate`. + const interpolatedForValidation = interpolateDocument(normalized); + const interpolatedRemotes = + isObject(interpolatedForValidation) && isObject(interpolatedForValidation["remotes"]) + ? interpolatedForValidation["remotes"] + : undefined; + + // Merge the matching `[remotes.*]` override over the RAW (pre-`env()`- + // interpolation) document — Go's `loadFromFile` duplicate-check/selection + // loop runs on viper's raw string values, before `LoadEnvHook` ever resolves + // `env(...)` (`config.go:594-611`, `decode_hooks.go:13-26`); see + // `applyRemoteOverride`'s doc comment. The match/merge itself always runs + // (callers that don't request a `projectRef` just never match a remote, so + // the base document loads verbatim), but the duplicate-`project_id`/format + // checks only run when `goViperCompat` is set — see `applyRemoteOverride`. + let documentForDecode: unknown = normalized; let appliedRemote: string | undefined; - if (options?.projectRef !== undefined && isObject(interpolated)) { - const resolved = yield* applyRemoteOverride(interpolated, options.projectRef); + if (isObject(normalized)) { + const resolved = yield* applyRemoteOverride( + normalized, + interpolatedRemotes, + options?.projectRef, + goViperCompat, + ); documentForDecode = resolved.document; appliedRemote = resolved.appliedRemote; } - const config = yield* parseProjectConfig(documentForDecode, format, filePath, appliedRemote); + // The merge above ran on the raw document, so any `env(...)` reference in + // the winning remote's subtree (or elsewhere in the base) still needs + // resolving before decode — mirrors Go's `LoadEnvHook` running on the + // post-merge viper store inside `c.load(v)`. When no remote matched, this + // recomputes the same substitutions `interpolatedForValidation` already + // made (documentForDecode is just `normalized` again) — a redundant walk on + // that path, but correctness on the match+`env()` path matters more than + // avoiding it. + documentForDecode = isObject(documentForDecode) + ? interpolateDocument(documentForDecode) + : documentForDecode; + + // Strip Go's deprecated `auth.external.{linkedin,slack}` provider ids from + // the POST-remote-merge document, matching `external.validate()` running + // once on the final effective config (see `normalizeDeprecatedExternalProviders`). + const { document: normalizedForDecode, deprecatedProviders } = + normalizeDeprecatedExternalProviders(documentForDecode); + // Warn on stderr, matching Go's `external.validate()` (`config.go:1418-1423`). + // Go's own format string is a raw string literal ending in a literal + // backslash-n (raw string literals never process escapes, and `Fprintf` + // doesn't append a newline the way `Fprintln` does), so Go's actual stderr + // bytes have no real line break after this message — a library-internal + // artifact, not the parity-relevant part, same call already made for + // `LegacyInvalidPortEnvOverrideError` in the legacy shell. Not reproduced + // byte-for-byte; `Console.error` supplies a normal trailing newline instead. + if (goViperCompat) { + for (const ext of deprecatedProviders) { + yield* Console.error( + `WARN: disabling deprecated "${ext}" provider. Please use [auth.external.${ext}_oidc] instead`, + ); + } + } + + const config = yield* parseProjectConfig(normalizedForDecode, format, filePath, appliedRemote); return { path: filePath, @@ -505,7 +766,7 @@ export const loadProjectConfigFile = Effect.fnUntraced(function* ( config, schemaRef: getSchemaRef(document), ignoredPaths: [], - document: isObject(documentForDecode) ? documentForDecode : undefined, + document: isObject(normalizedForDecode) ? normalizedForDecode : undefined, appliedRemote, } satisfies LoadedProjectConfig; }); @@ -515,7 +776,7 @@ export const loadProjectConfig = Effect.fnUntraced(function* ( options?: LoadProjectConfigOptions, ) { const fs = yield* FileSystem.FileSystem; - const project = yield* findProjectPaths(cwd); + const project = yield* findProjectPaths(cwd, { search: options?.search }); if (project === null) { return null; @@ -528,7 +789,7 @@ export const loadProjectConfig = Effect.fnUntraced(function* ( ? project.configPath : project.configPath.replace(/config\.json$/, "config.toml"); - if (yield* fs.exists(jsonPath)) { + if (!options?.tomlOnly && (yield* fs.exists(jsonPath))) { const json = yield* loadProjectConfigFile(jsonPath, options); return { diff --git a/packages/config/src/io.unit.test.ts b/packages/config/src/io.unit.test.ts index 6d70f191e6..a09b321502 100644 --- a/packages/config/src/io.unit.test.ts +++ b/packages/config/src/io.unit.test.ts @@ -16,6 +16,7 @@ import { loadProjectConfig, loadProjectConfigFile, saveProjectConfig, + type LoadProjectConfigOptions, } from "./io.ts"; import { loadProjectConfig as loadProjectConfigFromNode } from "./node.ts"; import { projectConfigStoreLayer } from "./project-config.layer.ts"; @@ -173,6 +174,45 @@ describe("config io", () => { ).toThrow(); }); + test("only validates the highest-priority enabled sms provider during decode (Go switch parity)", () => { + // Go's `(s *sms) validate()` (`apps/cli-go/pkg/config/config.go:1348-1410`) is a boolean + // `switch` that inspects providers in a fixed priority order (twilio, twilio_verify, + // messagebird, textlocal, vonage) and validates ONLY the first enabled one — a later + // enabled-but-incomplete provider is never even looked at. A complete, higher-priority + // `twilio` block plus an incomplete, lower-priority `messagebird` block must decode fine. + const config = decodeProjectConfig({ + auth: { + sms: { + twilio: { + enabled: true, + account_sid: "AC123", + message_service_sid: "MG123", + auth_token: "secret", + }, + messagebird: { + enabled: true, + }, + }, + }, + }); + expect(config.auth.sms.twilio.enabled).toBe(true); + expect(config.auth.sms.messagebird.enabled).toBe(true); + }); + + test("rejects an incomplete sms provider when no higher-priority provider is enabled", () => { + expect(() => + decodeProjectConfig({ + auth: { + sms: { + messagebird: { + enabled: true, + }, + }, + }, + }), + ).toThrow(/auth\.sms\.messagebird\.originator/); + }); + test("requires enabled smtp fields during decode", () => { expect(() => decodeProjectConfig({ @@ -187,6 +227,24 @@ describe("config io", () => { ).toThrow(); }); + test("decodes an unmodeled email template/notification name (Go map[string] parity)", () => { + // Go's `Auth.Email.Template`/`Notification` are genuine `map[string]emailTemplate`/ + // `map[string]notification` (`apps/cli-go/pkg/config/auth.go:247-248`) — open maps with no + // key restriction; `(e *email) validate(fsys)` (`pkg/config/config.go:1293-1313`) iterates + // every entry regardless of name. An unrecognized key like `[auth.email.template.custom]` + // is a legitimate config shape Go accepts, not a decode error. + const config = decodeProjectConfig({ + auth: { + email: { + template: { custom: { subject: "Hi" } }, + notification: { custom_notice: { enabled: true, content_path: "custom.html" } }, + }, + }, + }); + expect(config.auth.email.template["custom"]?.subject).toBe("Hi"); + expect(config.auth.email.notification["custom_notice"]?.enabled).toBe(true); + }); + test("requires enabled external provider credentials during decode", () => { expect(() => decodeProjectConfig({ @@ -323,6 +381,50 @@ major_version = 16 } }); + // Go's `NewPathBuilder`/`Config.Load` (`apps/cli-go/pkg/config/utils.go: + // 43-48`) only ever resolves `supabase/config.toml` — it has no concept of a + // JSON project config file. Go-parity callers (legacy `status`/`stop`) pass + // `tomlOnly: true` so a stray `config.json` never wins over `config.toml`. + test("loads TOML instead of JSON when tomlOnly is set, even if JSON exists", async () => { + const cwd = makeTempProject(); + const jsonPath = await runConfigEffect(configJsonPath(cwd)); + const tomlPath = await runConfigEffect(configTomlPath(cwd)); + + try { + await mkdir(join(cwd, "supabase"), { recursive: true }); + await writeFile(jsonPath, encodeProjectConfigToJson(sampleConfig)); + await writeFile( + tomlPath, + `project_id = "toml-ref" + +[db] +major_version = 16 +`, + ); + + const loaded = await runConfigEffect(loadProjectConfig(cwd, { tomlOnly: true })); + expect(loaded?.format).toBe("toml"); + expect(loaded?.config.project_id).toBe("toml-ref"); + } finally { + await rm(cwd, { recursive: true, force: true }); + } + }); + + test("returns null when tomlOnly is set and only JSON exists", async () => { + const cwd = makeTempProject(); + const jsonPath = await runConfigEffect(configJsonPath(cwd)); + + try { + await mkdir(join(cwd, "supabase"), { recursive: true }); + await writeFile(jsonPath, encodeProjectConfigToJson(sampleConfig)); + + const loaded = await runConfigEffect(loadProjectConfig(cwd, { tomlOnly: true })); + expect(loaded).toBeNull(); + } finally { + await rm(cwd, { recursive: true, force: true }); + } + }); + test("loads TOML when JSON is absent", async () => { const cwd = makeTempProject(); const tomlPath = await runConfigEffect(configTomlPath(cwd)); @@ -889,6 +991,128 @@ enabled = "env(SUPABASE_ANALYTICS_ENABLED)" } }); + test.each([ + ["1", true], + ["TRUE", true], + ["T", true], + ["True", true], + ["0", false], + ["f", false], + ["FALSE", false], + ] as const)( + "resolves env() on boolean fields using Go's strconv.ParseBool acceptance set (%s -> %s)", + async (envValue, expected) => { + const cwd = makeTempProject(); + + try { + await mkdir(join(cwd, "supabase"), { recursive: true }); + await writeFile( + join(cwd, "supabase", "config.toml"), + `project_id = "ref_123" + +[analytics] +enabled = "env(SUPABASE_ANALYTICS_ENABLED)" +`, + ); + await writeFile(join(cwd, "supabase", ".env"), `SUPABASE_ANALYTICS_ENABLED=${envValue}\n`); + + const loaded = await runConfigEffect(loadProjectConfig(cwd)); + expect(loaded!.config.analytics.enabled).toBe(expected); + } finally { + await rm(cwd, { recursive: true, force: true }); + } + }, + ); + + test("splits a comma-separated string literal into a slice (Go's StringToSliceHookFunc)", async () => { + // Go's `newDecodeHook` (`apps/cli-go/pkg/config/config.go:775-784`) wires + // `mapstructure.StringToSliceHookFunc(",")` unconditionally, so a plain + // string value for a `[]string` field like `additional_redirect_urls` + // decodes fine in Go — not just via `env(...)`. + const cwd = makeTempProject(); + + try { + await mkdir(join(cwd, "supabase"), { recursive: true }); + await writeFile( + join(cwd, "supabase", "config.toml"), + `project_id = "ref_123" + +[auth] +additional_redirect_urls = "http://a,http://b" +`, + ); + + const loaded = await runConfigEffect(loadProjectConfig(cwd, { goViperCompat: true })); + expect(loaded!.config.auth.additional_redirect_urls).toEqual(["http://a", "http://b"]); + } finally { + await rm(cwd, { recursive: true, force: true }); + } + }); + + test("splits an env()-substituted comma-separated string into a slice", async () => { + const cwd = makeTempProject(); + + try { + await mkdir(join(cwd, "supabase"), { recursive: true }); + await writeFile( + join(cwd, "supabase", "config.toml"), + `project_id = "ref_123" + +[auth] +additional_redirect_urls = "env(SUPABASE_REDIRECT_URLS)" +`, + ); + await writeFile(join(cwd, "supabase", ".env"), "SUPABASE_REDIRECT_URLS=http://a,http://b\n"); + + const loaded = await runConfigEffect(loadProjectConfig(cwd, { goViperCompat: true })); + expect(loaded!.config.auth.additional_redirect_urls).toEqual(["http://a", "http://b"]); + } finally { + await rm(cwd, { recursive: true, force: true }); + } + }); + + test("an empty string literal for a slice field decodes to an empty array", async () => { + const cwd = makeTempProject(); + + try { + await mkdir(join(cwd, "supabase"), { recursive: true }); + await writeFile( + join(cwd, "supabase", "config.toml"), + `project_id = "ref_123" + +[auth] +additional_redirect_urls = "" +`, + ); + + const loaded = await runConfigEffect(loadProjectConfig(cwd, { goViperCompat: true })); + expect(loaded!.config.auth.additional_redirect_urls).toEqual([]); + } finally { + await rm(cwd, { recursive: true, force: true }); + } + }); + + test("an actual array value for a slice field is left untouched", async () => { + const cwd = makeTempProject(); + + try { + await mkdir(join(cwd, "supabase"), { recursive: true }); + await writeFile( + join(cwd, "supabase", "config.toml"), + `project_id = "ref_123" + +[auth] +additional_redirect_urls = ["http://a", "http://b"] +`, + ); + + const loaded = await runConfigEffect(loadProjectConfig(cwd)); + expect(loaded!.config.auth.additional_redirect_urls).toEqual(["http://a", "http://b"]); + } finally { + await rm(cwd, { recursive: true, force: true }); + } + }); + test("preserves env() literals on string fields when the var is unset (Go parity)", async () => { const cwd = makeTempProject(); @@ -910,6 +1134,28 @@ jwt_secret = "env(MISSING_SECRET)" } }); + test("preserves env() literals on string fields when the var is set but empty (Go parity)", async () => { + const cwd = makeTempProject(); + + try { + await mkdir(join(cwd, "supabase"), { recursive: true }); + await writeFile( + join(cwd, "supabase", "config.toml"), + `project_id = "ref_123" + +[auth] +jwt_secret = "env(MISSING_SECRET)" +`, + ); + await writeFile(join(cwd, "supabase", ".env"), "MISSING_SECRET=\n"); + + const loaded = await runConfigEffect(loadProjectConfig(cwd)); + expect(loaded!.config.auth.jwt_secret).toBe("env(MISSING_SECRET)"); + } finally { + await rm(cwd, { recursive: true, force: true }); + } + }); + test("fails to decode a numeric field when env var is unset", async () => { const cwd = makeTempProject(); @@ -968,6 +1214,163 @@ port = "env(SUPABASE_DB_PORT_TEST)" await rm(cwd, { recursive: true, force: true }); } }); + + // Regression coverage for the default-off (`goViperCompat` omitted) path — + // these pin pre-PR-#5765 behavior so `next/`, `packages/stack`, and the + // functions manifest (none of which pass `goViperCompat`) don't inherit the + // Go-parity legacy shell's stricter/wider semantics. + test("loads successfully with a duplicate [remotes.*] project_id when goViperCompat is omitted", async () => { + const cwd = makeTempProject(); + + try { + await mkdir(join(cwd, "supabase"), { recursive: true }); + await writeFile( + join(cwd, "supabase", "config.toml"), + `project_id = "baseref" + +[remotes.a] +project_id = "dupref" + +[remotes.b] +project_id = "dupref" +`, + ); + + const loaded = await runConfigEffect(loadProjectConfig(cwd)); + expect(loaded).not.toBeNull(); + expect(loaded!.config.project_id).toBe("baseref"); + } finally { + await rm(cwd, { recursive: true, force: true }); + } + }); + + test("loads successfully with an invalid [remotes.*] project_id format when goViperCompat is omitted", async () => { + const cwd = makeTempProject(); + + try { + await mkdir(join(cwd, "supabase"), { recursive: true }); + await writeFile( + join(cwd, "supabase", "config.toml"), + `project_id = "baseref" + +[remotes.bad] +project_id = "not-a-ref" +`, + ); + + const loaded = await runConfigEffect(loadProjectConfig(cwd)); + expect(loaded).not.toBeNull(); + expect(loaded!.config.project_id).toBe("baseref"); + } finally { + await rm(cwd, { recursive: true, force: true }); + } + }); + + test("does not split a comma-separated string literal for an array field when goViperCompat is omitted", async () => { + const cwd = makeTempProject(); + + try { + await mkdir(join(cwd, "supabase"), { recursive: true }); + await writeFile( + join(cwd, "supabase", "config.toml"), + `project_id = "ref_123" + +[auth] +additional_redirect_urls = "http://a,http://b" +`, + ); + + const exit = await Effect.runPromiseExit( + loadProjectConfig(cwd).pipe(Effect.provide(BunServices.layer)), + ); + + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + const error = Cause.findErrorOption(exit.cause); + expect(Option.isSome(error)).toBe(true); + if (Option.isSome(error)) { + expect((error.value as { _tag: string })._tag).toBe("ProjectConfigParseError"); + } + } + } finally { + await rm(cwd, { recursive: true, force: true }); + } + }); + + test("does not warn on a deprecated provider (but still strips it) when goViperCompat is omitted", async () => { + const cwd = makeTempProject(); + const warnings: Array = []; + const errorSpy = vi.spyOn(console, "error").mockImplementation((...args) => { + warnings.push(args.map((a) => String(a)).join(" ")); + }); + + try { + await mkdir(join(cwd, "supabase"), { recursive: true }); + await writeFile( + join(cwd, "supabase", "config.toml"), + `project_id = "abc123" + +[auth.external.slack] +enabled = true +`, + ); + + const loaded = await runConfigEffect(loadProjectConfig(cwd)); + expect("slack" in loaded!.config.auth.external).toBe(false); + expect(warnings.some((m) => m.includes("is deprecated"))).toBe(false); + } finally { + errorSpy.mockRestore(); + await rm(cwd, { recursive: true, force: true }); + } + }); + + test("does not resolve a lowercase-named env() reference when goViperCompat is omitted", async () => { + const previous = process.env.lowercase_ref_default_off_test; + process.env.lowercase_ref_default_off_test = "lowercase-ref-value"; + const cwd = makeTempProject(); + + try { + await mkdir(join(cwd, "supabase"), { recursive: true }); + await writeFile( + join(cwd, "supabase", "config.toml"), + `project_id = "env(lowercase_ref_default_off_test)"\n`, + ); + + const loaded = await runConfigEffect(loadProjectConfig(cwd)); + expect(loaded!.config.project_id).toBe("env(lowercase_ref_default_off_test)"); + } finally { + if (previous === undefined) { + delete process.env.lowercase_ref_default_off_test; + } else { + process.env.lowercase_ref_default_off_test = previous; + } + await rm(cwd, { recursive: true, force: true }); + } + }); + + test("resolves a lowercase-named env() reference when goViperCompat is true", async () => { + const previous = process.env.lowercase_ref_default_on_test; + process.env.lowercase_ref_default_on_test = "lowercase-ref-value"; + const cwd = makeTempProject(); + + try { + await mkdir(join(cwd, "supabase"), { recursive: true }); + await writeFile( + join(cwd, "supabase", "config.toml"), + `project_id = "env(lowercase_ref_default_on_test)"\n`, + ); + + const loaded = await runConfigEffect(loadProjectConfig(cwd, { goViperCompat: true })); + expect(loaded!.config.project_id).toBe("lowercase-ref-value"); + } finally { + if (previous === undefined) { + delete process.env.lowercase_ref_default_on_test; + } else { + process.env.lowercase_ref_default_on_test = previous; + } + await rm(cwd, { recursive: true, force: true }); + } + }); }); describe("config io [remotes.*] merge", () => { @@ -978,6 +1381,14 @@ describe("config io [remotes.*] merge", () => { return cwd; } + // Remote `project_id`s below are valid 20-lowercase-letter refs (Go's + // `refPattern`, `config.go:558`) — `Config.Validate` rejects every + // `[remotes.*].project_id` against that pattern unconditionally on every + // config load (`config.go:996-1001`), so test fixtures must satisfy it too, + // even for scenarios that don't care about the ref's specific value. + const PREVIEW_REF = "previewrefaaaaaaaaaa"; + const STAGING_REF = "stagingrefaaaaaaaaaa"; + const BASE_WITH_REMOTES = `project_id = "baseref" [api] @@ -989,13 +1400,13 @@ max_rows = 123 major_version = 15 [remotes.preview] -project_id = "previewref" +project_id = "${PREVIEW_REF}" [remotes.preview.api] schemas = ["remote_only"] max_rows = 999 [remotes.staging] -project_id = "stagingref" +project_id = "${STAGING_REF}" [remotes.staging.api] enabled = false `; @@ -1003,10 +1414,10 @@ enabled = false test("merges the matching remote subtree over the base before decode", async () => { const cwd = await writeTomlProject(BASE_WITH_REMOTES); try { - const loaded = await runConfigEffect(loadProjectConfig(cwd, { projectRef: "previewref" })); + const loaded = await runConfigEffect(loadProjectConfig(cwd, { projectRef: PREVIEW_REF })); expect(loaded!.appliedRemote).toBe("preview"); // remote block's project_id overrides the base - expect(loaded!.config.project_id).toBe("previewref"); + expect(loaded!.config.project_id).toBe(PREVIEW_REF); // remote scalar wins expect(loaded!.config.api.max_rows).toBe(999); // array replaced wholesale (not element-merged) @@ -1037,9 +1448,7 @@ major_version = "not-a-number" ); try { const exit = await Effect.runPromiseExit( - loadProjectConfig(cwd, { projectRef: "previewref" }).pipe( - Effect.provide(BunServices.layer), - ), + loadProjectConfig(cwd, { projectRef: PREVIEW_REF }).pipe(Effect.provide(BunServices.layer)), ); expect(Exit.isFailure(exit)).toBe(true); if (!Exit.isFailure(exit)) { @@ -1069,7 +1478,10 @@ major_version = "not-a-number" } }); - test("does not merge remotes when no projectRef is requested", async () => { + test("does not merge remotes when no projectRef is requested and none has an empty project_id", async () => { + // `projectRef` defaults to "" (Go's own `Config.ProjectId` default for + // commands with no `--project-ref` flag), so this only stays unmerged + // because neither remote's `project_id` is empty. const cwd = await writeTomlProject(BASE_WITH_REMOTES); try { const loaded = await runConfigEffect(loadProjectConfig(cwd)); @@ -1081,6 +1493,42 @@ major_version = "not-a-number" } }); + test("rejects duplicate project_id across remotes even when no projectRef is requested", async () => { + // Go's duplicate-project_id check (config.go:594-602) runs unconditionally + // on every config load, inside the same loop that resolves the [remotes.*] + // override — it is not gated on a caller actually selecting a remote. + // status/stop (internal/utils/flags/config_path.go:11) never bind a + // `--project-ref` flag, so they hit this check with `Config.ProjectId == ""`, + // and it must still fail on a config-wide duplicate. + const cwd = await writeTomlProject(`project_id = "baseref" + +[remotes.a] +project_id = "dupref" + +[remotes.b] +project_id = "dupref" +`); + try { + const message = await Effect.runPromise( + loadProjectConfig(cwd, { goViperCompat: true }).pipe( + Effect.catchTag("DuplicateRemoteProjectIdError", (error) => + Effect.succeed(error.message), + ), + Effect.provide(BunServices.layer), + ), + ); + expect(message).toBe("duplicate project_id for [remotes.b] and [remotes.a]"); + } finally { + await rm(cwd, { recursive: true, force: true }); + } + }); + + // `goViperCompat` is required even though a `projectRef` is passed: the + // duplicate/format checks in `applyRemoteOverride` are gated solely on + // `goViperCompat`, not on whether a remote is being selected — the remote + // match/merge itself stays unconditional, but pre-PR-#5765 callers that + // pass a `projectRef` without opting into Go parity no longer get these + // checks for free. test("rejects duplicate project_id across remotes with Go's message", async () => { const cwd = await writeTomlProject(`project_id = "baseref" @@ -1092,7 +1540,7 @@ project_id = "dupref" `); try { const message = await Effect.runPromise( - loadProjectConfig(cwd, { projectRef: "dupref" }).pipe( + loadProjectConfig(cwd, { projectRef: "dupref", goViperCompat: true }).pipe( Effect.catchTag("DuplicateRemoteProjectIdError", (error) => Effect.succeed(error.message), ), @@ -1122,7 +1570,7 @@ project_id = "dupref" `); try { const message = await Effect.runPromise( - loadProjectConfig(cwd, { projectRef: "previewref" }).pipe( + loadProjectConfig(cwd, { projectRef: "previewref", goViperCompat: true }).pipe( Effect.catchTag("DuplicateRemoteProjectIdError", (error) => Effect.succeed(error.message), ), @@ -1150,7 +1598,7 @@ max_rows = 2 `); try { const message = await Effect.runPromise( - loadProjectConfig(cwd, { projectRef: "previewref" }).pipe( + loadProjectConfig(cwd, { projectRef: "previewref", goViperCompat: true }).pipe( Effect.catchTag("DuplicateRemoteProjectIdError", (error) => Effect.succeed(error.message), ), @@ -1163,16 +1611,41 @@ max_rows = 2 } }); + test("rejects a remote project_id that is not a valid 20-letter ref, even with no projectRef requested", async () => { + // Go's Config.Validate (config.go:996-1001) checks every [remotes.*].project_id + // against refPattern unconditionally on every config load — not only the one + // that ends up selected — so this must fail closed before status/stop reach + // Docker, exactly like Go, even when the caller never selects a remote. + const cwd = await writeTomlProject(`project_id = "baseref" + +[remotes.bad] +project_id = "not-a-ref" +`); + try { + const message = await Effect.runPromise( + loadProjectConfig(cwd, { goViperCompat: true }).pipe( + Effect.catchTag("InvalidRemoteProjectIdError", (error) => Effect.succeed(error.message)), + Effect.provide(BunServices.layer), + ), + ); + expect(message).toBe( + "Invalid config for remotes.bad.project_id. Must be like: abcdefghijklmnopqrst", + ); + } finally { + await rm(cwd, { recursive: true, force: true }); + } + }); + test("the merged document carries pointer sections introduced by the remote", async () => { const cwd = await writeTomlProject(`project_id = "baseref" [remotes.preview] -project_id = "previewref" +project_id = "${PREVIEW_REF}" [remotes.preview.db.ssl_enforcement] enabled = true `); try { - const loaded = await runConfigEffect(loadProjectConfig(cwd, { projectRef: "previewref" })); + const loaded = await runConfigEffect(loadProjectConfig(cwd, { projectRef: PREVIEW_REF })); // `legacyPresenceIn` reads `document` to detect optional pointer sections; // a remote-introduced `db.ssl_enforcement` must be present there. const db = loaded!.document?.db; @@ -1189,12 +1662,12 @@ enabled = true enabled = true [remotes.preview] -project_id = "previewref" +project_id = "${PREVIEW_REF}" [remotes.preview.api] max_rows = 5 `); try { - const loaded = await runConfigEffect(loadProjectConfig(cwd, { projectRef: "previewref" })); + const loaded = await runConfigEffect(loadProjectConfig(cwd, { projectRef: PREVIEW_REF })); expect(loaded!.config.db.seed.enabled).toBe(false); } finally { await rm(cwd, { recursive: true, force: true }); @@ -1205,18 +1678,103 @@ max_rows = 5 const cwd = await writeTomlProject(`project_id = "baseref" [remotes.preview] -project_id = "previewref" +project_id = "${PREVIEW_REF}" [remotes.preview.db.seed] enabled = true `); try { - const loaded = await runConfigEffect(loadProjectConfig(cwd, { projectRef: "previewref" })); + const loaded = await runConfigEffect(loadProjectConfig(cwd, { projectRef: PREVIEW_REF })); expect(loaded!.config.db.seed.enabled).toBe(true); } finally { await rm(cwd, { recursive: true, force: true }); } }); + test("resolves env() on a lowercase-named variable, matching Go's case-agnostic matcher", async () => { + // Go's `LoadEnvHook` (`apps/cli-go/pkg/config/decode_hooks.go:11`) is + // `^env\((.*)\)$` — it doesn't restrict the captured name's case, so + // `project_id = "env(project_id)"` resolves against a same-case env var + // in the Go CLI. This isn't specific to `project_id`; any string field + // goes through the same pre-decode walk. This case-agnostic matching is + // itself one of the four Go-viper-parity behaviors gated by + // `goViperCompat` — without it, the strict SCREAMING_SNAKE_CASE matcher + // wouldn't match this lowercase name at all. + const previous = process.env.project_id; + process.env.project_id = "lowercase-ref"; + const cwd = await writeTomlProject(`project_id = "env(project_id)"\n`); + try { + const loaded = await runConfigEffect(loadProjectConfig(cwd, { goViperCompat: true })); + expect(loaded!.config.project_id).toBe("lowercase-ref"); + } finally { + if (previous === undefined) { + delete process.env.project_id; + } else { + process.env.project_id = previous; + } + await rm(cwd, { recursive: true, force: true }); + } + }); + + test("does not match a remote whose project_id is env(REF) against the resolved ref (Go parity)", async () => { + // Go's `loadFromFile` duplicate-check/selection loop reads viper's RAW + // string values (`config.go:596-610`) and only calls `c.load(v)` — which + // resolves `env(...)` via `LoadEnvHook` — afterward (`config.go:611`, + // `decode_hooks.go:13-26`). So a `[remotes.x] project_id = "env(REF)"` + // never matches a caller-supplied, already-resolved `REF`: Go compares the + // literal `env(REF)` string, not what it resolves to. + const previous = process.env.SUPABASE_REMOTE_ENV_REF_TEST; + process.env.SUPABASE_REMOTE_ENV_REF_TEST = PREVIEW_REF; + const cwd = await writeTomlProject(`project_id = "baseref" + +[api] +max_rows = 1 + +[remotes.preview] +project_id = "env(SUPABASE_REMOTE_ENV_REF_TEST)" +[remotes.preview.api] +max_rows = 999 +`); + try { + const loaded = await runConfigEffect(loadProjectConfig(cwd, { projectRef: PREVIEW_REF })); + expect(loaded!.appliedRemote).toBeUndefined(); + expect(loaded!.config.api.max_rows).toBe(1); + } finally { + if (previous === undefined) { + delete process.env.SUPABASE_REMOTE_ENV_REF_TEST; + } else { + process.env.SUPABASE_REMOTE_ENV_REF_TEST = previous; + } + await rm(cwd, { recursive: true, force: true }); + } + }); + + test("validates a remote's env(REF) project_id format against its resolved value, not the literal", async () => { + // Go's `Config.Validate` (`config.go:989-1001`) runs entirely after the + // struct decode, by which point `LoadEnvHook` has already resolved + // `env(...)` — so it validates the RESOLVED project_id against the + // 20-lowercase-letter pattern, not the literal `env(REF)` string (which + // would never match the pattern itself). + const previous = process.env.SUPABASE_REMOTE_ENV_REF_FORMAT_TEST; + process.env.SUPABASE_REMOTE_ENV_REF_FORMAT_TEST = PREVIEW_REF; + const cwd = await writeTomlProject(`project_id = "baseref" + +[remotes.preview] +project_id = "env(SUPABASE_REMOTE_ENV_REF_FORMAT_TEST)" +`); + try { + const loaded = await runConfigEffect(loadProjectConfig(cwd)); + expect(loaded!.appliedRemote).toBeUndefined(); + expect(loaded!.config.project_id).toBe("baseref"); + } finally { + if (previous === undefined) { + delete process.env.SUPABASE_REMOTE_ENV_REF_FORMAT_TEST; + } else { + process.env.SUPABASE_REMOTE_ENV_REF_FORMAT_TEST = previous; + } + await rm(cwd, { recursive: true, force: true }); + } + }); + test("resolves env() references inside the matching remote before merge", async () => { const previous = process.env.SUPABASE_REMOTE_MAX_ROWS_TEST; process.env.SUPABASE_REMOTE_MAX_ROWS_TEST = "777"; @@ -1226,12 +1784,12 @@ enabled = true max_rows = 1 [remotes.preview] -project_id = "previewref" +project_id = "${PREVIEW_REF}" [remotes.preview.api] max_rows = "env(SUPABASE_REMOTE_MAX_ROWS_TEST)" `); try { - const loaded = await runConfigEffect(loadProjectConfig(cwd, { projectRef: "previewref" })); + const loaded = await runConfigEffect(loadProjectConfig(cwd, { projectRef: PREVIEW_REF })); expect(loaded!.config.api.max_rows).toBe(777); } finally { if (previous === undefined) { @@ -1242,6 +1800,81 @@ max_rows = "env(SUPABASE_REMOTE_MAX_ROWS_TEST)" await rm(cwd, { recursive: true, force: true }); } }); + + // Go's `Config.Validate` only checks `remotes.*.project_id` format for + // every remote (`config.go:996-1001`, "Since remote config is merged to + // base, we only need to validate the project_id field") — every other + // business-rule check (`Auth.External.validate()`, etc.) runs exactly once, + // against the merged effective config (`config.go:1136-1152`), never + // iterated over `c.Remotes[*]`. A non-selected `[remotes.*]` block's own + // business-rule violations must not fail the whole config load. + test("loads an unselected remote whose external provider is enabled without a secret", async () => { + const cwd = await writeTomlProject( + `project_id = "baseref" + +[remotes.staging] +project_id = "${STAGING_REF}" + +[remotes.staging.auth.external.github] +enabled = true +`, + ); + try { + // No projectRef requested, so [remotes.staging] is never selected/merged — + // Go would never business-rule-validate it, even though it decodes fine + // structurally. + const loaded = await runConfigEffect(loadProjectConfig(cwd)); + expect(loaded!.appliedRemote).toBeUndefined(); + expect(loaded!.config.remotes.staging?.auth.external.github.enabled).toBe(true); + } finally { + await rm(cwd, { recursive: true, force: true }); + } + }); + + test("still validates the same remote's external provider once it is selected", async () => { + const cwd = await writeTomlProject( + `project_id = "baseref" + +[remotes.staging] +project_id = "${STAGING_REF}" + +[remotes.staging.auth.external.github] +enabled = true +`, + ); + try { + // Selecting [remotes.staging] merges it into the effective config, which + // Go DOES business-rule-validate (config.go:1136-1152) — a required + // `client_id`/`secret` is missing, so this must still fail. + const exit = await Effect.runPromiseExit( + loadProjectConfig(cwd, { projectRef: STAGING_REF }).pipe(Effect.provide(BunServices.layer)), + ); + expect(Exit.isFailure(exit)).toBe(true); + } finally { + await rm(cwd, { recursive: true, force: true }); + } + }); + + test("still fails on a structurally malformed value inside an unselected remote", async () => { + // Go's `UnmarshalExact` always structurally decodes every remote + // (`config.go:246,749-756`) regardless of selection — only the + // merged-config-only business rules are skipped for a non-selected + // remote, not type/shape decoding. + const cwd = await writeTomlProject( + `${BASE_WITH_REMOTES} +[remotes.staging.db] +major_version = "not-a-number" +`, + ); + try { + const exit = await Effect.runPromiseExit( + loadProjectConfig(cwd).pipe(Effect.provide(BunServices.layer)), + ); + expect(Exit.isFailure(exit)).toBe(true); + } finally { + await rm(cwd, { recursive: true, force: true }); + } + }); }); describe("config io deprecated [inbucket] back-compat", () => { @@ -1341,7 +1974,7 @@ port = 22222 `project_id = "abc123" [remotes.staging] -project_id = "stagingref" +project_id = "stagingrefaaaaaaaaaa" [remotes.staging.inbucket] enabled = true @@ -1376,3 +2009,124 @@ port = 54324 expect(warnings.some((m) => m.includes("is deprecated"))).toBe(false); }); }); + +describe("config io deprecated [auth.external.{linkedin,slack}] back-compat", () => { + let warnings: Array = []; + let errorSpy: ReturnType | undefined; + + function captureWarnings() { + warnings = []; + errorSpy = vi.spyOn(console, "error").mockImplementation((...args) => { + warnings.push(args.map((a) => String(a)).join(" ")); + }); + } + + afterEach(() => { + errorSpy?.mockRestore(); + errorSpy = undefined; + }); + + async function loadToml(contents: string, options?: LoadProjectConfigOptions) { + const cwd = makeTempProject(); + const path = await runConfigEffect(configTomlPath(cwd)); + await mkdir(join(cwd, "supabase"), { recursive: true }); + await writeFile(path, contents); + try { + return await runConfigEffect(loadProjectConfigFile(path, options)); + } finally { + await rm(cwd, { recursive: true, force: true }); + } + } + + test("loads a bare [auth.external.slack] block without required fields", async () => { + captureWarnings(); + const loaded = await loadToml( + `project_id = "abc123" + +[auth.external.slack] +enabled = true +`, + { goViperCompat: true }, + ); + + expect("slack" in loaded.config.auth.external).toBe(false); + expect(loaded.document).not.toHaveProperty("auth.external.slack"); + expect( + warnings.some((m) => + m.includes( + 'WARN: disabling deprecated "slack" provider. Please use [auth.external.slack_oidc] instead', + ), + ), + ).toBe(true); + }); + + test("loads a bare [auth.external.linkedin] block without required fields", async () => { + captureWarnings(); + const loaded = await loadToml( + `project_id = "abc123" + +[auth.external.linkedin] +enabled = true +`, + { goViperCompat: true }, + ); + + expect("linkedin" in loaded.config.auth.external).toBe(false); + expect( + warnings.some((m) => + m.includes( + 'WARN: disabling deprecated "linkedin" provider. Please use [auth.external.linkedin_oidc] instead', + ), + ), + ).toBe(true); + }); + + test("does not warn when the deprecated section is present but disabled", async () => { + captureWarnings(); + const loaded = await loadToml( + `project_id = "abc123" + +[auth.external.slack] +enabled = false +`, + ); + + expect("slack" in loaded.config.auth.external).toBe(false); + expect(warnings.some((m) => m.includes("is deprecated"))).toBe(false); + }); + + test("does not warn when only [auth.external.slack_oidc] is used", async () => { + captureWarnings(); + const loaded = await loadToml( + `project_id = "abc123" + +[auth.external.slack_oidc] +enabled = true +client_id = "abc" +secret = "shh" +`, + ); + + expect(loaded.config.auth.external.slack_oidc.enabled).toBe(true); + expect(warnings.some((m) => m.includes("is deprecated"))).toBe(false); + }); + + test("strips a deprecated [remotes.*.auth.external.slack] block without warning for an unselected remote", async () => { + captureWarnings(); + const loaded = await loadToml( + `project_id = "abc123" + +[remotes.staging] +project_id = "stagingrefaaaaaaaaaa" + +[remotes.staging.auth.external.slack] +enabled = true +`, + ); + + // Not requesting `projectRef` means no remote is selected, so `remotes` survives + // decode verbatim (minus the deprecated key) rather than being merged/dropped. + expect(loaded.config.remotes.staging?.auth.external).not.toHaveProperty("slack"); + expect(warnings.some((m) => m.includes("is deprecated"))).toBe(false); + }); +}); diff --git a/packages/config/src/lib/env.ts b/packages/config/src/lib/env.ts index bd16819293..23a0c30056 100644 --- a/packages/config/src/lib/env.ts +++ b/packages/config/src/lib/env.ts @@ -1,11 +1,22 @@ import { Schema, SchemaAST } from "effect"; -export const ENV_PATTERN = "^env\\([A-Z_][A-Z0-9_]*\\)$"; -export const ENV_CAPTURE_REGEX = /^env\(([A-Z_][A-Z0-9_]*)\)$/; +// Go's `LoadEnvHook` matcher (`apps/cli-go/pkg/config/decode_hooks.go:11`) is +// `^env\((.*)\)$` — permissive on the captured name's case/content, and +// reused verbatim for secrets (`secret.go:99`) and the unset-var warning +// (`config.go:1195`). Matching that exactly (not an uppercase-only +// restriction) so e.g. `project_id = "env(project_id)"` substitutes the same +// way it does in the Go CLI. +export const ENV_PATTERN = "^env\\((.*)\\)$"; +export const ENV_CAPTURE_REGEX = /^env\((.*)\)$/; +// Pre-PR-#5765 strict matcher: SCREAMING_SNAKE_CASE names only. Selected when +// `goViperCompat` is off so non-Go-parity surfaces (next/, packages/stack, the +// functions manifest) keep the narrower matching they had before PR #5765 +// widened env() resolution to Go's case-agnostic `^env\((.*)\)$`. +export const ENV_CAPTURE_REGEX_STRICT = /^env\(([A-Z_][A-Z0-9_]*)\)$/; const envRegex = new RegExp(ENV_PATTERN); -export function isEnvReference(value: string): boolean { - return envRegex.test(value); +export function isEnvReference(value: string, goViperCompat: boolean): boolean { + return (goViperCompat ? ENV_CAPTURE_REGEX : ENV_CAPTURE_REGEX_STRICT).test(value); } interface EnvAnnotations extends Schema.Annotations.Documentation { @@ -52,12 +63,29 @@ export const secret = (annotations?: SecretAnnotations) => // and the value is still a string, coerce it. This mirrors Go's // mapstructure chain where `LoadEnvHook` returns a string and subsequent // hooks convert it to the target type. -// - Coercion is only attempted on strings produced by env() substitution. -// Pre-existing string literals at non-string paths are left untouched — -// they'll surface as schema errors at decode time with their original -// value, preserving error clarity. +// - Number/boolean coercion is only attempted on strings produced by env() +// substitution. Pre-existing string literals at non-string paths are left +// untouched — they'll surface as schema errors at decode time with their +// original value, preserving error clarity. +// - Array coercion is the one exception: if the schema at that path expects +// a homogeneous string array, ANY string leaf (substituted or a plain +// literal) is split on `,` — mirroring Go's `StringToSliceHookFunc(",")` +// (`apps/cli-go/pkg/config/config.go:775-784`), which is wired +// unconditionally into the decode hook chain regardless of where the +// string came from (e.g. `additional_redirect_urls = "http://a,http://b"` +// decodes fine in Go today, not just via `env(...)`). -type ExpectedType = "number" | "boolean" | "string" | "unknown"; +type ExpectedType = "number" | "boolean" | "string" | "array" | "unknown"; + +// Go decodes an env()-substituted boolean via mapstructure's weakly-typed +// `decodeBool`, which runs `strconv.ParseBool` on the string — a wider +// acceptance set than the literal `"true"`/`"false"` this module used to +// require. Mirrors `legacyParseGoBool`'s `GO_BOOL_TRUE`/`GO_BOOL_FALSE` +// (`apps/cli/src/legacy/shared/legacy-db-config.toml-read.ts:615-616`); +// duplicated here (not imported) so `packages/config` doesn't depend on +// `apps/cli`. +const GO_BOOL_TRUE = new Set(["1", "t", "T", "TRUE", "true", "True"]); +const GO_BOOL_FALSE = new Set(["0", "f", "F", "FALSE", "false", "False", ""]); // Unwrap Suspend (lazy AST refs from recursive schemas). Other transformation // wrappers expose the target type via `.ast` directly, so no additional @@ -69,6 +97,20 @@ function unwrapAst(ast: SchemaAST.AST): SchemaAST.AST { return ast; } +// A homogeneous `Schema.Array(Schema.String)` compiles to an `Arrays` AST +// node with no fixed tuple `elements` and a single `rest` spread type. Only +// this shape (not a fixed string tuple, and not a mixed-type array) is +// eligible for Go's `StringToSliceHookFunc(",")` coercion below — mirroring +// that Go itself only wires the hook for `[]string`-kind targets +// (`apps/cli-go/pkg/config/config.go:775-784`), not fixed-arity tuples. +function isHomogeneousStringArray(node: SchemaAST.AST): boolean { + if (node._tag !== "Arrays" || node.elements.length !== 0 || node.rest.length !== 1) { + return false; + } + const spread = node.rest[0]; + return spread !== undefined && unwrapAst(spread)._tag === "String"; +} + function leafExpectedType(ast: SchemaAST.AST): ExpectedType { const node = unwrapAst(ast); switch (node._tag) { @@ -78,6 +120,8 @@ function leafExpectedType(ast: SchemaAST.AST): ExpectedType { return "boolean"; case "String": return "string"; + case "Arrays": + return isHomogeneousStringArray(node) ? "array" : "unknown"; case "Union": { // Walk Union branches in declared order; first concrete primitive wins. // For unions like `Schema.Union(Schema.Number, Schema.Null)` this picks @@ -156,23 +200,39 @@ function coerceLeaf(value: unknown, expected: ExpectedType): unknown { return value; } if (expected === "boolean") { - if (value === "true") return true; - if (value === "false") return false; + if (GO_BOOL_TRUE.has(value)) return true; + if (GO_BOOL_FALSE.has(value)) return false; return value; } + if (expected === "array") { + // Go's `mapstructure.StringToSliceHookFunc(",")` (wired in + // `apps/cli-go/pkg/config/config.go:775-784`): an empty string decodes to + // an empty slice, otherwise the string is split on the separator with no + // further trimming of the resulting elements. + return value === "" ? [] : value.split(","); + } return value; } -function substituteEnvLeaf(value: string, env: Readonly>): string { - const match = ENV_CAPTURE_REGEX.exec(value); +function substituteEnvLeaf( + value: string, + env: Readonly>, + goViperCompat: boolean, +): string { + const match = (goViperCompat ? ENV_CAPTURE_REGEX : ENV_CAPTURE_REGEX_STRICT).exec(value); if (match === null) { return value; } const envName = match[1]; - if (envName === undefined || !Object.prototype.hasOwnProperty.call(env, envName)) { + const resolved = envName === undefined ? undefined : env[envName]; + // Go's LoadEnvHook only substitutes when the env var is non-empty + // (`apps/cli-go/pkg/config/decode_hooks.go:19-24`: `len(env) > 0`), so a + // key that's present but empty (e.g. a dotenv `KEY=` line) preserves the + // `env(KEY)` literal exactly like an unset key, rather than substituting "". + if (resolved === undefined || resolved === "") { return value; } - return env[envName] ?? value; + return resolved; } function isDeferredEnvField(ast: SchemaAST.AST): boolean { @@ -196,11 +256,12 @@ function walk( document: unknown, env: Readonly>, ast: SchemaAST.AST | null, + goViperCompat: boolean, ): unknown { if (Array.isArray(document)) { return document.map((item, index) => { const child = ast === null ? null : descendAst(ast, String(index)); - return walk(item, env, child); + return walk(item, env, child, goViperCompat); }); } @@ -208,7 +269,7 @@ function walk( const result: Record = {}; for (const [key, value] of Object.entries(document)) { const child = ast === null ? null : descendAst(ast, key); - result[key] = walk(value, env, child); + result[key] = walk(value, env, child, goViperCompat); } return result; } @@ -220,18 +281,37 @@ function walk( if (ast !== null && isDeferredEnvField(ast)) { return document; } + + const substituted = substituteEnvLeaf(document, env, goViperCompat); + const expected = ast === null ? "unknown" : leafExpectedType(ast); + + // Go's `StringToSliceHookFunc(",")` (`apps/cli-go/pkg/config/config.go: + // 775-784`) is wired unconditionally into `v.UnmarshalExact`'s decode + // hook chain, so it splits ANY string being decoded into a `[]string` + // field — a plain TOML literal (`additional_redirect_urls = "a,b"`) just + // as much as an `env()`-substituted one. Unlike the number/boolean + // coercion below (scoped to substituted values only, since TOML already + // decodes literal numbers/booleans to their native type), array coercion + // must also apply to literal strings that never went through + // `substituteEnvLeaf`. Gated by `goViperCompat`: when off, the string is + // left unsplit — literal and substituted alike — so an array-typed field + // fed a string fails decode instead of silently coercing, matching + // pre-PR-#5765 behavior. + if (expected === "array") { + return goViperCompat ? coerceLeaf(substituted, expected) : substituted; + } + // Substitute env() then coerce based on the schema's expected type at this // path. Only the substituted form is fed to coercion — literal strings at // non-string paths are left untouched so the decoder can report them with // their original value. - const substituted = substituteEnvLeaf(document, env); if (substituted === document) { return document; } if (ast === null) { return substituted; } - return coerceLeaf(substituted, leafExpectedType(ast)); + return coerceLeaf(substituted, expected); } return document; @@ -242,8 +322,11 @@ function walk( * * Walks the raw parsed document and the schema AST in parallel. For every * string leaf matching `env(VAR)`: - * 1. Substitutes `env[VAR]` if set, else preserves the literal verbatim - * (Go-parity with `apps/cli-go/pkg/config/decode_hooks.go:14-21`). + * 1. Substitutes `env[VAR]` if set AND non-empty, else preserves the + * literal verbatim (Go-parity with + * `apps/cli-go/pkg/config/decode_hooks.go:14-21`, which gates on + * `len(env) > 0` — a set-but-empty var, e.g. a dotenv `KEY=` line, + * leaves the `env(KEY)` literal untouched just like an unset one). * 2. If the schema at that path expects Number or Boolean, coerces the * substituted string to the expected primitive — mirroring Go's * mapstructure chain where `LoadEnvHook` returns a string that the next @@ -255,6 +338,7 @@ export function interpolateEnvReferencesAgainstSchema( document: unknown, env: Readonly>, schema: { readonly ast: SchemaAST.AST }, + options?: { readonly goViperCompat?: boolean }, ): unknown { - return walk(document, env, schema.ast); + return walk(document, env, schema.ast, options?.goViperCompat ?? false); } diff --git a/packages/config/src/paths.ts b/packages/config/src/paths.ts index e41017793d..bf29f44dcc 100644 --- a/packages/config/src/paths.ts +++ b/packages/config/src/paths.ts @@ -31,10 +31,41 @@ const findConfigInRoot = Effect.fnUntraced(function* (root: string) { } satisfies ProjectPaths; }); -export const findProjectPaths = Effect.fnUntraced(function* (cwd: string) { +export interface FindProjectPathsOptions { + /** + * When `false`, only `cwd` itself is checked for `supabase/config.{json,toml}` — + * no ancestor climb. Go's own resolution never searches twice: an explicit + * `--workdir`/`SUPABASE_WORKDIR` is used exactly as given (`ChangeWorkDir`, + * `apps/cli-go/internal/utils/misc.go:231-247`), and once `os.Chdir`'d there, + * `config.toml` is read as a plain relative path with no further ancestor + * search (`NewPathBuilder`, `pkg/config/utils.go:43-48`). Ancestor climbing in + * Go only ever happens once, as the *default* when workdir is unset + * (`getProjectRoot`, `internal/utils/misc.go:209-224`). + * + * Callers that already hold an authoritative, Go-equivalent project root + * (e.g. the legacy `stop`/`status` ports' `cliConfig.workdir`, which mirrors + * `ChangeWorkDir`'s own explicit-vs-default resolution) should pass `false` + * here to avoid a second, un-Go-like ancestor search that could otherwise + * pick up an unrelated ancestor project's config. + * + * Defaults to `true` (the original ancestor-search behavior), so existing + * callers are unaffected. + */ + readonly search?: boolean; +} + +export const findProjectPaths = Effect.fnUntraced(function* ( + cwd: string, + options?: FindProjectPathsOptions, +) { const path = yield* Path.Path; - let current = path.resolve(cwd); + const start = path.resolve(cwd); + + if (options?.search === false) { + return yield* findConfigInRoot(start); + } + let current = start; while (true) { const match = yield* findConfigInRoot(current); diff --git a/packages/config/src/project.ts b/packages/config/src/project.ts index 68953f2b1b..daa9cc0e08 100644 --- a/packages/config/src/project.ts +++ b/packages/config/src/project.ts @@ -1,10 +1,9 @@ import { Effect, FileSystem, Redacted } from "effect"; import { ProjectConfigSchema } from "./base.ts"; import { ProjectEnvParseError } from "./errors.ts"; -import { ENV_CAPTURE_REGEX, isEnvReference } from "./lib/env.ts"; +import { ENV_CAPTURE_REGEX, ENV_CAPTURE_REGEX_STRICT, isEnvReference } from "./lib/env.ts"; import { findProjectPaths, type ProjectPaths } from "./paths.ts"; -const envReferencePattern = ENV_CAPTURE_REGEX; const dotEnvLinePattern = /^\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?$/; @@ -45,6 +44,41 @@ function normalizeAmbientEnv( return values; } +// Detects a line of the form `KEY=...` (or `KEY: ...`) whose +// quoted value does NOT close on that same physical line — the start of a +// godotenv-style multiline quoted value (e.g. a PEM block). Returns the quote +// character and the index of the opening quote within `line`, or `null` if +// the line doesn't open an unterminated quote (either no quote at all, or one +// that already closes on this line). +const dotEnvValueOpenerPattern = /^\s*(?:export\s+)?[\w.-]+(?:\s*=\s*?|:\s+?)(['"`])/; + +function findUnescapedQuoteIndex(text: string, quote: string, from: number): number { + for (let i = from; i < text.length; i += 1) { + if (text[i] === quote && text[i - 1] !== "\\") { + return i; + } + } + return -1; +} + +function detectOpenQuoteStart(line: string): { quote: string; openIndex: number } | null { + const openerMatch = dotEnvValueOpenerPattern.exec(line); + if (openerMatch === null) { + return null; + } + const quote = openerMatch[1]; + if (quote === undefined) { + return null; + } + const openIndex = openerMatch[0].length - 1; + if (findUnescapedQuoteIndex(line, quote, openIndex + 1) !== -1) { + // Already closes on this same line — this isn't the multiline case, so + // whatever made the outer match fail is a genuine parse error. + return null; + } + return { quote, openIndex }; +} + function parseDotEnvValue(rawValue: string): string { let value = rawValue.trim(); const maybeQuote = value[0]; @@ -78,7 +112,40 @@ function parseDotEnv( continue; } - const match = dotEnvLinePattern.exec(line); + let candidate = line; + let consumedThrough = index; + + // Check for an unterminated quote BEFORE attempting the single-line + // match: `dotEnvLinePattern`'s value alternatives fall back to an + // unquoted match (`[^#\r\n]+`) when none of the quoted alternatives + // close on this line, which would otherwise "succeed" with a truncated, + // still-quote-prefixed value instead of signaling a multiline value — + // masking the real bug rather than triggering accumulation. This is a + // godotenv-style quoted value spanning multiple physical lines (e.g. a + // PEM block); Go's `loadNestedEnv` parses this fine (`godotenv@v1.5.1`'s + // cursor-based scanner never splits into lines up front; see + // `legacy-dotenv.ts` for the Go-compatible reference implementation used + // elsewhere in this repo). Accumulate subsequent lines until the opened + // quote closes (or EOF), then match the same per-line pattern against + // the joined multiline chunk — its quoted-value alternatives use + // negated character classes (`[^"]` etc.), which already match embedded + // newlines once given the full span. + const opener = detectOpenQuoteStart(line); + if (opener !== null) { + for (let next = index + 1; next < lines.length; next += 1) { + const nextLine = lines[next]; + if (nextLine === undefined) { + continue; + } + candidate += "\n" + nextLine; + consumedThrough = next; + if (findUnescapedQuoteIndex(candidate, opener.quote, opener.openIndex + 1) !== -1) { + break; + } + } + } + + const match = dotEnvLinePattern.exec(candidate); if (match === null) { return yield* Effect.fail(new ProjectEnvParseError({ path, line: index + 1 })); @@ -92,6 +159,7 @@ function parseDotEnv( } values[key] = parseDotEnvValue(rawValue); + index = consumedThrough; } return values; @@ -113,13 +181,36 @@ function applySource( export interface LoadProjectEnvironmentOptions { readonly cwd: string; readonly baseEnv?: Readonly>; + /** See {@link FindProjectPathsOptions.search}. */ + readonly search?: boolean; + /** + * Skip reading/parsing `paths.envLocalPath` (`supabase/.env.local`) + * entirely. Mirrors Go's `loadDefaultEnv` (`apps/cli-go/pkg/config/ + * config.go:1243-1250`), which omits `.env.local` from its candidate + * filename list whenever `SUPABASE_ENV=test` — so a malformed or + * intentionally non-test `.env.local` is invisible to Go in that mode and + * must not fail config loading here either. Defaults to `false` so + * existing callers that don't have a `SUPABASE_ENV` gate of their own + * (`next/`, `secrets set`) are unaffected. + */ + readonly skipEnvLocal?: boolean; +} + +export interface ResolveProjectOptions { + /** + * Opt into Go/viper-parity `env()` matching (case-agnostic + * `^env\((.*)\)$`). Defaults to `false`, which uses the pre-PR-#5765 strict + * SCREAMING_SNAKE_CASE matcher (`ENV_CAPTURE_REGEX_STRICT`). Only the + * Go-parity legacy shell sets this to `true`. + */ + readonly goViperCompat?: boolean; } export const loadProjectEnvironment = Effect.fnUntraced(function* ( options: LoadProjectEnvironmentOptions, ) { const fs = yield* FileSystem.FileSystem; - const paths = yield* findProjectPaths(options.cwd); + const paths = yield* findProjectPaths(options.cwd, { search: options.search }); if (paths === null) { return null; @@ -136,7 +227,7 @@ export const loadProjectEnvironment = Effect.fnUntraced(function* ( loadedPaths.push(paths.envPath); } - if (yield* fs.exists(paths.envLocalPath)) { + if (!options.skipEnvLocal && (yield* fs.exists(paths.envLocalPath))) { const contents = yield* fs.readFileString(paths.envLocalPath); const parsed = yield* parseDotEnv(paths.envLocalPath, contents); applySource(values, sources, parsed, ".env.local"); @@ -216,21 +307,33 @@ function isSecretPath(path: ReadonlyArray): boolean { return secretPathPatterns.some((pattern) => matchesPathPattern(pattern, path)); } -function interpolateLeafValue(value: string, env: Readonly>): string { - const match = envReferencePattern.exec(value); +function interpolateLeafValue( + value: string, + env: Readonly>, + goViperCompat: boolean, +): string { + const match = (goViperCompat ? ENV_CAPTURE_REGEX : ENV_CAPTURE_REGEX_STRICT).exec(value); const envName = match?.[1]; if (envName === undefined) { return value; } - // Preserve the literal `env(VAR)` verbatim when VAR is unset. Matches Go's - // `apps/cli-go/pkg/config/decode_hooks.go:14-21` (LoadEnvHook). - if (!Object.prototype.hasOwnProperty.call(env, envName)) { + const resolved = env[envName]; + // Preserve the literal `env(VAR)` verbatim when VAR is unset OR present but + // empty (e.g. a dotenv `KEY=` line). Matches Go's `LoadEnvHook` + // (`apps/cli-go/pkg/config/decode_hooks.go:19-24`: `len(env) > 0`), which + // only substitutes a non-empty value — same gate as `substituteEnvLeaf` in + // `lib/env.ts`. Without this, a present-but-empty `env(...)` secret (e.g. + // `edge_runtime.secrets.FOO = "env(EMPTY)"`) resolves to `""` here, gets + // redacted by `redactValue` as a real value instead of skipped as an + // unresolved literal, and `secrets set` uploads a blank secret Go would + // never send. + if (resolved === undefined || resolved === "") { return value; } - return env[envName] ?? value; + return resolved; } function toPathSegments(path: string): ReadonlyArray { @@ -241,44 +344,48 @@ function toPathSegments(path: string): ReadonlyArray { return path.split(".").filter((segment) => segment.length > 0); } -function interpolateValue(value: unknown, env: Readonly>): unknown { +function interpolateValue( + value: unknown, + env: Readonly>, + goViperCompat: boolean, +): unknown { if (Array.isArray(value)) { - return value.map((item) => interpolateValue(item, env)); + return value.map((item) => interpolateValue(item, env, goViperCompat)); } if (typeof value === "object" && value !== null) { const result: Record = {}; for (const [key, child] of Object.entries(value)) { - result[key] = interpolateValue(child, env); + result[key] = interpolateValue(child, env, goViperCompat); } return result; } if (typeof value === "string") { - return interpolateLeafValue(value, env); + return interpolateLeafValue(value, env, goViperCompat); } return value; } -function redactValue(value: unknown, path: ReadonlyArray = []): unknown { +function redactValue(value: unknown, path: ReadonlyArray, goViperCompat: boolean): unknown { if (Array.isArray(value)) { - return value.map((item, index) => redactValue(item, [...path, String(index)])); + return value.map((item, index) => redactValue(item, [...path, String(index)], goViperCompat)); } if (typeof value === "object" && value !== null) { const result: Record = {}; for (const [key, child] of Object.entries(value)) { - result[key] = redactValue(child, [...path, key]); + result[key] = redactValue(child, [...path, key], goViperCompat); } return result; } - if (typeof value === "string" && isSecretPath(path) && !isEnvReference(value)) { + if (typeof value === "string" && isSecretPath(path) && !isEnvReference(value, goViperCompat)) { return Redacted.make(value, { label: path.join(".") }); } @@ -289,15 +396,17 @@ function resolveProjectValueAtPath( value: unknown, projectEnv: ProjectEnvironment, path: ReadonlyArray, + goViperCompat: boolean, ): unknown { - const interpolated = interpolateValue(value, projectEnv.values); - return redactValue(interpolated, path); + const interpolated = interpolateValue(value, projectEnv.values, goViperCompat); + return redactValue(interpolated, path, goViperCompat); } export function resolveProjectValue( value: T, projectEnv: ProjectEnvironment, configPath: string, + options?: ResolveProjectOptions, ): Effect.Effect> { return Effect.sync( () => @@ -305,6 +414,7 @@ export function resolveProjectValue( value, projectEnv, toPathSegments(configPath), + options?.goViperCompat ?? false, ) as ResolvedProjectValue, ); } @@ -313,6 +423,7 @@ export function resolveProjectSubtree( value: T, projectEnv: ProjectEnvironment, pathPrefix: string, + options?: ResolveProjectOptions, ): Effect.Effect> { return Effect.sync( () => @@ -320,6 +431,7 @@ export function resolveProjectSubtree( value, projectEnv, toPathSegments(pathPrefix), + options?.goViperCompat ?? false, ) as ResolvedProjectValue, ); } diff --git a/packages/config/src/project.unit.test.ts b/packages/config/src/project.unit.test.ts index de22a465fe..9f29094bc2 100644 --- a/packages/config/src/project.unit.test.ts +++ b/packages/config/src/project.unit.test.ts @@ -6,7 +6,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { Effect, FileSystem, Path, Redacted } from "effect"; import { findProjectRootFor, loadProjectEnvironmentFor } from "./bun.ts"; -import { ProjectConfigParseError } from "./errors.ts"; +import { ProjectConfigParseError, ProjectEnvParseError } from "./errors.ts"; import { findProjectPaths, loadProjectConfig, @@ -50,6 +50,41 @@ describe("project discovery and lazy env resolution", () => { } }); + test("search: false only checks cwd itself, matching Go's exact-workdir resolution", async () => { + // Mirrors Go's `ChangeWorkDir` (`apps/cli-go/internal/utils/misc.go:231-247`): + // an explicit workdir is used exactly as given, with no ancestor climb — + // callers that already hold a Go-equivalent project root (e.g. the legacy + // `stop`/`status` ports' `cliConfig.workdir`) pass `search: false` to avoid + // picking up an unrelated ancestor project. + const cwd = makeTempProject(); + const repoRoot = join(cwd, "repo"); + const packageRoot = join(repoRoot, "apps", "web"); + const nestedCwd = join(packageRoot, "src", "components"); + + try { + await mkdir(join(repoRoot, "supabase"), { recursive: true }); + await mkdir(nestedCwd, { recursive: true }); + await writeFile(join(repoRoot, "supabase", "config.toml"), 'project_id = "repo"\n'); + + // nestedCwd has no supabase/ of its own; only an ancestor (repoRoot) does. + const searched = await runConfigEffect(findProjectPaths(nestedCwd)); + expect(searched?.projectRoot).toBe(repoRoot); + + const unsearched = await runConfigEffect(findProjectPaths(nestedCwd, { search: false })); + expect(unsearched).toBeNull(); + + const configAtRepoRoot = await runConfigEffect(findProjectPaths(repoRoot, { search: false })); + expect(configAtRepoRoot?.projectRoot).toBe(repoRoot); + + expect(await runConfigEffect(loadProjectConfig(nestedCwd, { search: false }))).toBeNull(); + expect( + await runConfigEffect(loadProjectEnvironment({ cwd: nestedCwd, search: false })), + ).toBeNull(); + } finally { + await rm(cwd, { recursive: true, force: true }); + } + }); + test("loads env from the discovered supabase directory with the right precedence", async () => { const cwd = makeTempProject(); const repoRoot = join(cwd, "repo"); @@ -107,6 +142,105 @@ describe("project discovery and lazy env resolution", () => { } }); + test("parses a multiline double-quoted .env value (godotenv/Go parity)", async () => { + const cwd = makeTempProject(); + + try { + await mkdir(join(cwd, "supabase"), { recursive: true }); + await writeFile(join(cwd, "supabase", "config.toml"), 'project_id = "ref_123"\n'); + await writeFile( + join(cwd, "supabase", ".env"), + [ + 'PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----', + "MIIEpAIBAAKCAQEA1c7+9z5Pad7OejecsQ0bu3aumga", + '-----END RSA PRIVATE KEY-----"', + "OTHER=value", + "", + ].join("\n"), + ); + + const projectEnv = await runConfigEffect(loadProjectEnvironment({ cwd })); + + expect(projectEnv).not.toBeNull(); + expect(projectEnv?.values.PRIVATE_KEY).toBe( + [ + "-----BEGIN RSA PRIVATE KEY-----", + "MIIEpAIBAAKCAQEA1c7+9z5Pad7OejecsQ0bu3aumga", + "-----END RSA PRIVATE KEY-----", + ].join("\n"), + ); + expect(projectEnv?.values.OTHER).toBe("value"); + } finally { + await rm(cwd, { recursive: true, force: true }); + } + }); + + test("parses a multiline single-quoted .env value followed by a trailing comment", async () => { + const cwd = makeTempProject(); + + try { + await mkdir(join(cwd, "supabase"), { recursive: true }); + await writeFile(join(cwd, "supabase", "config.toml"), 'project_id = "ref_123"\n'); + await writeFile( + join(cwd, "supabase", ".env"), + ["MULTI='line one", "line two' # trailing comment", "AFTER=ok", ""].join("\n"), + ); + + const projectEnv = await runConfigEffect(loadProjectEnvironment({ cwd })); + + expect(projectEnv).not.toBeNull(); + expect(projectEnv?.values.MULTI).toBe(["line one", "line two"].join("\n")); + expect(projectEnv?.values.AFTER).toBe("ok"); + } finally { + await rm(cwd, { recursive: true, force: true }); + } + }); + + test("still fails a genuinely malformed .env line (not a multiline quote)", async () => { + const cwd = makeTempProject(); + + try { + await mkdir(join(cwd, "supabase"), { recursive: true }); + await writeFile(join(cwd, "supabase", "config.toml"), 'project_id = "ref_123"\n'); + await writeFile(join(cwd, "supabase", ".env"), "!!!not-a-valid-line\n"); + + await expect(runConfigEffect(loadProjectEnvironment({ cwd }))).rejects.toBeInstanceOf( + ProjectEnvParseError, + ); + } finally { + await rm(cwd, { recursive: true, force: true }); + } + }); + + test("skipEnvLocal ignores .env.local entirely, matching Go's SUPABASE_ENV=test gate", async () => { + // Go's `loadDefaultEnv` (`apps/cli-go/pkg/config/config.go:1243-1250`) omits + // `.env.local` from its candidate filename list whenever `SUPABASE_ENV=test`, + // so a malformed `.env.local` is invisible to Go in that mode. Callers that + // reproduce this gate (`status`/`stop` handlers) pass `skipEnvLocal: true`. + const cwd = makeTempProject(); + + try { + await mkdir(join(cwd, "supabase"), { recursive: true }); + await writeFile(join(cwd, "supabase", "config.toml"), 'project_id = "ref_123"\n'); + await writeFile(join(cwd, "supabase", ".env"), "FROM_ENV=1\n"); + // Malformed — would normally throw ProjectEnvParseError. + await writeFile(join(cwd, "supabase", ".env.local"), "!!!not-a-valid-line\n"); + + const projectEnv = await runConfigEffect(loadProjectEnvironment({ cwd, skipEnvLocal: true })); + + expect(projectEnv).not.toBeNull(); + expect(projectEnv?.values.FROM_ENV).toBe("1"); + expect(projectEnv?.loadedPaths).toEqual([join(cwd, "supabase", ".env")]); + + // Without the flag, the same malformed file still fails as before. + await expect(runConfigEffect(loadProjectEnvironment({ cwd }))).rejects.toBeInstanceOf( + ProjectEnvParseError, + ); + } finally { + await rm(cwd, { recursive: true, force: true }); + } + }); + test("leaves [api].auto_expose_new_tables unset by default and round-trips an explicit value", async () => { const cwd = makeTempProject(); const projectRoot = join(cwd, "repo"); @@ -214,6 +348,9 @@ jwt_secret = "env(AUTH_JWT_SECRET)" [edge_runtime.secrets] api_key = "env(EDGE_API_KEY)" +[remotes.preview] +project_id = "previewrefaaaaaaaaaa" + [remotes.preview.auth] jwt_secret = "env(PREVIEW_JWT_SECRET)" `, @@ -282,6 +419,44 @@ jwt_secret = "env(MISSING_SECRET)" } }); + // Go's `LoadEnvHook` (`apps/cli-go/pkg/config/decode_hooks.go:19-24`) only + // substitutes a non-empty env var (`len(env) > 0`) — a present-but-empty + // dotenv line (`EMPTY_SECRET=`) is treated the same as an unset var, so the + // literal `env(...)` reference is preserved rather than resolved to `""`. + test("resolveProjectValue preserves env() literal when the env var is present but empty (Go parity)", async () => { + const cwd = makeTempProject(); + const projectRoot = join(cwd, "repo"); + + try { + await mkdir(join(projectRoot, "supabase"), { recursive: true }); + await writeFile( + join(projectRoot, "supabase", "config.toml"), + `project_id = "ref_123" + +[edge_runtime.secrets] +foo = "env(EMPTY_SECRET)" +`, + ); + await writeFile(join(projectRoot, "supabase", ".env"), "EMPTY_SECRET=\n"); + + const loaded = await runConfigEffect(loadProjectConfig(projectRoot)); + const projectEnv = await runConfigEffect(loadProjectEnvironment({ cwd: projectRoot })); + + const resolved = await runConfigEffect( + resolveProjectValue( + loaded!.config.edge_runtime.secrets!.foo, + projectEnv!, + "edge_runtime.secrets.foo", + ), + ); + + expect(Redacted.isRedacted(resolved)).toBe(false); + expect(resolved).toBe("env(EMPTY_SECRET)"); + } finally { + await rm(cwd, { recursive: true, force: true }); + } + }); + test("resolveProjectSubtree preserves env() literals nested inside the selected subtree", async () => { const cwd = makeTempProject(); const projectRoot = join(cwd, "repo"); @@ -334,4 +509,75 @@ account_sid = "AC123" await rm(cwd, { recursive: true, force: true }); } }); + + // Pins the pre-PR-#5765 strict SCREAMING_SNAKE_CASE `env()` matcher as the + // default for `resolveProjectValue`/`resolveProjectSubtree`, since `next/` + // and `packages/stack` call these without ever passing `goViperCompat`. + test("resolveProjectValue does not resolve a lowercase-named env() reference by default", async () => { + const cwd = makeTempProject(); + const projectRoot = join(cwd, "repo"); + + try { + await mkdir(join(projectRoot, "supabase"), { recursive: true }); + await writeFile( + join(projectRoot, "supabase", "config.toml"), + `project_id = "ref_123" + +[auth] +jwt_secret = "env(lowercase_secret)" +`, + ); + await writeFile(join(projectRoot, "supabase", ".env"), "lowercase_secret=super-secret\n"); + + const loaded = await runConfigEffect(loadProjectConfig(projectRoot)); + const projectEnv = await runConfigEffect(loadProjectEnvironment({ cwd: projectRoot })); + + const resolved = await runConfigEffect( + resolveProjectValue(loaded!.config.auth.jwt_secret, projectEnv!, "auth.jwt_secret"), + ); + + expect(Redacted.isRedacted(resolved)).toBe(true); + if (!Redacted.isRedacted(resolved)) { + throw new Error("Expected auth.jwt_secret to be redacted."); + } + expect(Redacted.value(resolved)).toBe("env(lowercase_secret)"); + } finally { + await rm(cwd, { recursive: true, force: true }); + } + }); + + test("resolveProjectValue resolves a lowercase-named env() reference when goViperCompat is true", async () => { + const cwd = makeTempProject(); + const projectRoot = join(cwd, "repo"); + + try { + await mkdir(join(projectRoot, "supabase"), { recursive: true }); + await writeFile( + join(projectRoot, "supabase", "config.toml"), + `project_id = "ref_123" + +[auth] +jwt_secret = "env(lowercase_secret)" +`, + ); + await writeFile(join(projectRoot, "supabase", ".env"), "lowercase_secret=super-secret\n"); + + const loaded = await runConfigEffect(loadProjectConfig(projectRoot)); + const projectEnv = await runConfigEffect(loadProjectEnvironment({ cwd: projectRoot })); + + const resolved = await runConfigEffect( + resolveProjectValue(loaded!.config.auth.jwt_secret, projectEnv!, "auth.jwt_secret", { + goViperCompat: true, + }), + ); + + expect(Redacted.isRedacted(resolved)).toBe(true); + if (!Redacted.isRedacted(resolved)) { + throw new Error("Expected auth.jwt_secret to be redacted."); + } + expect(Redacted.value(resolved)).toBe("super-secret"); + } finally { + await rm(cwd, { recursive: true, force: true }); + } + }); }); From 9173a5418127e31976f4d8218a863e1378896668 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 9 Jul 2026 00:14:00 +0000 Subject: [PATCH 25/79] fix(deps): bump the npm-major group with 6 updates (#5835) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the npm-major group with 6 updates: | Package | From | To | | --- | --- | --- | | [@anthropic-ai/claude-agent-sdk](https://github.com/anthropics/claude-agent-sdk-typescript) | `0.3.197` | `0.3.198` | | [@anthropic-ai/sdk](https://github.com/anthropics/anthropic-sdk-typescript) | `0.109.0` | `0.109.1` | | [posthog-node](https://github.com/PostHog/posthog-js/tree/HEAD/packages/node) | `5.39.1` | `5.39.2` | | [next](https://github.com/vercel/next.js) | `16.2.9` | `16.2.10` | | [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `26.0.1` | `26.1.0` | | [@typescript/native-preview](https://github.com/microsoft/typescript-go) | `7.0.0-dev.20260630.1` | `7.0.0-dev.20260701.1` | Updates `@anthropic-ai/claude-agent-sdk` from 0.3.197 to 0.3.198
Release notes

Sourced from @​anthropic-ai/claude-agent-sdk's releases.

v0.3.198

What's changed

  • Added a runtime warning when canUseTool is configured alongside allowedTools or bypassPermissions, which shadow the callback
  • Added per-server request_timeout_ms option to mcp_set_servers control request
  • Fixed SDKUserMessage.isSynthetic not being mapped to isMeta on ingestion, which could cause synthetic messages to be treated as real user messages
  • Fixed workflow progress events silently dropping earliest agents from the list while the phase counter remained correct

Update

npm install @anthropic-ai/claude-agent-sdk@0.3.198
# or
yarn add @anthropic-ai/claude-agent-sdk@0.3.198
# or
pnpm add @anthropic-ai/claude-agent-sdk@0.3.198
# or
bun add @anthropic-ai/claude-agent-sdk@0.3.198
Changelog

Sourced from @​anthropic-ai/claude-agent-sdk's changelog.

0.3.198

  • Added a runtime warning when canUseTool is configured alongside allowedTools or bypassPermissions, which shadow the callback
  • Added per-server request_timeout_ms option to mcp_set_servers control request
  • Fixed SDKUserMessage.isSynthetic not being mapped to isMeta on ingestion, which could cause synthetic messages to be treated as real user messages
  • Fixed workflow progress events silently dropping earliest agents from the list while the phase counter remained correct
Commits

Updates `@anthropic-ai/sdk` from 0.109.0 to 0.109.1
Release notes

Sourced from @​anthropic-ai/sdk's releases.

sdk: v0.109.1

0.109.1 (2026-07-01)

Full Changelog: sdk-v0.109.0...sdk-v0.109.1

Chores

  • api: remove some nonfunctional types from the SDKs (cc4dd4e)
Changelog

Sourced from @​anthropic-ai/sdk's changelog.

0.109.1 (2026-07-01)

Full Changelog: sdk-v0.109.0...sdk-v0.109.1

Chores

  • api: remove some nonfunctional types from the SDKs (cc4dd4e)
Commits

Updates `posthog-node` from 5.39.1 to 5.39.2
Release notes

Sourced from posthog-node's releases.

posthog-node@5.39.2

5.39.2

Patch Changes

  • #4028 a664b81 Thanks @​marandaneto! - Make Node flush() wait for pending asynchronous SDK work before draining the event queue, so events produced by helpers like captureException() are not missed. Pending work rejections no longer prevent queued events from flushing. (2026-07-01)
  • Updated dependencies [a664b81]:
    • @​posthog/core@​1.39.3
Changelog

Sourced from posthog-node's changelog.

5.39.2

Patch Changes

  • #4028 a664b81 Thanks @​marandaneto! - Make Node flush() wait for pending asynchronous SDK work before draining the event queue, so events produced by helpers like captureException() are not missed. Pending work rejections no longer prevent queued events from flushing. (2026-07-01)
  • Updated dependencies [a664b81]:
    • @​posthog/core@​1.39.3
Commits

Updates `next` from 16.2.9 to 16.2.10
Release notes

Sourced from next's releases.

v16.2.10

Contains no changes except publishing @next/swc-wasm-web which was accidentally not published since 16.2.4.

Commits

Updates `@types/node` from 26.0.1 to 26.1.0
Commits

Updates `@typescript/native-preview` from 7.0.0-dev.20260630.1 to 7.0.0-dev.20260701.1
Commits

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/cli/package.json | 6 +- apps/docs/package.json | 4 +- pnpm-lock.yaml | 389 +++++++++++++++++++++-------------------- pnpm-workspace.yaml | 2 +- 4 files changed, 201 insertions(+), 200 deletions(-) diff --git a/apps/cli/package.json b/apps/cli/package.json index 345a397df7..c6c604f1ff 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -43,8 +43,8 @@ "jose": "^6.2.3" }, "devDependencies": { - "@anthropic-ai/claude-agent-sdk": "^0.3.197", - "@anthropic-ai/sdk": "^0.109.0", + "@anthropic-ai/claude-agent-sdk": "^0.3.198", + "@anthropic-ai/sdk": "^0.109.1", "@clack/prompts": "^1.6.0", "@effect/atom-react": "catalog:", "@effect/platform-bun": "catalog:", @@ -76,7 +76,7 @@ "oxlint-tsgolint": "catalog:", "pg": "^8.22.0", "pg-copy-streams": "^7.0.0", - "posthog-node": "^5.39.1", + "posthog-node": "^5.39.2", "react": "^19.2.7", "react-devtools-core": "^7.0.1", "semantic-release": "^25.0.5", diff --git a/apps/docs/package.json b/apps/docs/package.json index 492c9218e3..ff5e585113 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -11,13 +11,13 @@ "fumadocs-core": "^16.10.7", "fumadocs-mdx": "^15.0.13", "fumadocs-ui": "^16.10.7", - "next": "^16.2.9", + "next": "^16.2.10", "react": "^19.2.7", "react-dom": "^19.2.7" }, "devDependencies": { "@types/mdx": "^2.0.14", - "@types/node": "^26.0.0", + "@types/node": "^26.1.0", "@types/react": "^19.2.17", "@types/react-dom": "^19.1.6", "typescript": "^6.0.3" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 32618873a5..ef5928c0fa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -37,8 +37,8 @@ catalogs: specifier: ^1.3.14 version: 1.3.14 '@typescript/native-preview': - specifier: 7.0.0-dev.20260630.1 - version: 7.0.0-dev.20260630.1 + specifier: 7.0.0-dev.20260701.1 + version: 7.0.0-dev.20260701.1 '@vitest/coverage-istanbul': specifier: ^4.1.9 version: 4.1.9 @@ -97,11 +97,11 @@ importers: version: 6.2.3 devDependencies: '@anthropic-ai/claude-agent-sdk': - specifier: ^0.3.197 - version: 0.3.197(@anthropic-ai/sdk@0.109.0(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(zod@4.4.3) + specifier: ^0.3.198 + version: 0.3.198(@anthropic-ai/sdk@0.109.1(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(zod@4.4.3) '@anthropic-ai/sdk': - specifier: ^0.109.0 - version: 0.109.0(zod@4.4.3) + specifier: ^0.109.1 + version: 0.109.1(zod@4.4.3) '@clack/prompts': specifier: ^1.6.0 version: 1.6.0 @@ -155,7 +155,7 @@ importers: version: 19.2.17 '@typescript/native-preview': specifier: 'catalog:' - version: 7.0.0-dev.20260630.1 + version: 7.0.0-dev.20260701.1 '@vercel/detect-agent': specifier: ^1.2.3 version: 1.2.3 @@ -196,8 +196,8 @@ importers: specifier: ^7.0.0 version: 7.0.0 posthog-node: - specifier: ^5.39.1 - version: 5.39.1 + specifier: ^5.39.2 + version: 5.39.2 react: specifier: ^19.2.7 version: 19.2.7 @@ -215,7 +215,7 @@ importers: version: 7.4.5 vitest: specifier: 'catalog:' - version: 4.1.9(@types/node@26.0.1)(@vitest/coverage-istanbul@4.1.9)(vite@8.0.14(@types/node@26.0.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) + version: 4.1.9(@types/node@26.1.1)(@vitest/coverage-istanbul@4.1.9)(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) yaml: specifier: ^2.9.0 version: 2.9.0 @@ -259,7 +259,7 @@ importers: version: 1.3.14 '@typescript/native-preview': specifier: 'catalog:' - version: 7.0.0-dev.20260630.1 + version: 7.0.0-dev.20260701.1 '@vitest/coverage-istanbul': specifier: 'catalog:' version: 4.1.9(vitest@4.1.9) @@ -277,22 +277,22 @@ importers: version: 0.24.0 vitest: specifier: 'catalog:' - version: 4.1.9(@types/node@26.0.1)(@vitest/coverage-istanbul@4.1.9)(vite@8.0.14(@types/node@26.0.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) + version: 4.1.9(@types/node@26.1.1)(@vitest/coverage-istanbul@4.1.9)(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) apps/docs: dependencies: fumadocs-core: specifier: ^16.10.7 - version: 16.10.7(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.23.0(react@19.2.7))(next@16.2.9(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3) + version: 16.10.7(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.23.0(react@19.2.7))(next@16.2.10(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3) fumadocs-mdx: specifier: ^15.0.13 - version: 15.0.13(@types/mdast@4.0.4)(@types/mdx@2.0.14)(@types/react@19.2.17)(fumadocs-core@16.10.7(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.23.0(react@19.2.7))(next@16.2.9(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3))(next@16.2.9(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(rolldown@1.0.2)(vite@8.0.14(@types/node@26.0.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) + version: 15.0.13(@types/mdast@4.0.4)(@types/mdx@2.0.14)(@types/react@19.2.17)(fumadocs-core@16.10.7(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.23.0(react@19.2.7))(next@16.2.10(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3))(next@16.2.10(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(rolldown@1.0.2)(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) fumadocs-ui: specifier: ^16.10.7 - version: 16.10.7(@types/mdx@2.0.14)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(fumadocs-core@16.10.7(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.23.0(react@19.2.7))(next@16.2.9(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3))(next@16.2.9(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 16.10.7(@types/mdx@2.0.14)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(fumadocs-core@16.10.7(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.23.0(react@19.2.7))(next@16.2.10(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3))(next@16.2.10(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) next: - specifier: ^16.2.9 - version: 16.2.9(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + specifier: ^16.2.10 + version: 16.2.10(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react: specifier: ^19.2.7 version: 19.2.7 @@ -304,8 +304,8 @@ importers: specifier: ^2.0.14 version: 2.0.14 '@types/node': - specifier: ^26.0.0 - version: 26.0.1 + specifier: ^26.1.0 + version: 26.1.1 '@types/react': specifier: ^19.2.17 version: 19.2.17 @@ -339,7 +339,7 @@ importers: version: 1.3.14 '@typescript/native-preview': specifier: 'catalog:' - version: 7.0.0-dev.20260630.1 + version: 7.0.0-dev.20260701.1 '@vitest/coverage-istanbul': specifier: 'catalog:' version: 4.1.9(vitest@4.1.9) @@ -357,7 +357,7 @@ importers: version: 0.24.0 vitest: specifier: 'catalog:' - version: 4.1.9(@types/node@26.0.1)(@vitest/coverage-istanbul@4.1.9)(vite@8.0.14(@types/node@26.0.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) + version: 4.1.9(@types/node@26.1.1)(@vitest/coverage-istanbul@4.1.9)(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) packages/cli-darwin-arm64: {} @@ -381,7 +381,7 @@ importers: version: 1.3.14 '@typescript/native-preview': specifier: 'catalog:' - version: 7.0.0-dev.20260630.1 + version: 7.0.0-dev.20260701.1 '@vitest/coverage-istanbul': specifier: 'catalog:' version: 4.1.9(vitest@4.1.9) @@ -399,7 +399,7 @@ importers: version: 0.24.0 vitest: specifier: 'catalog:' - version: 4.1.9(@types/node@26.0.1)(@vitest/coverage-istanbul@4.1.9)(vite@8.0.14(@types/node@26.0.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) + version: 4.1.9(@types/node@26.1.1)(@vitest/coverage-istanbul@4.1.9)(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) packages/cli-windows-arm64: {} @@ -431,7 +431,7 @@ importers: version: 1.3.14 '@typescript/native-preview': specifier: 'catalog:' - version: 7.0.0-dev.20260630.1 + version: 7.0.0-dev.20260701.1 '@vitest/coverage-istanbul': specifier: 'catalog:' version: 4.1.9(vitest@4.1.9) @@ -449,7 +449,7 @@ importers: version: 0.24.0 vitest: specifier: 'catalog:' - version: 4.1.9(@types/node@26.0.1)(@vitest/coverage-istanbul@4.1.9)(vite@8.0.14(@types/node@26.0.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) + version: 4.1.9(@types/node@26.1.1)(@vitest/coverage-istanbul@4.1.9)(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) packages/process-compose: dependencies: @@ -471,7 +471,7 @@ importers: version: 1.3.14 '@typescript/native-preview': specifier: 'catalog:' - version: 7.0.0-dev.20260630.1 + version: 7.0.0-dev.20260701.1 '@vitest/coverage-istanbul': specifier: 'catalog:' version: 4.1.9(vitest@4.1.9) @@ -489,7 +489,7 @@ importers: version: 0.24.0 vitest: specifier: 'catalog:' - version: 4.1.9(@types/node@26.0.1)(@vitest/coverage-istanbul@4.1.9)(vite@8.0.14(@types/node@26.0.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) + version: 4.1.9(@types/node@26.1.1)(@vitest/coverage-istanbul@4.1.9)(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) packages/stack: dependencies: @@ -523,7 +523,7 @@ importers: version: 1.3.14 '@typescript/native-preview': specifier: 'catalog:' - version: 7.0.0-dev.20260630.1 + version: 7.0.0-dev.20260701.1 '@vitest/coverage-istanbul': specifier: 'catalog:' version: 4.1.9(vitest@4.1.9) @@ -541,7 +541,7 @@ importers: version: 0.24.0 vitest: specifier: 'catalog:' - version: 4.1.9(@types/node@26.0.1)(@vitest/coverage-istanbul@4.1.9)(vite@8.0.14(@types/node@26.0.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) + version: 4.1.9(@types/node@26.1.1)(@vitest/coverage-istanbul@4.1.9)(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) tools/nx-plugins: dependencies: @@ -550,7 +550,7 @@ importers: version: 23.0.1(nx@23.0.1(@swc-node/register@1.11.1(@swc/core@1.15.43)(@swc/types@0.1.27)(typescript@6.0.3))(@swc/core@1.15.43)) vitest: specifier: 'catalog:' - version: 4.1.9(@types/node@26.0.1)(@vitest/coverage-istanbul@4.1.9)(vite@8.0.14(@types/node@26.0.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) + version: 4.1.9(@types/node@26.1.1)(@vitest/coverage-istanbul@4.1.9)(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) packages: @@ -570,60 +570,60 @@ packages: resolution: {integrity: sha512-p+CMKJ93HFmLkjXKlXiVGlMQEuRb6H0MokBSwUsX+S6BRX8eV5naFZpQJFfJHjRZY0Hmnqy1/r6UWl3x+19zYA==} engines: {node: '>=18'} - '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.197': - resolution: {integrity: sha512-jC6WvH5Hr6APTfbMjo4nC6LlyMMqbpCMwiHXIw7/AsQXIHQhZ+cRRMesQlV6UFI1l3O53gLZHzsG9cXwfrPHKw==} + '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.198': + resolution: {integrity: sha512-ZmiAybQKIKcP1qEAE/vfXvfxtKxG9CnJn98QTXC5Zxiwuy7Mllx2ALXh9dfmsf0V87CGEodlZQmMgUJotNIsUw==} cpu: [arm64] os: [darwin] - '@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.197': - resolution: {integrity: sha512-ZQNvGkMrTyatBlHTIQ4w2i2aLBuvq355UP/FDLnVXIH8l23RsL1x/0w9P+dqB7EmY9OZi/cPxSrpskpo+dZWLA==} + '@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.198': + resolution: {integrity: sha512-XwH5vgN46WSwg8aC1OagNofnJpV/G1ciEu118GEKer8ZhVkq/dvK/DqShxMkb6r1jV7u5IJ7zPXu9uKliyNJAw==} cpu: [x64] os: [darwin] - '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.197': - resolution: {integrity: sha512-VuIGXsLGK/aqSQ0tTBqqPVNzjefWS5SWnK8mlYyQitT4s5UDzHXJm0UZBTGxRtlcS0e2+QAHKwbGBCq1ZKSXjg==} + '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.198': + resolution: {integrity: sha512-Q7lKVNjIrUQ2B/AR77OvRf0zeOdEjonFVaR9FYrrwtzGeEqum69WSht5nM7Y7el3wjbNi0/eV0QTUM0DlsTEfw==} cpu: [arm64] os: [linux] libc: [musl] - '@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.197': - resolution: {integrity: sha512-pWhQgCtAft4EGM4Zn24HRad1a/k2u6oA+2uM/KCdjehfKtooDiHfMNd1yzXY/n9AEBWP0RHB2Vz3mJ30X2pVAg==} + '@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.198': + resolution: {integrity: sha512-qmz8dxEtDIlKntU5qYe0R4aWTxTue5S7zIQknatLX7aJ6HN/nq1aCNXWn5smTH2FViBkUPPR+sCIsNwSk6AT6Q==} cpu: [arm64] os: [linux] libc: [glibc] - '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.197': - resolution: {integrity: sha512-3Tuy7XhD4UIKE4A4RPmKJcbL7Q/3dcB1hEWQt2lKP7c/DlixeEv+tRzvpnFZKhFX2hy0tkBk3QjkozSAacMC/w==} + '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.198': + resolution: {integrity: sha512-h1SrWVIMjLInYNPlf+TxXuKTOdoiOfJLBSoQG97315Z2Nh0IpBfqWExlqYTtPCgKE7q2iga31U283QfHpIDlSQ==} cpu: [x64] os: [linux] libc: [musl] - '@anthropic-ai/claude-agent-sdk-linux-x64@0.3.197': - resolution: {integrity: sha512-AUccrbdcv4Hy/GteP/gYLjG/zDP+fe2BFtDMctEfRFVz40DazYDcOyW1+nIgSTQtxf5jSTAVVf3cNuXB2CZwlw==} + '@anthropic-ai/claude-agent-sdk-linux-x64@0.3.198': + resolution: {integrity: sha512-Zqxyz2AT1UM5WlOOoLJhLssZDgZo8rBK5ku6daveK12zp+UTJGZhGsjFghz1/ASxH08KqOTbUePNTORnPhHAEQ==} cpu: [x64] os: [linux] libc: [glibc] - '@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.197': - resolution: {integrity: sha512-Wx8uiAKBenDuL8lWQmrqnX5ppljaH5unQ9cKiCz2/9Kgf09dgnrwbX8n/FhndCZR8PmYw539eWwYVrSVc/bl6w==} + '@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.198': + resolution: {integrity: sha512-mjIHf1HFiRuXefewWTaNZFlTZlCaEt/xsRjc1nSTCEEpFolZayVhrDKz+O2QFVcDtPl8x8GeYSL0kiikg1DZjQ==} cpu: [arm64] os: [win32] - '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.197': - resolution: {integrity: sha512-ZXJO/VvR3SI4G0gwthWeFXWdHB5RXPu3rtfGRcKZ/YgtDeW17rQ+LZIJTk2ywzbLb8EvlghR5JPgn293hC179Q==} + '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.198': + resolution: {integrity: sha512-y3HLuCCz1kDwUrhd6OnqO+d5BUpTFSzNUsPT9kf3r1vk9HYKF+eMC9eIlcOhiW2kX491kxEvuEOfqgIkGx15cg==} cpu: [x64] os: [win32] - '@anthropic-ai/claude-agent-sdk@0.3.197': - resolution: {integrity: sha512-XNIi8W1tb+QfMkcK+5kepOC6BsxG8wtupd72H+pIPzIJypVQhHy7FoX+KBMtTRYwtl+5dsjKyABhjWXebeUilw==} + '@anthropic-ai/claude-agent-sdk@0.3.198': + resolution: {integrity: sha512-xt469sSCyclTPtzpLAg0Aschy665GiRMgZKabSmESbGUA5/H56HcILVOiFxclXswkeMUk2fQxfHJUY9UZfiTnA==} engines: {node: '>=18.0.0'} peerDependencies: '@anthropic-ai/sdk': '>=0.93.0' '@modelcontextprotocol/sdk': ^1.29.0 zod: ^4.0.0 - '@anthropic-ai/sdk@0.109.0': - resolution: {integrity: sha512-y7P4eLyW5uNut4fXpOUEHqhJwx7dnxrWAfCQE4Lcgm0hSFQuIeHa7CWEKE5dFolEjQJE/RFKkppjri05r2OK/Q==} + '@anthropic-ai/sdk@0.109.1': + resolution: {integrity: sha512-q9OnEKLr5H9nxSuXdgDgJhxfYMiE+AaUEBze2Gk91UcaaLnsN+Lx5fbCYywiqurU/APLdwv23x03Wm6WN3EBsg==} hasBin: true peerDependencies: zod: ^3.25.0 || ^4.0.0 @@ -781,6 +781,9 @@ packages: '@emnapi/runtime@1.11.1': resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} + '@emnapi/runtime@1.11.2': + resolution: {integrity: sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==} + '@emnapi/runtime@1.4.5': resolution: {integrity: sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==} @@ -1304,57 +1307,57 @@ packages: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 - '@next/env@16.2.9': - resolution: {integrity: sha512-ki5VxxXfzD/9TDe13wyeTKIjQTAwBVpnr8KhRDUr8ltMUq1/NBpWNT5tiPoxiGl+PHM4X2ahSOiPk6iAimIzPg==} + '@next/env@16.2.10': + resolution: {integrity: sha512-zLPxg9M0MEHmygpj5OuxjQ+vHMiy/K7cSp74G8ecYolmgUWw0RwN02tF56npup/+qaI8JB97hQgS/r2Hb6QwVA==} - '@next/swc-darwin-arm64@16.2.9': - resolution: {integrity: sha512-HkfxNYUCmcct0Xsqib5KxqMSHV4AHJq857BNRchyBDs4YS19aHzVfn1kDuBYKqLLQBjXgnkIsjV2Kd4d2wzYhw==} + '@next/swc-darwin-arm64@16.2.10': + resolution: {integrity: sha512-v9IdJCa0H0mbo+8z5zwUpOk1Vj7RjkcI5uNYf5Ws1y6szf/p3Mzl9hLaST8SCt6L9h8NGnruZcd2+o0NTNwDhA==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@16.2.9': - resolution: {integrity: sha512-7IAtK4MeybpqRV9GRABWEhJ62mOS+rzWOzOTFie4cSEtm12xsoOMJRcECoZx3FHPzFAqN/IJtHqWAFOLfl152w==} + '@next/swc-darwin-x64@16.2.10': + resolution: {integrity: sha512-17IS0jJRViROGmA9uGdNR8VPJpfbnaVG7E9qhso5jDLkmyd0lSDORWxbcKINzcFqzZqGwGtMSnrFRxBpuUYjLQ==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@16.2.9': - resolution: {integrity: sha512-hBD75iWpUtkL9SmQmcRhmLomn9jgkPzCEkbOcLgHymPEKzv+6ONy13RRiIEz/iEObjkS2Jlb5gYS2XGoS3X4rw==} + '@next/swc-linux-arm64-gnu@16.2.10': + resolution: {integrity: sha512-GRQRsRtuciNJvB54AvvuQTiq0oZtFwa1owQqtZD8wwnGpM2L39MV22kpI72YSXLKIyY40LC66EiLFv4PiicXxg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [glibc] - '@next/swc-linux-arm64-musl@16.2.9': - resolution: {integrity: sha512-qZTI3pf9SGc/obr8NkQAekBxmp1QK+kVm+VAf3BALLfFAj+1kUhkTxmrWpVos9R/UYIA8AWX2p6cGI5WdwzVUA==} + '@next/swc-linux-arm64-musl@16.2.10': + resolution: {integrity: sha512-zkN9MQYS7UQBro+FnISUq1itaQjXI9xqISzuQ+2bc921NcJ1x4yPCqrn77tVN6/dOOXaaWVX3k6/bR07pPwK+A==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [musl] - '@next/swc-linux-x64-gnu@16.2.9': - resolution: {integrity: sha512-xm0HfRNX+UkH4R3c18ynswjj5o5uEj/7iI9p9omdtTSIsRCzQqkGMA+10nzJ4EHnYC3as65IMhbbl5fWRUWHYg==} + '@next/swc-linux-x64-gnu@16.2.10': + resolution: {integrity: sha512-iCVJnwvrPYECvA6WM/7+oo+OiTvedIKLxtCLAZP4xZR3nXa1zmzZyLPbYCmWvpd4CvMYF1EMTafd0ii3DygLvA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [glibc] - '@next/swc-linux-x64-musl@16.2.9': - resolution: {integrity: sha512-QumimHkGEG6vM3PfEDWKyKen03NcqLOkeKB1EfcPe7VxzmEiCa4jNnMyBn/US5zcd/VE1CI+O8Ovb3lfjVHfGw==} + '@next/swc-linux-x64-musl@16.2.10': + resolution: {integrity: sha512-ov2g4H0dHY9bPoOU83m91hWT7Iq5qy13bUnyyshLU3HGR1Ownn0X9QpmDPc5iIUaahTp7f7LeGAhV4DSFtackw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [musl] - '@next/swc-win32-arm64-msvc@16.2.9': - resolution: {integrity: sha512-hzQpKZvw8rAwI6A2uQh6SacCSvNAXaIkPNsWwzqqfRiIMiXMfH936skDhz1OO6KpvdKkJrgHHtqQOq5PIXOvdQ==} + '@next/swc-win32-arm64-msvc@16.2.10': + resolution: {integrity: sha512-DwAnhLX76HQiFFQNgWlcK+JzlnD1rZ+UK/WY0ZMI/deXpvgnesjNYrqcfo1JzBuz4Kf7o3brIBL0glI1junatA==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@16.2.9': - resolution: {integrity: sha512-qr2VL3Ce5QrwgO2yh1ujSBawrimjVKX8FGF/cOynmdYKJY0BdHpGVNIRK1tqONB10Vkm25Ub1BD2bkjWs4+96w==} + '@next/swc-win32-x64-msvc@16.2.10': + resolution: {integrity: sha512-0JXq3b85Jk9Jg4ntLUbXSPvoDw3gpZou7twuKdoFG2jOw635v7+IiXfTaa0TxVMyx78pUjnrVYwLgjKfX4e6/A==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -2107,11 +2110,11 @@ packages: resolution: {integrity: sha512-//0sR/cow/s4ICQaYoAobOl4aU8cjU6x/V24V7XkKotb9+O+3zySIYp146vpaobYHnxa4pZX8NkV54Z5AwbDKA==} engines: {node: '>=12'} - '@posthog/core@1.39.6': - resolution: {integrity: sha512-o6ajIwN5zXoNP0D4H/QPmOyibNTUkSyOR6ya7AG5U2ywXx4awo72L2KnCoiZPQM5x/bXv6jPBdimH8M18Ax0aw==} + '@posthog/core@1.40.0': + resolution: {integrity: sha512-oGDbIwlTquNwdHbEL5ZLEkuW4UFkkEanfx3QAxDgyVbISv+OAA6YGQwrvo0JD3MUJEbJZvyh8XsX+WYDGw9XHw==} - '@posthog/types@1.392.1': - resolution: {integrity: sha512-Qg6Gl7/1vlr8+gPtBi5gwnLgAgiyFoKOVmTvTtDcvya9cpTwZfna7rQmkGQ4B63CunUYNNbOlqcwiUwUDyTK6w==} + '@posthog/types@1.393.0': + resolution: {integrity: sha512-vzWeEJZ7ERQhFRoQYaP5jzN1JvIu46UJyHXsuv+dTGW2r3sMgREOhNxXLZjmFHwZ8/FOHQoyqqQmXTCXZSfMSg==} '@radix-ui/number@1.1.2': resolution: {integrity: sha512-ceTwaxc4I5IOi97DgCotl3pqiyRGvffcc0oOsE2dQYaJOFIDsDt4VWG6xEbg1QePv9QWausCEIppud/tJ1wNig==} @@ -2832,8 +2835,8 @@ packages: '@types/node@25.9.3': resolution: {integrity: sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg==} - '@types/node@26.0.1': - resolution: {integrity: sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw==} + '@types/node@26.1.1': + resolution: {integrity: sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==} '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} @@ -2864,50 +2867,50 @@ packages: '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} - '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260630.1': - resolution: {integrity: sha512-zo1TYD32r+MKOTnzhB1YGr2Jrw14tzGR/rpfr6hRMDz0kV9k2CWVvtOYOmOtRdmuO4KWZcWtVX13QyaPGhA8Hw==} + '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260701.1': + resolution: {integrity: sha512-rm3v+3Mcrj91NPwG0mjaLfbJjydCDX/skF82cQw0K6XWsEK7wHmpLEF1xPOWGnV05RhTYJac9VoKUjgcADkbXA==} engines: {node: '>=16.20.0'} cpu: [arm64] os: [darwin] - '@typescript/native-preview-darwin-x64@7.0.0-dev.20260630.1': - resolution: {integrity: sha512-57RYyZQQ6+drfu+CagVdqUvQwNesvuu/rJb/au66jv+figfpDOugD0S6ruQl1SxpFFfr0lCny3KEplsBdqFDEg==} + '@typescript/native-preview-darwin-x64@7.0.0-dev.20260701.1': + resolution: {integrity: sha512-jtbtV3A+cmkmDuqs56Qj3Hb+NmOAMHFX+FTLhCagJybjsng1lOl1h8vYaYg3F6EMgSiZI66hIpB9TMi3n8jFEw==} engines: {node: '>=16.20.0'} cpu: [x64] os: [darwin] - '@typescript/native-preview-linux-arm64@7.0.0-dev.20260630.1': - resolution: {integrity: sha512-QQxNyZ9rVbE6lUqctrrRiTtA5Z0w2FFBy8SkiP/eDkuRy2WXrVpMQYntNn6d4nO1nWTmWJR1z/CS+H2rsrl8gg==} + '@typescript/native-preview-linux-arm64@7.0.0-dev.20260701.1': + resolution: {integrity: sha512-SPEfXZhCRff6kPYrqJIkKkKBy1rxPk0Wmam7U0AOuMQtXTNPmqCtx54J/F6nfGR89ATTDTDr69MujFAG+lFQVg==} engines: {node: '>=16.20.0'} cpu: [arm64] os: [linux] - '@typescript/native-preview-linux-arm@7.0.0-dev.20260630.1': - resolution: {integrity: sha512-FLJSSt3bUmRpzWlr5cDE+td40FoDy/MT+VDYk4JGouisJo7g7GPjuF+fYOfsKI/RbD9f6gDFTyFPAoTLVVR/9g==} + '@typescript/native-preview-linux-arm@7.0.0-dev.20260701.1': + resolution: {integrity: sha512-c22PWjLMecd2xsAZRlpC1mnr8GZUjnFS32URPd2NhKOEx/6Dp7bPZvI+7NNXDXTew/fkgAaZvhLTWHGG64gqGw==} engines: {node: '>=16.20.0'} cpu: [arm] os: [linux] - '@typescript/native-preview-linux-x64@7.0.0-dev.20260630.1': - resolution: {integrity: sha512-WqNPvUBngGfnkunqn3IuIkkF6PA/HPSSbVucolO7stc9DxWZqfRals6/C8RTvuQaif9qt+bcY9U6lLXuDFyClA==} + '@typescript/native-preview-linux-x64@7.0.0-dev.20260701.1': + resolution: {integrity: sha512-Y9NX6qGOCQD4zdCXJCABB8TL3IkFsWVlwMQ3OGtQ8A/OzyIFN01QyHWFgB5AJFZQoasryb+nnhu9r2IY5KpBwA==} engines: {node: '>=16.20.0'} cpu: [x64] os: [linux] - '@typescript/native-preview-win32-arm64@7.0.0-dev.20260630.1': - resolution: {integrity: sha512-awATkrGGm2L1IraQgaw21VXWtAqv79DJ57No/J65Y+bL8InOVR2zu+zCH+5E44m8bh9IXwnShI7cAdvp02WNEw==} + '@typescript/native-preview-win32-arm64@7.0.0-dev.20260701.1': + resolution: {integrity: sha512-7IRD34O2Kp7mIplQEC/HgLyehmKL1FGlTYhlAqxxZfKhSt694yytd2dCCnxMMA/aPHjcqBke04wNPOOhQ2ezcA==} engines: {node: '>=16.20.0'} cpu: [arm64] os: [win32] - '@typescript/native-preview-win32-x64@7.0.0-dev.20260630.1': - resolution: {integrity: sha512-M/+P66Gsss/CANV+MkKVFeNi9jljYYR3N2COf/WrVYcDwrHyiYHZYExfTw2cEbbkH8LcawXAekp9d23bevBR4Q==} + '@typescript/native-preview-win32-x64@7.0.0-dev.20260701.1': + resolution: {integrity: sha512-zVfayHxvFZAQ0hJbegDcfhJamyQpLn6ahMDATAra+EkA3+/nbTW3VjCJ3h1V2PSNTY9rTPzGroFURV5cvErh3A==} engines: {node: '>=16.20.0'} cpu: [x64] os: [win32] - '@typescript/native-preview@7.0.0-dev.20260630.1': - resolution: {integrity: sha512-5OTvy2YpoDsOwAPqj4LkI4mrlAtc3mRzNdHAPp/1JWUvsKSRTzqAB2JPVD4qHUkAd9qY89yb758kc7fGyn3gbw==} + '@typescript/native-preview@7.0.0-dev.20260701.1': + resolution: {integrity: sha512-eXPtWsAj0s06kHWVDlBj7ABwoyNDHuW2gCbQ+bYGlyymDYteiAydelhyhyzUsenzGraPLdd/OPwEUB8/KUdpBw==} engines: {node: '>=16.20.0'} hasBin: true @@ -3205,8 +3208,8 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - baseline-browser-mapping@2.10.37: - resolution: {integrity: sha512-girxaJ7WZssDOFhzCGZTDKoTa1gk6A1TbflaYTpykLJ4UU9Fz9kx1aREM8JCuoVHbL8X8T/mJg7w2oYSq72Oig==} + baseline-browser-mapping@2.10.42: + resolution: {integrity: sha512-c/jurFrDLyui7o1J86yLkRu4LMsTYcBohveus7/I2Hzdn9KIP2bdJPTue/lR1KH46enoPbD77GKeSYNdyPoD3Q==} engines: {node: '>=6.0.0'} hasBin: true @@ -3291,8 +3294,8 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} - caniuse-lite@1.0.30001799: - resolution: {integrity: sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==} + caniuse-lite@1.0.30001803: + resolution: {integrity: sha512-g/uHREV2ZpK9qMalCsWaxmA6ol+DX8GYhuf3T40RKoP+oL7vhRJh8LNt73PCjpnR6l14FzfPrB5Yux4PKm2meg==} caseless@0.12.0: resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} @@ -5125,11 +5128,6 @@ packages: mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} - nanoid@3.3.12: - resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - nanoid@3.3.15: resolution: {integrity: sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -5159,8 +5157,8 @@ packages: react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc - next@16.2.9: - resolution: {integrity: sha512-MEOJiq/UvuezAdqVSceHbqDgZt1kDw2tpGVOlsdIoJsQdbN2JY2hpVG4xnXGkbdJUOEWhnRfiu/O4Hpc9Juwww==} + next@16.2.10: + resolution: {integrity: sha512-2som5AVXb3kE6Yjine3/mNbBayYF58eguBWIVVUdr1y/L426xyVEgYxgBG+1QC34P2x5E+tcDup6XkuOAX3dCA==} engines: {node: '>=20.9.0'} hasBin: true peerDependencies: @@ -5667,8 +5665,8 @@ packages: postgres-range@1.1.4: resolution: {integrity: sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==} - posthog-node@5.39.1: - resolution: {integrity: sha512-ZvpNfbTg6mhuPScGXE+Yqh2mJpELHShHT98qjKD7cBdXVtIDOZ8Xoh7m3JkK9H1r0GiqEztnoBNe9aL1qOZCeg==} + posthog-node@5.39.2: + resolution: {integrity: sha512-5piMedjlQ2x+UKLvHWTC5ls5/T1dDZKE1Pu5AKkYh9EkbZOjvu0cac6lWFB7mgbGkKQ0I1bhbjDx1QAYRJ7Unw==} engines: {node: ^20.20.0 || >=22.22.0} peerDependencies: rxjs: ^7.0.0 @@ -6854,46 +6852,46 @@ snapshots: ansi-styles: 6.2.3 is-fullwidth-code-point: 5.1.0 - '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.197': + '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.198': optional: true - '@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.197': + '@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.198': optional: true - '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.197': + '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.198': optional: true - '@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.197': + '@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.198': optional: true - '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.197': + '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.198': optional: true - '@anthropic-ai/claude-agent-sdk-linux-x64@0.3.197': + '@anthropic-ai/claude-agent-sdk-linux-x64@0.3.198': optional: true - '@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.197': + '@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.198': optional: true - '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.197': + '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.198': optional: true - '@anthropic-ai/claude-agent-sdk@0.3.197(@anthropic-ai/sdk@0.109.0(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(zod@4.4.3)': + '@anthropic-ai/claude-agent-sdk@0.3.198(@anthropic-ai/sdk@0.109.1(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(zod@4.4.3)': dependencies: - '@anthropic-ai/sdk': 0.109.0(zod@4.4.3) + '@anthropic-ai/sdk': 0.109.1(zod@4.4.3) '@modelcontextprotocol/sdk': 1.29.0(zod@4.4.3) zod: 4.4.3 optionalDependencies: - '@anthropic-ai/claude-agent-sdk-darwin-arm64': 0.3.197 - '@anthropic-ai/claude-agent-sdk-darwin-x64': 0.3.197 - '@anthropic-ai/claude-agent-sdk-linux-arm64': 0.3.197 - '@anthropic-ai/claude-agent-sdk-linux-arm64-musl': 0.3.197 - '@anthropic-ai/claude-agent-sdk-linux-x64': 0.3.197 - '@anthropic-ai/claude-agent-sdk-linux-x64-musl': 0.3.197 - '@anthropic-ai/claude-agent-sdk-win32-arm64': 0.3.197 - '@anthropic-ai/claude-agent-sdk-win32-x64': 0.3.197 + '@anthropic-ai/claude-agent-sdk-darwin-arm64': 0.3.198 + '@anthropic-ai/claude-agent-sdk-darwin-x64': 0.3.198 + '@anthropic-ai/claude-agent-sdk-linux-arm64': 0.3.198 + '@anthropic-ai/claude-agent-sdk-linux-arm64-musl': 0.3.198 + '@anthropic-ai/claude-agent-sdk-linux-x64': 0.3.198 + '@anthropic-ai/claude-agent-sdk-linux-x64-musl': 0.3.198 + '@anthropic-ai/claude-agent-sdk-win32-arm64': 0.3.198 + '@anthropic-ai/claude-agent-sdk-win32-x64': 0.3.198 - '@anthropic-ai/sdk@0.109.0(zod@4.4.3)': + '@anthropic-ai/sdk@0.109.1(zod@4.4.3)': dependencies: json-schema-to-ts: 3.1.1 standardwebhooks: 1.0.0 @@ -7090,7 +7088,7 @@ snapshots: '@effect/vitest@4.0.0-beta.93(effect@4.0.0-beta.93)(vitest@4.1.9)': dependencies: effect: 4.0.0-beta.93 - vitest: 4.1.9(@types/node@26.0.1)(@vitest/coverage-istanbul@4.1.9)(vite@8.0.14(@types/node@26.0.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) + vitest: 4.1.9(@types/node@26.1.1)(@vitest/coverage-istanbul@4.1.9)(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) '@emnapi/core@1.10.0': dependencies: @@ -7128,6 +7126,11 @@ snapshots: dependencies: tslib: 2.8.1 + '@emnapi/runtime@1.11.2': + dependencies: + tslib: 2.8.1 + optional: true + '@emnapi/runtime@1.4.5': dependencies: tslib: 2.8.1 @@ -7338,7 +7341,7 @@ snapshots: '@img/sharp-wasm32@0.34.5': dependencies: - '@emnapi/runtime': 1.11.1 + '@emnapi/runtime': 1.11.2 optional: true '@img/sharp-win32-arm64@0.34.5': @@ -7523,30 +7526,30 @@ snapshots: '@tybys/wasm-util': 0.10.3 optional: true - '@next/env@16.2.9': {} + '@next/env@16.2.10': {} - '@next/swc-darwin-arm64@16.2.9': + '@next/swc-darwin-arm64@16.2.10': optional: true - '@next/swc-darwin-x64@16.2.9': + '@next/swc-darwin-x64@16.2.10': optional: true - '@next/swc-linux-arm64-gnu@16.2.9': + '@next/swc-linux-arm64-gnu@16.2.10': optional: true - '@next/swc-linux-arm64-musl@16.2.9': + '@next/swc-linux-arm64-musl@16.2.10': optional: true - '@next/swc-linux-x64-gnu@16.2.9': + '@next/swc-linux-x64-gnu@16.2.10': optional: true - '@next/swc-linux-x64-musl@16.2.9': + '@next/swc-linux-x64-musl@16.2.10': optional: true - '@next/swc-win32-arm64-msvc@16.2.9': + '@next/swc-win32-arm64-msvc@16.2.10': optional: true - '@next/swc-win32-x64-msvc@16.2.9': + '@next/swc-win32-x64-msvc@16.2.10': optional: true '@noble/ciphers@1.3.0': {} @@ -8007,11 +8010,11 @@ snapshots: '@pnpm/network.ca-file': 1.0.2 config-chain: 1.1.13 - '@posthog/core@1.39.6': + '@posthog/core@1.40.0': dependencies: - '@posthog/types': 1.392.1 + '@posthog/types': 1.393.0 - '@posthog/types@1.392.1': {} + '@posthog/types@1.393.0': {} '@radix-ui/number@1.1.2': {} @@ -8711,7 +8714,7 @@ snapshots: dependencies: undici-types: 7.24.6 - '@types/node@26.0.1': + '@types/node@26.1.1': dependencies: undici-types: 8.3.0 @@ -8738,7 +8741,7 @@ snapshots: '@types/responselike@1.0.0': dependencies: - '@types/node': 26.0.1 + '@types/node': 26.1.1 '@types/unist@2.0.11': {} @@ -8746,38 +8749,38 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 26.0.1 + '@types/node': 26.1.1 - '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260630.1': + '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260701.1': optional: true - '@typescript/native-preview-darwin-x64@7.0.0-dev.20260630.1': + '@typescript/native-preview-darwin-x64@7.0.0-dev.20260701.1': optional: true - '@typescript/native-preview-linux-arm64@7.0.0-dev.20260630.1': + '@typescript/native-preview-linux-arm64@7.0.0-dev.20260701.1': optional: true - '@typescript/native-preview-linux-arm@7.0.0-dev.20260630.1': + '@typescript/native-preview-linux-arm@7.0.0-dev.20260701.1': optional: true - '@typescript/native-preview-linux-x64@7.0.0-dev.20260630.1': + '@typescript/native-preview-linux-x64@7.0.0-dev.20260701.1': optional: true - '@typescript/native-preview-win32-arm64@7.0.0-dev.20260630.1': + '@typescript/native-preview-win32-arm64@7.0.0-dev.20260701.1': optional: true - '@typescript/native-preview-win32-x64@7.0.0-dev.20260630.1': + '@typescript/native-preview-win32-x64@7.0.0-dev.20260701.1': optional: true - '@typescript/native-preview@7.0.0-dev.20260630.1': + '@typescript/native-preview@7.0.0-dev.20260701.1': optionalDependencies: - '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260630.1 - '@typescript/native-preview-darwin-x64': 7.0.0-dev.20260630.1 - '@typescript/native-preview-linux-arm': 7.0.0-dev.20260630.1 - '@typescript/native-preview-linux-arm64': 7.0.0-dev.20260630.1 - '@typescript/native-preview-linux-x64': 7.0.0-dev.20260630.1 - '@typescript/native-preview-win32-arm64': 7.0.0-dev.20260630.1 - '@typescript/native-preview-win32-x64': 7.0.0-dev.20260630.1 + '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260701.1 + '@typescript/native-preview-darwin-x64': 7.0.0-dev.20260701.1 + '@typescript/native-preview-linux-arm': 7.0.0-dev.20260701.1 + '@typescript/native-preview-linux-arm64': 7.0.0-dev.20260701.1 + '@typescript/native-preview-linux-x64': 7.0.0-dev.20260701.1 + '@typescript/native-preview-win32-arm64': 7.0.0-dev.20260701.1 + '@typescript/native-preview-win32-x64': 7.0.0-dev.20260701.1 '@ungap/structured-clone@1.3.2': {} @@ -8957,7 +8960,7 @@ snapshots: magicast: 0.5.3 obug: 2.1.1 tinyrainbow: 3.1.0 - vitest: 4.1.9(@types/node@26.0.1)(@vitest/coverage-istanbul@4.1.9)(vite@8.0.14(@types/node@26.0.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) + vitest: 4.1.9(@types/node@26.1.1)(@vitest/coverage-istanbul@4.1.9)(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) transitivePeerDependencies: - supports-color @@ -8970,13 +8973,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.9(vite@8.0.14(@types/node@26.0.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0))': + '@vitest/mocker@4.1.9(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0))': dependencies: '@vitest/spy': 4.1.9 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.14(@types/node@26.0.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0) + vite: 8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0) '@vitest/pretty-format@4.1.9': dependencies: @@ -9137,7 +9140,7 @@ snapshots: base64-js@1.5.1: {} - baseline-browser-mapping@2.10.37: {} + baseline-browser-mapping@2.10.42: {} bcrypt-pbkdf@1.0.2: dependencies: @@ -9204,8 +9207,8 @@ snapshots: browserslist@4.28.2: dependencies: - baseline-browser-mapping: 2.10.37 - caniuse-lite: 1.0.30001799 + baseline-browser-mapping: 2.10.42 + caniuse-lite: 1.0.30001803 electron-to-chromium: 1.5.363 node-releases: 2.0.46 update-browserslist-db: 1.2.3(browserslist@4.28.2) @@ -9226,7 +9229,7 @@ snapshots: bun-types@1.3.14: dependencies: - '@types/node': 26.0.1 + '@types/node': 26.1.1 bytes@3.1.2: {} @@ -9254,7 +9257,7 @@ snapshots: callsites@3.1.0: {} - caniuse-lite@1.0.30001799: {} + caniuse-lite@1.0.30001803: {} caseless@0.12.0: {} @@ -10057,7 +10060,7 @@ snapshots: fsevents@2.3.3: optional: true - fumadocs-core@16.10.7(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.23.0(react@19.2.7))(next@16.2.9(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3): + fumadocs-core@16.10.7(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.23.0(react@19.2.7))(next@16.2.10(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3): dependencies: '@orama/orama': 3.1.18 estree-util-value-to-estree: 3.5.0 @@ -10083,21 +10086,21 @@ snapshots: '@types/mdast': 4.0.4 '@types/react': 19.2.17 lucide-react: 1.23.0(react@19.2.7) - next: 16.2.9(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + next: 16.2.10(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) zod: 4.4.3 transitivePeerDependencies: - supports-color - fumadocs-mdx@15.0.13(@types/mdast@4.0.4)(@types/mdx@2.0.14)(@types/react@19.2.17)(fumadocs-core@16.10.7(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.23.0(react@19.2.7))(next@16.2.9(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3))(next@16.2.9(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(rolldown@1.0.2)(vite@8.0.14(@types/node@26.0.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)): + fumadocs-mdx@15.0.13(@types/mdast@4.0.4)(@types/mdx@2.0.14)(@types/react@19.2.17)(fumadocs-core@16.10.7(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.23.0(react@19.2.7))(next@16.2.10(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3))(next@16.2.10(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(rolldown@1.0.2)(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)): dependencies: '@mdx-js/mdx': 3.1.1 '@standard-schema/spec': 1.1.0 chokidar: 5.0.0 esbuild: 0.28.1 estree-util-value-to-estree: 3.5.0 - fumadocs-core: 16.10.7(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.23.0(react@19.2.7))(next@16.2.9(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3) + fumadocs-core: 16.10.7(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.23.0(react@19.2.7))(next@16.2.10(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3) js-yaml: 5.2.1 mdast-util-mdx: 3.0.0 picocolors: 1.1.1 @@ -10113,14 +10116,14 @@ snapshots: '@types/mdast': 4.0.4 '@types/mdx': 2.0.14 '@types/react': 19.2.17 - next: 16.2.9(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + next: 16.2.10(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react: 19.2.7 rolldown: 1.0.2 - vite: 8.0.14(@types/node@26.0.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0) + vite: 8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0) transitivePeerDependencies: - supports-color - fumadocs-ui@16.10.7(@types/mdx@2.0.14)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(fumadocs-core@16.10.7(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.23.0(react@19.2.7))(next@16.2.9(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3))(next@16.2.9(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + fumadocs-ui@16.10.7(@types/mdx@2.0.14)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(fumadocs-core@16.10.7(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.23.0(react@19.2.7))(next@16.2.10(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3))(next@16.2.10(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7): dependencies: '@fuma-translate/react': 1.0.2(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@fumadocs/tailwind': 0.0.5 @@ -10136,7 +10139,7 @@ snapshots: '@radix-ui/react-tabs': 1.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) class-variance-authority: 0.7.1 cnfast: 0.0.8 - fumadocs-core: 16.10.7(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.23.0(react@19.2.7))(next@16.2.9(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3) + fumadocs-core: 16.10.7(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.23.0(react@19.2.7))(next@16.2.10(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3) lucide-react: 1.23.0(react@19.2.7) motion: 12.42.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) next-themes: 0.4.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -10150,7 +10153,7 @@ snapshots: optionalDependencies: '@types/mdx': 2.0.14 '@types/react': 19.2.17 - next: 16.2.9(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + next: 16.2.10(react-dom@19.2.7(react@19.2.7))(react@19.2.7) transitivePeerDependencies: - '@emotion/is-prop-valid' - '@tailwindcss/oxide' @@ -11451,8 +11454,6 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 - nanoid@3.3.12: {} - nanoid@3.3.15: {} negotiator@0.6.3: {} @@ -11470,25 +11471,25 @@ snapshots: react: 19.2.7 react-dom: 19.2.7(react@19.2.7) - next@16.2.9(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + next@16.2.10(react-dom@19.2.7(react@19.2.7))(react@19.2.7): dependencies: - '@next/env': 16.2.9 + '@next/env': 16.2.10 '@swc/helpers': 0.5.15 - baseline-browser-mapping: 2.10.37 - caniuse-lite: 1.0.30001799 + baseline-browser-mapping: 2.10.42 + caniuse-lite: 1.0.30001803 postcss: 8.4.31 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) styled-jsx: 5.1.6(react@19.2.7) optionalDependencies: - '@next/swc-darwin-arm64': 16.2.9 - '@next/swc-darwin-x64': 16.2.9 - '@next/swc-linux-arm64-gnu': 16.2.9 - '@next/swc-linux-arm64-musl': 16.2.9 - '@next/swc-linux-x64-gnu': 16.2.9 - '@next/swc-linux-x64-musl': 16.2.9 - '@next/swc-win32-arm64-msvc': 16.2.9 - '@next/swc-win32-x64-msvc': 16.2.9 + '@next/swc-darwin-arm64': 16.2.10 + '@next/swc-darwin-x64': 16.2.10 + '@next/swc-linux-arm64-gnu': 16.2.10 + '@next/swc-linux-arm64-musl': 16.2.10 + '@next/swc-linux-x64-gnu': 16.2.10 + '@next/swc-linux-x64-musl': 16.2.10 + '@next/swc-win32-arm64-msvc': 16.2.10 + '@next/swc-win32-x64-msvc': 16.2.10 sharp: 0.34.5 transitivePeerDependencies: - '@babel/core' @@ -12038,7 +12039,7 @@ snapshots: postcss@8.4.31: dependencies: - nanoid: 3.3.12 + nanoid: 3.3.15 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -12070,9 +12071,9 @@ snapshots: postgres-range@1.1.4: {} - posthog-node@5.39.1: + posthog-node@5.39.2: dependencies: - '@posthog/core': 1.39.6 + '@posthog/core': 1.40.0 pretty-ms@9.3.0: dependencies: @@ -13191,7 +13192,7 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite@8.0.14(@types/node@26.0.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0): + vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0): dependencies: lightningcss: 1.32.0 picomatch: 4.0.5 @@ -13199,16 +13200,16 @@ snapshots: rolldown: 1.0.2 tinyglobby: 0.2.17 optionalDependencies: - '@types/node': 26.0.1 + '@types/node': 26.1.1 esbuild: 0.28.1 fsevents: 2.3.3 jiti: 2.7.0 yaml: 2.9.0 - vitest@4.1.9(@types/node@26.0.1)(@vitest/coverage-istanbul@4.1.9)(vite@8.0.14(@types/node@26.0.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)): + vitest@4.1.9(@types/node@26.1.1)(@vitest/coverage-istanbul@4.1.9)(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.9 - '@vitest/mocker': 4.1.9(vite@8.0.14(@types/node@26.0.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) + '@vitest/mocker': 4.1.9(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) '@vitest/pretty-format': 4.1.9 '@vitest/runner': 4.1.9 '@vitest/snapshot': 4.1.9 @@ -13225,10 +13226,10 @@ snapshots: tinyexec: 1.2.4 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: 8.0.14(@types/node@26.0.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0) + vite: 8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 26.0.1 + '@types/node': 26.1.1 '@vitest/coverage-istanbul': 4.1.9(vitest@4.1.9) transitivePeerDependencies: - msw diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index eb8f8de15a..c4b733934e 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -22,7 +22,7 @@ catalog: "@swc/core": "^1.15.43" "@tsconfig/bun": "^1.0.10" "@types/bun": "^1.3.14" - "@typescript/native-preview": "7.0.0-dev.20260630.1" + "@typescript/native-preview": "7.0.0-dev.20260701.1" "@vitest/coverage-istanbul": "^4.1.9" "effect": "4.0.0-beta.93" "knip": "^6.17.1" From 9e35980e40d4839c4a5ece81b753428e97c7e3d9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 9 Jul 2026 07:09:12 +0000 Subject: [PATCH 26/79] chore(ci): bump the actions-major group with 4 updates (#5836) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the actions-major group with 4 updates: [github/codeql-action/init](https://github.com/github/codeql-action), [github/codeql-action/analyze](https://github.com/github/codeql-action), [docker/login-action](https://github.com/docker/login-action) and [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action). Updates `github/codeql-action/init` from 4.36.2 to 4.36.3
Release notes

Sourced from github/codeql-action/init's releases.

v4.36.3

No user facing changes.

Changelog

Sourced from github/codeql-action/init's changelog.

CodeQL Action Changelog

See the releases page for the relevant changes to the CodeQL CLI and language packs.

[UNRELEASED]

No user facing changes.

4.37.0 - 08 Jul 2026

  • Update default CodeQL bundle version to 2.26.0. #3995
  • In addition to the existing input format, the config-file input for the codeql-action/init step will soon support a new [owner/]repo[@ref][:path] format. All components except the repository name are optional. If omitted, owner defaults to the same owner as the repository the analysis is running for, ref to main, and path to .github/codeql-action.yaml. Support for this format ships in this version of the CodeQL Action, but will only be enabled over the coming weeks. #3973

4.36.3 - 01 Jul 2026

No user facing changes.

4.36.2 - 04 Jun 2026

  • Cache CodeQL CLI version information across Actions steps. #3943
  • Reduce requests while waiting for analysis processing by using exponential backoff when polling SARIF processing status. #3937
  • Update default CodeQL bundle version to 2.25.6. #3948

4.36.1 - 02 Jun 2026

No user facing changes.

4.36.0 - 22 May 2026

  • Breaking change: Bump the minimum required CodeQL bundle version to 2.19.4. #3894
  • Add support for SHA-256 Git object IDs. #3893
  • Update default CodeQL bundle version to 2.25.5. #3926

4.35.5 - 15 May 2026

  • We have improved how the JavaScript bundles for the CodeQL Action are generated to avoid duplication across bundles and reduce the size of the repository by around 70%. This should have no effect on the runtime behaviour of the CodeQL Action. #3899
  • For performance and accuracy reasons, improved incremental analysis will now only be enabled on a pull request when diff-informed analysis is also enabled for that run. If diff-informed analysis is unavailable (for example, because the PR diff ranges could not be computed), the action will fall back to a full analysis. #3791
  • If multiple inputs are provided for the GitHub-internal analysis-kinds input, only code-scanning will be enabled. The analysis-kinds input is experimental, for GitHub-internal use only, and may change without notice at any time. #3892
  • Added an experimental change which, when running a Code Scanning analysis for a PR with improved incremental analysis enabled, prefers CodeQL CLI versions that have a cached overlay-base database for the configured languages. This speeds up analysis for a repository when there is not yet a cached overlay-base database for the latest CLI version. We expect to roll this change out to everyone in May. #3880

4.35.4 - 07 May 2026

  • Update default CodeQL bundle version to 2.25.4. #3881

4.35.3 - 01 May 2026

  • Upcoming breaking change: Add a deprecation warning for customers using CodeQL version 2.19.3 and earlier. These versions of CodeQL were discontinued on 9 April 2026 alongside GitHub Enterprise Server 3.15, and will be unsupported by the next minor release of the CodeQL Action. #3837
  • Configurations for private registries that use Cloudsmith or GCP OIDC are now accepted. #3850
  • Best-effort connection tests for private registries now use GET requests instead of HEAD for better compatibility with various registry implementations. For NuGet feeds, the test is now always performed against the service index. #3853
  • Fixed a bug where two diagnostics produced within the same millisecond could overwrite each other on disk, causing one of them to be lost. #3852

... (truncated)

Commits
  • 54f647b Merge pull request #3984 from github/update-v4.36.3-1f34ec164
  • e78819e Trigger checks
  • 2c9d3d6 Update changelog for v4.36.3
  • 1f34ec1 Merge pull request #3983 from github/mbg/repo-props/ff-for-config-file-prop
  • d5f0145 Log when repository property has a value but is ignored
  • f27f563 Add test for when the FF is off
  • 0025d0f Use FF
  • f7fa18f Add FF for config file repo property
  • 628fc3f Merge pull request #3979 from github/henrymercer/overlay-db-cleanup-size-tele...
  • 9cfb67b Add clarifying comments
  • Additional commits viewable in compare view

Updates `github/codeql-action/analyze` from 4.36.2 to 4.36.3
Release notes

Sourced from github/codeql-action/analyze's releases.

v4.36.3

No user facing changes.

Changelog

Sourced from github/codeql-action/analyze's changelog.

CodeQL Action Changelog

See the releases page for the relevant changes to the CodeQL CLI and language packs.

[UNRELEASED]

No user facing changes.

4.37.0 - 08 Jul 2026

  • Update default CodeQL bundle version to 2.26.0. #3995
  • In addition to the existing input format, the config-file input for the codeql-action/init step will soon support a new [owner/]repo[@ref][:path] format. All components except the repository name are optional. If omitted, owner defaults to the same owner as the repository the analysis is running for, ref to main, and path to .github/codeql-action.yaml. Support for this format ships in this version of the CodeQL Action, but will only be enabled over the coming weeks. #3973

4.36.3 - 01 Jul 2026

No user facing changes.

4.36.2 - 04 Jun 2026

  • Cache CodeQL CLI version information across Actions steps. #3943
  • Reduce requests while waiting for analysis processing by using exponential backoff when polling SARIF processing status. #3937
  • Update default CodeQL bundle version to 2.25.6. #3948

4.36.1 - 02 Jun 2026

No user facing changes.

4.36.0 - 22 May 2026

  • Breaking change: Bump the minimum required CodeQL bundle version to 2.19.4. #3894
  • Add support for SHA-256 Git object IDs. #3893
  • Update default CodeQL bundle version to 2.25.5. #3926

4.35.5 - 15 May 2026

  • We have improved how the JavaScript bundles for the CodeQL Action are generated to avoid duplication across bundles and reduce the size of the repository by around 70%. This should have no effect on the runtime behaviour of the CodeQL Action. #3899
  • For performance and accuracy reasons, improved incremental analysis will now only be enabled on a pull request when diff-informed analysis is also enabled for that run. If diff-informed analysis is unavailable (for example, because the PR diff ranges could not be computed), the action will fall back to a full analysis. #3791
  • If multiple inputs are provided for the GitHub-internal analysis-kinds input, only code-scanning will be enabled. The analysis-kinds input is experimental, for GitHub-internal use only, and may change without notice at any time. #3892
  • Added an experimental change which, when running a Code Scanning analysis for a PR with improved incremental analysis enabled, prefers CodeQL CLI versions that have a cached overlay-base database for the configured languages. This speeds up analysis for a repository when there is not yet a cached overlay-base database for the latest CLI version. We expect to roll this change out to everyone in May. #3880

4.35.4 - 07 May 2026

  • Update default CodeQL bundle version to 2.25.4. #3881

4.35.3 - 01 May 2026

  • Upcoming breaking change: Add a deprecation warning for customers using CodeQL version 2.19.3 and earlier. These versions of CodeQL were discontinued on 9 April 2026 alongside GitHub Enterprise Server 3.15, and will be unsupported by the next minor release of the CodeQL Action. #3837
  • Configurations for private registries that use Cloudsmith or GCP OIDC are now accepted. #3850
  • Best-effort connection tests for private registries now use GET requests instead of HEAD for better compatibility with various registry implementations. For NuGet feeds, the test is now always performed against the service index. #3853
  • Fixed a bug where two diagnostics produced within the same millisecond could overwrite each other on disk, causing one of them to be lost. #3852

... (truncated)

Commits
  • 54f647b Merge pull request #3984 from github/update-v4.36.3-1f34ec164
  • e78819e Trigger checks
  • 2c9d3d6 Update changelog for v4.36.3
  • 1f34ec1 Merge pull request #3983 from github/mbg/repo-props/ff-for-config-file-prop
  • d5f0145 Log when repository property has a value but is ignored
  • f27f563 Add test for when the FF is off
  • 0025d0f Use FF
  • f7fa18f Add FF for config file repo property
  • 628fc3f Merge pull request #3979 from github/henrymercer/overlay-db-cleanup-size-tele...
  • 9cfb67b Add clarifying comments
  • Additional commits viewable in compare view

Updates `docker/login-action` from 4.2.0 to 4.3.0
Release notes

Sourced from docker/login-action's releases.

v4.3.0

Full Changelog: https://github.com/docker/login-action/compare/v4.2.0...v4.3.0

Commits
  • c99871d Merge pull request #1030 from docker/dependabot/npm_and_yarn/aws-sdk-dependen...
  • b433555 [dependabot skip] chore: update generated content
  • 678a46a build(deps): bump the aws-sdk-dependencies group across 1 directory with 2 up...
  • f9a0aea Merge pull request #1031 from docker/dependabot/npm_and_yarn/sigstore-4.1.1
  • cc1e4cb build(deps): bump sigstore from 4.1.0 to 4.1.1
  • 02e1730 Merge pull request #1029 from docker/dependabot/npm_and_yarn/sigstore/verify-...
  • b548518 build(deps): bump @​sigstore/verify from 3.1.0 to 3.1.1
  • a244be3 Merge pull request #1027 from docker/dependabot/npm_and_yarn/docker/actions-t...
  • ee0d698 [dependabot skip] chore: update generated content
  • 127dc2c build(deps): bump @​docker/actions-toolkit from 0.91.0 to 0.92.0
  • Additional commits viewable in compare view

Updates `docker/setup-buildx-action` from 4.1.0 to 4.2.0
Release notes

Sourced from docker/setup-buildx-action's releases.

v4.2.0

Full Changelog: https://github.com/docker/setup-buildx-action/compare/v4.1.0...v4.2.0

Commits
  • bb05f3f Merge pull request #580 from docker/dependabot/npm_and_yarn/docker/actions-to...
  • 321c814 [dependabot skip] chore: update generated content
  • b9a36ef build(deps): bump @​docker/actions-toolkit from 0.91.0 to 0.92.0
  • ebeab24 Merge pull request #570 from docker/dependabot/npm_and_yarn/undici-6.27.0
  • 5c7b8ae [dependabot skip] chore: update generated content
  • 037e618 build(deps): bump undici from 6.25.0 to 6.27.0
  • 66080e5 Merge pull request #577 from docker/dependabot/npm_and_yarn/sigstore-4.1.1
  • 409aef0 Merge pull request #562 from docker/dependabot/npm_and_yarn/js-yaml-4.2.0
  • 49c6e42 build(deps): bump sigstore from 4.1.0 to 4.1.1
  • 2211273 [dependabot skip] chore: update generated content
  • Additional commits viewable in compare view

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
--------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Julien Goux --- .github/workflows/build-cli-artifacts.yml | 14 +++++++++++--- .github/workflows/cli-go-codeql.yml | 4 ++-- .github/workflows/cli-go-mirror-image.yml | 6 +++--- .github/workflows/cli-go-pg-prove.yml | 10 +++++----- .github/workflows/cli-go-publish-migra.yml | 10 +++++----- .github/workflows/mirror-template-images.yml | 2 +- 6 files changed, 27 insertions(+), 19 deletions(-) diff --git a/.github/workflows/build-cli-artifacts.yml b/.github/workflows/build-cli-artifacts.yml index 8faf9d0377..af7110def1 100644 --- a/.github/workflows/build-cli-artifacts.yml +++ b/.github/workflows/build-cli-artifacts.yml @@ -81,10 +81,18 @@ jobs: run: go mod download -x - name: Install nfpm + env: + NFPM_VERSION: "2.47.0" + NFPM_SHA256: "0660ca602b2d2d2ae4781a06c692b3eeb9d437ffea05b831d76e41f4a3188783" run: | - echo 'deb [trusted=yes] https://repo.goreleaser.com/apt/ /' | sudo tee /etc/apt/sources.list.d/goreleaser.list - sudo apt-get update - sudo apt-get install -y nfpm + set -euo pipefail + asset="nfpm_${NFPM_VERSION}_Linux_x86_64.tar.gz" + curl -fsSL -o /tmp/nfpm.tar.gz \ + "https://github.com/goreleaser/nfpm/releases/download/v${NFPM_VERSION}/${asset}" + echo "${NFPM_SHA256} /tmp/nfpm.tar.gz" | sha256sum -c + tar -xzf /tmp/nfpm.tar.gz -C /tmp nfpm + sudo install -m 0755 /tmp/nfpm /usr/local/bin/nfpm + nfpm --version - name: Install rcodesign env: diff --git a/.github/workflows/cli-go-codeql.yml b/.github/workflows/cli-go-codeql.yml index 3a7c41cbed..9759f2f7e6 100644 --- a/.github/workflows/cli-go-codeql.yml +++ b/.github/workflows/cli-go-codeql.yml @@ -67,7 +67,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 + uses: github/codeql-action/init@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -95,7 +95,7 @@ jobs: exit 1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 + uses: github/codeql-action/analyze@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3 with: category: "/language:${{matrix.language}}" defaults: diff --git a/.github/workflows/cli-go-mirror-image.yml b/.github/workflows/cli-go-mirror-image.yml index 3c9484e266..18921bf791 100644 --- a/.github/workflows/cli-go-mirror-image.yml +++ b/.github/workflows/cli-go-mirror-image.yml @@ -38,15 +38,15 @@ jobs: with: role-to-assume: ${{ secrets.PROD_AWS_ROLE }} aws-region: us-east-1 - - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + - uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4.3.0 with: registry: public.ecr.aws - - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + - uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4.3.0 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 + - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 - name: Mirror image env: SOURCE_IMAGE: docker.io/${{ github.event.client_payload.image || inputs.image }} diff --git a/.github/workflows/cli-go-pg-prove.yml b/.github/workflows/cli-go-pg-prove.yml index 4cd2b086e5..993e78bb0c 100644 --- a/.github/workflows/cli-go-pg-prove.yml +++ b/.github/workflows/cli-go-pg-prove.yml @@ -11,7 +11,7 @@ jobs: outputs: image_tag: supabase/pg_prove:${{ steps.version.outputs.pg_prove }} steps: - - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 + - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 - uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 with: load: true @@ -42,10 +42,10 @@ jobs: image_digest: ${{ steps.build.outputs.digest }} steps: - run: docker context create builders - - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 + - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 with: endpoint: builders - - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + - uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4.3.0 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} @@ -66,8 +66,8 @@ jobs: - build_image runs-on: ubuntu-latest steps: - - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 - - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 + - uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4.3.0 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} diff --git a/.github/workflows/cli-go-publish-migra.yml b/.github/workflows/cli-go-publish-migra.yml index 95449a8baa..189e6beea4 100644 --- a/.github/workflows/cli-go-publish-migra.yml +++ b/.github/workflows/cli-go-publish-migra.yml @@ -11,7 +11,7 @@ jobs: outputs: image_tag: supabase/migra:${{ steps.version.outputs.migra }} steps: - - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 + - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 - uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 with: load: true @@ -42,10 +42,10 @@ jobs: image_digest: ${{ steps.build.outputs.digest }} steps: - run: docker context create builders - - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 + - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 with: endpoint: builders - - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + - uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4.3.0 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} @@ -66,8 +66,8 @@ jobs: - build_image runs-on: ubuntu-latest steps: - - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 - - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 + - uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4.3.0 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} diff --git a/.github/workflows/mirror-template-images.yml b/.github/workflows/mirror-template-images.yml index 40fa3be500..ee7483d2e9 100644 --- a/.github/workflows/mirror-template-images.yml +++ b/.github/workflows/mirror-template-images.yml @@ -50,7 +50,7 @@ jobs: dependency-firewall-token: ${{ secrets.DF_FIREWALL_TOKEN }} - name: Log in to ghcr.io - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4.3.0 with: registry: ghcr.io username: ${{ github.actor }} From fa397576f77e71d61ddc5e2638015bfbf8e72327 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 9 Jul 2026 09:11:26 +0200 Subject: [PATCH 27/79] fix(docker): bump supabase/storage-api from v1.63.1 to v1.64.1 in /apps/cli-go/pkg/config/templates in the docker-minor group (#5834) Bumps the docker-minor group in /apps/cli-go/pkg/config/templates with 1 update: supabase/storage-api. Updates `supabase/storage-api` from v1.63.1 to v1.64.1 [![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=supabase/storage-api&package-manager=docker&previous-version=v1.63.1&new-version=v1.64.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
--------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- apps/cli-go/pkg/config/templates/Dockerfile | 2 +- packages/stack/src/versions.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/cli-go/pkg/config/templates/Dockerfile b/apps/cli-go/pkg/config/templates/Dockerfile index b2703b51fc..f285fe5ead 100644 --- a/apps/cli-go/pkg/config/templates/Dockerfile +++ b/apps/cli-go/pkg/config/templates/Dockerfile @@ -12,7 +12,7 @@ FROM timberio/vector:0.53.0-alpine AS vector FROM supabase/supavisor:2.9.7 AS supavisor FROM supabase/gotrue:v2.192.0 AS gotrue FROM supabase/realtime:v2.112.9 AS realtime -FROM supabase/storage-api:v1.63.1 AS storage +FROM supabase/storage-api:v1.64.1 AS storage FROM supabase/logflare:1.46.0 AS logflare # Append to JobImages when adding new dependencies below FROM supabase/pgadmin-schema-diff:cli-0.0.5 AS differ diff --git a/packages/stack/src/versions.ts b/packages/stack/src/versions.ts index bd675abf2d..e9196cea68 100644 --- a/packages/stack/src/versions.ts +++ b/packages/stack/src/versions.ts @@ -51,7 +51,7 @@ export const DEFAULT_VERSIONS: VersionManifest = { auth: "2.192.0", "edge-runtime": "1.74.2", realtime: "2.112.9", - storage: "1.63.1", + storage: "1.64.1", imgproxy: "v3.8.0", mailpit: "v1.30.2", pgmeta: "0.96.6", From 425be7db4acf8b695bbebd067ac3cbfbe1e7a012 Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Thu, 9 Jul 2026 10:01:08 +0100 Subject: [PATCH 28/79] fix(cli): remove doubled "Expected: Expected" prefix from Flag.choice/primitive errors (#5831) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What changed Any `Flag.choice(...)`/`Flag.choiceWithValue(...)`-backed flag (`--size` on `projects create`/`branches create`, `--dns-resolver`, `--output-format`, `--output`/`-o`, `--agent`) showed a doubled "Expected: Expected ..." prefix when given an invalid value, e.g.: ``` Invalid value for flag --size: "nano". Expected: Expected "micro" | "small" | ... , got "nano" ``` Root cause: several `Primitive`s under `effect@4.0.0-beta.93`'s `effect/unstable/cli` (`choice` — used by `Flag.choice`/`Flag.choiceWithValue` — plus the schema-backed `integer`, `float`, `boolean`, and `date`) fail with a raw message that already starts with the word "Expected" (e.g. `` `Expected ${validChoices}, got ${value}` `` for `choice`, or `Expected a valid date, got Invalid Date` for `date`), and `CliError.InvalidValue`'s own `message` getter (`CliError.ts:359-364`) independently prepends its own `"Expected: "` label on top of that — producing the doubling for any flag or argument backed by one of these primitives. This is a bug in the vendored `effect` beta package itself, not in this repo's code. Workaround (until upstream `effect` is fixed) added to the shared CLI error-formatting layer (`subcommand-flag-suggestions.ts`), which already carries similar per-tag rewrites for `UnrecognizedOption`/`UnknownSubcommand`: collapse the literal `"Expected: Expected "` substring down to a single `"Expected "` whenever it appears in an `InvalidValue` error's rendered message. Collapsing the exact doubled substring (rather than rebuilding the surrounding "Invalid value for ..." template ourselves) keeps the fix tied to the precise defect, covers every affected primitive uniformly, and lets every other part of the message keep tracking upstream's own wording if it changes. Verified against `go-parity-auditor`: Go's real CLI shows a completely different message for the same scenario (`Error: invalid argument "bogus" for "--size" flag: must be one of [ large | medium | ... ]`, from `apps/cli-go/internal/utils/enum.go` wrapped by cobra/pflag). Full byte-parity with Go's wording is out of scope here — this ticket is specifically about the doubled-prefix defect in the TS/Effect-native message, not a "TS wording differs from Go" report; rewriting every `Flag.choice` flag's phrasing to match Go's template would be a separate, larger change. ## Known related (not fixed here) The underlying `Primitive.choice` failure text also appends a redundant `, got "X"` suffix, so the invalid value still appears twice in the final message (once right after the flag name, once after "got"). That's a pre-existing wart in the same vendored `effect` failure string, not something this fix introduces, and collapsing it would mean post-processing `effect`'s `expected` string further — bigger than this "doubled prefix" ticket calls for. Left as-is. Fixes CLI-1898 --- .../src/shared/cli/invalid-value-message.ts | 54 +++++++++++ .../shared/cli/subcommand-flag-suggestions.ts | 15 +++ .../subcommand-flag-suggestions.unit.test.ts | 97 +++++++++++++++++++ apps/cli/src/shared/output/normalize-error.ts | 40 ++++++++ .../output/normalize-error.unit.test.ts | 93 ++++++++++++++++++ .../shared/output/text-formatter.unit.test.ts | 18 ++++ 6 files changed, 317 insertions(+) create mode 100644 apps/cli/src/shared/cli/invalid-value-message.ts diff --git a/apps/cli/src/shared/cli/invalid-value-message.ts b/apps/cli/src/shared/cli/invalid-value-message.ts new file mode 100644 index 0000000000..8bf079581d --- /dev/null +++ b/apps/cli/src/shared/cli/invalid-value-message.ts @@ -0,0 +1,54 @@ +// Workaround for a doubled "Expected: Expected ..." prefix in +// effect@4.0.0-beta.93's own primitive parsers. Several `Primitive`s under +// `effect/unstable/cli` (`choice` — used by `Flag.choice`/ +// `Flag.choiceWithValue` — plus the schema-backed `integer`, `float`, +// `boolean`, and `date`) fail with a raw message that already starts with +// the word "Expected" (e.g. `Expected "micro" | "small", got "nano"` or +// `Expected a valid date, got Invalid Date`), and `CliError.InvalidValue`'s +// own `message` getter independently prepends its own `"Expected: "` label +// on top of that — so any flag or argument backed by one of these +// primitives renders "Expected: Expected ...". Detect this from +// `error.expected` (the field the buggy primitives actually populate) +// rather than searching the fully composed `error.message`: `error.value` +// is user-controlled and interpolated into that same message (including a +// second time inside `expected` itself, via `choice`'s "got " +// suffix), so a message-wide, first-occurrence string replace can target +// the wrong spot if the value itself happens to contain the literal text +// "Expected: Expected ". Anchoring on `error.expected` and rebuilding the +// message from the same template `CliError.InvalidValue` uses avoids ever +// scanning `error.value`. Remove once upstream `effect` fixes this (see +// CLI-1898). +// +// Shared by two call sites that each see `InvalidValue` failures at a +// different point in `effect`'s CLI runtime: +// - `subcommand-flag-suggestions.ts` formats errors that reach the +// `CliOutput.Formatter` via the `ShowHelp` envelope — i.e. ordinary +// subcommand/argument flags, validated while `Command.runWith` parses the +// command tree. +// - `normalize-error.ts` formats errors from `GlobalFlag.setting` flags +// (`--output-format`, and the legacy `--output`/`-o`, `--dns-resolver`, +// `--agent`), which `Command.runWith` validates in a later step that runs +// *outside* the `ShowHelp` path and therefore never reaches the +// formatter — it surfaces as a raw failure through `runCli`'s catch-all +// instead. +const EXPECTED_PREFIX = "Expected "; + +export interface InvalidValueMessageFields { + readonly option: string; + readonly value: string; + readonly expected: string; + readonly kind: "flag" | "argument"; +} + +/** + * Rebuilds a `CliError.InvalidValue` message from its own template when + * `expected` carries the doubled "Expected" prefix. Returns `undefined` when + * `expected` is unaffected, so callers can fall back to the error's own + * untouched `message`. + */ +export function formatInvalidValueMessage(error: InvalidValueMessageFields): string | undefined { + if (!error.expected.startsWith(EXPECTED_PREFIX)) return undefined; + return error.kind === "argument" + ? `Invalid value for argument <${error.option}>: "${error.value}". ${error.expected}` + : `Invalid value for flag --${error.option}: "${error.value}". ${error.expected}`; +} diff --git a/apps/cli/src/shared/cli/subcommand-flag-suggestions.ts b/apps/cli/src/shared/cli/subcommand-flag-suggestions.ts index 1cc5124f98..c6da39c2e1 100644 --- a/apps/cli/src/shared/cli/subcommand-flag-suggestions.ts +++ b/apps/cli/src/shared/cli/subcommand-flag-suggestions.ts @@ -1,4 +1,5 @@ import type { CliError, Command, HelpDoc } from "effect/unstable/cli"; +import { formatInvalidValueMessage } from "./invalid-value-message.ts"; export interface CliErrorSuggestionContext { readonly rootCommand: Command.Command.Any; @@ -228,6 +229,20 @@ export function formatCliErrorsForDisplay( continue; } + if (error._tag === "InvalidValue") { + const message = formatInvalidValueMessage(error); + if (message !== undefined) { + changed = true; + formatted.push({ + _tag: error._tag, + message, + source: error, + changed: true, + }); + continue; + } + } + formatted.push({ _tag: error._tag, message: error.message, source: error, changed: false }); } diff --git a/apps/cli/src/shared/cli/subcommand-flag-suggestions.unit.test.ts b/apps/cli/src/shared/cli/subcommand-flag-suggestions.unit.test.ts index 079256d354..595b0969c6 100644 --- a/apps/cli/src/shared/cli/subcommand-flag-suggestions.unit.test.ts +++ b/apps/cli/src/shared/cli/subcommand-flag-suggestions.unit.test.ts @@ -122,4 +122,101 @@ describe("subcommand flag placement suggestions", () => { ); expect(errors.errors[0]?.message).not.toContain("--project-ref=jacraenyzrorgjhsdvvf "); }); + + it("collapses the doubled 'Expected: Expected' prefix for an invalid choice flag value", () => { + const errors = formatCliErrorsForDisplay([ + new CliError.InvalidValue({ + option: "size", + value: "nano", + expected: 'Expected "micro" | "small" | "medium", got "nano"', + kind: "flag", + }), + ]); + + expect(errors.changed).toBe(true); + expect(errors.errors).toHaveLength(1); + expect(errors.errors[0]?.message).toBe( + 'Invalid value for flag --size: "nano". Expected "micro" | "small" | "medium", got "nano"', + ); + expect(errors.errors[0]?.message).not.toMatch(/Expected:\s*Expected/); + }); + + it("collapses the doubled 'Expected: Expected' prefix for an invalid choice argument value", () => { + const errors = formatCliErrorsForDisplay([ + new CliError.InvalidValue({ + option: "level", + value: "bogus", + expected: 'Expected "debug" | "info", got "bogus"', + kind: "argument", + }), + ]); + + expect(errors.changed).toBe(true); + expect(errors.errors[0]?.message).toBe( + 'Invalid value for argument : "bogus". Expected "debug" | "info", got "bogus"', + ); + expect(errors.errors[0]?.message).not.toMatch(/Expected:\s*Expected/); + }); + + it("also collapses the doubled prefix for a non-choice primitive whose failure text starts with 'Expected' (e.g. an invalid integer flag value)", () => { + // Real failure text from effect@4.0.0-beta.93's schema-backed `Primitive.integer` + // (also affects `float`, `boolean`, and `date` — every primitive whose parse + // failure happens to start with the word "Expected" hits the same doubling). + const errors = formatCliErrorsForDisplay([ + new CliError.InvalidValue({ + option: "port", + value: "abc", + expected: 'Expected a string representing a finite number, got "abc"', + kind: "flag", + }), + ]); + + expect(errors.changed).toBe(true); + expect(errors.errors[0]?.message).toBe( + 'Invalid value for flag --port: "abc". Expected a string representing a finite number, got "abc"', + ); + expect(errors.errors[0]?.message).not.toMatch(/Expected:\s*Expected/); + }); + + it("leaves invalid-value errors whose expected text does not start with 'Expected' unchanged", () => { + // Real failure text from effect@4.0.0-beta.93's `Primitive.keyValuePair` — + // it never starts with the word "Expected", so it isn't doubled by + // `CliError.InvalidValue`'s own "Expected: " prefix and needs no rewriting. + const errors = formatCliErrorsForDisplay([ + new CliError.InvalidValue({ + option: "define", + value: "bogus", + expected: "Invalid key=value format. Expected format: key=value, got: bogus", + kind: "flag", + }), + ]); + + expect(errors.changed).toBe(false); + expect(errors.errors[0]?.changed).toBe(false); + expect(errors.errors[0]?.message).toBe( + 'Invalid value for flag --define: "bogus". Expected: Invalid key=value format. Expected format: key=value, got: bogus', + ); + }); + + it("does not corrupt a value that itself contains the literal 'Expected: Expected' text", () => { + // Regression test: the fix must anchor on `error.expected` (the field the + // buggy primitive actually populates) rather than searching the fully + // composed `error.message`, since `error.value` is user-controlled and is + // interpolated into that same message twice (once directly, once again + // inside `expected`'s "got " suffix). A value that happens to + // contain the literal doubled-prefix text must be left untouched. + const errors = formatCliErrorsForDisplay([ + new CliError.InvalidValue({ + option: "env", + value: "Expected: Expected nano", + expected: 'Expected "dev" | "staging" | "prod", got "Expected: Expected nano"', + kind: "flag", + }), + ]); + + expect(errors.changed).toBe(true); + expect(errors.errors[0]?.message).toBe( + 'Invalid value for flag --env: "Expected: Expected nano". Expected "dev" | "staging" | "prod", got "Expected: Expected nano"', + ); + }); }); diff --git a/apps/cli/src/shared/output/normalize-error.ts b/apps/cli/src/shared/output/normalize-error.ts index a143b414b3..19cc375913 100644 --- a/apps/cli/src/shared/output/normalize-error.ts +++ b/apps/cli/src/shared/output/normalize-error.ts @@ -1,4 +1,5 @@ import { Cause, Option } from "effect"; +import { formatInvalidValueMessage } from "../cli/invalid-value-message.ts"; type NormalizedCliError = { readonly code: string; @@ -17,6 +18,16 @@ const readString = (value: ErrorRecord, key: string): string | undefined => { return typeof field === "string" && field.trim().length > 0 ? field.trim() : undefined; }; +// Unlike `readString`, does not trim or reject empty strings. Use this for +// fields that carry raw user input (e.g. `CliError.InvalidValue#value`), +// where an empty string or meaningful surrounding whitespace is a legitimate +// value the user typed (`supabase --output-format ''`) and must be preserved +// and reported verbatim rather than normalized away. +const readRawString = (value: ErrorRecord, key: string): string | undefined => { + const field = value[key]; + return typeof field === "string" ? field : undefined; +}; + const mappedError = (error: ErrorRecord): NormalizedCliError | undefined => { const tag = readString(error, "_tag"); switch (tag) { @@ -75,6 +86,35 @@ const mappedError = (error: ErrorRecord): NormalizedCliError | undefined => { : "Error: required flag(s) not set", }; } + case "InvalidValue": { + // `CliError.InvalidValue` for a `GlobalFlag.setting` flag (e.g. + // `--output-format`, or the legacy `--output`/`-o`, `--dns-resolver`, + // `--agent`) never reaches `CliOutput.Formatter` — `Command.runWith` + // validates those flags in a step that runs outside the `ShowHelp` + // path, so the failure lands here instead. Apply the same + // doubled-"Expected"-prefix workaround `subcommand-flag-suggestions.ts` + // applies for the `ShowHelp`-formatted case (see CLI-1898), so every + // `InvalidValue` failure — whichever path it takes — renders the same + // way. + const option = readString(error, "option"); + // Raw read: `value` is the exact argv token the user typed and can + // legitimately be `""` or carry surrounding whitespace — `readString` + // would trim it or drop it entirely, either masking the bug this case + // exists to fix or misreporting what the user actually typed. + const value = readRawString(error, "value"); + const expected = readString(error, "expected"); + const kind = readString(error, "kind"); + if ( + option !== undefined && + value !== undefined && + expected !== undefined && + (kind === "flag" || kind === "argument") + ) { + const message = formatInvalidValueMessage({ option, value, expected, kind }); + if (message !== undefined) return { code: tag, message }; + } + return undefined; + } case "ShowHelp": { // Effect CLI wraps parse errors in a ShowHelp envelope (`CliError.ts`) // whose `errors` array holds the underlying causes. If exactly one of diff --git a/apps/cli/src/shared/output/normalize-error.unit.test.ts b/apps/cli/src/shared/output/normalize-error.unit.test.ts index 1eba0f07f6..107600ac13 100644 --- a/apps/cli/src/shared/output/normalize-error.unit.test.ts +++ b/apps/cli/src/shared/output/normalize-error.unit.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from "vitest"; import { Cause } from "effect"; +import { CliError } from "effect/unstable/cli"; import { formatCliError, normalizeCause, normalizeCliError } from "./normalize-error.ts"; describe("normalizeCliError", () => { @@ -50,6 +51,98 @@ describe("normalizeCliError", () => { }); }); + test("InvalidValue collapses the doubled 'Expected: Expected' prefix (e.g. a bad GlobalFlag.setting value)", () => { + // Regression test for CLI-1898: `--output-format`/`--dns-resolver`/`--agent`/ + // legacy `--output` are `GlobalFlag.setting` flags backed by `Flag.choice`. + // `Command.runWith` validates their values in a step that runs outside the + // `ShowHelp` path, so a bad value never reaches `CliOutput.Formatter` (and + // `subcommand-flag-suggestions.ts`'s fix) — it surfaces here instead. + const error = new CliError.InvalidValue({ + option: "output-format", + value: "bogus", + expected: 'Expected "text" | "json" | "stream-json", got "bogus"', + kind: "flag", + }); + + expect(normalizeCliError(error)).toEqual({ + code: "InvalidValue", + message: + 'Invalid value for flag --output-format: "bogus". Expected "text" | "json" | "stream-json", got "bogus"', + }); + }); + + test("InvalidValue preserves an empty invalid value (e.g. `--output-format ''`)", () => { + // Regression test for a Codex review finding on CLI-1898: `value` is raw + // user input read straight off argv, so `''` is a legitimate way to + // trigger this failure. Reading it through the trim-and-reject-empty + // `readString` helper would fail the guard and leak the original + // doubled "Expected: Expected" message instead of fixing it. + const error = new CliError.InvalidValue({ + option: "output-format", + value: "", + expected: 'Expected "text" | "json" | "stream-json", got ""', + kind: "flag", + }); + + expect(normalizeCliError(error)).toEqual({ + code: "InvalidValue", + message: + 'Invalid value for flag --output-format: "". Expected "text" | "json" | "stream-json", got ""', + }); + }); + + test("InvalidValue preserves surrounding whitespace in the invalid value (e.g. `--output-format ' json'`)", () => { + // Regression test for the same Codex finding: trimming `value` would + // report a different string than what the user actually typed. + const error = new CliError.InvalidValue({ + option: "output-format", + value: " json", + expected: 'Expected "text" | "json" | "stream-json", got " json"', + kind: "flag", + }); + + expect(normalizeCliError(error)).toEqual({ + code: "InvalidValue", + message: + 'Invalid value for flag --output-format: " json". Expected "text" | "json" | "stream-json", got " json"', + }); + }); + + test("InvalidValue leaves an already-clean 'expected' message untouched", () => { + const error = new CliError.InvalidValue({ + option: "define", + value: "bogus", + expected: "Invalid key=value format. Expected format: key=value, got: bogus", + kind: "flag", + }); + + expect(normalizeCliError(error)).toEqual({ + code: "InvalidValue", + message: + 'Invalid value for flag --define: "bogus". Expected: Invalid key=value format. Expected format: key=value, got: bogus', + }); + }); + + test("ShowHelp envelope unwraps a single InvalidValue with the same doubled-prefix fix", () => { + const error = { + _tag: "ShowHelp", + commandPath: ["db", "lint"], + errors: [ + new CliError.InvalidValue({ + option: "level", + value: "bogus", + expected: 'Expected "warning" | "error", got "bogus"', + kind: "flag", + }), + ], + }; + + expect(normalizeCliError(error)).toEqual({ + code: "InvalidValue", + message: 'Invalid value for flag --level: "bogus". Expected "warning" | "error", got "bogus"', + }); + }); + test("ShowHelp envelope unwraps a single MissingOption to Cobra wording", () => { // Effect CLI raises `ShowHelp` containing the parse error in its `errors` // array. We unwrap to surface the actionable message instead of "Help requested". diff --git a/apps/cli/src/shared/output/text-formatter.unit.test.ts b/apps/cli/src/shared/output/text-formatter.unit.test.ts index f49cf597ea..fff1a4a17b 100644 --- a/apps/cli/src/shared/output/text-formatter.unit.test.ts +++ b/apps/cli/src/shared/output/text-formatter.unit.test.ts @@ -53,4 +53,22 @@ describe("textCliOutputFormatter", () => { expect(text).toContain("Did you mean this?"); expect(text).toContain("--plan"); }); + + it("does not double the 'Expected' prefix for an invalid choice flag value", () => { + const formatter = textCliOutputFormatter(); + + const text = formatter.formatErrors([ + new CliError.InvalidValue({ + option: "size", + value: "nano", + expected: 'Expected "micro" | "small" | "medium", got "nano"', + kind: "flag", + }), + ]); + + expect(text).toContain( + 'Invalid value for flag --size: "nano". Expected "micro" | "small" | "medium", got "nano"', + ); + expect(text).not.toMatch(/Expected:\s*Expected/); + }); }); From f59048754023de58f8c182c6272829f9102c260c Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Thu, 9 Jul 2026 10:46:09 +0100 Subject: [PATCH 29/79] fix(cli): resolve global/persistent flag values in legacy telemetry (#5830) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What changed Go's `changedFlags(cmd)` (`apps/cli-go/cmd/root_analytics.go:52-76`) walks `cmd.Parent()` at every ancestor collecting `PersistentFlags()` (global/root flags: `--debug`, `--yes`, `--experimental`, `--create-ticket`, `--output`, `--dns-resolver`, `--agent`, `--profile`, `--workdir`, `--network-id`) in addition to a command's own flags, then reports booleans and enum-typed flags verbatim in `cli_command_executed` telemetry. The TS port's `withLegacyCommandInstrumentation` had no way to resolve *any* global/persistent flag's value — only the invoking command's own locally-declared `flags` option was ever consulted. So a changed global flag always fell back to the literal string `""`, even a boolean like `--debug`, diverging from Go's `flags: {debug: true}`. This adds `legacyGlobalFlagValues` (`shared/legacy/global-flags.ts`) to resolve every global flag's live value via `Effect.serviceOption` (a no-op outside the real CLI tree, so it adds no `R` requirement and no per-command wiring), and threads it into `buildFlagsMap` as a fallback used only when a changed CLI flag name isn't already declared in the invoking command's own `flags` record — mirroring Go's ancestor-walk precedence (a command's own flag always wins on a name collision, e.g. `db diff`'s local `--output` file-path flag shadowing the global `--output` enum, exactly as it does in Go's cobra tree). `safeFlags`/`config`-based safety classification is now restricted to values that actually came from the handler's own `flags` record, so a future per-command safe/choice annotation can never be misapplied to an unrelated global flag's value purely by CLI-name collision (raised in review). Scope is deliberately narrow: this fixes the 4 boolean globals (`--debug`, `--yes`, `--experimental`, `--create-ticket`) immediately. The 3 global choice flags (`--output`, `--dns-resolver`, `--agent`) remain redacted — that gap is the already-tracked CLI-1904, not this ticket. Fixes CLI-1896. ## Why Found during CLI-1868's review (architect-reviewer finding) as a systemic gap affecting all ~73 `withLegacyCommandInstrumentation({ flags })` call sites — it was previously unreachable for `telemetry enable`/`disable` specifically (hardcoded to `analytics: false`), but CLI-1868 made it reachable there, surfacing the gap. ## Reviewer-relevant context - One judgement call deliberately left open: global choice flags (`--output`/`--dns-resolver`/`--agent`) still redact. This is intentional — CLI-1904 already tracks teaching the safety pipeline about global `EnumFlag`s, and folding it into this change would blur two independent tickets. - Heads-up for whoever owns the CLI PostHog dashboards: `flags.debug` (and `yes`/`experimental`/`create-ticket`) will now emit real booleans going forward instead of the literal string `""` for these four flags specifically. Any saved insight/funnel doing exact-string matching on `""` for these keys will stop matching after this ships. --- apps/cli/AGENTS.md | 1 + .../legacy/shared/legacy-db-target-flags.ts | 90 ++++++++ .../legacy-db-target-flags.unit.test.ts | 107 ++++++++- .../legacy-command-instrumentation.ts | 146 ++++++++---- ...egacy-command-instrumentation.unit.test.ts | 217 +++++++++++++++++- apps/cli/src/shared/legacy/global-flags.ts | 44 +++- .../shared/legacy/global-flags.unit.test.ts | 83 +++++++ 7 files changed, 646 insertions(+), 42 deletions(-) create mode 100644 apps/cli/src/shared/legacy/global-flags.unit.test.ts diff --git a/apps/cli/AGENTS.md b/apps/cli/AGENTS.md index 3d136879cd..da6b1bb60b 100644 --- a/apps/cli/AGENTS.md +++ b/apps/cli/AGENTS.md @@ -286,6 +286,7 @@ The legacy shell sends the same PostHog events to the same product analytics pip - **Pass `flags` to the wrapper** so boolean flag values can be detected and logged verbatim: `handler(flags).pipe(withLegacyCommandInstrumentation({ flags }), ...)`. Sensitive values become the literal string `""` to match Go. - **Use `safeFlags: ["flag-name"]`** to whitelist flags that Go marks with `markFlagTelemetrySafe` (grep `apps/cli-go/cmd/*.go`). Today these are `--project-ref` (sso, branches, link, functions, projects/api-keys), `--project-id` (gen/types), `--org-id` (projects/create), and `--version` (migration/squash). - **Pass `config` (the command's own flag config record) to the wrapper** if it has any `Flag.choice`/`Flag.choiceWithValue` flags: `withLegacyCommandInstrumentation({ flags, config })`. Every choice flag declared in that command's own `config` is auto-detected and treated as safe, mirroring Go's `isEnumFlag` (`cmd/root_analytics.go:110-116`), which checks `flag.Value.(*utils.EnumFlag)` unconditionally — no per-flag `safeFlags` entry needed, and it stays correct as choices are added or removed. This does NOT cover global/root flags (`--output`, `--dns-resolver`, `--agent` in `shared/legacy/global-flags.ts`) even though Go's equivalents are also `EnumFlag` — see CLI-1904. +- **Global/persistent flags (`shared/legacy/global-flags.ts`) resolve automatically** — the wrapper reads `legacyGlobalFlagValues` (via `Effect.serviceOption`, so it's a no-op outside the real CLI tree) and falls back to it whenever a changed flag name isn't in the handler's own `flags` record, mirroring Go's `changedFlags()` walking `cmd.Parent()`'s `PersistentFlags()` (`cmd/root_analytics.go:53-76`). No per-command wiring needed. Boolean globals (`--debug`, `--yes`, `--experimental`, `--create-ticket`) therefore already report their real value through the existing boolean-is-safe rule — but ONLY when a command's own `flags` record doesn't already declare that CLI name (a command's own flag always wins, e.g. `db diff`'s local `--output`); the three global choice flags (`--output`, `--dns-resolver`, `--agent`) still redact until CLI-1904 teaches the safety pipeline about global `EnumFlag`s. - **Proxy handlers (`LegacyGoProxy.exec`) must NOT wrap with any instrumentation.** The Go subprocess fires its own telemetry; a TS wrapper would double-count `cli_command_executed`. - **When promoting a command from proxy to native, reproduce every `phtelemetry.*` call in the Go counterpart.** Grep `apps/cli-go/internal//` for `service.Capture`, `service.Alias`, `service.Identify`, `service.GroupIdentify`, and `TrackUpgradeSuggested`. The current Go custom events that legacy ports must reproduce when natively ported: diff --git a/apps/cli/src/legacy/shared/legacy-db-target-flags.ts b/apps/cli/src/legacy/shared/legacy-db-target-flags.ts index eb891c924a..6eac57c3cd 100644 --- a/apps/cli/src/legacy/shared/legacy-db-target-flags.ts +++ b/apps/cli/src/legacy/shared/legacy-db-target-flags.ts @@ -45,6 +45,25 @@ export interface LegacyDbTargetSelection { * (`src/shared/legacy/global-flags.ts`, `src/shared/cli/global-flags.ts`). * `Flag.string` / `Flag.choice` / `Flag.integer` → value-consuming; * `Flag.boolean` → not. + * + * Also consulted by `extractChangedFlagNames` + * (`legacy/telemetry/legacy-command-instrumentation.ts`), which scans EVERY + * legacy command's raw argv, not just the db-target/global subset above — so + * this set additionally lists every other value-consuming (non-boolean) flag + * declared anywhere under `legacy/commands/`. Without an entry here, a bare + * `--some-local-flag ` is mis-scanned: the value token is treated as a + * separate flag rather than skipped, and if that token happens to look like a + * global flag's long name (e.g. `secrets set --env-file --debug`, where + * `--debug` is `--env-file`'s value under pflag semantics), CLI-1896's + * global-flag fallback fabricates a `flags.debug` value Go never records for + * that invocation. `legacy-db-target-flags.unit.test.ts` statically scans + * every `*.command.ts` file's directly-declared `Flag.string`/`Flag.integer`/ + * `Flag.choice`/`Flag.choiceWithValue` calls and asserts they're all + * represented here, so a new command that adds a value-consuming flag and + * forgets to register it fails CI. That scan cannot see flag names built + * through a helper indirection (`issue.command.ts`'s + * `legacyIssueOptionalTextFlag`, `status.command.ts`'s `csvStringSliceFlag`) + * — those flags are listed below by hand and excluded from the scan. */ export const VALUE_CONSUMING_LONG_FLAGS = new Set([ // db-family command flags @@ -72,6 +91,74 @@ export const VALUE_CONSUMING_LONG_FLAGS = new Set([ "network-id", "dns-resolver", "agent", + // Every other value-consuming flag declared directly across legacy/commands/ + // (CLI-1896 review follow-up — see the doc comment above). + "add-domains", + "algorithm", + "attribute-mapping-file", + "auth", + "config", + "custom-hostname", + "db-allow-cidr", + "db-password", + "db-unban-ip", + "desired-subdomain", + "diff-engine", + "domains", + "env-file", + "exclude", + "exp", + "file", + "from", + "from-backup", + "git-branch", + "import-map", + "inspect-mode", + "lang", + "last", + "metadata-file", + "metadata-url", + "name", + "name-id-format", + "notify-url", + "org-id", + "override-name", + "payload", + "plan", + "project-id", + "project-ref", + "query-timeout", + "region", + "remove-domains", + "role", + "size", + "status", + "sub", + "swift-access-control", + "template", + "timestamp", + "to", + "token", + "valid-for", + "version", + // Declared through a name-parameterized helper, invisible to the static + // scan (see the doc comment above): `issue.command.ts`'s + // `legacyIssueOptionalTextFlag` and `status.command.ts`'s + // `csvStringSliceFlag`. + "additional-context", + "area", + "command", + "actual-output", + "expected-behavior", + "reproduce", + "crash-report-id", + "docker-services", + "problem", + "proposed-solution", + "alternatives", + "link", + "issue-type", + "improvement", ]); /** @@ -83,6 +170,9 @@ export const VALUE_CONSUMING_SHORT_FLAGS = new Set([ "o", // --output / -o "p", // --password / -p (migration list, db push/pull/dump/remote) "j", // --jobs / -j (storage cp) + "f", // --file / -f (db dump/diff/query, db schema declarative sync) + "t", // --template / -t (test new); --type / -t (sso add); --timestamp / -t (backups restore) + "x", // --exclude / -x (start, db dump) ]); /** diff --git a/apps/cli/src/legacy/shared/legacy-db-target-flags.unit.test.ts b/apps/cli/src/legacy/shared/legacy-db-target-flags.unit.test.ts index 2b32ca5ca5..9799b0a636 100644 --- a/apps/cli/src/legacy/shared/legacy-db-target-flags.unit.test.ts +++ b/apps/cli/src/legacy/shared/legacy-db-target-flags.unit.test.ts @@ -1,5 +1,12 @@ +import { readdirSync, readFileSync, statSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; import { describe, expect, it } from "vitest"; -import { resolveLegacyDbTargetFlags } from "./legacy-db-target-flags.ts"; +import { + resolveLegacyDbTargetFlags, + VALUE_CONSUMING_LONG_FLAGS, + VALUE_CONSUMING_SHORT_FLAGS, +} from "./legacy-db-target-flags.ts"; describe("resolveLegacyDbTargetFlags", () => { it("returns empty setFlags and undefined connType when no args", () => { @@ -185,3 +192,101 @@ describe("resolveLegacyDbTargetFlags", () => { expect(result.setFlags).toEqual(["local"]); }); }); + +describe("VALUE_CONSUMING_LONG_FLAGS / VALUE_CONSUMING_SHORT_FLAGS completeness (CLI-1896 review)", () => { + // `legacy/telemetry/legacy-command-instrumentation.ts`'s `extractChangedFlagNames` + // relies on these two sets to know which flag consumes the next raw-argv + // token as its value, across EVERY legacy command (not just the db-target + // subset this file's other describe block covers) — see the doc comment on + // `VALUE_CONSUMING_LONG_FLAGS` in `legacy-db-target-flags.ts`. This scan is + // static-source-based (same technique as + // `shared/cli/code-structure.unit.test.ts`) rather than importing every + // command module, so it can only see flag names declared as a literal + // string argument to `Flag.string`/`Flag.integer`/`Flag.choice`/ + // `Flag.choiceWithValue`/`Flag.float` — it cannot trace a name passed + // through a helper function (`issue.command.ts`'s + // `legacyIssueOptionalTextFlag`, `status.command.ts`'s + // `csvStringSliceFlag`), so those two files are excluded below; their flag + // names are registered by hand in `VALUE_CONSUMING_LONG_FLAGS` instead. + const commandsDir = fileURLToPath(new URL("../commands", import.meta.url)); + const INDIRECT_NAME_FILES = new Set(["issue.command.ts", "status.command.ts"]); + const VALUE_FLAG_KINDS = ["string", "integer", "choice", "choiceWithValue", "float"]; + + function walk(dir: string): Array { + return readdirSync(dir).flatMap((entry) => { + const fullPath = path.join(dir, entry); + const stats = statSync(fullPath); + if (stats.isDirectory()) return walk(fullPath); + return entry.endsWith(".command.ts") ? [fullPath] : []; + }); + } + + interface DeclaredFlag { + readonly file: string; + readonly name: string; + readonly alias: string | undefined; + } + + function extractDeclaredFlags(filePath: string): Array { + const source = readFileSync(filePath, "utf8"); + const callRegex = /Flag\.(string|integer|choice|choiceWithValue|float|boolean)\(/g; + const calls = Array.from(source.matchAll(callRegex), (match) => ({ + index: match.index, + kind: match[1]!, + })); + + const declared: Array = []; + for (let i = 0; i < calls.length; i++) { + const current = calls[i]!; + if (!VALUE_FLAG_KINDS.includes(current.kind)) continue; + + // Name declared as a literal string (e.g. `Flag.string("schema")`). + // A name passed as an identifier (`Flag.string(name)`) doesn't match + // and is silently skipped — see INDIRECT_NAME_FILES above. + const remainder = source.slice(current.index); + const nameMatch = remainder.match(/^Flag\.\w+\(\s*"([a-zA-Z0-9-]+)"/); + if (!nameMatch) continue; + + // The alias, if any, is somewhere in the `.pipe(...)` chain between + // this flag declaration and the next one. + const windowEnd = i + 1 < calls.length ? calls[i + 1]!.index : source.length; + const window = source.slice(current.index, windowEnd); + const aliasMatch = window.match(/withAlias\(\s*"([a-zA-Z0-9])"\s*\)/); + + declared.push({ file: filePath, name: nameMatch[1]!, alias: aliasMatch?.[1] }); + } + return declared; + } + + it("registers every directly-declared value-consuming flag name in VALUE_CONSUMING_LONG_FLAGS", () => { + const missing: Array = []; + + for (const filePath of walk(commandsDir)) { + if (INDIRECT_NAME_FILES.has(path.basename(filePath))) continue; + + for (const flag of extractDeclaredFlags(filePath)) { + if (!VALUE_CONSUMING_LONG_FLAGS.has(flag.name)) { + missing.push(`${flag.name} (${path.relative(commandsDir, flag.file)})`); + } + } + } + + expect(missing).toEqual([]); + }); + + it("registers every directly-declared value-consuming flag's shorthand in VALUE_CONSUMING_SHORT_FLAGS", () => { + const missing: Array = []; + + for (const filePath of walk(commandsDir)) { + if (INDIRECT_NAME_FILES.has(path.basename(filePath))) continue; + + for (const flag of extractDeclaredFlags(filePath)) { + if (flag.alias !== undefined && !VALUE_CONSUMING_SHORT_FLAGS.has(flag.alias)) { + missing.push(`-${flag.alias} (--${flag.name}, ${path.relative(commandsDir, flag.file)})`); + } + } + } + + expect(missing).toEqual([]); + }); +}); diff --git a/apps/cli/src/legacy/telemetry/legacy-command-instrumentation.ts b/apps/cli/src/legacy/telemetry/legacy-command-instrumentation.ts index 206a0e5c6f..8b38786463 100644 --- a/apps/cli/src/legacy/telemetry/legacy-command-instrumentation.ts +++ b/apps/cli/src/legacy/telemetry/legacy-command-instrumentation.ts @@ -6,7 +6,11 @@ import { getCommandRuntimeSpanName, } from "../../shared/runtime/command-runtime.service.ts"; import { Output } from "../../shared/output/output.service.ts"; -import { LegacyOutputFlag } from "../../shared/legacy/global-flags.ts"; +import { + LEGACY_GLOBAL_FLAGS, + LegacyOutputFlag, + legacyGlobalFlagValues, +} from "../../shared/legacy/global-flags.ts"; import { ProcessControl } from "../../shared/runtime/process-control.service.ts"; import { withAnalyticsContext } from "../../shared/telemetry/analytics-context.ts"; import { Analytics } from "../../shared/telemetry/analytics.service.ts"; @@ -50,11 +54,15 @@ interface LegacyCommandInstrumentationOptions; - // Short-flag → canonical-flag-name map (e.g. `{ s: "schema" }`). Go's - // `changedFlags()` uses pflag's `Visit`, which reports the CANONICAL flag name - // whether the user typed the long form (`--schema`) or the registered shorthand - // (`-s`). Pass a command's shorthands here so a `-s public` invocation records - // the `schema` flag in telemetry, matching Go (cmd/root_analytics.go:53-76). + // Short-flag → canonical-flag-name map (e.g. `{ s: "schema" }`) for this + // command's OWN flags. Go's `changedFlags()` uses pflag's `Visit`, which + // reports the CANONICAL flag name whether the user typed the long form + // (`--schema`) or the registered shorthand (`-s`). Pass a command's + // shorthands here so a `-s public` invocation records the `schema` flag in + // telemetry, matching Go (cmd/root_analytics.go:53-76). Global shorthands + // (currently just `-o` for `--output`, cmd/root.go:330) are merged in + // automatically via `GLOBAL_SHORT_ALIASES` below — no per-command wiring + // needed for those (CLI-1896 review follow-up). readonly aliases?: Readonly>; } @@ -226,41 +234,79 @@ function isWrappedParam(param: Param.Any): param is Param.Any & WrappedParam { return "param" in param; } +// Unwraps down to the underlying `Single` param the same way `--help` +// rendering does. Shared by `getChoiceFlagNames` and `GLOBAL_SHORT_ALIASES` +// below — both need the leaf `Single` to read its type-visible `name`/ +// `aliases`/`primitiveType` fields. Returns `undefined` only if the variant +// union gains an unrecognized future case (fails closed, see the +// `isWrappedParam` doc above for why this hand-rolled unwrap exists instead of +// the `@internal` `Param.extractSingleParams`). +function unwrapToSingleParam(param: Param.Any): Param.Single | undefined { + if (Param.isSingle(param)) return param; + if (isWrappedParam(param)) return unwrapToSingleParam(param.param); + return undefined; +} + // Mirrors Go's `isEnumFlag` (`cmd/root_analytics.go:110-116`), which checks // `flag.Value.(*utils.EnumFlag)` unconditionally — every enum flag is -// telemetry-safe, no per-flag annotation needed. Unwraps down to the -// underlying `Single` param the same way `--help` rendering does, then checks -// its primitive's `_tag` for `Flag.choice`/`Flag.choiceWithValue`. Restricted to -// `kind === Param.flagKind` so a same-named `Argument.choice` positional (none -// exist today) can never be mistaken for a `--flag`. +// telemetry-safe, no per-flag annotation needed. Checks the unwrapped +// `Single`'s primitive `_tag` for `Flag.choice`/`Flag.choiceWithValue`. +// Restricted to `kind === Param.flagKind` so a same-named `Argument.choice` +// positional (none exist today) can never be mistaken for a `--flag`. function getChoiceFlagNames(config: Record | undefined): ReadonlySet { const names = new Set(); if (config === undefined) return names; - const visit = (param: Param.Any): void => { - if (Param.isSingle(param)) { - if (param.kind === Param.flagKind && param.primitiveType._tag === "Choice") { - names.add(param.name); - } - return; - } - if (isWrappedParam(param)) { - visit(param.param); - } - }; - for (const param of Object.values(config)) { - visit(param); + const single = unwrapToSingleParam(param); + if ( + single !== undefined && + single.kind === Param.flagKind && + single.primitiveType._tag === "Choice" + ) { + names.add(single.name); + } } return names; } -function buildFlagsMap>( - flags: Flags | undefined, - safeFlagSet: ReadonlySet, - changedFlagNames: ReadonlyArray, - choiceFlagNames: ReadonlySet, -): Record | undefined { +// Short-flag → canonical-name entries for every global/persistent flag that +// declares a shorthand alias, derived from `LEGACY_GLOBAL_FLAGS` itself so +// this never drifts from the single source of truth — today that resolves to +// just `{ o: "output" }`, from `LegacyOutputFlag`'s own `Flag.withAlias("o")`. +// Mirrors Go's persistent-flag shorthand registration: `-o` is the only +// global with a real shorthand (`cmd/root.go:330` registers it on +// `--output`; every other persistent flag has none). `pflag.Visit` reports +// the canonical `flag.Name` for either form (`cmd/root_analytics.go:53-76`), +// so `-o json` must resolve to `output` here the same way `--output json` +// already does. Merged ahead of each command's own `aliases` in +// `extractChangedFlagNames` so a command's own alias still wins on conflict, +// consistent with `buildFlagsMap`'s local-flag-shadows-global rule +// (CLI-1896 review follow-up). +const GLOBAL_SHORT_ALIASES: Readonly> = (() => { + const aliases: Record = {}; + for (const globalFlag of LEGACY_GLOBAL_FLAGS) { + const single = unwrapToSingleParam(globalFlag.flag); + if (single === undefined) continue; + for (const alias of single.aliases) { + aliases[alias] = single.name; + } + } + return aliases; +})(); + +function buildFlagsMap>(options: { + readonly flags: Flags | undefined; + // Live global/persistent flag values (`legacyGlobalFlagValues`), keyed by + // CLI flag name — the fallback source for a changed flag the handler never + // declared locally (e.g. `debug`), matching Go's `changedFlags()` walking + // `cmd.Parent()`'s `PersistentFlags()` (`cmd/root_analytics.go:53-76`). + readonly globalFlagValues: Record; + readonly safeFlagSet: ReadonlySet; + readonly changedFlagNames: ReadonlyArray; + readonly choiceFlagNames: ReadonlySet; +}): Record | undefined { + const { flags, globalFlagValues, safeFlagSet, changedFlagNames, choiceFlagNames } = options; if (changedFlagNames.length === 0) return undefined; const result: Record = {}; @@ -272,15 +318,27 @@ function buildFlagsMap>( } for (const cliName of changedFlagNames) { - const rawValue = handlerFlagsByCliName.get(cliName); + // A command's own flag always wins over a global/persistent flag sharing + // the same CLI name — mirrored from Go's cobra flag-shadowing, e.g. `db + // diff`'s local `--output` file-path flag (`cmd/db.go:622`) shadows the + // root's global `--output` enum (`cmd/root.go:330`). Only fall back to + // the live global-flag value when the handler never declared this name. + const isFromHandler = handlerFlagsByCliName.has(cliName); + const rawValue = isFromHandler ? handlerFlagsByCliName.get(cliName) : globalFlagValues[cliName]; const value = normalizeFlagValue(rawValue); - if (safeFlagSet.has(cliName) || choiceFlagNames.has(cliName) || typeof value === "boolean") { - result[cliName] = value ?? REDACTED_VALUE; - continue; - } - - result[cliName] = REDACTED_VALUE; + // `safeFlagSet`/`choiceFlagNames` classify a flag as safe by CLI NAME, + // sourced from this command's own `safeFlags`/`config` options — they may + // only vouch for a value that actually came from this command's own + // `flags` record. A value resolved from the global-flag fallback is safe + // solely because it's boolean (Go's `isBooleanFlag` branch applies + // unconditionally); it must never inherit a *different* command's + // per-flag safe/choice annotation just because the CLI name matches. + const isSafe = + typeof value === "boolean" || + (isFromHandler && (safeFlagSet.has(cliName) || choiceFlagNames.has(cliName))); + + result[cliName] = isSafe ? (value ?? REDACTED_VALUE) : REDACTED_VALUE; } return result; @@ -324,8 +382,18 @@ function withLegacyCommandAnalyticsImplementation { ); }); + // Global/persistent flag parity (CLI-1896): Go's changedFlags() walks + // cmd.Parent()'s PersistentFlags() in addition to the leaf's own flags + // (cmd/root_analytics.go:53-76), so a global flag like --debug resolves to + // its real value even though no command declares it locally. The wrapper + // reads shared/legacy/global-flags.ts itself rather than relying on the + // per-command `flags` option to carry global flag values. + + it.live("records a changed global boolean flag's real value (e.g. --debug)", () => { + // Go reports `flags: {debug: true}` for `supabase --debug telemetry disable` + // (isBooleanFlag is always safe, regardless of markFlagTelemetrySafe) — the + // TS port previously had no way to resolve `debug` at all and fell back to + // "" for every global flag, even booleans. + const analytics = mockContextualAnalytics(); + + return Effect.void.pipe( + withLegacyCommandInstrumentation({ flags: {} }), + Effect.provide(analytics.layer), + Effect.provide(mockProcessControl().layer), + Effect.provide(mockOutput({ format: "text" }).layer), + Effect.provide(Stdio.layerTest({ args: Effect.succeed(["backups", "list", "--debug"]) })), + Effect.provide(commandRuntimeLayer(["backups", "list"])), + Effect.provide(Layer.succeed(LegacyDebugFlag, true)), + Effect.tap(() => + Effect.sync(() => { + const event = analytics.captured[0]; + expect(event?.properties.flags).toEqual({ debug: true }); + }), + ), + ); + }); + + it.live("merges a changed global flag alongside the command's own local flags", () => { + const analytics = mockContextualAnalytics(); + + return Effect.void.pipe( + withLegacyCommandInstrumentation({ + flags: { projectRef: Option.some("abcdefghijklmnopqrst") }, + }), + Effect.provide(analytics.layer), + Effect.provide(mockProcessControl().layer), + Effect.provide(mockOutput({ format: "text" }).layer), + Effect.provide( + Stdio.layerTest({ + args: Effect.succeed([ + "secrets", + "list", + "--project-ref", + "abcdefghijklmnopqrst", + "--debug", + ]), + }), + ), + Effect.provide(commandRuntimeLayer(["secrets", "list"])), + Effect.provide(Layer.succeed(LegacyDebugFlag, true)), + Effect.tap(() => + Effect.sync(() => { + const event = analytics.captured[0]; + expect(event?.properties.flags).toEqual({ + "project-ref": "", + debug: true, + }); + }), + ), + ); + }); + + it.live( + "still redacts a changed global string flag like --workdir (Go never marks it telemetry-safe)", + () => { + const analytics = mockContextualAnalytics(); + + return Effect.void.pipe( + withLegacyCommandInstrumentation({ flags: {} }), + Effect.provide(analytics.layer), + Effect.provide(mockProcessControl().layer), + Effect.provide(mockOutput({ format: "text" }).layer), + Effect.provide( + Stdio.layerTest({ + args: Effect.succeed(["backups", "list", "--workdir", "/tmp/project"]), + }), + ), + Effect.provide(commandRuntimeLayer(["backups", "list"])), + Effect.provide(Layer.succeed(LegacyWorkdirFlag, Option.some("/tmp/project"))), + Effect.tap(() => + Effect.sync(() => { + const event = analytics.captured[0]; + expect(event?.properties.flags).toEqual({ workdir: "" }); + }), + ), + ); + }, + ); + + it.live( + "still redacts a changed global choice flag like --dns-resolver (global EnumFlag safety is CLI-1904's scope, not this fix's)", + () => { + const analytics = mockContextualAnalytics(); + + return Effect.void.pipe( + withLegacyCommandInstrumentation({ flags: {} }), + Effect.provide(analytics.layer), + Effect.provide(mockProcessControl().layer), + Effect.provide(mockOutput({ format: "text" }).layer), + Effect.provide( + Stdio.layerTest({ + args: Effect.succeed(["backups", "list", "--dns-resolver", "https"]), + }), + ), + Effect.provide(commandRuntimeLayer(["backups", "list"])), + Effect.provide(Layer.succeed(LegacyDnsResolverFlag, "https" as const)), + Effect.tap(() => + Effect.sync(() => { + const event = analytics.captured[0]; + expect(event?.properties.flags).toEqual({ "dns-resolver": "" }); + }), + ), + ); + }, + ); + + it.live("falls back to redacted when a changed global flag's service isn't wired", () => { + // Defensive case: `Effect.serviceOption` must never throw/defect when a + // narrow harness (or, hypothetically, an incompletely-wired real command) + // doesn't provide a global flag's context — it degrades to the prior + // REDACTED_VALUE behavior instead of crashing. + const analytics = mockContextualAnalytics(); + + return Effect.void.pipe( + withLegacyCommandInstrumentation({ flags: {} }), + Effect.provide(analytics.layer), + Effect.provide(mockProcessControl().layer), + Effect.provide(mockOutput({ format: "text" }).layer), + Effect.provide(Stdio.layerTest({ args: Effect.succeed(["backups", "list", "--debug"]) })), + Effect.provide(commandRuntimeLayer(["backups", "list"])), + // Note: no LegacyDebugFlag layer provided. + Effect.tap(() => + Effect.sync(() => { + const event = analytics.captured[0]; + expect(event?.properties.flags).toEqual({ debug: "" }); + }), + ), + ); + }); + it.live("stops recording flags at the -- end-of-options sentinel", () => { // `test db -- --linked`: pflag stops parsing flags at `--`, so `--linked` // is a positional arg, not a changed flag. changedFlags() never sees it. @@ -999,4 +1148,70 @@ describe("withLegacyCommandInstrumentation", () => { ), ); }); + + // CLI-1896 review follow-up (Codex): a global flag's SHORTHAND must resolve + // through the same fallback its long form already does. + + it.live("resolves a global flag's shorthand (-o) through the global fallback", () => { + // `-o json` must resolve to the canonical `output` flag the same way + // `--output json` already does: Go's `pflag.Visit` reports the canonical + // `flag.Name` for either form (`cmd/root_analytics.go:53-76`), and `-o` is + // `--output`'s only registered persistent shorthand (`cmd/root.go:330`). + const analytics = mockContextualAnalytics(); + + return Effect.void.pipe( + withLegacyCommandInstrumentation({ flags: {} }), + Effect.provide(analytics.layer), + Effect.provide(mockProcessControl().layer), + Effect.provide(mockOutput({ format: "text" }).layer), + Effect.provide(Stdio.layerTest({ args: Effect.succeed(["backups", "list", "-o", "json"]) })), + Effect.provide(commandRuntimeLayer(["backups", "list"])), + Effect.provide(Layer.succeed(LegacyOutputFlag, Option.some("json" as const))), + Effect.tap(() => + Effect.sync(() => { + const event = analytics.captured[0]; + // `output` is a global choice flag — still redacted (CLI-1904's + // scope), but it must be PRESENT, not silently dropped. + expect(event?.properties.flags).toEqual({ output: "" }); + }), + ), + ); + }); + + it.live( + "does not fabricate a global flag from a local flag's value token (secrets set --env-file --debug)", + () => { + // `--env-file` is a value-consuming local string flag. In bare + // space-separated form, pflag consumes the very next token as its + // VALUE regardless of its shape, so Go's changedFlags() never marks + // `debug` as changed for this invocation — the whole token is + // `env-file`'s value. Without `env-file` registered in + // VALUE_CONSUMING_LONG_FLAGS, extractChangedFlagNames would wrongly + // treat the trailing `--debug` as a separate flag, and CLI-1896's + // global-flag fallback would then fabricate a `flags.debug` value Go + // never records. + const analytics = mockContextualAnalytics(); + + return Effect.void.pipe( + withLegacyCommandInstrumentation({ + flags: { envFile: Option.some("--debug") }, + }), + Effect.provide(analytics.layer), + Effect.provide(mockProcessControl().layer), + Effect.provide(mockOutput({ format: "text" }).layer), + Effect.provide( + Stdio.layerTest({ + args: Effect.succeed(["secrets", "set", "--env-file", "--debug"]), + }), + ), + Effect.provide(commandRuntimeLayer(["secrets", "set"])), + Effect.tap(() => + Effect.sync(() => { + const event = analytics.captured[0]; + expect(event?.properties.flags).toEqual({ "env-file": "" }); + }), + ), + ); + }, + ); }); diff --git a/apps/cli/src/shared/legacy/global-flags.ts b/apps/cli/src/shared/legacy/global-flags.ts index c53c3ea999..59796bb6aa 100644 --- a/apps/cli/src/shared/legacy/global-flags.ts +++ b/apps/cli/src/shared/legacy/global-flags.ts @@ -1,4 +1,4 @@ -import { Effect } from "effect"; +import { Effect, Option } from "effect"; import { Flag, GlobalFlag } from "effect/unstable/cli"; import { CliArgs } from "../cli/cli-args.service.ts"; @@ -95,6 +95,48 @@ export const LEGACY_GLOBAL_FLAGS = [ LegacyAgentFlag, ] as const; +/** + * Resolves the current value of every global/persistent flag above, keyed by + * its own CLI flag name (each flag's `.id`, e.g. `debug`, `workdir`). Used by + * `legacy/telemetry/legacy-command-instrumentation.ts` to mirror Go's + * `changedFlags()` walking `cmd.Parent()`'s `PersistentFlags()` in addition to + * a command's own flags (`cmd/root_analytics.go:53-76`) — global flags here + * live in a single Effect-context-wide registry rather than per-ancestor + * `pflag.FlagSet`s, so this reads all of them unconditionally instead of + * walking a parent chain (CLI-1896). + * + * Read via `Effect.serviceOption` (adds no `R` requirement) so a caller that + * hasn't wired the global-flag context — e.g. a focused unit test — simply + * gets an empty record instead of a missing-service defect; production always + * provides every global flag through `Command.withGlobalFlags` at the CLI + * root (`legacy/cli/root.ts`). + * + * Reads each flag individually (rather than looping `LEGACY_GLOBAL_FLAGS`) + * because each `Setting` has a distinct value type `A` — a homogeneous + * loop widens the union in a way `Effect.serviceOption` can't resolve back to + * a single service lookup without an `as` cast, which this codebase forbids. + * `global-flags.unit.test.ts` asserts the resolved id set stays exactly in + * sync with `LEGACY_GLOBAL_FLAGS` — extend both together when adding a new + * global flag. + */ +export const legacyGlobalFlagValues = Effect.gen(function* () { + const values: Record = {}; + const setIfPresent = (id: string, option: Option.Option) => { + if (Option.isSome(option)) values[id] = option.value; + }; + setIfPresent(LegacyAgentFlag.id, yield* Effect.serviceOption(LegacyAgentFlag)); + setIfPresent(LegacyCreateTicketFlag.id, yield* Effect.serviceOption(LegacyCreateTicketFlag)); + setIfPresent(LegacyDebugFlag.id, yield* Effect.serviceOption(LegacyDebugFlag)); + setIfPresent(LegacyDnsResolverFlag.id, yield* Effect.serviceOption(LegacyDnsResolverFlag)); + setIfPresent(LegacyExperimentalFlag.id, yield* Effect.serviceOption(LegacyExperimentalFlag)); + setIfPresent(LegacyNetworkIdFlag.id, yield* Effect.serviceOption(LegacyNetworkIdFlag)); + setIfPresent(LegacyOutputFlag.id, yield* Effect.serviceOption(LegacyOutputFlag)); + setIfPresent(LegacyProfileFlag.id, yield* Effect.serviceOption(LegacyProfileFlag)); + setIfPresent(LegacyWorkdirFlag.id, yield* Effect.serviceOption(LegacyWorkdirFlag)); + setIfPresent(LegacyYesFlag.id, yield* Effect.serviceOption(LegacyYesFlag)); + return values; +}); + const PFLAG_FALSE_VALUES = new Set(["0", "f", "F", "false", "FALSE", "False"]); /** diff --git a/apps/cli/src/shared/legacy/global-flags.unit.test.ts b/apps/cli/src/shared/legacy/global-flags.unit.test.ts new file mode 100644 index 0000000000..ec54f34982 --- /dev/null +++ b/apps/cli/src/shared/legacy/global-flags.unit.test.ts @@ -0,0 +1,83 @@ +import { describe, expect, it } from "@effect/vitest"; +import { Effect, Layer, Option } from "effect"; +import { + LEGACY_GLOBAL_FLAGS, + LegacyAgentFlag, + LegacyCreateTicketFlag, + LegacyDebugFlag, + LegacyDnsResolverFlag, + LegacyExperimentalFlag, + LegacyNetworkIdFlag, + LegacyOutputFlag, + LegacyProfileFlag, + LegacyWorkdirFlag, + LegacyYesFlag, + legacyGlobalFlagValues, +} from "./global-flags.ts"; + +describe("legacyGlobalFlagValues", () => { + it.live( + "resolves every flag declared in LEGACY_GLOBAL_FLAGS by id (CLI-1896 drift guard: an 11th global flag added here without a matching read in legacyGlobalFlagValues fails this test instead of silently redacting forever)", + () => { + const layer = Layer.mergeAll( + Layer.succeed(LegacyAgentFlag, "yes" as const), + Layer.succeed(LegacyCreateTicketFlag, true), + Layer.succeed(LegacyDebugFlag, true), + Layer.succeed(LegacyDnsResolverFlag, "https" as const), + Layer.succeed(LegacyExperimentalFlag, true), + Layer.succeed(LegacyNetworkIdFlag, Option.some("my-network")), + Layer.succeed(LegacyOutputFlag, Option.some("json" as const)), + Layer.succeed(LegacyProfileFlag, "custom-profile"), + Layer.succeed(LegacyWorkdirFlag, Option.some("/tmp/project")), + Layer.succeed(LegacyYesFlag, true), + ); + + return legacyGlobalFlagValues.pipe( + Effect.provide(layer), + Effect.tap((values) => + Effect.sync(() => { + // The key set must exactly match LEGACY_GLOBAL_FLAGS's own ids — + // this is what fails loudly if the array grows without a matching + // read here. + expect(Object.keys(values).sort()).toEqual( + LEGACY_GLOBAL_FLAGS.map((flag) => flag.id).sort(), + ); + expect(values).toEqual({ + agent: "yes", + "create-ticket": true, + debug: true, + "dns-resolver": "https", + experimental: true, + "network-id": Option.some("my-network"), + output: Option.some("json"), + profile: "custom-profile", + workdir: Option.some("/tmp/project"), + yes: true, + }); + }), + ), + ); + }, + ); + + it.live("omits every flag when no global-flag context is provided", () => { + return legacyGlobalFlagValues.pipe( + Effect.tap((values) => + Effect.sync(() => { + expect(values).toEqual({}); + }), + ), + ); + }); + + it.live("only includes flags whose service was actually provided", () => { + return legacyGlobalFlagValues.pipe( + Effect.provide(Layer.succeed(LegacyDebugFlag, true)), + Effect.tap((values) => + Effect.sync(() => { + expect(values).toEqual({ debug: true }); + }), + ), + ); + }); +}); From 98a3d87d08d95d38f745a691d4dae0af947c2e97 Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Thu, 9 Jul 2026 10:47:48 +0100 Subject: [PATCH 30/79] fix(cli): use cobra's mutual-exclusivity error template for sso update (#5832) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What kind of change does this PR introduce? Bug fix (Go-parity divergence). ## What is the current behavior? `sso update`'s `--domains`/`--add-domains`/`--remove-domains` mutex checks emitted custom, hand-written messages (`"only one of --domains or --add-domains may be set"`) instead of cobra's real `validateExclusiveFlagGroups` template (`flag_groups.go:204`), and gated "is this flag set" on the *parsed value's* `.length > 0` rather than cobra's actual `pflag.Changed` semantics — so `--domains=` (explicit but empty) was silently treated as unset, the same "changed vs truthy" class of bug CLI-1860 fixed for `functions download`'s `--use-docker`. Fixes [CLI-1902](https://linear.app/supabase/issue/CLI-1902/sso-update-use-cobras-mutual-exclusivity-error-template-for-domains). ## What is the new behavior? Reuses the `hasExplicitLongFlag`/`cobraMutuallyExclusiveErrorMessage` helpers already hoisted to `shared/cli/cobra-flag-groups.ts` (#5804) so `sso update` emits cobra's byte-exact error text for `--domains`/`--add-domains`/`--remove-domains`, keyed off raw argv rather than the parsed flag value. While reviewing, three independent reviewer passes (architect/engineer/DX) converged on two more in-scope gaps in the same handler that were cheap to close alongside the ticket's stated fix: - `--metadata-file`/`--metadata-url` had the identical divergence (Go also registers this pair with `MarkFlagsMutuallyExclusive`, `cmd/sso.go:178`) — folded into the same cobra-parity check rather than leaving one mutex group byte-exact and the other hand-written in the same command. - The mutex checks ran *after* the provider-ID UUID validation, but cobra runs `ValidateFlagGroups` before `RunE` (`command.go:1010,1014`) while Go's UUID check lives inside `RunE` (`cmd/sso.go:90-91`) — so a flag-group violation must win over an invalid provider ID when both apply. Reordered to match. Added regression tests for the exact cobra error text (both `--domains` groups and the metadata group), the changed-vs-truthy gap, the two-groups-not-a-3-way-group behavior, the group-check ordering when all three domain flags collide, and the mutex-before-UUID precedence — plus a flag-parser unit test proving `--domains=` really resolves to `[]`. `update.handler.ts`'s pre-existing branch-coverage gaps (GET/PUT network-failure paths, `--skip-url-validation`, no-access-token, malformed GET body) are unrelated to this change and were left alone; independently verified via `git stash` that none of them were introduced by this diff. --- .../sso/update/update.command.unit.test.ts | 17 ++ .../commands/sso/update/update.handler.ts | 100 +++++-- .../sso/update/update.integration.test.ts | 247 ++++++++++++++++-- apps/cli/src/shared/cli/cobra-flag-groups.ts | 53 ++++ .../shared/cli/cobra-flag-groups.unit.test.ts | 88 ++++++- 5 files changed, 462 insertions(+), 43 deletions(-) diff --git a/apps/cli/src/legacy/commands/sso/update/update.command.unit.test.ts b/apps/cli/src/legacy/commands/sso/update/update.command.unit.test.ts index 0afeb0e4d2..6f88558830 100644 --- a/apps/cli/src/legacy/commands/sso/update/update.command.unit.test.ts +++ b/apps/cli/src/legacy/commands/sso/update/update.command.unit.test.ts @@ -47,6 +47,23 @@ describe("legacy sso update domain flags (pflag StringSlice parity)", () => { expect(removeDomains).toEqual(["example.com", "example.org"]); }); + test("--domains= (explicit empty value) parses to an empty array, not a missing flag", async () => { + // Backs the "changed vs truthy" mutex-check fix (CLI-1902): the handler's + // `hasExplicitLongFlag` reads raw argv rather than this parsed value + // precisely because `--domains=` collapses to `[]` here, indistinguishable + // from the flag never being passed at all if you only looked at `.length`. + const [, domains] = await Effect.runPromise( + legacySsoUpdateDomainsFlag + .parse({ + flags: { domains: [""] }, + arguments: [], + }) + .pipe(Effect.provide(BunServices.layer)), + ); + + expect(domains).toEqual([]); + }); + test("rejects malformed CSV (bare quote)", async () => { const exit = await Effect.runPromise( legacySsoUpdateDomainsFlag diff --git a/apps/cli/src/legacy/commands/sso/update/update.handler.ts b/apps/cli/src/legacy/commands/sso/update/update.handler.ts index ca86765455..aba1d6833f 100644 --- a/apps/cli/src/legacy/commands/sso/update/update.handler.ts +++ b/apps/cli/src/legacy/commands/sso/update/update.handler.ts @@ -1,5 +1,5 @@ import type { SupabaseApiError } from "@supabase/api/effect"; -import { Effect, Option, Result } from "effect"; +import { Effect, Option, Result, Stdio } from "effect"; import * as HttpClient from "effect/unstable/http/HttpClient"; import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest"; @@ -7,6 +7,10 @@ import { LegacyPlatformApi } from "../../../auth/legacy-platform-api.service.ts" import { LegacyCliConfig } from "../../../config/legacy-cli-config.service.ts"; import { LegacyProjectRefResolver } from "../../../config/legacy-project-ref.service.ts"; import { LegacyOutputFlag } from "../../../../shared/legacy/global-flags.ts"; +import { + cobraMutuallyExclusiveErrorMessage, + hasExplicitValueFlag, +} from "../../../../shared/cli/cobra-flag-groups.ts"; import { Output } from "../../../../shared/output/output.service.ts"; import { encodeGoJson, @@ -48,6 +52,40 @@ const mapGetStatusOrNetwork = mapLegacyHttpError({ statusMessage: (_status, body) => `unexpected error fetching identity provider: ${body}`, }); +const SSO_UPDATE_COMMAND_PATH = ["sso", "update"] as const; + +/** + * Registration order mirrors Go's `cmd/sso.go:178-180` — three independent + * `MarkFlagsMutuallyExclusive` groups (`metadata-file`/`metadata-url` plus two + * 2-element groups sharing `--domains`, not one 3-way group). Cobra checks + * groups in `sort.Strings`-order of the joined group key (`flag_groups.go:189`), + * which happens to match registration order here: "domains add-domains" < + * "domains remove-domains" < "metadata-file metadata-url" alphabetically. + */ +const SSO_UPDATE_MUTEX_GROUPS = [ + ["domains", "add-domains"], + ["domains", "remove-domains"], + ["metadata-file", "metadata-url"], +] as const; + +/** + * Every value-taking (non-boolean) flag `sso update` declares + * (`update.command.ts`) — tells `hasExplicitValueFlag` which bare tokens + * consume the next argv token as their value. `--skip-url-validation` is this + * command's only boolean flag and is deliberately excluded; booleans never + * consume a following token. + */ +const SSO_UPDATE_VALUE_FLAG_NAMES = new Set([ + "project-ref", + "domains", + "add-domains", + "remove-domains", + "metadata-file", + "metadata-url", + "attribute-mapping-file", + "name-id-format", +]); + const handleGetError = (ref: string, providerId: string, cause: SupabaseApiError) => Effect.gen(function* () { const mapped = yield* Effect.flip(mapGetStatusOrNetwork(cause)); @@ -104,34 +142,50 @@ export const legacySsoUpdate = Effect.fn("legacy.sso.update")(function* ( const resolver = yield* LegacyProjectRefResolver; const linkedProjectCache = yield* LegacyLinkedProjectCache; const telemetryState = yield* LegacyTelemetryState; + const stdio = yield* Stdio.Stdio; + const rawArgs = yield* stdio.args; yield* Effect.gen(function* () { + // cobra runs `ValidateFlagGroups` (`command.go:1010`) before `RunE` + // (`command.go:1014`), and Go's provider-ID format check lives inside + // `RunE` (`cmd/sso.go:90-91`) — so a mutex violation must win over an + // invalid provider ID when both apply. Keep this block ahead of + // `validateUuid` below to match that precedence. + // + // "Set" follows cobra's `pflag.Changed` — whether the flag was passed at + // all — not the resulting value. `--domains`/`--add-domains`/ + // `--remove-domains` all default to `[]`, so `--domains=` (parses to an + // empty array) must still count as "set"; gating on `.length > 0` would + // miss it, the same "changed vs truthy" gap CLI-1860 fixed for + // `functions download`'s `--use-docker`. + // + // `hasExplicitValueFlag` (not the simpler `hasExplicitLongFlag`) is + // required here because every flag in these groups takes a value: a bare + // `--metadata-file --metadata-url` is pflag consuming `--metadata-url` as + // `metadata-file`'s (oddly named) value, not two flags being set — see + // that function's doc comment. + for (const group of SSO_UPDATE_MUTEX_GROUPS) { + const changed = group.filter((flagName) => + hasExplicitValueFlag( + rawArgs, + SSO_UPDATE_COMMAND_PATH, + SSO_UPDATE_VALUE_FLAG_NAMES, + flagName, + ), + ); + if (changed.length > 1) { + return yield* Effect.fail( + new LegacySsoMutexFlagError({ + message: cobraMutuallyExclusiveErrorMessage(group, changed), + }), + ); + } + } + const providerId = yield* validateUuid(flags.providerId).pipe( Result.match({ onFailure: Effect.fail, onSuccess: Effect.succeed }), ); - if (flags.domains.length > 0 && flags.addDomains.length > 0) { - return yield* Effect.fail( - new LegacySsoMutexFlagError({ - message: "only one of --domains or --add-domains may be set", - }), - ); - } - if (flags.domains.length > 0 && flags.removeDomains.length > 0) { - return yield* Effect.fail( - new LegacySsoMutexFlagError({ - message: "only one of --domains or --remove-domains may be set", - }), - ); - } - if (Option.isSome(flags.metadataFile) && Option.isSome(flags.metadataUrl)) { - return yield* Effect.fail( - new LegacySsoMutexFlagError({ - message: "only one of --metadata-file or --metadata-url may be set", - }), - ); - } - const ref = yield* resolver.resolve(flags.projectRef); yield* Effect.gen(function* () { diff --git a/apps/cli/src/legacy/commands/sso/update/update.integration.test.ts b/apps/cli/src/legacy/commands/sso/update/update.integration.test.ts index c1060092cb..c76206a29b 100644 --- a/apps/cli/src/legacy/commands/sso/update/update.integration.test.ts +++ b/apps/cli/src/legacy/commands/sso/update/update.integration.test.ts @@ -2,7 +2,7 @@ import { writeFileSync } from "node:fs"; import { join } from "node:path"; import { describe, expect, it } from "@effect/vitest"; -import { Effect, Exit, Option } from "effect"; +import { Effect, Exit, Layer, Option, Stdio } from "effect"; import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse"; import { mockAnalytics, mockOutput } from "../../../../../tests/helpers/mocks.ts"; @@ -45,6 +45,13 @@ interface SetupOpts { putStatus?: number; putBody?: unknown; upgradeGate?: "gated" | "notGated"; + /** + * Raw argv the handler sees via `Stdio.Stdio` — drives the + * `hasExplicitLongFlag`-based mutex checks. Defaults to a bare invocation + * with none of the mutually-exclusive domain flags present; tests that + * exercise those checks must pass the matching flags explicitly here. + */ + cliArgs?: ReadonlyArray; } function jsonResponse( @@ -122,15 +129,20 @@ function setup(opts: SetupOpts = {}) { }); const cliConfig = mockLegacyCliConfig({ workdir: tempRoot.current }); - const layer = buildLegacyTestRuntime({ - out, - api: { layer: api.layer, httpClientLayer: api.httpClientLayer }, - cliConfig, - telemetry: telemetry.layer, - linkedProjectCache: cache.layer, - analytics, - goOutput: opts.goOutput === undefined ? Option.none() : Option.some(opts.goOutput), - }); + const layer = Layer.mergeAll( + buildLegacyTestRuntime({ + out, + api: { layer: api.layer, httpClientLayer: api.httpClientLayer }, + cliConfig, + telemetry: telemetry.layer, + linkedProjectCache: cache.layer, + analytics, + goOutput: opts.goOutput === undefined ? Option.none() : Option.some(opts.goOutput), + }), + Stdio.layerTest({ + args: Effect.succeed(opts.cliArgs ?? ["sso", "update", VALID_PROVIDER_ID]), + }), + ); return { layer, out, api, analytics, telemetry, cache }; } @@ -200,40 +212,237 @@ describe("legacy sso update integration", () => { }).pipe(Effect.provide(layer)); }); - it.live("mutex check: --domains + --add-domains fails", () => { - const { layer } = setup(); + it.live("mutex check: --domains + --add-domains fails with cobra's exact error text", () => { + const { layer } = setup({ + cliArgs: ["sso", "update", VALID_PROVIDER_ID, "--domains", "a.com", "--add-domains", "b.com"], + }); return Effect.gen(function* () { const exit = yield* Effect.exit( legacySsoUpdate({ ...defaultFlags, domains: ["a.com"], addDomains: ["b.com"] }), ); expect(Exit.isFailure(exit)).toBe(true); if (Exit.isFailure(exit)) { - expect(JSON.stringify(exit.cause)).toContain("LegacySsoMutexFlagError"); + const dump = JSON.stringify(exit.cause); + expect(dump).toContain("LegacySsoMutexFlagError"); + // Byte-matches cobra's `validateExclusiveFlagGroups` template + // (`flag_groups.go:204`): group in registration order, changed flags + // sorted alphabetically — "add-domains" < "domains". + expect(dump).toContain( + "if any flags in the group [domains add-domains] are set none of the others can be; [add-domains domains] were all set", + ); } }).pipe(Effect.provide(layer)); }); - it.live("mutex check: --domains + --remove-domains fails", () => { - const { layer } = setup(); + it.live("mutex check: --domains + --remove-domains fails with cobra's exact error text", () => { + const { layer } = setup({ + cliArgs: [ + "sso", + "update", + VALID_PROVIDER_ID, + "--domains", + "a.com", + "--remove-domains", + "b.com", + ], + }); return Effect.gen(function* () { const exit = yield* Effect.exit( legacySsoUpdate({ ...defaultFlags, domains: ["a.com"], removeDomains: ["b.com"] }), ); expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + const dump = JSON.stringify(exit.cause); + expect(dump).toContain("LegacySsoMutexFlagError"); + expect(dump).toContain( + "if any flags in the group [domains remove-domains] are set none of the others can be; [domains remove-domains] were all set", + ); + } }).pipe(Effect.provide(layer)); }); - it.live("mutex check: --metadata-file + --metadata-url fails", () => { - const { layer } = setup(); + it.live( + "mutex check: an explicit but empty --domains= still conflicts with --add-domains (changed, not truthy)", + () => { + // `--domains=` parses to an empty array, but cobra's `pflag.Changed` + // tracks that the flag was passed at all, not the resulting value — the + // same "changed vs truthy" gap CLI-1860 fixed for `functions download`'s + // `--use-docker`. Gating on `.length > 0` would miss this combination. + const { layer } = setup({ + cliArgs: ["sso", "update", VALID_PROVIDER_ID, "--domains=", "--add-domains", "b.com"], + }); + return Effect.gen(function* () { + const exit = yield* Effect.exit( + legacySsoUpdate({ ...defaultFlags, domains: [], addDomains: ["b.com"] }), + ); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(JSON.stringify(exit.cause)).toContain("LegacySsoMutexFlagError"); + } + }).pipe(Effect.provide(layer)); + }, + ); + + it.live( + "mutex check: --add-domains and --remove-domains together are not mutually exclusive", + () => { + // Go only registers ("domains","add-domains") and ("domains","remove-domains") + // as separate 2-element groups (`cmd/sso.go:179-180`) — add-domains and + // remove-domains together, without --domains, is not a violation. + const { layer } = setup({ + cliArgs: [ + "sso", + "update", + VALID_PROVIDER_ID, + "--add-domains", + "b.com", + "--remove-domains", + "c.com", + ], + }); + return Effect.gen(function* () { + const exit = yield* Effect.exit( + legacySsoUpdate({ ...defaultFlags, addDomains: ["b.com"], removeDomains: ["c.com"] }), + ); + expect(Exit.isSuccess(exit)).toBe(true); + }).pipe(Effect.provide(layer)); + }, + ); + + it.live("mutex check: all three domain flags set reports the --add-domains group first", () => { + // Pins the `SSO_UPDATE_MUTEX_GROUPS` array order: cobra's sorted-key + // iteration ("domains add-domains" < "domains remove-domains") means the + // add-domains group is checked — and its error returned — first when all + // three domain flags collide at once. + const { layer } = setup({ + cliArgs: [ + "sso", + "update", + VALID_PROVIDER_ID, + "--domains", + "a.com", + "--add-domains", + "b.com", + "--remove-domains", + "c.com", + ], + }); + return Effect.gen(function* () { + const exit = yield* Effect.exit( + legacySsoUpdate({ + ...defaultFlags, + domains: ["a.com"], + addDomains: ["b.com"], + removeDomains: ["c.com"], + }), + ); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + const dump = JSON.stringify(exit.cause); + expect(dump).toContain( + "if any flags in the group [domains add-domains] are set none of the others can be; [add-domains domains] were all set", + ); + expect(dump).not.toContain("remove-domains"); + } + }).pipe(Effect.provide(layer)); + }); + + it.live("mutex check: a flag-group violation wins over an invalid provider ID", () => { + // Cobra runs `ValidateFlagGroups` before `RunE` (`command.go:1010,1014`); + // Go's provider-ID format check lives inside `RunE` (`cmd/sso.go:90-91`). + // So an invalid UUID combined with a mutex violation must surface the + // mutex error, not `LegacySsoInvalidUuidError`. + const { layer } = setup({ + cliArgs: ["sso", "update", "not-a-uuid", "--domains", "a.com", "--add-domains", "b.com"], + }); return Effect.gen(function* () { const exit = yield* Effect.exit( legacySsoUpdate({ ...defaultFlags, - metadataFile: Option.some("/tmp/x.xml"), - metadataUrl: Option.some("https://idp.example.com/m"), + providerId: "not-a-uuid", + domains: ["a.com"], + addDomains: ["b.com"], }), ); expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + const dump = JSON.stringify(exit.cause); + expect(dump).toContain("LegacySsoMutexFlagError"); + expect(dump).not.toContain("LegacySsoInvalidUuidError"); + } + }).pipe(Effect.provide(layer)); + }); + + it.live( + "mutex check: --metadata-file + --metadata-url fails with cobra's exact error text", + () => { + const { layer } = setup({ + cliArgs: [ + "sso", + "update", + VALID_PROVIDER_ID, + "--metadata-file", + "/tmp/x.xml", + "--metadata-url", + "https://idp.example.com/m", + ], + }); + return Effect.gen(function* () { + const exit = yield* Effect.exit( + legacySsoUpdate({ + ...defaultFlags, + metadataFile: Option.some("/tmp/x.xml"), + metadataUrl: Option.some("https://idp.example.com/m"), + }), + ); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + const dump = JSON.stringify(exit.cause); + expect(dump).toContain("LegacySsoMutexFlagError"); + // Go registers this pair too (`cmd/sso.go:178`) — it was left emitting + // a hand-written message alongside the domains groups' custom text + // before this fix; now all three of `sso update`'s mutex groups on + // this command share the same byte-exact cobra template. + expect(dump).toContain( + "if any flags in the group [metadata-file metadata-url] are set none of the others can be; [metadata-file metadata-url] were all set", + ); + } + }).pipe(Effect.provide(layer)); + }, + ); + + it.live( + "mutex check: a bare --metadata-file followed by --metadata-url is not a violation", + () => { + // pflag's `--flag arg` branch consumes the very next argv token as the + // value unconditionally (`flag.go:1013-1031`), so real cobra parses this + // as `metadata-file` receiving the literal value `"--metadata-url"` — + // `metadata-url` is never parsed as its own flag and stays unset. The + // TS CLI's own parser (unlike pflag) never hands a dash-prefixed token + // to a non-boolean flag as a bare value, so here both flags resolve to + // `Option.none()` — but the raw-argv mutex scan must reach the same + // "not a violation" conclusion pflag does, not double-count the + // `--metadata-url` token as a second explicit flag. + const { layer } = setup({ + cliArgs: ["sso", "update", VALID_PROVIDER_ID, "--metadata-file", "--metadata-url"], + }); + return Effect.gen(function* () { + const exit = yield* Effect.exit(legacySsoUpdate(defaultFlags)); + expect(Exit.isSuccess(exit)).toBe(true); + }).pipe(Effect.provide(layer)); + }, + ); + + it.live("mutex check: a bare --add-domains followed by --domains=... is not a violation", () => { + // Same consumed-value class as the metadata-file/metadata-url case + // above, but for the domains group: pflag would hand `add-domains` the + // literal value `"--domains=x.com"` and never parse `--domains` at all. + const { layer } = setup({ + cliArgs: ["sso", "update", VALID_PROVIDER_ID, "--add-domains", "--domains=x.com"], + }); + return Effect.gen(function* () { + const exit = yield* Effect.exit(legacySsoUpdate(defaultFlags)); + expect(Exit.isSuccess(exit)).toBe(true); }).pipe(Effect.provide(layer)); }); diff --git a/apps/cli/src/shared/cli/cobra-flag-groups.ts b/apps/cli/src/shared/cli/cobra-flag-groups.ts index 56a58da563..bb3e20716a 100644 --- a/apps/cli/src/shared/cli/cobra-flag-groups.ts +++ b/apps/cli/src/shared/cli/cobra-flag-groups.ts @@ -28,6 +28,59 @@ export function hasExplicitLongFlag( return false; } +/** + * Like `hasExplicitLongFlag`, but aware that a bare (`=`-less) occurrence of a + * *value-taking* flag consumes the very next argv token as its value — + * matching pflag's `parseLongArg` (`flag.go:1013-1031`), which takes the next + * raw arg unconditionally once a long flag needs a value, with no check that + * the token looks like another flag. + * + * Without this, scanning independently per flag name (as `hasExplicitLongFlag` + * does) can mistake a consumed value for a literal occurrence of a sibling + * mutex flag: `--metadata-file --metadata-url` is pflag's `metadata-file` + * flag being handed the (oddly named, but valid) string value + * `"--metadata-url"` — cobra never parses `--metadata-url` as its own flag, + * so `metadata-url.Changed` stays `false`. A naive scan sees both tokens and + * wrongly reports both as set. + * + * `valueFlagNames` must list every value-taking (non-boolean) flag declared + * on the command being scanned, so the scan knows which bare tokens consume a + * following value; boolean flags never consume one and must be omitted. This + * only covers flags local to the command — a global/inherited value-taking + * flag immediately preceding a mutex flag without `=` can still be misread + * the same way; closing that fully would mean teaching this scan about every + * flag reachable at parse time, not just the command's own, which is a + * bigger, cross-cutting change. + */ +export function hasExplicitValueFlag( + rawArgs: ReadonlyArray, + commandPath: ReadonlyArray, + valueFlagNames: ReadonlySet, + flagName: string, +): boolean { + const commandIndex = rawArgs.findIndex((_, index) => + commandPath.every((segment, offset) => rawArgs[index + offset] === segment), + ); + const scoped = commandIndex !== -1; + const tokens = scoped ? rawArgs.slice(commandIndex + commandPath.length) : rawArgs; + + for (let index = 0; index < tokens.length; index += 1) { + const token = tokens[index]; + if (token === undefined || (scoped && token === "--")) { + return false; + } + if (token === `--${flagName}` || token.startsWith(`--${flagName}=`)) { + return true; + } + if (token.startsWith("--") && !token.includes("=") && valueFlagNames.has(token.slice(2))) { + // Bare occurrence of a value-taking flag — skip the token it consumes + // so it can't be mistaken for a literal occurrence of `flagName`. + index += 1; + } + } + return false; +} + /** * Byte-matches cobra's `validateExclusiveFlagGroups` error * (`flag_groups.go:204`): `group` is the full mutually-exclusive set in diff --git a/apps/cli/src/shared/cli/cobra-flag-groups.unit.test.ts b/apps/cli/src/shared/cli/cobra-flag-groups.unit.test.ts index d1318cf4c2..2a410121da 100644 --- a/apps/cli/src/shared/cli/cobra-flag-groups.unit.test.ts +++ b/apps/cli/src/shared/cli/cobra-flag-groups.unit.test.ts @@ -1,5 +1,9 @@ import { describe, expect, test } from "vitest"; -import { cobraMutuallyExclusiveErrorMessage, hasExplicitLongFlag } from "./cobra-flag-groups.ts"; +import { + cobraMutuallyExclusiveErrorMessage, + hasExplicitLongFlag, + hasExplicitValueFlag, +} from "./cobra-flag-groups.ts"; const COMMAND_PATH = ["functions", "deploy"] as const; @@ -44,6 +48,88 @@ describe("hasExplicitLongFlag", () => { }); }); +describe("hasExplicitValueFlag", () => { + const SSO_UPDATE_PATH = ["sso", "update"] as const; + const VALUE_FLAGS = new Set(["metadata-file", "metadata-url", "domains", "add-domains"]); + + test("finds a bare flag after the command path", () => { + expect( + hasExplicitValueFlag( + ["sso", "update", "id", "--metadata-file", "foo.xml"], + SSO_UPDATE_PATH, + VALUE_FLAGS, + "metadata-file", + ), + ).toBe(true); + }); + + test("finds a flag with an inline value", () => { + expect( + hasExplicitValueFlag( + ["sso", "update", "id", "--domains=a.com"], + SSO_UPDATE_PATH, + VALUE_FLAGS, + "domains", + ), + ).toBe(true); + }); + + test("does not mistake a value-taking flag's consumed value for a sibling flag", () => { + // pflag's `--flag arg` branch consumes the next token unconditionally + // (`flag.go:1013-1031`), so `--metadata-file --metadata-url` gives + // `metadata-file` the literal value `"--metadata-url"` and never parses + // `--metadata-url` as its own flag. + const args = ["sso", "update", "id", "--metadata-file", "--metadata-url"]; + expect(hasExplicitValueFlag(args, SSO_UPDATE_PATH, VALUE_FLAGS, "metadata-file")).toBe(true); + expect(hasExplicitValueFlag(args, SSO_UPDATE_PATH, VALUE_FLAGS, "metadata-url")).toBe(false); + }); + + test("does not mistake a value-taking flag's consumed value for a sibling flag, reversed", () => { + const args = ["sso", "update", "id", "--metadata-url", "--metadata-file"]; + expect(hasExplicitValueFlag(args, SSO_UPDATE_PATH, VALUE_FLAGS, "metadata-url")).toBe(true); + expect(hasExplicitValueFlag(args, SSO_UPDATE_PATH, VALUE_FLAGS, "metadata-file")).toBe(false); + }); + + test("an inline (`=`) value is never treated as consuming the next token", () => { + // `--metadata-file=--metadata-url` is one token: metadata-file's value is + // the literal string "--metadata-url", and no token is consumed after it. + const args = ["sso", "update", "id", "--metadata-file=--metadata-url", "--domains", "a.com"]; + expect(hasExplicitValueFlag(args, SSO_UPDATE_PATH, VALUE_FLAGS, "metadata-file")).toBe(true); + expect(hasExplicitValueFlag(args, SSO_UPDATE_PATH, VALUE_FLAGS, "metadata-url")).toBe(false); + expect(hasExplicitValueFlag(args, SSO_UPDATE_PATH, VALUE_FLAGS, "domains")).toBe(true); + }); + + test("a real, non-adjacent occurrence of both flags is still detected", () => { + const args = ["sso", "update", "id", "--metadata-file", "foo.xml", "--metadata-url", "url"]; + expect(hasExplicitValueFlag(args, SSO_UPDATE_PATH, VALUE_FLAGS, "metadata-file")).toBe(true); + expect(hasExplicitValueFlag(args, SSO_UPDATE_PATH, VALUE_FLAGS, "metadata-url")).toBe(true); + }); + + test("returns false when the flag is absent", () => { + expect( + hasExplicitValueFlag(["sso", "update", "id"], SSO_UPDATE_PATH, VALUE_FLAGS, "domains"), + ).toBe(false); + }); + + test("stops scanning at a -- terminator", () => { + expect( + hasExplicitValueFlag( + ["sso", "update", "id", "--", "--domains"], + SSO_UPDATE_PATH, + VALUE_FLAGS, + "domains", + ), + ).toBe(false); + }); + + test("falls back to a bare scan when the command path is not found", () => { + expect(hasExplicitValueFlag(["--domains"], SSO_UPDATE_PATH, VALUE_FLAGS, "domains")).toBe(true); + expect(hasExplicitValueFlag(["--metadata-file"], SSO_UPDATE_PATH, VALUE_FLAGS, "domains")).toBe( + false, + ); + }); +}); + describe("cobraMutuallyExclusiveErrorMessage", () => { test("byte-matches cobra's validateExclusiveFlagGroups template", () => { expect( From eadafcb88f26033191a01d2ed1059760206bf3f1 Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Thu, 9 Jul 2026 11:03:05 +0100 Subject: [PATCH 31/79] docs(cli): link Effect-TS upstream issue for doubled-Expected workaround (#5837) ## What changed Files [Effect-TS/effect#6312](https://github.com/Effect-TS/effect/issues/6312) for the doubled `"Expected: Expected ..."` prefix bug in `effect@4.0.0-beta.93`'s CLI primitive parsers (`Primitive.choice`/schema-backed `integer`/`float`/`boolean`/`date`), per [review feedback on #5831](https://github.com/supabase/cli/pull/5831#pullrequestreview-4661453095), and adds a `TODO` in `invalid-value-message.ts` linking to it, so the workaround is easy to find and remove once upstream fixes the underlying bug. --- apps/cli/src/shared/cli/invalid-value-message.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/cli/src/shared/cli/invalid-value-message.ts b/apps/cli/src/shared/cli/invalid-value-message.ts index 8bf079581d..ff895e2a94 100644 --- a/apps/cli/src/shared/cli/invalid-value-message.ts +++ b/apps/cli/src/shared/cli/invalid-value-message.ts @@ -19,6 +19,9 @@ // scanning `error.value`. Remove once upstream `effect` fixes this (see // CLI-1898). // +// TODO: remove once Effect-TS/effect#6312 is fixed upstream. +// https://github.com/Effect-TS/effect/issues/6312 +// // Shared by two call sites that each see `InvalidValue` failures at a // different point in `effect`'s CLI runtime: // - `subcommand-flag-suggestions.ts` formats errors that reach the From 7f3a7e1efbd0b525db115e5b0b686c90fca9671d Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Thu, 9 Jul 2026 13:29:18 +0100 Subject: [PATCH 32/79] fix(cli): validate config before the destructive db reset --local recreate (#5840) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What changed `supabase db reset --local` now runs full Go-parity config validation (`legacyCheckDbToml`, matching Go's `flags.LoadConfig`) at the top of its local branch, before `AssertSupabaseDbIsRunning`/the destructive container recreate — the same pre-destructive-work gate `db start` and `db push` already have. A broken `supabase/config.toml` (unterminated TOML, an undecryptable `encrypted:` vault secret, an unparseable `env(VAR)` boolean, an explicit empty `project_id`) now aborts before the local database is ever recreated, instead of only surfacing later (or never) during bucket seeding. A genuinely **missing** `config.toml` is still tolerated, unchanged: Go's `Config.Load` defaults `project_id` to the current directory's basename when no config file exists, so `Validate` never rejects it — this is exactly the mechanism the `cli-e2e` parity suite relies on when it runs `db push --local` / `db reset --local` from a project with no `config.toml`. Only a config file that is *present but broken* is now caught earlier. `db reset --local`'s post-recreate bucket-seeding step catches a `LegacySeedConfigLoadError` from its own config reload and warns-and-continues rather than failing the command. An earlier version of this PR removed that fallback (reasoning: Go loads `config.toml` exactly once into memory, so it can never reach "recreate succeeded, then a later reload of the same file fails"). A Codex review on this PR caught that this doesn't transfer to the TS port: the new pre-recreate gate resolves `env(VAR)` via the Go-parity nested-env reader (sees `supabase/.env.development`, the project root, etc.), but the post-recreate reload goes through `@supabase/config`'s narrower loader (`supabase/.env`/`.env.local` only) — so a genuinely Go-valid config can pass the gate and the real recreate, then hard-fail only at the later, narrower reload, after the local database has already been dropped and rebuilt. The fallback is restored, with a comment naming the actual env-file-set gap instead of generic "loader strictness." ## Why Linear [CLI-1877](https://linear.app/supabase/issue/CLI-1877/reject-directlocal-db-push-db-reset-without-project-config-go): a Codex P1 finding on #5715 flagged that the native `db reset`/`db push`/`db start` port tolerates a missing project config more broadly than Go, and that `db reset --local`'s config validation happened too late relative to the destructive recreate. Investigation (via `go-parity-auditor`, cross-checked against the compiled Go binary) found the issue's literal premise — "reject direct/local db push + db reset without project config" — does **not** match current Go behavior: Go tolerates a missing config file by defaulting `project_id` to the cwd basename, and the TS port already matches that. Implementing a hard reject-on-missing-config would have been a *new* divergence from Go and would have broken the currently-green `cli-e2e` parity tests (exactly the regression risk the issue's own "deferred from #5715" note called out). `db start`'s and `db push`'s validation-ordering guarantees were also found to already be correct and already covered by existing tests. The one real, narrow gap was `db reset --local`'s ordering guarantee being implicit — buried inside a config resolver whose test double bypasses it entirely — rather than explicit and independently testable. This PR closes that gap. ## Test plan - Added regression tests in `reset.integration.test.ts` for a local reset: malformed config.toml, an unparseable boolean, an undecryptable vault secret, and an explicit empty `project_id`, all asserting the destructive recreate never runs; a test pinning that a broken config wins over the "not running" error; and a test confirming a genuinely missing config.toml is still tolerated. - `pnpm check:all` and the full `apps/cli` unit + integration suite (4758 tests) pass. - Ran the 4-agent `review-changes` procedure (architect/engineer/security/DX) against the diff and worked every finding; see "Judgement calls" below for what a `review-adjudicator` settled and what remains a documented, deliberate trade-off. ## Judgement calls / open notes - The `review-changes` engineer pass flagged that rewriting a pre-existing test orphaned the bucket-seed warn-and-skip branch, regressing coverage. A `review-adjudicator` pass at the time concluded there was no Go-parity reason to keep that fallback and recommended deleting it (done in the second commit) — a subsequent Codex review on the open PR found a concrete Go-valid config shape (an `env(VAR)` value sourced from `supabase/.env.development`) that the deletion broke, so the fallback was restored with an accurate comment (see "What changed" above). - The new pre-recreate gate is currently unreachable in production (the resolver's own internal read already validates first and would already reject a broken config identically) — it exists for defense-in-depth and so the "validate before destroy" guarantee is enforced directly by the handler and stays covered by a test even if the resolver is ever mocked or refactored to stop validating. Both the architect and DX review passes flagged this as worth calling out explicitly rather than as a blocking issue. - Not addressed here (flagged by review but out of scope): `LegacyDbConfigResolver.resolve` could return the validated config it already reads, so callers stop re-parsing `config.toml` up to 2-3 times on the local reset path. This is a pre-existing, codebase-wide pattern (`db push` does the same double-read), not something introduced by this PR, and is better done as its own resolver-contract refactor. Fixes CLI-1877. --- .../legacy/commands/db/reset/SIDE_EFFECTS.md | 18 +- .../legacy/commands/db/reset/reset.handler.ts | 37 +++- .../db/reset/reset.integration.test.ts | 169 ++++++++++++++++-- 3 files changed, 196 insertions(+), 28 deletions(-) diff --git a/apps/cli/src/legacy/commands/db/reset/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/db/reset/SIDE_EFFECTS.md index 6089303d32..27a67ad560 100644 --- a/apps/cli/src/legacy/commands/db/reset/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/db/reset/SIDE_EFFECTS.md @@ -11,15 +11,15 @@ primitives run behind the hidden Go `db __db-bootstrap` seam. Only the niche ## Files Read -| Path | Format | When | -| ------------------------------------------------------ | ---------- | ------------------------------------------------------------------------- | -| `/supabase/migrations/` | directory | to validate `--version` / resolve `--last`, and to load migrations | -| `/supabase/config.toml` | TOML | remote path + local bucket seeding (embedded defaults when absent) | -| `/.git/HEAD` (walked upward) | plain text | local path, for the `Finished … on branch .` line | -| `~/.supabase//project-ref` | plain text | `--linked`, to resolve the ref | -| `~/.supabase/access-token` | plain text | `--linked`, when `SUPABASE_ACCESS_TOKEN` unset and a temp role is minted | -| seed files from `--sql-paths` or `[db.seed].sql_paths` | SQL | when seeding is enabled (not `--no-seed`); `--sql-paths` overrides config | -| `/supabase/buckets/` | files | local path, when storage is up and `[storage.buckets]` configure objects | +| Path | Format | When | +| ------------------------------------------------------ | ---------- | --------------------------------------------------------------------------------------------------------------------- | +| `/supabase/migrations/` | directory | to validate `--version` / resolve `--last`, and to load migrations | +| `/supabase/config.toml` | TOML | always, parsed up front before any destructive work (embedded defaults when absent); re-read for local bucket seeding | +| `/.git/HEAD` (walked upward) | plain text | local path, for the `Finished … on branch .` line | +| `~/.supabase//project-ref` | plain text | `--linked`, to resolve the ref | +| `~/.supabase/access-token` | plain text | `--linked`, when `SUPABASE_ACCESS_TOKEN` unset and a temp role is minted | +| seed files from `--sql-paths` or `[db.seed].sql_paths` | SQL | when seeding is enabled (not `--no-seed`); `--sql-paths` overrides config | +| `/supabase/buckets/` | files | local path, when storage is up and `[storage.buckets]` configure objects | ## Files Written diff --git a/apps/cli/src/legacy/commands/db/reset/reset.handler.ts b/apps/cli/src/legacy/commands/db/reset/reset.handler.ts index 640392a7c0..46ecc8236f 100644 --- a/apps/cli/src/legacy/commands/db/reset/reset.handler.ts +++ b/apps/cli/src/legacy/commands/db/reset/reset.handler.ts @@ -243,6 +243,20 @@ export const legacyDbReset = Effect.fn("legacy.db.reset")(function* (flags: Lega // (running check, messages, bucket seeding, git-branch line, output shaping). // Mirrors `internal/db/reset/reset.go:57-77`. if (cfg.isLocal) { + // Go's `flags.LoadConfig` (root `PersistentPreRunE` → the local target's + // per-connType `LoadConfig`, `internal/utils/flags/db_url.go:77-80`) runs full + // config validation before `reset.Run` ever reaches `AssertSupabaseDbIsRunning` + // / the destructive `resetDatabase` (`internal/db/reset/reset.go:57-61`). The + // resolver's own local read (above, line 239) already performs the identical + // validation and would already reject a broken config before this point is + // reached — so today this re-validates for its own sake. Repeat it here anyway, + // as an explicit, independent gate (the same pattern `db start` and `db push` + // use), so the "malformed config aborts before the local database is recreated" + // guarantee is enforced by this handler directly and stays covered by a + // handler-level test even if the resolver's own internal read is ever mocked, + // relaxed, or refactored to stop validating. + yield* legacyCheckDbToml(fs, path, workdir); + // AssertSupabaseDbIsRunning — error if the local db container is down. const running = yield* seam.isDbRunning(); if (!running) { @@ -268,13 +282,22 @@ export const legacyDbReset = Effect.fn("legacy.db.reset")(function* (flags: Lega // Go's `buckets.Run(ctx, "", false, fsys)` — non-interactive: overwrite/prune // confirmations take their defaults instead of blocking on input. // - // Bucket seeding re-loads config.toml through the strict `@supabase/config` - // loader, which (unlike the Go-parity reader used elsewhere in reset) rejects some - // Go-valid configs — e.g. `[db.seed] enabled = "env(SEED_ENABLED)"`. The seam's Go - // `recreate` has already run Go's full `LoadConfig`+`Validate` on this same config, - // so a parse failure HERE is that loader-strictness gap, not a genuinely invalid - // config. Recreate already dropped/rebuilt the DB, so aborting now would leave the - // reset half-done; warn and skip buckets so `db reset` finishes like Go instead. + // `legacyCheckDbToml` above resolves `env(VAR)` via `legacyLoadProjectEnv`, + // which mirrors Go's full nested-env walk (`.env..local`, + // `.env.local`, `.env.`, `.env`, across both `supabase/` and the + // project root — `pkg/config/config.go:1220-1257`). This reload instead goes + // through `@supabase/config`'s `loadProjectConfig` → `loadProjectEnvironment`, + // which only ever reads `supabase/.env`/`.env.local` plus ambient env + // (`packages/config/src/project.ts:209-245`) — regardless of `goViperCompat`, + // which only widens `env(VAR)` matching, not the file set consulted. So a + // config whose `env(VAR)` reference is backed by e.g. `supabase/.env.development` + // is genuinely Go-valid (Go's `godotenv.Load` calls `os.Setenv`, so the value is + // real ambient env by the time Go resolves it — `config.go:1260-1261`) and + // already passed `legacyCheckDbToml` and the real recreate above, but this + // narrower reload can still reject it. A `LegacySeedConfigLoadError` here is + // that env-file-set gap, not a genuinely invalid config — and recreate already + // dropped/rebuilt the DB, so aborting now would leave the reset half-done; warn + // and skip buckets so `db reset` finishes like Go instead. yield* legacySeedBucketsRun({ projectRef: "", emitSummary: false, diff --git a/apps/cli/src/legacy/commands/db/reset/reset.integration.test.ts b/apps/cli/src/legacy/commands/db/reset/reset.integration.test.ts index 853f933134..cc3e1f47a3 100644 --- a/apps/cli/src/legacy/commands/db/reset/reset.integration.test.ts +++ b/apps/cli/src/legacy/commands/db/reset/reset.integration.test.ts @@ -329,6 +329,117 @@ describe("legacy db reset", () => { }); }); + it.live("proceeds with a local reset when no config file is present", () => { + // Go's `Config.Load` tolerates a missing `config.toml`: `Eject` defaults an empty + // `project_id` to the cwd basename (`pkg/config/config.go:563-570`), so `Validate` + // never sees an empty required field and the CLI proceeds — exactly the mechanism + // the cli-e2e parity suite relies on when it runs `db reset --local` from a project + // with no config.toml. A missing config must not become a hard failure here. + const { layer, seam } = setup(tmp.current, { + args: ["db", "reset"], + isLocal: true, + running: true, + }); + return Effect.gen(function* () { + yield* legacyDbReset(DEFAULT_FLAGS).pipe(Effect.provide(layer)); + expect(seam.recreateCalls).toHaveLength(1); + }); + }); + + it.live("fails a local reset before the destructive recreate on a malformed config.toml", () => { + // Go's `flags.LoadConfig` (the local target's `LoadConfig`, `db_url.go:77-80`) runs + // full config validation before `reset.Run` reaches `AssertSupabaseDbIsRunning` / + // `resetDatabase` (`internal/db/reset/reset.go:57-61`). A broken config.toml must + // abort before the local database is ever recreated. + const { layer, seam } = setup(tmp.current, { + toml: 'project_id = "unterminated\n', + args: ["db", "reset"], + isLocal: true, + running: true, + }); + return Effect.gen(function* () { + const exit = yield* legacyDbReset(DEFAULT_FLAGS).pipe(Effect.provide(layer), Effect.exit); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(JSON.stringify(exit.cause)).toContain("failed to load config"); + } + expect(seam.recreateCalls).toHaveLength(0); + }); + }); + + it.live( + "fails a local reset on a malformed config.toml even when the database is not running", + () => { + // Pins Go's exact ordering: `flags.LoadConfig` runs in the root `PersistentPreRunE`, + // strictly before `reset.Run` ever calls `AssertSupabaseDbIsRunning` + // (`internal/db/reset/reset.go:57`). So a broken config must surface as a config + // error even when the local database is ALSO not running — the config check must + // win the race, not the "is not running" check. + const { layer, seam } = setup(tmp.current, { + toml: 'project_id = "unterminated\n', + args: ["db", "reset"], + isLocal: true, + running: false, + }); + return Effect.gen(function* () { + const exit = yield* legacyDbReset(DEFAULT_FLAGS).pipe(Effect.provide(layer), Effect.exit); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + const cause = JSON.stringify(exit.cause); + expect(cause).toContain("failed to load config"); + expect(cause).not.toContain("is not running."); + } + expect(seam.recreateCalls).toHaveLength(0); + }); + }, + ); + + it.live("fails a local reset before the destructive recreate on an undecryptable secret", () => { + // Regression: Go's `flags.LoadConfig` decrypts every `encrypted:` secret before + // `reset.Run` recreates the local database, so an undecryptable secret must abort + // before the destructive recreate, not surface later (or never) during bucket + // seeding. + const { layer, seam } = setup(tmp.current, { + toml: 'project_id = "test"\n\n[db.vault]\nmy_secret = "encrypted:anything"\n', + args: ["db", "reset"], + isLocal: true, + running: true, + }); + return Effect.gen(function* () { + const exit = yield* legacyDbReset(DEFAULT_FLAGS).pipe(Effect.provide(layer), Effect.exit); + expect(Exit.isFailure(exit)).toBe(true); + // Assert on the stable "failed to parse config:" prefix rather than the exact + // decrypt-failure tail, which depends on whether an ambient `DOTENV_PRIVATE_KEY*` + // is set (missing key vs. a base64/decrypt failure) — either way, the config + // load must fail before the destructive recreate. + if (Exit.isFailure(exit)) { + expect(JSON.stringify(exit.cause)).toContain("failed to parse config:"); + } + expect(seam.recreateCalls).toHaveLength(0); + }); + }); + + it.live("fails a local reset before the destructive recreate on an empty project_id", () => { + // Go's `config.Validate` rejects an explicit `project_id = ""` (a present override + // that resolved to empty, unlike an absent field) before the local recreate. + const { layer, seam } = setup(tmp.current, { + toml: 'project_id = ""\n', + args: ["db", "reset"], + isLocal: true, + running: true, + }); + return Effect.gen(function* () { + const exit = yield* legacyDbReset(DEFAULT_FLAGS).pipe(Effect.provide(layer), Effect.exit); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(JSON.stringify(exit.cause)).toContain( + "Missing required field in config: project_id", + ); + } + expect(seam.recreateCalls).toHaveLength(0); + }); + }); + it.live("seeds buckets after a local reset when storage is ready", () => { const { layer, seam } = setup(tmp.current, { toml: 'project_id = "test"\n', @@ -346,16 +457,16 @@ describe("legacy db reset", () => { }); }); - it.live("finishes a local reset when bucket seeding hits a strict-loader-rejected config", () => { - // The bucket-seeding core re-loads config via the strict `@supabase/config` loader. - // `SEED_ENABLED=maybe` is invalid for both Go's `strconv.ParseBool` and the TS - // loader's coercion, so the reload fails during decode (unlike e.g. `1`/`true`, - // which both now accept). The seam's Go recreate already validated + rebuilt the - // DB, so aborting here would leave the reset half-done — warn and skip buckets so - // reset finishes like Go. + it.live("fails a local reset before the destructive recreate on an unparseable boolean", () => { + // `SEED_ENABLED=maybe` cannot be resolved by Go's `strconv.ParseBool`, so + // `flags.LoadConfig` aborts on this config before `reset.Run` ever reaches + // `AssertSupabaseDbIsRunning`/`resetDatabase`. Previously this surfaced only much + // later (if at all) via the bucket-seeding core's own reload, AFTER the local + // database had already been recreated — this must now abort up front instead, + // via the pre-recreate `legacyCheckDbToml` gate. const previous = process.env["SEED_ENABLED"]; process.env["SEED_ENABLED"] = "maybe"; - const { layer, out, seam } = setup(tmp.current, { + const { layer, seam } = setup(tmp.current, { toml: 'project_id = "test"\n\n[db.seed]\nenabled = "env(SEED_ENABLED)"\n', args: ["db", "reset"], isLocal: true, @@ -363,10 +474,12 @@ describe("legacy db reset", () => { storageReady: true, }); return Effect.gen(function* () { - yield* legacyDbReset(DEFAULT_FLAGS).pipe(Effect.provide(layer)); - expect(out.stderrText).toContain("skipped seeding storage buckets"); - expect(out.stderrText).toContain("Finished "); - expect(seam.recreateCalls).toHaveLength(1); + const exit = yield* legacyDbReset(DEFAULT_FLAGS).pipe(Effect.provide(layer), Effect.exit); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(JSON.stringify(exit.cause)).toContain("invalid db.seed.enabled"); + } + expect(seam.recreateCalls).toHaveLength(0); }).pipe( Effect.ensuring( Effect.sync(() => { @@ -377,6 +490,38 @@ describe("legacy db reset", () => { ); }); + it.live( + "finishes a local reset when bucket seeding can't see an env(VAR) value the pre-recreate gate saw", + () => { + // `legacyCheckDbToml` (the pre-recreate gate) resolves `env(VAR)` via + // `legacyLoadProjectEnv`, which mirrors Go's full nested-env walk and sees + // `supabase/.env.development` — a real, Go-valid env source + // (`pkg/config/config.go:1220-1257`; `godotenv.Load` calls `os.Setenv`, so this + // is genuinely ambient env by the time Go itself resolves `env(VAR)`, + // `config.go:1260-1261`). The post-recreate bucket-seed reload instead goes + // through `@supabase/config`'s `loadProjectEnvironment`, which only ever reads + // `supabase/.env`/`.env.local` + ambient env (`packages/config/src/project.ts: + // 209-245) — it can't see `.env.development` at all. So this Go-valid config + // passes the gate and the real recreate, then can't be re-resolved by the + // reload; the reset must still finish (warn-and-skip), not hard-fail after the + // local database has already been dropped and rebuilt. + const { layer, out, seam } = setup(tmp.current, { + toml: 'project_id = "test"\n\n[db.seed]\nenabled = "env(SEED_ENABLED)"\n', + files: { "supabase/.env.development": "SEED_ENABLED=true\n" }, + args: ["db", "reset"], + isLocal: true, + running: true, + storageReady: true, + }); + return Effect.gen(function* () { + yield* legacyDbReset(DEFAULT_FLAGS).pipe(Effect.provide(layer)); + expect(seam.recreateCalls).toHaveLength(1); + expect(out.stderrText).toContain("skipped seeding storage buckets"); + expect(out.stderrText).toContain("Finished "); + }); + }, + ); + it.live("uses the detected git branch in the Finished line", () => { const { layer, out } = setup(tmp.current, { toml: 'project_id = "test"\n', From 7bc8457b89a78aeb097e9ecfdbb9b0a48e45ab7d Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Thu, 9 Jul 2026 13:29:29 +0100 Subject: [PATCH 33/79] fix(cli): propagate delegated Go child exit codes through finalizers (#5841) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What kind of change does this PR introduce? Bug fix — legacy-shell child exit-code and delegated-path semantics. ## What is the current behavior? Fixes [CLI-1879](https://linear.app/supabase/issue/CLI-1879/legacy-shell-child-exit-code-delegated-path-semantics-finalizers-json). Three related gaps around Go-delegated subprocess paths in the legacy shell: 1. `LegacyGoProxy.exec`/`execCapture` (`shared/legacy/go-proxy.layer.ts`) called `ProcessControl.exit()` directly on a non-zero child exit or "binary not found". That's a real `process.exit()` — it skips every `Effect.ensuring` finalizer between the call site and `runCli` (telemetry flush, command instrumentation), and loses the child's exact exit code (collapsing everything to whatever `process.exit()` was called with, with no chance for `runCli`'s own exit-code logic to run). 2. Because `execCapture`'s non-zero-exit branch hard-exited the process, it never reached `withJsonErrorHandling`, so a `--output-format json`/`stream-json` `db reset --experimental` failure emitted no structured error envelope at all — the process just died mid-flight. 3. `db reset --experimental --linked` (no resolved version) unconditionally resolved the linked DB connection — including minting/verifying a temporary Postgres login role over the Management API — before checking whether the remaining flow delegates to the Go child. On that branch the resolved connection was never used: the delegated Go child re-runs its own `ParseDatabaseConfig` (and mints its own temp role) once it starts, so the TS-side mint was pure duplicate privileged work. (A fourth item from the same issue — forwarding `--linked=false`'s target selector verbatim to the delegated child — was already correctly implemented and already covered by a passing test; no code change was needed there.) ## What is the new behavior? - New `LegacyGoChildExitError` (`shared/legacy/legacy-go-child-exit.error.ts`) carries a spawned child's exact exit code via Effect's `Runtime.errorExitCode` marker. `LegacyGoProxy.exec`/`execCapture` and the hidden `db __db-bootstrap` seam now fail with this typed error instead of calling `ProcessControl.exit()`, so the failure flows through the normal Effect channel: finalizers run first, then `runCli`/`withJsonErrorHandling` exit with the child's real code — in every output format, including a Ctrl-C mid-recreate (e.g. exit `130`) instead of a generic `1`. - `runCli`'s `handledProgram` special-cases `LegacyGoChildExitError` (by concrete type) to skip its own generic `output.fail` stderr line, since the child already printed its own detailed failure to the inherited stderr and Go itself never prints a second line on top of that. This is deliberately **not** keyed on Effect's shared `Runtime.errorReported` marker — `CliError.ShowHelp` also sets that marker, and gating on it would have silently suppressed the Go-parity `Error: required flag(s) "..." not set` message for every missing-required-flag error. (Caught by architect review before merge; regression test added.) - `withJsonErrorHandling` now reads `Runtime.getErrorExitCode` instead of hardcoding `1` when setting the process exit code, so the exact code propagates under `json`/`stream-json` too, not just text mode. - `db reset --experimental --linked` now checks whether it's about to delegate to the Go child *before* calling `LegacyDbConfigResolver.resolve()`, skipping the redundant temp-role mint on that path. The linked-project-cache finalizer is unaffected (it already reads a separately pre-loaded ref, exactly so this case still works). - Updated `SIDE_EFFECTS.md`'s exit-code tables for `db reset`/`db start` to reflect the child's exact code, and touched-up a couple of stale doc comments describing the old `process.exit()`-based behavior. ### Deliberately left open (judgement calls, not blockers) - The JSON error envelope for a delegated linked+experimental connection/mint failure is now a generic `"supabase-go exited with code N (see stderr for details)"` rather than the specific TS error the old (duplicate-minting) code path used to surface. This is an accepted tradeoff: the real detail is on the inherited stderr, and Go itself has no JSON error-envelope concept to hold this to a parity standard against. - The bootstrap seam's JSON error `code` field changes from `LegacyDbBootstrapError` to `LegacyGoChildExitError` for a non-zero child exit specifically. Nothing in this codebase treats that field as a stable public contract (most `Legacy*Error` tags already leak their raw class name into it), so this wasn't treated as a compatibility break. - The sibling `db __shadow` seam (`legacy-pgdelta.seam.layer.ts`) intentionally keeps its own generic domain error rather than adopting `LegacyGoChildExitError` — its failure is a TS-authored summary over noisy docker/pgdelta stderr (not a passthrough of a real user-facing Go child), and Go itself collapses shadow-DB failures to a generic exit `1`. Left a comment in place explaining the divergence so a future reader doesn't "fix" it into inconsistency. ## Test plan New/updated coverage in `go-proxy.layer.unit.test.ts`, `run.unit.test.ts`, `json-error-handling.unit.test.ts`, `reset.integration.test.ts`, and `start.integration.test.ts` — exact exit-code propagation, finalizers running after a non-zero exit, the JSON error envelope, the skipped pre-delegation resolve (and that the sibling `--db-url` delegate path still resolves), and a regression guard for the `ShowHelp`/`MissingOption` suppression bug caught during review. --- .../legacy/commands/bootstrap/SIDE_EFFECTS.md | 9 +- .../legacy/commands/db/reset/SIDE_EFFECTS.md | 29 +-- .../legacy/commands/db/reset/reset.handler.ts | 82 ++++--- .../db/reset/reset.integration.test.ts | 201 ++++++++++++++++-- .../declarative/declarative.smart-target.ts | 7 +- .../shared/legacy-db-bootstrap.seam.layer.ts | 29 ++- .../legacy-db-bootstrap.seam.service.ts | 10 +- .../db/shared/legacy-pgdelta.seam.layer.ts | 7 + .../legacy/commands/db/start/SIDE_EFFECTS.md | 16 +- .../db/start/start.integration.test.ts | 60 +++++- apps/cli/src/shared/cli/run.ts | 36 +++- apps/cli/src/shared/cli/run.unit.test.ts | 59 ++++- apps/cli/src/shared/legacy/go-proxy.layer.ts | 36 +++- .../shared/legacy/go-proxy.layer.unit.test.ts | 166 +++++++++++---- .../cli/src/shared/legacy/go-proxy.service.ts | 19 +- .../legacy/legacy-go-child-exit.error.ts | 55 +++++ .../src/shared/output/json-error-handling.ts | 10 +- .../output/json-error-handling.unit.test.ts | 20 ++ 18 files changed, 688 insertions(+), 163 deletions(-) create mode 100644 apps/cli/src/shared/legacy/legacy-go-child-exit.error.ts diff --git a/apps/cli/src/legacy/commands/bootstrap/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/bootstrap/SIDE_EFFECTS.md index 6c5c7cefbe..2a28897a48 100644 --- a/apps/cli/src/legacy/commands/bootstrap/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/bootstrap/SIDE_EFFECTS.md @@ -99,10 +99,11 @@ Human banners are suppressed; a single structured result is emitted: - **Interim Go-proxy delegation for migration push.** The push step shells out to the bundled Go binary (`db push --include-roles --include-seed`) until `db push` gets its own native port (separate Linear issue). The sub-step is **not** instrumentation-wrapped (the subprocess fires - its own push telemetry). Known divergence: `LegacyGoProxy.exec` propagates the exit code, so Go's - push backoff is **not** reproduced (single attempt) — to be restored when `db push` is natively - ported. (`LegacyGoProxy.exec` exits the process on a non-zero exit rather than returning a - failure, so the step cannot be wrapped in `Effect.retry`.) + its own push telemetry). Known divergence: Go's push backoff is **not** reproduced (single + attempt) — to be restored when `db push` is natively ported. (`LegacyGoProxy.exec` fails with a + typed `LegacyGoChildExitError` on a non-zero exit rather than exiting the process — CLI-1879 — so + the step COULD now be wrapped in `Effect.retry`; leaving that unimplemented here is a deliberate + scope decision for the native `db push` port, not a technical blocker.) - **DB password is forwarded on the same channel the user supplied it (CLI-1617).** The proxy must be called 1:1 with the user's input: a flag stays a flag, an env var stays an env var. So when the user passed `-p/--password`, the push sub-step receives `--password ` (flag → flag); when diff --git a/apps/cli/src/legacy/commands/db/reset/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/db/reset/SIDE_EFFECTS.md index 27a67ad560..0934154974 100644 --- a/apps/cli/src/legacy/commands/db/reset/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/db/reset/SIDE_EFFECTS.md @@ -81,18 +81,23 @@ seeded over the Storage gateway (reusing the `seed buckets` local path). ## Exit Codes -| Code | Condition | -| ---- | ---------------------------------------------------------------- | -| `0` | success | -| `1` | mutually exclusive target flags (`[db-url linked local]`) | -| `1` | `--version` + `--last` together (`[last version]`) | -| `1` | `--version` not an integer (`invalid version number`) | -| `1` | `--version` has no matching migration file | -| `1` | local: database not running (`supabase start is not running.`) | -| `1` | user declined the reset confirmation (`context canceled`) | -| `1` | `config.toml` parse failure | -| `1` | drop / migrate / seed / vault apply failure, or connection error | -| `1` | local: container recreate / storage health-gate failure (seam) | +| Code | Condition | +| -------------------- | -------------------------------------------------------------------------------------------------------------------------- | +| `0` | success | +| `1` | mutually exclusive target flags (`[db-url linked local]`) | +| `1` | `--version` + `--last` together (`[last version]`) | +| `1` | `--version` not an integer (`invalid version number`) | +| `1` | `--version` has no matching migration file | +| `1` | local: database not running (`supabase start is not running.`) | +| `1` | user declined the reset confirmation (`context canceled`) | +| `1` | `config.toml` parse failure | +| `1` | drop / migrate / seed / vault apply failure, or connection error | +| child's exact code\* | local: container recreate / storage health-gate failure (seam), or `--experimental`/`--linked` delegate (proxy) child exit | + +\* The `db __db-bootstrap` seam and the `--experimental` remote delegate both +propagate the spawned `supabase-go` child's real exit code (e.g. `130` after a +Ctrl-C mid-recreate) instead of collapsing every failure to `1` — in every +`--output-format` (CLI-1879). ## Output diff --git a/apps/cli/src/legacy/commands/db/reset/reset.handler.ts b/apps/cli/src/legacy/commands/db/reset/reset.handler.ts index 46ecc8236f..f1637df50a 100644 --- a/apps/cli/src/legacy/commands/db/reset/reset.handler.ts +++ b/apps/cli/src/legacy/commands/db/reset/reset.handler.ts @@ -227,6 +227,36 @@ export const legacyDbReset = Effect.fn("legacy.db.reset")(function* (flags: Lega } const connType = target.connType ?? "local"; + // Single source of truth for "does this reset delegate to the Go child?" — + // checked at both delegation sites below (before `resolve()` for a linked + // target, after it for a `--db-url` target) so the two call sites can never + // drift apart. + const shouldDelegateExperimental = experimental && resolvedVersion === ""; + + // Delegates the remaining `--experimental` schema-files apply path + // (`apply.MigrateAndSeed`, not ported) to the Go child. In text mode inherit + // its stdio. Under a machine-output mode (`--output-format json|stream-json`) + // the Go child emits no TS envelope, so suppress its stdout (capture + discard) + // and emit the same structured success the native local and remote paths do, + // keeping the JSON contract consistent across all reset paths. + const delegateExperimentalReset = () => + Effect.gen(function* () { + const env = { SUPABASE_TELEMETRY_DISABLED: "1" }; + if (output.format === "text") { + yield* proxy.exec(buildResetArgs(flags, connType, yes), { env }); + } else { + // Machine-output mode is non-interactive: give the Go child a non-TTY stdin + // (`stdin: "ignore"`) so it can't block on (or be answered at) Go's + // destructive reset prompt — it takes the default `false`, matching the + // native reset path which suppresses prompts under json/stream-json. + yield* proxy.execCapture(buildResetArgs(flags, connType, yes), { env, stdin: "ignore" }); + yield* output.success("Reset remote database.", { + target: "remote", + version: resolvedVersion, + }); + } + }); + // Go's ParseDatabaseConfig runs LoadProjectRef BEFORE the fallible linked // resolution (db_url.go:87-95), and Execute() writes the linked-project cache // even when a later step errors (root.go:171-181). Pre-load the ref so the @@ -235,7 +265,23 @@ export const legacyDbReset = Effect.fn("legacy.db.reset")(function* (flags: Lega if (connType === "linked") { const refResolver = yield* LegacyProjectRefResolver; linkedRefForCache = yield* refResolver.loadProjectRef(Option.none()); + + // A linked target is never local (`resolver.resolve()`'s "linked" branch + // always returns `isLocal: false`), so the delegated-experimental check can + // run BEFORE calling `resolve()`. This matters: for `connType === "linked"`, + // `resolve()` mints/verifies a temporary Postgres login role over the + // Management API — and the delegated Go child re-runs that exact same + // `ParseDatabaseConfig` work itself once delegation happens. Calling + // `resolve()` here would mint the temp role twice for zero downstream use on + // this branch (Go's own reset flow mints it exactly once, as part of the code + // path being delegated to — confirmed against `apps/cli-go/internal/utils/ + // flags/db_url.go`'s `NewDbConfigWithPassword`/`initLoginRole`). CLI-1879. + if (shouldDelegateExperimental) { + yield* delegateExperimentalReset(); + return; + } } + const cfg = yield* resolver.resolve({ dbUrl: flags.dbUrl, connType, dnsResolver }); // Local target → native local reset. The container-recreate primitives live @@ -331,34 +377,20 @@ export const legacyDbReset = Effect.fn("legacy.db.reset")(function* (flags: Lega return; } - // Resolve the linked ref before any return so the post-run cache (Go's - // `PersistentPostRun` `ensureProjectGroupsCached`) is written even on the - // delegated `--experimental` path below — the Go child runs with telemetry - // disabled and skips that cache, so the TS finalizer must own it. + // Re-confirm `linkedRefForCache` from the now-resolved `cfg.ref` for the native + // remote linked path below (a linked+experimental+versionless target already + // delegated and returned above, before `resolve()` was ever called — see the + // `connType === "linked"` block earlier in this function). A `connType === + // "db-url"` target leaves `linkedRefForCache` as whatever the pre-load block + // set (nothing, for `db-url`), since this assignment only fires when linked. const linkedRef = Option.getOrUndefined(cfg.ref ?? Option.none()); if (connType === "linked" && linkedRef !== undefined) linkedRefForCache = linkedRef; - // Remote path. The niche `--experimental` schema-files apply path - // (`apply.MigrateAndSeed`) is not ported; delegate it to the Go child. In text - // mode inherit its stdio. Under a machine-output mode (`--output-format - // json|stream-json`) the Go child emits no TS envelope, so suppress its stdout - // (capture + discard) and emit the same structured success the native local and - // remote paths do, keeping the JSON contract consistent across all reset paths. - if (experimental && resolvedVersion === "") { - const env = { SUPABASE_TELEMETRY_DISABLED: "1" }; - if (output.format === "text") { - yield* proxy.exec(buildResetArgs(flags, connType, yes), { env }); - } else { - // Machine-output mode is non-interactive: give the Go child a non-TTY stdin - // (`stdin: "ignore"`) so it can't block on (or be answered at) Go's - // destructive reset prompt — it takes the default `false`, matching the - // native reset path which suppresses prompts under json/stream-json. - yield* proxy.execCapture(buildResetArgs(flags, connType, yes), { env, stdin: "ignore" }); - yield* output.success("Reset remote database.", { - target: "remote", - version: resolvedVersion, - }); - } + // Remaining remote target: a `--db-url` pointing at a non-local host (the + // `connType === "linked"` case already delegated above, before `resolve()`, + // without resolving a connection at all). + if (shouldDelegateExperimental) { + yield* delegateExperimentalReset(); return; } diff --git a/apps/cli/src/legacy/commands/db/reset/reset.integration.test.ts b/apps/cli/src/legacy/commands/db/reset/reset.integration.test.ts index cc3e1f47a3..48d25519c8 100644 --- a/apps/cli/src/legacy/commands/db/reset/reset.integration.test.ts +++ b/apps/cli/src/legacy/commands/db/reset/reset.integration.test.ts @@ -3,7 +3,7 @@ import { dirname, join } from "node:path"; import { BunServices } from "@effect/platform-bun"; import { describe, expect, it } from "@effect/vitest"; -import { Effect, Exit, Layer, Option } from "effect"; +import { Cause, Effect, Exit, Layer, Option } from "effect"; import * as HttpClient from "effect/unstable/http/HttpClient"; import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse"; @@ -31,6 +31,7 @@ import { LegacyYesFlag, } from "../../../../shared/legacy/global-flags.ts"; import { LegacyGoProxy } from "../../../../shared/legacy/go-proxy.service.ts"; +import { LegacyGoChildExitError } from "../../../../shared/legacy/legacy-go-child-exit.error.ts"; import type { OutputFormat } from "../../../../shared/output/types.ts"; import { LegacyDbConfigResolver } from "../../../shared/legacy-db-config.service.ts"; import type { @@ -69,15 +70,24 @@ const DEFAULT_FLAGS: LegacyDbResetFlags = { last: Option.none(), }; +/** + * Tracks every `resolve`/`resolvePoolerFallback` invocation so tests can prove a + * connection was (or, for the delegated-experimental path, was NOT) resolved — + * `resolve()` mints/verifies a temporary Postgres login role over the Management + * API, so calling it on a path that immediately discards the result is wasted + * (and duplicated) work (CLI-1879). + */ function mockResolver(opts: { isLocal: boolean; ref?: string; omitRef?: boolean; resolveFails?: boolean; }) { - return Layer.succeed(LegacyDbConfigResolver, { - resolve: (_flags: LegacyDbConfigFlags) => - opts.resolveFails === true + let calls = 0; + const layer = Layer.succeed(LegacyDbConfigResolver, { + resolve: (_flags: LegacyDbConfigFlags) => { + calls++; + return opts.resolveFails === true ? Effect.fail( new LegacyDbConfigConnectTempRoleError({ message: "failed to create login role: network error", @@ -91,9 +101,19 @@ function mockResolver(opts: { isLocal: opts.isLocal, ref: opts.ref !== undefined ? Option.some(opts.ref) : Option.none(), }) satisfies LegacyResolvedDbConfig, - ), - resolvePoolerFallback: () => Effect.succeed(Option.none()), + ); + }, + resolvePoolerFallback: () => { + calls++; + return Effect.succeed(Option.none()); + }, }); + return { + layer, + get calls() { + return calls; + }, + }; } function mockConnection(opts: { remoteSeeds?: Readonly> }) { @@ -142,8 +162,15 @@ function mockConnection(opts: { remoteSeeds?: Readonly> } * Stateful mock of the container-bootstrap seam. `running` drives * `AssertSupabaseDbIsRunning`; `storageReady` drives the bucket-seed gate. Records * the recreate args so tests can assert version / `--no-seed` propagation. + * `awaitStorageReadyExitCode`, when set, fails `awaitStorageReady` with a + * `LegacyGoChildExitError` carrying that code — simulating the seam's real + * `captureStdout` bootstrap-child path exiting non-zero (CLI-1879). */ -function mockBootstrapSeam(opts: { running?: boolean; storageReady?: boolean }) { +function mockBootstrapSeam(opts: { + running?: boolean; + storageReady?: boolean; + awaitStorageReadyExitCode?: number; +}) { const recreateCalls: Array<{ version: string; noSeed: boolean; @@ -164,8 +191,18 @@ function mockBootstrapSeam(opts: { running?: boolean; storageReady?: boolean }) awaitStorageReady: () => Effect.sync(() => { storageChecked = true; - return opts.storageReady ?? false; - }), + }).pipe( + Effect.flatMap(() => + opts.awaitStorageReadyExitCode !== undefined + ? Effect.fail( + new LegacyGoChildExitError({ + exitCode: opts.awaitStorageReadyExitCode, + message: `failed to bootstrap the local database: exit ${opts.awaitStorageReadyExitCode}`, + }), + ) + : Effect.succeed(opts.storageReady ?? false), + ), + ), }); return { layer, @@ -188,14 +225,33 @@ const mockStorageHttp = Layer.succeed( ), ); -function mockProxy() { +/** + * `execCaptureExitCode`, when set, makes `execCapture` fail with a + * `LegacyGoChildExitError` carrying that code instead of succeeding — simulating + * a delegated Go child exiting non-zero under a machine-output mode (CLI-1879). + */ +function mockProxy(opts: { execCaptureExitCode?: number } = {}) { const calls: Array<{ args: ReadonlyArray; env?: Record }> = []; const layer = Layer.succeed(LegacyGoProxy, { - exec: (args, opts) => + exec: (args, execOpts) => Effect.sync(() => { - calls.push({ args, env: opts?.env }); + calls.push({ args, env: execOpts?.env }); }), - execCapture: () => Effect.succeed(""), + execCapture: (args, execOpts) => + Effect.sync(() => { + calls.push({ args, env: execOpts?.env }); + }).pipe( + Effect.flatMap(() => + opts.execCaptureExitCode !== undefined + ? Effect.fail( + new LegacyGoChildExitError({ + exitCode: opts.execCaptureExitCode, + message: `supabase-go exited with code ${opts.execCaptureExitCode}`, + }), + ) + : Effect.succeed(""), + ), + ), }); return { layer, @@ -222,6 +278,8 @@ function setup( resolveFails?: boolean; running?: boolean; storageReady?: boolean; + awaitStorageReadyExitCode?: number; + execCaptureExitCode?: number; }, ) { if (opts.toml !== undefined) { @@ -236,25 +294,30 @@ function setup( const out = mockOutput({ format: opts.format ?? "text", promptConfirmResponses: opts.confirm }); const conn = mockConnection(opts); - const proxy = mockProxy(); - const seam = mockBootstrapSeam({ running: opts.running, storageReady: opts.storageReady }); + const proxy = mockProxy({ execCaptureExitCode: opts.execCaptureExitCode }); + const seam = mockBootstrapSeam({ + running: opts.running, + storageReady: opts.storageReady, + awaitStorageReadyExitCode: opts.awaitStorageReadyExitCode, + }); const telemetry = mockLegacyTelemetryStateTracked(); const linkedCache = mockLegacyLinkedProjectCacheTracked(); // The local-reset bucket-seed core statically requires the (lazy) Management-API // factory; never invoked on `--local` (projectRef === ""). const platformApi = mockLegacyPlatformApiService({}); + const resolver = mockResolver({ + isLocal: opts.isLocal ?? false, + ref: opts.ref ?? LEGACY_VALID_REF, + omitRef: opts.omitRef, + resolveFails: opts.resolveFails, + }); const layer = Layer.mergeAll( out.layer, conn.layer, proxy.layer, seam.layer, - mockResolver({ - isLocal: opts.isLocal ?? false, - ref: opts.ref ?? LEGACY_VALID_REF, - omitRef: opts.omitRef, - resolveFails: opts.resolveFails, - }), + resolver.layer, mockLegacyCliConfig({ workdir }), BunServices.layer, mockRuntimeInfo(), @@ -284,7 +347,7 @@ function setup( telemetry.layer, linkedCache.layer, ); - return { layer, out, conn, proxy, seam, telemetry, linkedCache }; + return { layer, out, conn, proxy, seam, telemetry, linkedCache, resolver }; } const migrationFile = (version: string, body = "create table t ();") => ({ @@ -863,6 +926,94 @@ describe("legacy db reset", () => { }); }); + it.live("does not resolve a linked DB connection before delegating an experimental reset", () => { + const { layer, proxy, resolver } = setup(tmp.current, { + toml: 'project_id = "test"\n', + experimental: true, + }); + return Effect.gen(function* () { + yield* legacyDbReset({ ...DEFAULT_FLAGS, linked: true }).pipe(Effect.provide(layer)); + expect(proxy.calls).toHaveLength(1); + // The delegated Go child re-runs its own connection resolution (including + // minting/verifying the temp login role) once it starts — the TS wrapper + // must not do that same Management-API work first only to discard it (CLI-1879). + expect(resolver.calls).toBe(0); + }); + }); + + it.live("still caches the linked ref when delegating an experimental reset", () => { + // `linkedRefForCache` is pre-loaded via `LegacyProjectRefResolver.loadProjectRef` + // separately from `resolver.resolve()`, specifically so the post-run + // linked-project-cache finalizer still fires on this path even though + // `resolve()` itself is skipped entirely (CLI-1879). + const { layer, linkedCache } = setup(tmp.current, { + toml: 'project_id = "test"\n', + experimental: true, + ref: LEGACY_VALID_REF, + }); + return Effect.gen(function* () { + yield* legacyDbReset({ ...DEFAULT_FLAGS, linked: true }).pipe(Effect.provide(layer)); + expect(linkedCache.cached).toBe(true); + expect(linkedCache.cachedRef).toBe(LEGACY_VALID_REF); + }); + }); + + it.live( + "surfaces a delegated experimental-reset child failure as a LegacyGoChildExitError under json output", + () => { + const { layer } = setup(tmp.current, { + toml: 'project_id = "test"\n', + experimental: true, + format: "json", + execCaptureExitCode: 3, + }); + return Effect.gen(function* () { + // Under json/stream-json, the delegated path uses `execCapture` (non-text + // branch of `delegateExperimentalReset`) — this must flow through the normal + // Effect failure channel (reachable by `withJsonErrorHandling` at the + // command-wiring layer) instead of an immediate `ProcessControl.exit()` that a + // handler-level test could never observe (CLI-1879). + const exit = yield* legacyDbReset({ ...DEFAULT_FLAGS, linked: true }).pipe( + Effect.provide(layer), + Effect.exit, + ); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + const error = Cause.squash(exit.cause); + expect(error).toBeInstanceOf(LegacyGoChildExitError); + expect((error as LegacyGoChildExitError).exitCode).toBe(3); + } + }); + }, + ); + + it.live( + "propagates the storage-ready check's exact exit code and still flushes telemetry on a local reset", + () => { + // The bootstrap seam's `awaitStorageReady` (the `captureStdout` bootstrap-child + // path) failing non-zero must reach the handler as the exact `LegacyGoChildExitError` + // it fails with, and the handler's own `Effect.ensuring(telemetryState.flush)` + // finalizer must still run despite the typed failure (CLI-1879). + const { layer, telemetry } = setup(tmp.current, { + toml: 'project_id = "test"\n', + args: ["db", "reset"], + isLocal: true, + running: true, + awaitStorageReadyExitCode: 4, + }); + return Effect.gen(function* () { + const exit = yield* legacyDbReset(DEFAULT_FLAGS).pipe(Effect.provide(layer), Effect.exit); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + const error = Cause.squash(exit.cause); + expect(error).toBeInstanceOf(LegacyGoChildExitError); + expect((error as LegacyGoChildExitError).exitCode).toBe(4); + } + expect(telemetry.flushed).toBe(true); + }); + }, + ); + it.live("forwards the linked selector to the delegate even for --linked=false", () => { // Cobra `Changed` semantics: `--linked=false` still selects the linked/remote target in // the parent, so the delegated argv must carry `--linked` — otherwise the Go child falls @@ -962,7 +1113,7 @@ describe("legacy db reset", () => { }); it.live("forwards --db-url and --no-seed on an experimental remote db-url reset", () => { - const { layer, proxy } = setup(tmp.current, { + const { layer, proxy, resolver } = setup(tmp.current, { toml: 'project_id = "test"\n', experimental: true, args: ["db", "reset", "--db-url", "postgresql://db.example.com:5432/postgres"], @@ -981,6 +1132,10 @@ describe("legacy db reset", () => { "--no-seed", "--yes=false", ]); + // Unlike the `connType === "linked"` branch above, a `--db-url` target still + // resolves a connection before delegating — the pre-delegation skip (CLI-1879) + // is scoped to the linked branch only, not "never call resolve when delegating". + expect(resolver.calls).toBe(1); }); }); diff --git a/apps/cli/src/legacy/commands/db/schema/declarative/declarative.smart-target.ts b/apps/cli/src/legacy/commands/db/schema/declarative/declarative.smart-target.ts index 7e6cbab323..77fb087c26 100644 --- a/apps/cli/src/legacy/commands/db/schema/declarative/declarative.smart-target.ts +++ b/apps/cli/src/legacy/commands/db/schema/declarative/declarative.smart-target.ts @@ -169,9 +169,10 @@ export const legacyResolveSmartTargetUrl = Effect.fnUntraced(function* ( } if (shouldReset) { // Go runs reset in-process and returns the error (`cmd/db_schema_declarative.go:262-267`). - // Use the non-exiting seam (not LegacyGoProxy.exec, which process.exits on a - // non-zero child and would skip the handler's telemetry flush / error handling), - // and propagate a failure on a non-zero reset exit. + // `execInherit` (not `LegacyGoProxy.exec`) returns the child's exit code as a + // catchable value rather than exiting the host process — the same + // typed-failure design CLI-1879 gave `LegacyGoProxy.exec` itself, predating + // it here as its own seam. Propagate a failure on a non-zero reset exit. const seam = yield* LegacyDeclarativeSeam; // Forward --network-id: Go's in-process reset.Run honors the root viper // network-id (`apps/cli-go/internal/utils/docker.go:267-271`), so the diff --git a/apps/cli/src/legacy/commands/db/shared/legacy-db-bootstrap.seam.layer.ts b/apps/cli/src/legacy/commands/db/shared/legacy-db-bootstrap.seam.layer.ts index bcf5735978..d6b1793166 100644 --- a/apps/cli/src/legacy/commands/db/shared/legacy-db-bootstrap.seam.layer.ts +++ b/apps/cli/src/legacy/commands/db/shared/legacy-db-bootstrap.seam.layer.ts @@ -8,6 +8,7 @@ import { legacyResolveExperimental, } from "../../../../shared/legacy/global-flags.ts"; import { resolveBinary } from "../../../../shared/legacy/go-proxy.layer.ts"; +import { LegacyGoChildExitError } from "../../../../shared/legacy/legacy-go-child-exit.error.ts"; import { ProcessControl } from "../../../../shared/runtime/process-control.service.ts"; import { LegacyCliConfig } from "../../../config/legacy-cli-config.service.ts"; import { spawnContainerCli } from "../../../shared/legacy-container-cli.ts"; @@ -112,15 +113,19 @@ export const legacyDbBootstrapSeamLayer = Layer.effect( .exitCode(command) .pipe(Effect.mapError(() => seamFailure("failed to run supabase-go."))); if (exitCode !== 0) { - // Fail (rather than `processControl.exit`) so the handler's finalizers — - // `Effect.ensuring(telemetryState.flush)` + the legacy command - // instrumentation — still run; an immediate `process.exit` here would - // skip them. Go likewise exits non-zero on a bootstrap error only after - // its `PersistentPostRun`. The child's detailed failure is already on the - // inherited stderr. (Preserving the child's *exact* exit code while still - // running finalizers would require a shared `runCli` change — deferred.) + // `LegacyGoChildExitError` (not `seamFailure`/`processControl.exit`) so the + // handler's finalizers — `Effect.ensuring(telemetryState.flush)` + the legacy + // command instrumentation — still run (an immediate `process.exit` would skip + // them), AND the child's exact exit code (e.g. 130 after Ctrl-C cleanup) reaches + // `runCli`'s `processControl.exit()` instead of collapsing to a generic 1. The + // child's detailed failure is already on the inherited stderr, so `runCli` + // special-cases this error class to suppress its own normally-would-print + // generic stderr line — Go itself never prints a second line here. CLI-1879. return yield* Effect.fail( - seamFailure(`failed to bootstrap the local database: exit ${exitCode}`), + new LegacyGoChildExitError({ + exitCode, + message: `failed to bootstrap the local database: exit ${exitCode}`, + }), ); } return ""; @@ -138,8 +143,14 @@ export const legacyDbBootstrapSeamLayer = Layer.effect( Effect.mapError(() => seamFailure("failed to bootstrap the local database.")), ); if (exitCode !== 0) { + // See the `!captureStdout` branch above for why `LegacyGoChildExitError` + // replaces `seamFailure` here — same exact-code + finalizer + no-duplicate-line + // reasoning (CLI-1879). return yield* Effect.fail( - seamFailure(`failed to bootstrap the local database: exit ${exitCode}`), + new LegacyGoChildExitError({ + exitCode, + message: `failed to bootstrap the local database: exit ${exitCode}`, + }), ); } return decodeChunks(chunks); diff --git a/apps/cli/src/legacy/commands/db/shared/legacy-db-bootstrap.seam.service.ts b/apps/cli/src/legacy/commands/db/shared/legacy-db-bootstrap.seam.service.ts index 4c29ddb67a..157b290c4b 100644 --- a/apps/cli/src/legacy/commands/db/shared/legacy-db-bootstrap.seam.service.ts +++ b/apps/cli/src/legacy/commands/db/shared/legacy-db-bootstrap.seam.service.ts @@ -1,5 +1,6 @@ import { Context, type Effect } from "effect"; +import type { LegacyGoChildExitError } from "../../../../shared/legacy/legacy-go-child-exit.error.ts"; import type { LegacyDbBootstrapError } from "./legacy-db-bootstrap.errors.ts"; /** @@ -35,7 +36,7 @@ interface LegacyDbBootstrapSeamShape { */ readonly startDatabase: (opts: { readonly fromBackup?: string; - }) => Effect.Effect; + }) => Effect.Effect; /** * The PG14/PG15 container-recreate half of local `db reset` * (`reset.RecreateLocalDatabase`): recreate the db container/volume, init schema, @@ -51,7 +52,7 @@ interface LegacyDbBootstrapSeamShape { readonly version: string; readonly noSeed: boolean; readonly sqlPaths: ReadonlyArray; - }) => Effect.Effect; + }) => Effect.Effect; /** * The storage health gate local `db reset` runs before seeding buckets * (`reset.AwaitStorageReady`): if the storage container exists but is unhealthy, @@ -59,7 +60,10 @@ interface LegacyDbBootstrapSeamShape { * the caller should run the ported bucket seeding) and `false` when it does not * — matching Go, which silently skips buckets when storage is absent. */ - readonly awaitStorageReady: () => Effect.Effect; + readonly awaitStorageReady: () => Effect.Effect< + boolean, + LegacyDbBootstrapError | LegacyGoChildExitError + >; } export class LegacyDbBootstrapSeam extends Context.Service< diff --git a/apps/cli/src/legacy/commands/db/shared/legacy-pgdelta.seam.layer.ts b/apps/cli/src/legacy/commands/db/shared/legacy-pgdelta.seam.layer.ts index 29469069f8..6a52434c89 100644 --- a/apps/cli/src/legacy/commands/db/shared/legacy-pgdelta.seam.layer.ts +++ b/apps/cli/src/legacy/commands/db/shared/legacy-pgdelta.seam.layer.ts @@ -505,6 +505,13 @@ export const legacyDeclarativeSeamLayer = Layer.effect( }), ); +// Intentionally NOT `LegacyGoChildExitError` (contrast `legacy-db-bootstrap.seam.layer.ts`, +// fixed under CLI-1879): this seam's failure is a TS-authored domain summary over noisy +// docker/pgdelta child stderr, not a passthrough of a real Go-CLI child the user invoked +// directly — Go itself wraps every shadow-DB failure into a generic error that `cmd/root.go`'s +// `recoverAndExit` exits `1` for, so propagating THIS child's exact exit code would itself +// diverge from Go, and suppressing this message (as `LegacyGoChildExitError` does for its own, +// already-detailed child stderr) would drop the only actionable line the user sees. const failure = (exitCode?: number) => new LegacyDeclarativeShadowDbError({ message: diff --git a/apps/cli/src/legacy/commands/db/start/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/db/start/SIDE_EFFECTS.md index dcf8466552..dacbedff73 100644 --- a/apps/cli/src/legacy/commands/db/start/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/db/start/SIDE_EFFECTS.md @@ -49,12 +49,16 @@ fresh PG15 volume; that is internal to the seam, not the TS handler.) ## Exit Codes -| Code | Condition | -| ---- | --------------------------------------------------------------------- | -| `0` | success — database started, or already running | -| `1` | malformed `supabase/config.toml` | -| `1` | Docker daemon unreachable / inspect failure | -| `1` | container bootstrap failed (the seam cleans up via `DockerRemoveAll`) | +| Code | Condition | +| -------------------- | --------------------------------------------------------------------- | +| `0` | success — database started, or already running | +| `1` | malformed `supabase/config.toml` | +| `1` | Docker daemon unreachable / inspect failure | +| child's exact code\* | container bootstrap failed (the seam cleans up via `DockerRemoveAll`) | + +\* The `db __db-bootstrap` seam propagates the spawned `supabase-go` child's +real exit code (e.g. `130` after a Ctrl-C mid-bootstrap) instead of collapsing +every failure to `1` — in every `--output-format` (CLI-1879). ## Output diff --git a/apps/cli/src/legacy/commands/db/start/start.integration.test.ts b/apps/cli/src/legacy/commands/db/start/start.integration.test.ts index bc560dd44e..e53edc5f2b 100644 --- a/apps/cli/src/legacy/commands/db/start/start.integration.test.ts +++ b/apps/cli/src/legacy/commands/db/start/start.integration.test.ts @@ -3,7 +3,7 @@ import { join } from "node:path"; import { BunServices } from "@effect/platform-bun"; import { describe, expect, it } from "@effect/vitest"; -import { Effect, Exit, Layer, Option } from "effect"; +import { Cause, Effect, Exit, Layer, Option } from "effect"; import { mockOutput, mockRuntimeInfo } from "../../../../../tests/helpers/mocks.ts"; import { @@ -11,6 +11,7 @@ import { mockLegacyTelemetryStateTracked, useLegacyTempWorkdir, } from "../../../../../tests/helpers/legacy-mocks.ts"; +import { LegacyGoChildExitError } from "../../../../shared/legacy/legacy-go-child-exit.error.ts"; import type { OutputFormat } from "../../../../shared/output/types.ts"; import { LegacyDbBootstrapError } from "../shared/legacy-db-bootstrap.errors.ts"; import { LegacyDbBootstrapSeam } from "../shared/legacy-db-bootstrap.seam.service.ts"; @@ -22,22 +23,40 @@ const DEFAULT_FLAGS: LegacyDbStartFlags = { fromBackup: Option.none() }; /** * Stateful mock of the container-bootstrap seam. `running` drives * `AssertSupabaseDbIsRunning`; `runningFails` / `startFails` make the respective - * call fail (Docker daemon down / StartDatabase error). Records the args passed to - * `startDatabase`. + * call fail (Docker daemon down / StartDatabase error). `startExitCode`, when set, + * fails `startDatabase` with a `LegacyGoChildExitError` carrying that code instead — + * simulating the seam's real bootstrap child (`db __db-bootstrap --mode start`) + * exiting non-zero (CLI-1879). Records the args passed to `startDatabase`. */ -function mockSeam(opts: { running?: boolean; runningFails?: boolean; startFails?: boolean } = {}) { +function mockSeam( + opts: { + running?: boolean; + runningFails?: boolean; + startFails?: boolean; + startExitCode?: number; + } = {}, +) { const startCalls: Array<{ fromBackup?: string }> = []; const layer = Layer.succeed(LegacyDbBootstrapSeam, { isDbRunning: () => opts.runningFails === true ? Effect.fail(new LegacyDbBootstrapError({ message: "failed to inspect service" })) : Effect.succeed(opts.running ?? false), - startDatabase: (args: { fromBackup?: string }) => - opts.startFails === true + startDatabase: (args: { fromBackup?: string }) => { + if (opts.startExitCode !== undefined) { + return Effect.fail( + new LegacyGoChildExitError({ + exitCode: opts.startExitCode, + message: `failed to bootstrap the local database: exit ${opts.startExitCode}`, + }), + ); + } + return opts.startFails === true ? Effect.fail(new LegacyDbBootstrapError({ message: "failed to bootstrap" })) : Effect.sync(() => { startCalls.push(args); - }), + }); + }, recreateDatabase: () => Effect.void, awaitStorageReady: () => Effect.succeed(false), }); @@ -57,6 +76,7 @@ function setup( running?: boolean; runningFails?: boolean; startFails?: boolean; + startExitCode?: number; /** Caller cwd (Go's `CurrentDirAbs`) for relative `--from-backup` resolution. */ cwd?: string; }, @@ -211,6 +231,32 @@ describe("legacy db start", () => { }); }); + it.live( + "propagates the bootstrap child's exact exit code as LegacyGoChildExitError and still flushes telemetry", + () => { + // The bootstrap seam's `startDatabase` (the `!captureStdout` bootstrap-child + // path) failing non-zero must reach the handler as the exact `LegacyGoChildExitError` + // it fails with — not a generic `LegacyDbBootstrapError` collapsing every exit code to + // 1 — and the handler's own `Effect.ensuring(telemetryState.flush)` finalizer must + // still run despite the typed failure (CLI-1879). + const { layer, telemetry } = setup(tmp.current, { + toml: 'project_id = "test"\n', + running: false, + startExitCode: 3, + }); + return Effect.gen(function* () { + const exit = yield* legacyDbStart(DEFAULT_FLAGS).pipe(Effect.provide(layer), Effect.exit); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + const error = Cause.squash(exit.cause); + expect(error).toBeInstanceOf(LegacyGoChildExitError); + expect((error as LegacyGoChildExitError).exitCode).toBe(3); + } + expect(telemetry.flushed).toBe(true); + }); + }, + ); + it.live("emits a json result when the database is already running", () => { const { layer, out } = setup(tmp.current, { toml: 'project_id = "test"\n', diff --git a/apps/cli/src/shared/cli/run.ts b/apps/cli/src/shared/cli/run.ts index 48ab86fa05..b0db350a35 100644 --- a/apps/cli/src/shared/cli/run.ts +++ b/apps/cli/src/shared/cli/run.ts @@ -11,6 +11,7 @@ import { outputLayerFor } from "../output/output.layer.ts"; import { normalizeCause } from "../output/normalize-error.ts"; import type { OutputFormat } from "../output/types.ts"; import { Output } from "../output/output.service.ts"; +import { LegacyGoChildExitError } from "../legacy/legacy-go-child-exit.error.ts"; import { cliConfigLayer } from "../../next/config/cli-config.layer.ts"; import { projectHomeLayer } from "../../next/config/project-home.layer.ts"; import { ProjectLocalServiceVersions } from "../../next/config/project-local-service-versions.service.ts"; @@ -115,6 +116,27 @@ export function exitCodeForFailure(cause: Cause.Cause): number { return Runtime.getErrorExitCode(Cause.squash(cause)); } +/** + * Whether `handledProgram` should render its generic `output.fail` stderr line + * for a failed run, given the run's cause and the exit code `exitCodeForFailure` + * already computed for it. False for a clean exit (`0`), an interrupt (`130`), + * and a `LegacyGoChildExitError` (CLI-1879) — a delegated Go child already wrote + * its own detailed failure to the inherited stderr, so a second generic line + * here would be a line Go itself never prints. + * + * Checked by concrete type, NOT Effect's shared `[Runtime.errorReported]` + * marker: `CliError.ShowHelp` also sets that marker to `false`, for an + * unrelated reason (the CLI framework already rendered help/usage text) — + * gating on the marker would ALSO suppress `normalizeCause`'s Go-parity + * rendering for a `MissingOption` wrapped in `ShowHelp` (e.g. `Error: required + * flag(s) "type" not set`), a real parity regression. See the test suite for + * the regression this guards. + */ +export function shouldReportFailure(cause: Cause.Cause, exitCode: number): boolean { + if (exitCode === 0 || exitCode === 130) return false; + return !(Cause.squash(cause) instanceof LegacyGoChildExitError); +} + function projectContextLayerFor(runtimeLayer: Layer.Layer) { return projectContextLayer.pipe(Layer.provide(runtimeLayer), Layer.provide(BunServices.layer)); } @@ -242,13 +264,13 @@ export async function runCli(rootCommand: Command.Command.Any, options: RunCliOp const exit = yield* program.pipe(Effect.exit); if (Exit.isFailure(exit)) { const exitCode = exitCodeForFailure(exit.cause); - // Skip reporting for an interrupted run (130 — a signal, not a - // reportable error) and for a clean `ShowHelp` failure (0). Literal - // `--help` never reaches this branch — it's handled as a successful - // `GlobalFlag.Action` and exits 0 via the success path below. See - // `exitCodeForFailure` for why a "clean" ShowHelp failure (e.g. a bare - // group command with no subcommand) also maps to exit 0. - if (exitCode !== 0 && exitCode !== 130) { + // See `shouldReportFailure` for the reporting rules (and why they're + // NOT keyed on Effect's shared `[Runtime.errorReported]` marker). + // Literal `--help` never reaches this branch — it's handled as a + // successful `GlobalFlag.Action` and exits 0 via the success path + // below. See `exitCodeForFailure` for why a "clean" ShowHelp failure + // (e.g. a bare group command with no subcommand) also maps to exit 0. + if (shouldReportFailure(exit.cause, exitCode)) { yield* output.fail(normalizeCause(exit.cause)); } return yield* processControl.exit(exitCode); diff --git a/apps/cli/src/shared/cli/run.unit.test.ts b/apps/cli/src/shared/cli/run.unit.test.ts index f87d160533..290c9e9557 100644 --- a/apps/cli/src/shared/cli/run.unit.test.ts +++ b/apps/cli/src/shared/cli/run.unit.test.ts @@ -2,7 +2,13 @@ import { Cause } from "effect"; import { CliError } from "effect/unstable/cli"; import { describe, expect, it } from "vitest"; -import { exitCodeForFailure, extractCommandPath, shouldUseGlobalSignalInterrupt } from "./run.ts"; +import { LegacyGoChildExitError } from "../legacy/legacy-go-child-exit.error.ts"; +import { + exitCodeForFailure, + extractCommandPath, + shouldReportFailure, + shouldUseGlobalSignalInterrupt, +} from "./run.ts"; describe("extractCommandPath", () => { it("returns positional command-path tokens", () => { @@ -87,4 +93,55 @@ describe("exitCodeForFailure", () => { it("exits 130 when interrupted, regardless of any other failure reason", () => { expect(exitCodeForFailure(Cause.interrupt())).toBe(130); }); + + // CLI-1879: a delegated Go child's exact exit code (not just a generic 1) + // must reach the user, via the `LegacyGoChildExitError`'s + // `[Runtime.errorExitCode]` marker. + it("exits with a LegacyGoChildExitError's exact exit code", () => { + const cause = Cause.fail( + new LegacyGoChildExitError({ exitCode: 130, message: "supabase-go exited with code 130" }), + ); + expect(exitCodeForFailure(cause)).toBe(130); + }); +}); + +describe("shouldReportFailure", () => { + it("does not report a clean exit (0)", () => { + expect(shouldReportFailure(Cause.fail(new Error("unused")), 0)).toBe(false); + }); + + it("does not report an interrupt (130)", () => { + expect(shouldReportFailure(Cause.interrupt(), 130)).toBe(false); + }); + + // CLI-1879: the child already wrote its own detailed failure to the + // inherited stderr, so `runCli`'s generic line would be a duplicate Go + // itself never prints. + it("does not report a LegacyGoChildExitError", () => { + const cause = Cause.fail( + new LegacyGoChildExitError({ exitCode: 1, message: "supabase-go exited with code 1" }), + ); + expect(shouldReportFailure(cause, 1)).toBe(false); + }); + + it("reports a non-ShowHelp failure", () => { + expect(shouldReportFailure(Cause.fail(new Error("boom")), 1)).toBe(true); + }); + + // Regression guard: `CliError.ShowHelp` ALSO sets Effect's shared + // `[Runtime.errorReported]` marker to `false` (for an unrelated reason — the + // CLI framework already rendered help/usage text). `shouldReportFailure` + // must NOT key on that shared marker, or it would also suppress + // `normalizeCause`'s Go-parity rendering for a `MissingOption` wrapped in + // `ShowHelp` (e.g. `Error: required flag(s) "type" not set`) — silently + // dropping that message for every command with a required flag. + it("still reports a ShowHelp failure carrying a genuine validation error (e.g. a missing required flag)", () => { + const cause = Cause.fail( + new CliError.ShowHelp({ + commandPath: ["sso", "add"], + errors: [new CliError.MissingOption({ option: "--type" })], + }), + ); + expect(shouldReportFailure(cause, 1)).toBe(true); + }); }); diff --git a/apps/cli/src/shared/legacy/go-proxy.layer.ts b/apps/cli/src/shared/legacy/go-proxy.layer.ts index b190d9f109..d359113450 100644 --- a/apps/cli/src/shared/legacy/go-proxy.layer.ts +++ b/apps/cli/src/shared/legacy/go-proxy.layer.ts @@ -8,6 +8,7 @@ import * as ChildProcess from "effect/unstable/process/ChildProcess"; import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner"; import { CLI_VERSION } from "../cli/version.ts"; import { ProcessControl } from "../runtime/process-control.service.ts"; +import { LegacyGoChildExitError } from "./legacy-go-child-exit.error.ts"; import { LegacyGoProxy } from "./go-proxy.service.ts"; // --------------------------------------------------------------------------- @@ -139,8 +140,8 @@ export function makeGoProxyLayer(opts?: { * Override binary resolution. Primarily a test seam so specs don't have to * mutate `process.env.SUPABASE_GO_BINARY` or stub the filesystem: * - `string` — treat as the resolved Go binary path. - * - `{ notFound: [...] }` — simulate the not-found path; `.exec` will - * print the diagnostic and exit non-zero. + * - `{ notFound: [...] }` — simulate the not-found path; `.exec` will print + * the diagnostic and fail with a non-zero exit code. * * In production, leave unset and let `resolveBinary()` pick the right * artifact for the host platform. @@ -166,12 +167,16 @@ export function makeGoProxyLayer(opts?: { // CLI-1488: never silently fall back to `supabase` on PATH — // when the shim is on PATH and `supabase-go` is not co-located, // that fallback resolves to the shim itself and fork-bombs. - // Print a specific diagnostic and exit non-zero instead. + // Print a specific diagnostic and fail non-zero instead. yield* Effect.sync(() => { process.stderr.write(`${formatGoBinaryNotFoundError(resolved.notFound)}\n`); }); - yield* processControl.exit(1); - return; + return yield* Effect.fail( + new LegacyGoChildExitError({ + exitCode: 1, + message: "supabase-go binary not found", + }), + ); } const binary = resolved.found; @@ -212,7 +217,12 @@ export function makeGoProxyLayer(opts?: { }); const exitCode = yield* spawner.exitCode(command).pipe(Effect.orDie); if (exitCode !== 0) { - yield* processControl.exit(exitCode); + return yield* Effect.fail( + new LegacyGoChildExitError({ + exitCode, + message: `supabase-go exited with code ${exitCode} (see stderr for details)`, + }), + ); } }), ), @@ -223,7 +233,12 @@ export function makeGoProxyLayer(opts?: { yield* Effect.sync(() => { process.stderr.write(`${formatGoBinaryNotFoundError(resolved.notFound)}\n`); }); - return yield* processControl.exit(1); + return yield* Effect.fail( + new LegacyGoChildExitError({ + exitCode: 1, + message: "supabase-go binary not found", + }), + ); } const binary = resolved.found; yield* processControl.holdSignals(["SIGINT", "SIGTERM", "SIGHUP"]); @@ -252,7 +267,12 @@ export function makeGoProxyLayer(opts?: { ); const exitCode = yield* handle.exitCode.pipe(Effect.orDie); if (exitCode !== 0) { - return yield* processControl.exit(exitCode); + return yield* Effect.fail( + new LegacyGoChildExitError({ + exitCode, + message: `supabase-go exited with code ${exitCode} (see stderr for details)`, + }), + ); } return captured; }), diff --git a/apps/cli/src/shared/legacy/go-proxy.layer.unit.test.ts b/apps/cli/src/shared/legacy/go-proxy.layer.unit.test.ts index 8fc4713d7b..7a305197a8 100644 --- a/apps/cli/src/shared/legacy/go-proxy.layer.unit.test.ts +++ b/apps/cli/src/shared/legacy/go-proxy.layer.unit.test.ts @@ -1,7 +1,8 @@ import { describe, expect, it, vi } from "@effect/vitest"; -import { Deferred, Effect, Fiber, Layer, Sink, Stream } from "effect"; +import { Cause, Deferred, Effect, Exit, Fiber, Layer, Sink, Stream } from "effect"; import { ChildProcessSpawner } from "effect/unstable/process"; import { type CliProcessSignal, ProcessControl } from "../runtime/process-control.service.ts"; +import { LegacyGoChildExitError } from "./legacy-go-child-exit.error.ts"; import { LegacyGoProxy } from "./go-proxy.service.ts"; import { formatGoBinaryNotFoundError, makeGoProxyLayer } from "./go-proxy.layer.ts"; @@ -54,12 +55,14 @@ type HoldEvent = * event log. Each acquire gets a monotonically increasing id so tests can * pair an acquire with its release and distinguish concurrent scopes. * - * `exitBehavior`: - * - "never" → exit() blocks on Effect.never (test manages the fiber) - * - "terminateDie" → exit() dies with a tagged defect so callers can - * observe via Effect.exit without juggling fibers + * The layer under test no longer calls `ProcessControl.exit()` itself on a + * non-zero exit or an unresolved binary (CLI-1879 routes both through + * `LegacyGoChildExitError` instead, so `runCli` can run finalizers before + * exiting) — `exit()` here only guards against a future regression that + * reintroduces a direct call; it blocks on `Effect.never` since nothing in + * this file exercises it. */ -function mockProcessControl(opts: { exitBehavior?: "never" | "terminateDie" } = {}) { +function mockProcessControl() { const holdEvents: HoldEvent[] = []; const exitCalls: number[] = []; let nextHoldId = 0; @@ -67,11 +70,7 @@ function mockProcessControl(opts: { exitBehavior?: "never" | "terminateDie" } = const exit = (code: number) => Effect.sync(() => { exitCalls.push(code); - }).pipe( - Effect.flatMap(() => - opts.exitBehavior === "terminateDie" ? Effect.die("EXIT_CALLED" as const) : Effect.never, - ), - ); + }).pipe(Effect.flatMap(() => Effect.never)); return { get holdEvents() { @@ -284,18 +283,52 @@ describe("makeGoProxyLayer", () => { }).pipe(Effect.provide(layer)); }); - it.effect("propagates non-zero exit codes via ProcessControl.exit", () => { + it.effect("propagates non-zero exit codes via LegacyGoChildExitError", () => { const spawner = mockSpawner({ kind: "success", code: 7 }); - // Use the terminating exit variant so we can observe via Effect.exit - // without juggling forked fibers around Effect.never. - const pc = mockProcessControl({ exitBehavior: "terminateDie" }); + const pc = mockProcessControl(); const layer = makeGoProxyLayer({ binary: TEST_BINARY }).pipe( Layer.provide(Layer.mergeAll(spawner.layer, pc.layer)), ); return Effect.gen(function* () { const proxy = yield* LegacyGoProxy; - yield* proxy.exec(["some", "command"]).pipe(Effect.exit); - expect(pc.exitCalls).toEqual([7]); + const exit = yield* proxy.exec(["some", "command"]).pipe(Effect.exit); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + const error = Cause.squash(exit.cause); + expect(error).toBeInstanceOf(LegacyGoChildExitError); + expect((error as LegacyGoChildExitError).exitCode).toBe(7); + } + // The layer itself never calls `ProcessControl.exit` — that's now + // `runCli`'s job, after finalizers have run. + expect(pc.exitCalls).toEqual([]); + }).pipe(Effect.provide(layer)); + }); + + it.effect("lets an Effect.ensuring finalizer run after a non-zero exit (CLI-1879)", () => { + // The whole point of routing a non-zero exit through `LegacyGoChildExitError` + // instead of `ProcessControl.exit()` (a real `process.exit()` in production): + // a caller's own `Effect.ensuring` finalizer — e.g. a handler's + // `Effect.ensuring(telemetryState.flush)` — must still run. Under the old + // `processControl.exit()`-based implementation this finalizer would never fire + // (production: the process would already be dead; this mock's `exit()` blocks + // forever on `Effect.never`, so the fiber never reaches `Effect.exit` either). + const spawner = mockSpawner({ kind: "success", code: 5 }); + const pc = mockProcessControl(); + const layer = makeGoProxyLayer({ binary: TEST_BINARY }).pipe( + Layer.provide(Layer.mergeAll(spawner.layer, pc.layer)), + ); + let finalizerRan = false; + return Effect.gen(function* () { + const proxy = yield* LegacyGoProxy; + yield* proxy.exec(["some", "command"]).pipe( + Effect.ensuring( + Effect.sync(() => { + finalizerRan = true; + }), + ), + Effect.exit, + ); + expect(finalizerRan).toBe(true); }).pipe(Effect.provide(layer)); }); @@ -401,36 +434,75 @@ describe("makeGoProxyLayer", () => { // literal string "supabase" when no Go binary was found, which when run from // a PATH that contained the shim would fork-bomb the shim against itself // (silent multi-minute hang in CI followed by SIGTERM). The layer must now - // refuse to spawn anything and surface a specific diagnostic + non-zero exit. - it.effect("prints a diagnostic and exits 1 when supabase-go cannot be resolved", () => { - const spawner = mockSpawner({ kind: "success", code: 0 }); - const pc = mockProcessControl({ exitBehavior: "terminateDie" }); - const stderr = vi.spyOn(process.stderr, "write").mockImplementation(() => true); - const tried = [ - "$SUPABASE_GO_BINARY (unset)", - "/usr/local/bin/supabase-go (not found alongside the shim)", - ]; - const layer = makeGoProxyLayer({ binary: { notFound: tried } }).pipe( - Layer.provide(Layer.mergeAll(spawner.layer, pc.layer)), - ); - return Effect.gen(function* () { - const proxy = yield* LegacyGoProxy; - yield* proxy.exec(["db", "start"]).pipe(Effect.exit); - - // Did NOT spawn anything — the whole point is to refuse the fork-bomb. - expect(spawner.spawned).toHaveLength(0); - // Exited with code 1 via ProcessControl.exit. - expect(pc.exitCalls).toEqual([1]); - // Wrote the diagnostic to stderr, including each tried location. - expect(stderr).toHaveBeenCalledTimes(1); - const written = String(stderr.mock.calls[0]![0]); - expect(written).toContain("Could not find the `supabase-go` binary"); - expect(written).toContain("$SUPABASE_GO_BINARY (unset)"); - expect(written).toContain("/usr/local/bin/supabase-go"); - expect(written).toContain("SUPABASE_GO_BINARY"); - stderr.mockRestore(); - }).pipe(Effect.provide(layer)); - }); + // refuse to spawn anything and surface a specific diagnostic + a + // `LegacyGoChildExitError` carrying exit code 1. + it.effect( + "prints a diagnostic and fails with exit code 1 when supabase-go cannot be resolved", + () => { + const spawner = mockSpawner({ kind: "success", code: 0 }); + const pc = mockProcessControl(); + const stderr = vi.spyOn(process.stderr, "write").mockImplementation(() => true); + const tried = [ + "$SUPABASE_GO_BINARY (unset)", + "/usr/local/bin/supabase-go (not found alongside the shim)", + ]; + const layer = makeGoProxyLayer({ binary: { notFound: tried } }).pipe( + Layer.provide(Layer.mergeAll(spawner.layer, pc.layer)), + ); + return Effect.gen(function* () { + const proxy = yield* LegacyGoProxy; + const exit = yield* proxy.exec(["db", "start"]).pipe(Effect.exit); + + // Did NOT spawn anything — the whole point is to refuse the fork-bomb. + expect(spawner.spawned).toHaveLength(0); + // Failed with a LegacyGoChildExitError carrying exit code 1, rather than + // calling `ProcessControl.exit` directly — that's now `runCli`'s job. + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + const error = Cause.squash(exit.cause); + expect(error).toBeInstanceOf(LegacyGoChildExitError); + expect((error as LegacyGoChildExitError).exitCode).toBe(1); + } + expect(pc.exitCalls).toEqual([]); + // Wrote the diagnostic to stderr, including each tried location. + expect(stderr).toHaveBeenCalledTimes(1); + const written = String(stderr.mock.calls[0]![0]); + expect(written).toContain("Could not find the `supabase-go` binary"); + expect(written).toContain("$SUPABASE_GO_BINARY (unset)"); + expect(written).toContain("/usr/local/bin/supabase-go"); + expect(written).toContain("SUPABASE_GO_BINARY"); + stderr.mockRestore(); + }).pipe(Effect.provide(layer)); + }, + ); + + it.effect( + "execCapture also prints a diagnostic and fails with exit code 1 when supabase-go cannot be resolved", + () => { + const spawner = mockSpawner({ kind: "success", code: 0 }); + const pc = mockProcessControl(); + const stderr = vi.spyOn(process.stderr, "write").mockImplementation(() => true); + const tried = ["$SUPABASE_GO_BINARY (unset)"]; + const layer = makeGoProxyLayer({ binary: { notFound: tried } }).pipe( + Layer.provide(Layer.mergeAll(spawner.layer, pc.layer)), + ); + return Effect.gen(function* () { + const proxy = yield* LegacyGoProxy; + const exit = yield* proxy.execCapture(["db", "dump"]).pipe(Effect.exit); + + expect(spawner.spawned).toHaveLength(0); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + const error = Cause.squash(exit.cause); + expect(error).toBeInstanceOf(LegacyGoChildExitError); + expect((error as LegacyGoChildExitError).exitCode).toBe(1); + } + expect(pc.exitCalls).toEqual([]); + expect(stderr).toHaveBeenCalledTimes(1); + stderr.mockRestore(); + }).pipe(Effect.provide(layer)); + }, + ); it.effect("opens and closes a fresh hold scope per sequential exec call", () => { const spawner = mockSpawner({ kind: "success", code: 0 }); diff --git a/apps/cli/src/shared/legacy/go-proxy.service.ts b/apps/cli/src/shared/legacy/go-proxy.service.ts index e9539e75a4..6ea6a50eb4 100644 --- a/apps/cli/src/shared/legacy/go-proxy.service.ts +++ b/apps/cli/src/shared/legacy/go-proxy.service.ts @@ -1,11 +1,15 @@ import type { Effect } from "effect"; import { Context } from "effect"; +import type { LegacyGoChildExitError } from "./legacy-go-child-exit.error.ts"; interface LegacyGoProxyShape { /** * Forward the given args to the Go binary, inheriting stdin/stdout/stderr - * and propagating the exit code. On a non-zero exit the process exits with - * the same code — callers do not need to handle the failure case. + * and propagating the exit code. On a non-zero exit (or when the binary + * cannot be resolved at all), fails with `LegacyGoChildExitError` carrying + * the child's exact exit code; callers don't need to special-case it — it + * flows through the normal Effect failure channel up to `runCli`, which + * maps it to the real process exit code after running any finalizers. * * `opts.cwd` overrides the working directory for this call (falls back to the * layer's construction-time cwd). `opts.env` overlays extra environment @@ -17,13 +21,16 @@ interface LegacyGoProxyShape { readonly exec: ( args: ReadonlyArray, opts?: { readonly cwd?: string; readonly env?: Record }, - ) => Effect.Effect; + ) => Effect.Effect; /** * Like `exec`, but captures the child's stdout and returns it as a string * instead of inheriting stdout. stderr is still inherited (so progress / - * diagnostics pass straight through), and a non-zero exit still terminates the - * process with the same code. + * diagnostics pass straight through). On a non-zero exit (or when the binary + * cannot be resolved at all), fails with `LegacyGoChildExitError` carrying + * the child's exact exit code; callers don't need to special-case it — it + * flows through the normal Effect failure channel up to `runCli`, which + * maps it to the real process exit code after running any finalizers. * * `opts.stdin` controls the child's stdin: `"inherit"` (default) keeps the * child interactive (its prompts reach the terminal); `"ignore"` gives it a @@ -43,7 +50,7 @@ interface LegacyGoProxyShape { readonly env?: Record; readonly stdin?: "inherit" | "ignore"; }, - ) => Effect.Effect; + ) => Effect.Effect; } export class LegacyGoProxy extends Context.Service()( diff --git a/apps/cli/src/shared/legacy/legacy-go-child-exit.error.ts b/apps/cli/src/shared/legacy/legacy-go-child-exit.error.ts new file mode 100644 index 0000000000..d2b7f22ef3 --- /dev/null +++ b/apps/cli/src/shared/legacy/legacy-go-child-exit.error.ts @@ -0,0 +1,55 @@ +import { Data, Runtime } from "effect"; + +/** + * A spawned `supabase-go` child process — via `LegacyGoProxy.exec`/`execCapture`, + * or the hidden `db __db-bootstrap` seam (`legacy-db-bootstrap.seam.layer.ts`) — + * exited non-zero, or could not be spawned at all (binary not found). + * + * Carries the child's exact exit code through Effect's `Runtime.errorExitCode` + * marker, so both `runCli` (`shared/cli/run.ts`, text mode) and + * `withJsonErrorHandling` (`shared/output/json-error-handling.ts`, `json`/ + * `stream-json` mode) map the process's own exit code to this EXACT number — + * not a generic `1` — and only AFTER every `Effect.ensuring` finalizer between + * the call site and there has already run (telemetry flush, command + * instrumentation). Calling `ProcessControl.exit()` directly from deep inside a + * handler skips those finalizers entirely (`process.exit()` halts the process + * before the Effect runtime can unwind the remaining scopes) — this error type + * lets the child's status flow through the normal Effect failure channel + * instead, all the way up to the single `ProcessControl.exit()` call `runCli` + * itself makes once finalizers are done. See CLI-1879. + * + * `runCli`'s `handledProgram` special-cases this exact class (an `instanceof` + * check, not a shared Effect marker) to skip its generic `output.fail` stderr + * line in text mode — the child already wrote its own detailed failure (or, + * for the not-found case, `LegacyGoProxy`'s own specific diagnostic) to the + * parent's inherited stderr, and Go itself never prints a second, generic line + * on top of that. This is deliberately NOT keyed on Effect's shared + * `[Runtime.errorReported]` marker: `CliError.ShowHelp` also sets that marker + * to `false` for an unrelated reason (the CLI framework already rendered + * help/usage text), and gating on the marker there would ALSO suppress + * `normalizeCause`'s Go-parity rendering for a `MissingOption` wrapped in + * `ShowHelp` (e.g. `Error: required flag(s) "type" not set`) — a real parity + * regression. `withJsonErrorHandling` has no such collision (it runs upstream + * of `runCli`, catching every error uniformly) and still emits the structured + * JSON error envelope for this error like any other. + * + * The envelope's `message` is deliberately generic (`"supabase-go exited with + * code N (see stderr for details)"`) rather than the child's specific failure + * reason: the child's real detail is on stderr (see above), which a + * machine-output consumer reading only stdout won't see — this is an accepted, + * TS-only tradeoff (Go itself has no JSON error-envelope concept to match + * against here), not a parity gap. + * + * Invariant: `exitCode` must be a real non-zero child exit status (1-255, + * matching `ChildProcessSpawner.ExitCode`'s POSIX range), never `0` — every + * construction site guards on `exitCode !== 0` (or hardcodes `1` for the + * binary-not-found case) before constructing this error, since a `0` here + * would be a failure that both `runCli` and `withJsonErrorHandling` read back + * as a *successful* exit. + */ +export class LegacyGoChildExitError extends Data.TaggedError("LegacyGoChildExitError")<{ + readonly exitCode: number; + readonly message: string; +}> { + override readonly [Runtime.errorExitCode] = this.exitCode; +} diff --git a/apps/cli/src/shared/output/json-error-handling.ts b/apps/cli/src/shared/output/json-error-handling.ts index 756077692f..2a886e8cf8 100644 --- a/apps/cli/src/shared/output/json-error-handling.ts +++ b/apps/cli/src/shared/output/json-error-handling.ts @@ -1,4 +1,4 @@ -import { Effect } from "effect"; +import { Effect, Runtime } from "effect"; import { Output } from "./output.service.ts"; import { ProcessControl } from "../runtime/process-control.service.ts"; import { normalizeCliError } from "./normalize-error.ts"; @@ -13,7 +13,13 @@ export const withJsonErrorHandling = ( const processControl = yield* ProcessControl; if (output.format === "text") return yield* Effect.fail(error); yield* output.fail(normalizeCliError(error)); - yield* processControl.setExitCode(1); + // `Runtime.getErrorExitCode` defaults to 1 for any error without a + // `[Runtime.errorExitCode]` marker, so this is a no-op for every existing + // error type. It only changes behavior for an error that opts in — e.g. + // `LegacyGoChildExitError` (CLI-1879), so a delegated Go child's exact exit + // code (not just a generic 1) still reaches the user under json/stream-json, + // matching the exit code `runCli`'s text-mode path already propagates. + yield* processControl.setExitCode(Runtime.getErrorExitCode(error)); }), ), ); diff --git a/apps/cli/src/shared/output/json-error-handling.unit.test.ts b/apps/cli/src/shared/output/json-error-handling.unit.test.ts index e763b8df19..29e7ec90c8 100644 --- a/apps/cli/src/shared/output/json-error-handling.unit.test.ts +++ b/apps/cli/src/shared/output/json-error-handling.unit.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "@effect/vitest"; import { Data, Effect, Exit, Layer, Option } from "effect"; import { mockProcessControl } from "../../../tests/helpers/mocks.ts"; +import { LegacyGoChildExitError } from "../legacy/legacy-go-child-exit.error.ts"; import { Output } from "./output.service.ts"; import { withJsonErrorHandling } from "./json-error-handling.ts"; @@ -181,5 +182,24 @@ describe("withJsonErrorHandling", () => { expect(out.failCalls[0]?.message).toBe("plain error message"); }).pipe(Effect.provide(out.layer), Effect.provide(processControl.layer)); }); + + // CLI-1879: a delegated Go child's exact exit code must reach the user under + // json/stream-json too, not just a generic 1 — matching the exit code + // `runCli`'s text-mode path already propagates via the same + // `[Runtime.errorExitCode]` marker. + it.live("sets the exact exit code for a LegacyGoChildExitError, not a generic 1", () => { + const out = mockOutput("json"); + const processControl = mockProcessControl(); + return Effect.gen(function* () { + const error = new LegacyGoChildExitError({ + exitCode: 130, + message: "supabase-go exited with code 130 (see stderr for details)", + }); + yield* withJsonErrorHandling(Effect.fail(error)).pipe(Effect.provide(out.layer)); + expect(out.failCalls).toHaveLength(1); + expect(out.failCalls[0]?.code).toBe("LegacyGoChildExitError"); + expect(processControl.exitCode).toBe(130); + }).pipe(Effect.provide(out.layer), Effect.provide(processControl.layer)); + }); }); }); From 1bd4085cfc04129c32c139d5ba112aac6f0a87dd Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Thu, 9 Jul 2026 14:34:14 +0100 Subject: [PATCH 34/79] fix(cli): legacy shell honors project .env for SUPABASE_YES + auth.enabled remote precedence (CLI-1878) (#5839) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What Closes the remaining gaps in [CLI-1878](https://linear.app/supabase/issue/CLI-1878/legacy-shell-full-viper-env-override-semantics-project-env-remote): full viper env-override semantics (project `.env`, remote precedence, malformed bools) in the TS legacy shell. A `go-parity-auditor` pass determined most of the issue's original claims had already been fixed by an earlier PR (#5715) — project-`.env`-aware `SUPABASE_YES`/`SUPABASE_EXPERIMENTAL` resolvers, remote-config precedence, malformed-bool-fails-the-load, explicit-flag-beats-env, and case-agnostic `env(...)` resolution all already exist for `db push`/`db reset`/`db pull`/`config push`/`migration down`/`repair`/declarative-schema. This PR closes the **5 concrete gaps** that pass left open: 1. **`gen signing-key`** — the overwrite-confirmation prompt only consulted the shell env for `SUPABASE_YES`. Go's `flags.LoadConfig` loads the project `.env` before the prompt (`signingkeys.go:99,130`). 2. **`storage rm`** — same gap for the delete-confirmation prompt (both the `--local` and default `--linked` branches of Go's `ParseDatabaseConfig` load the project `.env` first). 3. **`migration fetch`** — same gap for the migrations-dir overwrite prompt (defaults to `--linked`, same `ParseDatabaseConfig` path). 4. **standalone `seed buckets`** — its fallback `yes` resolution (used when `db reset` doesn't pass a pre-resolved value) had the same gap. `db reset`'s own passthrough was already correct. 5. **`[remotes.*].auth.enabled` remote precedence** — this key was missing from `LEGACY_ENV_OVERRIDABLE_KEYS`, so a linked remote's `auth.enabled` TOML value could lose to a `SUPABASE_AUTH_ENABLED` env var instead of winning, unlike every other allowlisted key (Go's `mergeRemoteConfig` applies the whole matched block above `AutomaticEnv`). Each of the 4 handler fixes follows the existing `legacyLoadProjectEnv` + `legacyResolveYesWithProjectEnv` pattern already used by `db push`/`config push`/`migration down`/`repair`. ### Fixed along the way - `migration fetch`'s new project-`.env` load initially ran *before* the `[db-url linked local]` flag-conflict check — an ordering regression relative to Go and sibling `migration down`/`repair` (caught by `architect-reviewer`). Reordered so the flag check runs first; added a regression test that fails without the fix. ## Behavior changes to be aware of - `gen signing-key`, `storage rm`, `migration fetch`, and `seed buckets` now honor `SUPABASE_YES` set only in `supabase/.env`/`.env.local`/`.env.[.local]` (previously they only saw the shell env). This is a pure Go-parity fix, but on an earlier build of this TS legacy shell it was inert — a stale `SUPABASE_YES=true` left in a project's `.env` will now silently auto-confirm these destructive prompts. - A linked project's `[remotes.].auth.enabled` TOML value now correctly beats a `SUPABASE_AUTH_ENABLED` env var (previously the reverse). If anything relied on the env var overriding a remote block's explicit `auth.enabled`, that no longer happens. ## Test plan - `bun run test:core` — all unit + integration tests green (300 tests across the touched files, full suite unaffected). - `bun run check:all` — types/lint/fmt/knip clean. - New regression tests (integration, per this workspace's testing pyramid): - `signing-key.integration.test.ts` — project-`.env` `SUPABASE_YES` auto-confirms the overwrite even with a piped `n` (defensively clears any leaked shell `SUPABASE_YES` first). - `rm.integration.test.ts` — same, for the delete confirmation. - `buckets.integration.test.ts` — same, for the standalone `seed buckets` overwrite prompt. - `fetch.integration.test.ts` — same, for the migrations-dir overwrite prompt; plus a new test locking in the flag-conflict-before-env-read ordering fix (verified it fails without the reorder). - `legacy-db-config.toml-read.unit.test.ts` — 2 new unit tests for `auth.enabled`: a matched remote block beats the env var, and a control case where the env var still wins when the block omits the key. - Relocated one pre-existing test's fixture (`fetch.integration.test.ts`, "reports a write failure"): the file-collision now lives at `/supabase/migrations` instead of `/supabase` itself, since the latter would break the new project-`.env` read before ever reaching the `mkdir` under test. Verified this preserves the original test's coverage. ## Judgement calls deliberately left open - **`[y/N] y` echo doesn't say *why* it auto-confirmed.** `supabase-dx-reviewer` flagged that the prompt echo is byte-identical whether the answer came from `--yes`, the shell env, or a forgotten project `.env` value — but explicitly recommended *not* annotating it, since that would diverge from Go's byte-identical `console.PromptYesNo` output (this is a strict 1:1 port). Not changed here; a source annotation would need to land in the Go CLI first if ever desired. - **DB-bootstrap seam explicit `--experimental=false` forwarding.** `go-parity-auditor` flagged (but could not fully confirm, since it didn't trace the hidden `db __db-bootstrap` subprocess) that `legacy-db-bootstrap.seam.layer.ts` forwards `--experimental` to the Go child only when true, never an explicit false — for `db reset --experimental=false` with `SUPABASE_EXPERIMENTAL=true` inherited by the child, the child might re-resolve `true` independently. Flagged as a caveat, not a confirmed gap; out of scope here. - **`storage.enabled`/`realtime.enabled` don't read any `SUPABASE_*` env override at all** in `legacy-db-config.toml-read.ts`, unlike `auth.enabled`. This is a latent divergence noted by `go-parity-auditor` but is outside CLI-1878's 5 named items; left as a separate follow-up. 🤖 Generated with the `issue-autopilot` skill. --- .../commands/gen/signing-key/SIDE_EFFECTS.md | 19 +++--- .../gen/signing-key/signing-key.handler.ts | 13 +++- .../signing-key.integration.test.ts | 67 +++++++++++++++++++ .../commands/migration/fetch/SIDE_EFFECTS.md | 19 +++--- .../commands/migration/fetch/fetch.handler.ts | 18 ++++- .../migration/fetch/fetch.integration.test.ts | 62 ++++++++++++++++- .../commands/seed/buckets/SIDE_EFFECTS.md | 14 ++-- .../seed/buckets/buckets.integration.test.ts | 31 +++++++++ .../commands/storage/rm/SIDE_EFFECTS.md | 17 +++-- .../legacy/commands/storage/rm/rm.handler.ts | 22 ++++-- .../storage/rm/rm.integration.test.ts | 51 ++++++++++++++ .../shared/legacy-db-config.toml-read.ts | 8 ++- .../legacy-db-config.toml-read.unit.test.ts | 55 +++++++++++++++ .../src/legacy/shared/legacy-seed-buckets.ts | 19 ++++-- 14 files changed, 367 insertions(+), 48 deletions(-) diff --git a/apps/cli/src/legacy/commands/gen/signing-key/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/gen/signing-key/SIDE_EFFECTS.md index 2ea04a7fab..f7dab774da 100644 --- a/apps/cli/src/legacy/commands/gen/signing-key/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/gen/signing-key/SIDE_EFFECTS.md @@ -2,11 +2,12 @@ ## Files Read -| Path | Format | When | -| ----------------------------------------------- | --------------------------- | ------------------------------------------------------------------------------------ | -| `supabase/config.toml` / `supabase/config.json` | TOML / JSON | always when present in the active workdir; used to discover `auth.signing_keys_path` | -| `` | JSON array of JWKs | when `auth.signing_keys_path` is configured; loaded before overwrite or append | -| git ignore rules | git metadata / ignore files | best-effort after a successful write when the resulting file contains exactly 1 key | +| Path | Format | When | +| ----------------------------------------------- | --------------------------- | ------------------------------------------------------------------------------------- | +| `supabase/config.toml` / `supabase/config.json` | TOML / JSON | always when present in the active workdir; used to discover `auth.signing_keys_path` | +| `` | JSON array of JWKs | when `auth.signing_keys_path` is configured; loaded before overwrite or append | +| git ignore rules | git metadata / ignore files | best-effort after a successful write when the resulting file contains exactly 1 key | +| `/supabase/.env*`, `/.env*` | dotenv | always, to resolve `SUPABASE_YES` (CLI-1878; Go's `flags.LoadConfig`/`loadNestedEnv`) | ## Files Written @@ -22,9 +23,9 @@ ## Environment Variables -| Variable | Purpose | Required? | -| -------------- | ---------------------------------------------------------------------------------- | --------- | -| `SUPABASE_YES` | Auto-confirms the overwrite prompt, same as `--yes` (Go's `viper.GetBool("YES")`). | No | +| Variable | Purpose | Required? | +| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------- | +| `SUPABASE_YES` | Auto-confirms the overwrite prompt, same as `--yes` (Go's `viper.GetBool("YES")`). Read from the shell env OR the project `.env`/`.env.local`/`.env.[.local]` files (shell wins; CLI-1878). | No | ## Exit Codes @@ -57,7 +58,7 @@ Same as `--output-format json` above. - `--algorithm` accepts `ES256` (default, recommended) or `RS256`. - `--append` appends the new key to an existing keys file instead of overwriting. -- The overwrite prompt honors `SUPABASE_YES` and an explicit `--yes=false` override, matching Go's `viper.GetBool("YES")` precedence (flag wins over env; an omitted flag falls back to the env var). On non-TTY stdin, a piped `y`/`n` line is read within a 100ms timeout and honored before falling back to the default (`y`), matching Go's `Console.ReadLine`/`PromptYesNo` — a piped answer other than an exact `y`/`yes`/`n`/`no` (case-insensitive) also falls back to the default. +- The overwrite prompt honors `SUPABASE_YES` (shell env or the project `.env`/`.env.local`/`.env.[.local]` files, shell wins) and an explicit `--yes=false` override, matching Go's `viper.GetBool("YES")` precedence (flag wins over env; an omitted flag falls back to the env var, resolved after `flags.LoadConfig` loads the project env — CLI-1878). On non-TTY stdin, a piped `y`/`n` line is read within a 100ms timeout and honored before falling back to the default (`y`), matching Go's `Console.ReadLine`/`PromptYesNo` — a piped answer other than an exact `y`/`yes`/`n`/`no` (case-insensitive) also falls back to the default. - `auth.signing_keys_path` is resolved relative to the active `supabase/config.toml` or `supabase/config.json`. - Generated keys are JWKs, not PEM files. - No network or Management API calls are involved. diff --git a/apps/cli/src/legacy/commands/gen/signing-key/signing-key.handler.ts b/apps/cli/src/legacy/commands/gen/signing-key/signing-key.handler.ts index defbaad723..87a734da73 100644 --- a/apps/cli/src/legacy/commands/gen/signing-key/signing-key.handler.ts +++ b/apps/cli/src/legacy/commands/gen/signing-key/signing-key.handler.ts @@ -6,10 +6,11 @@ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; import { LegacyCliConfig } from "../../../config/legacy-cli-config.service.ts"; import { findGitRootPath } from "../../../../shared/git/git-root.ts"; +import { legacyLoadProjectEnv } from "../../../shared/legacy-db-config.toml-read.ts"; import { LegacyDebugLogger } from "../../../shared/legacy-debug-logger.service.ts"; import { legacyPromptYesNo } from "../../../shared/legacy-prompt-yes-no.ts"; import { LegacyTelemetryState } from "../../../telemetry/legacy-telemetry-state.service.ts"; -import { legacyResolveYes } from "../../../../shared/legacy/global-flags.ts"; +import { legacyResolveYesWithProjectEnv } from "../../../../shared/legacy/global-flags.ts"; import { Output } from "../../../../shared/output/output.service.ts"; import { Tty } from "../../../../shared/runtime/tty.service.ts"; import type { LegacyGenSigningKeyFlags } from "./signing-key.command.ts"; @@ -255,13 +256,21 @@ export const legacyGenSigningKey = Effect.fn("legacy.gen.signing-key")(function* const telemetryState = yield* LegacyTelemetryState; const output = yield* Output; const tty = yield* Tty; - const yes = yield* legacyResolveYes; const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const emphasize = (text: string) => styleIfTty(tty.stdoutIsTty, "bold", text); const warnText = (text: string) => styleIfTty(tty.stdoutIsTty, "yellow", text); return yield* Effect.gen(function* () { + // Go's `flags.LoadConfig` (`signingkeys.go:99`) loads the project `.env` files before the + // overwrite prompt reads `viper.GetBool("YES")` (`console.PromptYesNo`, `signingkeys.go:130`), + // so a `SUPABASE_YES` set only in `supabase/.env` must auto-confirm here too. Resolved inside + // this block (not above it) so a malformed/unreadable `.env` still flushes telemetry below, + // matching Go: telemetry attaches in root's `PersistentPreRunE` (`cmd/root.go:131-155`) + // before this command's own `RunE` runs `flags.LoadConfig`, so `service.Capture` still fires + // in Go even when that load fails. + const projectEnv = yield* legacyLoadProjectEnv(fs, path, cliConfig.workdir); + const yes = yield* legacyResolveYesWithProjectEnv(projectEnv); // Match Go's order: LoadConfig validates the configured signing-keys file before any key is // generated, so a broken config fails fast without doing throwaway crypto work. const signingKeysConfig = yield* loadSigningKeysConfig(cliConfig.workdir); diff --git a/apps/cli/src/legacy/commands/gen/signing-key/signing-key.integration.test.ts b/apps/cli/src/legacy/commands/gen/signing-key/signing-key.integration.test.ts index 549b3151f5..95439473a8 100644 --- a/apps/cli/src/legacy/commands/gen/signing-key/signing-key.integration.test.ts +++ b/apps/cli/src/legacy/commands/gen/signing-key/signing-key.integration.test.ts @@ -618,6 +618,49 @@ describe("legacy gen signing-key integration", () => { ); }); + it.live( + "auto-confirms from SUPABASE_YES in the project .env, even with a piped 'n' (CLI-1878)", + () => { + // SUPABASE_YES lives only in supabase/.env, not the shell. Go's `flags.LoadConfig` + // (`signingkeys.go:99`) loads the project `.env` files before the overwrite prompt reads + // `viper.GetBool("YES")` (`signingkeys.go:130`), so the overwrite auto-confirms and the + // piped `n` is never consumed — same precedence as the shell-env case above. + // + // Defensively clear a shell SUPABASE_YES: this test must prove the project-.env source + // specifically, not accidentally pass because a prior test in this file left the shell + // env set (the sibling shell-env tests above save/restore theirs). + const prev = process.env["SUPABASE_YES"]; + delete process.env["SUPABASE_YES"]; + const { layer } = setup({ stdinIsTty: false, pipedAnswer: "n" }); + return Effect.gen(function* () { + yield* Effect.tryPromise(() => + writeConfig('[auth]\nsigning_keys_path = "./signing_keys.json"\n'), + ); + yield* Effect.tryPromise(() => + writeFile(join(tempRoot.current, "supabase", "signing_keys.json"), "[]\n"), + ); + yield* Effect.tryPromise(() => + writeFile(join(tempRoot.current, "supabase", ".env"), "SUPABASE_YES=true\n"), + ); + + yield* legacyGenSigningKey({ algorithm: "ES256", append: false }); + + const saved = yield* Effect.tryPromise(() => + readFile(join(tempRoot.current, "supabase", "signing_keys.json"), "utf8"), + ); + const parsed = JSON.parse(saved) as ReadonlyArray>; + expect(parsed).toHaveLength(1); + }).pipe( + Effect.ensuring( + Effect.sync(() => { + if (prev !== undefined) process.env["SUPABASE_YES"] = prev; + }), + ), + Effect.provide(layer), + ); + }, + ); + it.live("an explicit --yes=false overrides SUPABASE_YES and honors a piped 'n'", () => { const prev = process.env["SUPABASE_YES"]; process.env["SUPABASE_YES"] = "1"; @@ -662,4 +705,28 @@ describe("legacy gen signing-key integration", () => { expect(telemetry?.flushed).toBe(true); }).pipe(Effect.provide(layer)); }); + + it.live("flushes telemetry state even when the project .env is malformed (Codex review)", () => { + // Go attaches the telemetry service in root's `PersistentPreRunE` (cmd/root.go:131-155), + // before this command's own `RunE` runs `flags.LoadConfig` (signingkeys.go:99), so + // `service.Capture` still fires even when that project-.env load fails. The project-env + // resolution here must live inside the `Effect.ensuring(telemetryState.flush)`-wrapped + // block for the same reason — locks in that fix. + const { layer, telemetry } = setup({ trackTelemetry: true }); + return Effect.gen(function* () { + yield* Effect.tryPromise(() => + mkdir(join(tempRoot.current, "supabase"), { recursive: true }), + ); + yield* Effect.tryPromise(() => + writeFile(join(tempRoot.current, "supabase", ".env"), "!=broken\n"), + ); + + const exit = yield* Effect.exit(legacyGenSigningKey({ algorithm: "ES256", append: false })); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(JSON.stringify(exit.cause)).toContain("LegacyDbConfigLoadError"); + } + expect(telemetry?.flushed).toBe(true); + }).pipe(Effect.provide(layer)); + }); }); diff --git a/apps/cli/src/legacy/commands/migration/fetch/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/migration/fetch/SIDE_EFFECTS.md index 7da5a26931..398edd9745 100644 --- a/apps/cli/src/legacy/commands/migration/fetch/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/migration/fetch/SIDE_EFFECTS.md @@ -2,9 +2,10 @@ ## Files Read -| Path | Format | When | -| -------------------------- | ---------- | ------------------------------------------------- | -| `~/.supabase/access-token` | plain text | when `SUPABASE_ACCESS_TOKEN` unset and `--linked` | +| Path | Format | When | +| --------------------------------------------- | ---------- | ------------------------------------------------------------------ | +| `~/.supabase/access-token` | plain text | when `SUPABASE_ACCESS_TOKEN` unset and `--linked` | +| `/supabase/.env*`, `/.env*` | dotenv | always, to resolve `SUPABASE_YES` (CLI-1878; Go's `loadNestedEnv`) | ## Files Written @@ -20,9 +21,10 @@ ## Environment Variables -| Variable | Purpose | Required? | -| ----------------------- | ------------------------------ | ------------------------------------------------------- | -| `SUPABASE_ACCESS_TOKEN` | auth token for `--linked` mode | no (falls back to keyring → `~/.supabase/access-token`) | +| Variable | Purpose | Required? | +| ----------------------- | --------------------------------- | ----------------------------------------------------------------------------------------------------------------- | +| `SUPABASE_ACCESS_TOKEN` | auth token for `--linked` mode | no (falls back to keyring → `~/.supabase/access-token`) | +| `SUPABASE_YES` | auto-confirm the overwrite prompt | no — read from the shell env OR the project `.env`/`.env.local`/`.env.[.local]` files (shell wins; CLI-1878) | ## Exit Codes @@ -54,8 +56,9 @@ Same structured `files` result delivered as an NDJSON `result` event. - When the migrations directory is non-empty, prompts `Do you want to overwrite existing files in supabase/migrations directory?` - (default **YES**). Declining exits non-zero (`context canceled`). `--yes` - auto-confirms; a non-interactive / machine-output run takes the default (YES). + (default **YES**). Declining exits non-zero (`context canceled`). `--yes` or + `SUPABASE_YES` (shell env or project `.env`) auto-confirms; a non-interactive / + machine-output run takes the default (YES). ## Notes diff --git a/apps/cli/src/legacy/commands/migration/fetch/fetch.handler.ts b/apps/cli/src/legacy/commands/migration/fetch/fetch.handler.ts index ccc5b69f43..6fadfea2b4 100644 --- a/apps/cli/src/legacy/commands/migration/fetch/fetch.handler.ts +++ b/apps/cli/src/legacy/commands/migration/fetch/fetch.handler.ts @@ -1,6 +1,9 @@ import { Effect, FileSystem, Option, Path } from "effect"; -import { LegacyDnsResolverFlag, legacyResolveYes } from "../../../../shared/legacy/global-flags.ts"; +import { + LegacyDnsResolverFlag, + legacyResolveYesWithProjectEnv, +} from "../../../../shared/legacy/global-flags.ts"; import { CliArgs } from "../../../../shared/cli/cli-args.service.ts"; import { Output } from "../../../../shared/output/output.service.ts"; import { LegacyCliConfig } from "../../../config/legacy-cli-config.service.ts"; @@ -8,6 +11,7 @@ import { LegacyProjectRefResolver } from "../../../config/legacy-project-ref.ser import { legacyBold } from "../../../shared/legacy-colors.ts"; import { LegacyDbConfigResolver } from "../../../shared/legacy-db-config.service.ts"; import { LegacyDbConnection } from "../../../shared/legacy-db-connection.service.ts"; +import { legacyLoadProjectEnv } from "../../../shared/legacy-db-config.toml-read.ts"; import { resolveLegacyDbTargetFlags } from "../../../shared/legacy-db-target-flags.ts"; import { legacyReadMigrationTable } from "../../../shared/legacy-migration-history.ts"; import { LegacyLinkedProjectCache } from "../../../telemetry/legacy-linked-project-cache.service.ts"; @@ -31,8 +35,10 @@ const runFetch = Effect.fnUntraced(function* ( const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const dnsResolver = yield* LegacyDnsResolverFlag; - const yes = yield* legacyResolveYes; // --yes OR SUPABASE_YES (Go viper AutomaticEnv, root.go:318-334). + // Flag-group mutual-exclusion first: cobra's `MarkFlagsMutuallyExclusive` validates at + // parse time, ahead of the root `PersistentPreRunE` (same ordering as `migration down`/ + // `repair`). if (target.setFlags.length > 1) { return yield* Effect.fail( new LegacyMigrationTargetFlagsError({ @@ -55,6 +61,14 @@ const runFetch = Effect.fnUntraced(function* ( dnsResolver, }); + // Go loads the project .env via loadNestedEnv INSIDE ParseDatabaseConfig (config.go:701), + // i.e. after the parse-time flag-group validation above — so a SUPABASE_YES set only in + // supabase/.env auto-confirms, but a flag conflict still surfaces before any .env read. + // Resolve --yes against the project env here, not just process.env (root.go:318-334). + // Same ordering as `migration down`/`repair`. + const projectEnv = yield* legacyLoadProjectEnv(fs, path, cliConfig.workdir); + const yes = yield* legacyResolveYesWithProjectEnv(projectEnv); + // Linked fetch caches the project ref on success (Go's `PersistentPostRun`). The ref is // loaded now (pre-run), but the cache write is attached to the body via `Effect.ensuring`, // so a declined prompt returns before it runs — matching Go (PostRun is skipped on a diff --git a/apps/cli/src/legacy/commands/migration/fetch/fetch.integration.test.ts b/apps/cli/src/legacy/commands/migration/fetch/fetch.integration.test.ts index f0fcd50ccf..b4c408487d 100644 --- a/apps/cli/src/legacy/commands/migration/fetch/fetch.integration.test.ts +++ b/apps/cli/src/legacy/commands/migration/fetch/fetch.integration.test.ts @@ -43,6 +43,8 @@ interface SetupOpts { readonly confirm?: boolean; readonly rows?: ReadonlyArray; readonly resolveFails?: boolean; + /** Raw argv seen by `resolveLegacyDbTargetFlags` (e.g. to exercise a flag conflict). */ + readonly cliArgs?: ReadonlyArray; } function setup(workdir: string, opts: SetupOpts = {}) { @@ -109,7 +111,7 @@ function setup(workdir: string, opts: SetupOpts = {}) { mockLegacyCliConfig({ workdir }), Layer.succeed(LegacyDnsResolverFlag, "native"), Layer.succeed(LegacyYesFlag, opts.yes ?? false), - Layer.succeed(CliArgs, { args: [] }), + Layer.succeed(CliArgs, { args: opts.cliArgs ?? [] }), mockTty({ stdinIsTty: opts.isTTY ?? true }), mockStdin( opts.isTTY ?? true, @@ -237,6 +239,27 @@ describe("legacy migration fetch", () => { }).pipe(Effect.provide(layer)); }); + it.live( + "auto-confirms the overwrite prompt from SUPABASE_YES in the project .env (Go loadNestedEnv)", + () => { + // SUPABASE_YES lives only in supabase/.env, not the shell — `fetch` defaults to + // `--linked` (Go: migration.go:161), and root's `ParseDatabaseConfig` loads the project + // `.env` files before `fetch.Run`'s overwrite prompt (root.go:118), so the overwrite + // auto-confirms with no --yes flag and no piped stdin answer (CLI-1878). + mkdirSync(migrationsDir(tmp.current), { recursive: true }); + writeFileSync(join(migrationsDir(tmp.current), "existing.sql"), "select 1;\n"); + writeFileSync(join(tmp.current, "supabase", ".env"), "SUPABASE_YES=true\n"); + const { layer, out } = setup(tmp.current, { + rows: [{ version: "20240101000000", name: "init", statements: ["create table a"] }], + }); + return Effect.gen(function* () { + yield* legacyMigrationFetch(flags()); + expect(out.stderrText).toContain("[Y/n] y"); + expect(readdirSync(migrationsDir(tmp.current))).toContain("20240101000000_init.sql"); + }).pipe(Effect.provide(layer)); + }, + ); + it.live("still prompts on stderr in json mode and proceeds on a piped yes", () => { // Go writes the prompt to stderr and reads stdin regardless of --output (console.go), // so --output-format json must NOT silently auto-accept: the overwrite prompt fires on @@ -332,8 +355,12 @@ describe("legacy migration fetch", () => { }); it.live("reports a write failure", () => { - // A file at /supabase makes `makeDirectory(supabase/migrations)` fail. - writeFileSync(join(tmp.current, "supabase"), "not a directory"); + // A file at /supabase/migrations makes `makeDirectory` fail. `supabase` itself + // must stay a real directory here: the handler's project-env load (CLI-1878, honoring + // Go's `loadNestedEnv`) reads `/supabase/.env*` before this mkdir, and a plain + // file at `/supabase` would make that read fail first (ENOTDIR) instead. + mkdirSync(join(tmp.current, "supabase"), { recursive: true }); + writeFileSync(join(tmp.current, "supabase", "migrations"), "not a directory"); const { layer } = setup(tmp.current, { rows: [] }); return Effect.gen(function* () { const exit = yield* legacyMigrationFetch(flags()).pipe(Effect.exit); @@ -362,4 +389,33 @@ describe("legacy migration fetch", () => { expect(out.promptConfirmCalls.length).toBe(0); }).pipe(Effect.provide(layer)); }); + + it.live( + "rejects --db-url combined with --linked before reading the project .env (CLI-1878)", + () => { + // Cobra's `MarkFlagsMutuallyExclusive` validates at parse time, ahead of the root + // `PersistentPreRunE` that runs `ParseDatabaseConfig`/`loadNestedEnv` — so a flag + // conflict must surface even when `supabase/.env` is malformed (which would abort a + // project-env load with a DIFFERENT error, `LegacyDbConfigLoadError`, if the env load + // ran first). Locks in the fix that reordered the project-env load in `fetch.handler.ts` + // to run after this flag-group check. + mkdirSync(join(tmp.current, "supabase"), { recursive: true }); + writeFileSync(join(tmp.current, "supabase", ".env"), "!=broken\n"); + const { layer } = setup(tmp.current, { + cliArgs: ["--db-url", "postgresql://x", "--linked"], + }); + return Effect.gen(function* () { + const exit = yield* legacyMigrationFetch( + flags({ dbUrl: Option.some("postgresql://x") }), + ).pipe(Effect.exit); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + const failure = Cause.findErrorOption(exit.cause); + expect(Option.isSome(failure) && failure.value._tag).toBe( + "LegacyMigrationTargetFlagsError", + ); + } + }).pipe(Effect.provide(layer)); + }, + ); }); diff --git a/apps/cli/src/legacy/commands/seed/buckets/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/seed/buckets/SIDE_EFFECTS.md index 67676ca81d..1f33b689e5 100644 --- a/apps/cli/src/legacy/commands/seed/buckets/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/seed/buckets/SIDE_EFFECTS.md @@ -7,12 +7,13 @@ stack is used; with `--linked` the remote project is used. ## Files Read -| Path | Format | When | -| ---------------------------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `/supabase/config.toml` | TOML | always, to read `[storage.buckets]` / `[storage.vector]` config; on `--linked`, the matching `[remotes.]` block (whose `project_id` equals the resolved project ref) is merged over the base config before decode, so remote-specific storage config takes effect | -| `/supabase//**` | any (bytes) | per configured bucket with a non-empty `objects_path`, recursively; a relative `objects_path` resolves under `supabase/` (Go `config.go:757-759`), an absolute path is used as-is | -| `/supabase/` | PEM text | local runs only, when `[api.tls] enabled = true` AND `api.tls.cert_path` is set; the file is read to obtain the CA certificate for trusting the local Kong HTTPS gateway. If `cert_path` is not set, the embedded `kong.local.crt` constant is used instead (no file read). | -| `/supabase/` | PEM text | local runs only, when `[api.tls] enabled = true` AND `api.tls.key_path` is set; read purely to validate the cert/key pairing (Go `config.go:845-861`) — the key content is not used by the CLI. If `cert_path` is set without `key_path` (or vice-versa), the command exits `1`. | +| Path | Format | When | +| --------------------------------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `/supabase/config.toml` | TOML | always, to read `[storage.buckets]` / `[storage.vector]` config; on `--linked`, the matching `[remotes.]` block (whose `project_id` equals the resolved project ref) is merged over the base config before decode, so remote-specific storage config takes effect | +| `/supabase//**` | any (bytes) | per configured bucket with a non-empty `objects_path`, recursively; a relative `objects_path` resolves under `supabase/` (Go `config.go:757-759`), an absolute path is used as-is | +| `/supabase/` | PEM text | local runs only, when `[api.tls] enabled = true` AND `api.tls.cert_path` is set; the file is read to obtain the CA certificate for trusting the local Kong HTTPS gateway. If `cert_path` is not set, the embedded `kong.local.crt` constant is used instead (no file read). | +| `/supabase/` | PEM text | local runs only, when `[api.tls] enabled = true` AND `api.tls.key_path` is set; read purely to validate the cert/key pairing (Go `config.go:845-861`) — the key content is not used by the CLI. If `cert_path` is set without `key_path` (or vice-versa), the command exits `1`. | +| `/supabase/.env*`, `/.env*` | dotenv | when no pre-resolved `yes` is passed in (the standalone command; `db reset --local` passes its own), to resolve `SUPABASE_YES` for the overwrite/prune prompts (CLI-1878; Go's `loadNestedEnv`) | ## Files Written @@ -71,6 +72,7 @@ Analytics bucket routes (`/storage/v1/iceberg/...`) are only reached when | `DOCKER_HOST` | when a `tcp://host:port` endpoint, the local services host falls back to it before `127.0.0.1` | no | | `SUPABASE_AUTH_SERVICE_ROLE_KEY` | when set and non-empty: for `--linked`, used as the service-role key (skips Management API key fetch); for local runs, used as the service-role key instead of `auth.service_role_key` (Go Viper AutomaticEnv parity) | no | | `SUPABASE_AUTH_JWT_SECRET` | local runs only: when set and non-empty, overrides `auth.jwt_secret` for service-role key derivation (Go Viper `AutomaticEnv`+`SUPABASE_` prefix parity, `config.go:492-497`) | no | +| `SUPABASE_YES` | auto-confirms the overwrite/prune prompts, same as `--yes`; read from the shell env OR the project `.env`/`.env.local`/`.env.[.local]` files (shell wins; CLI-1878) | no | ## Exit Codes diff --git a/apps/cli/src/legacy/commands/seed/buckets/buckets.integration.test.ts b/apps/cli/src/legacy/commands/seed/buckets/buckets.integration.test.ts index c91d172f5e..1d3ef890c9 100644 --- a/apps/cli/src/legacy/commands/seed/buckets/buckets.integration.test.ts +++ b/apps/cli/src/legacy/commands/seed/buckets/buckets.integration.test.ts @@ -1411,6 +1411,37 @@ describe("legacy seed buckets", () => { }); }); + it.live( + "auto-confirms the overwrite from SUPABASE_YES in the project .env (Go loadNestedEnv, CLI-1878)", + () => { + // SUPABASE_YES lives only in supabase/.env, not the shell or the --yes flag. `seed + // buckets` defaults to `--local` (Go: `cmd/seed.go:31`), and root's + // `ParseDatabaseConfig` loads the project `.env` files before `buckets.Run`'s + // overwrite prompt (`root.go:118`), so the standalone command's own fallback + // resolution (not the `db reset`-passed `opts.yes`) must read it too. + const { layer, out, requests } = setupLegacySeedBuckets(tmp.current, { + toml: "[storage.buckets.assets]\npublic = true\n", + files: { "supabase/.env": "SUPABASE_YES=true\n" }, + routes: [ + { method: "GET", match: "/storage/v1/bucket", body: [{ name: "assets", id: "assets" }] }, + { method: "PUT", match: "/storage/v1/bucket/assets", body: {} }, + ], + }); + return Effect.gen(function* () { + const exit = yield* legacySeedBuckets(DEFAULT_FLAGS).pipe( + Effect.provide(layer), + Effect.exit, + ); + expect(Exit.isSuccess(exit)).toBe(true); + expect(out.stderrText).toContain( + "already exists. Do you want to overwrite its properties? [Y/n] y", + ); + expect(out.stderrText).toContain("Updating Storage bucket: assets"); + expect(requests.some((r) => r.method === "PUT")).toBe(true); + }); + }, + ); + it.live("--yes prunes a stale vector bucket and echoes Go's prompt line", () => { const { layer, out, requests } = setupLegacySeedBuckets(tmp.current, { toml: "[storage.vector]\nenabled = true\n[storage.vector.buckets.vec1]\n", diff --git a/apps/cli/src/legacy/commands/storage/rm/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/storage/rm/SIDE_EFFECTS.md index 92ad3b6ccd..5bf3726586 100644 --- a/apps/cli/src/legacy/commands/storage/rm/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/storage/rm/SIDE_EFFECTS.md @@ -7,12 +7,13 @@ when `-r` is set. With no paths and `-r`, every bucket is cleared and deleted. ## Files Read -| Path | Format | When | -| ---------------------------------------- | ---------- | ----------------------------------------------------- | -| `/supabase/config.toml` | TOML | always (local creds; `[remotes.*]` merge when linked) | -| `~/.supabase/access-token` | plain text | linked path, when `SUPABASE_ACCESS_TOKEN` unset | -| `~/.supabase//linked-project.json` | JSON | linked path, to resolve the project ref | -| local Kong TLS cert/key | PEM | local + `api.enabled` + `api.tls.enabled` | +| Path | Format | When | +| --------------------------------------------- | ---------- | ------------------------------------------------------------------ | +| `/supabase/config.toml` | TOML | always (local creds; `[remotes.*]` merge when linked) | +| `~/.supabase/access-token` | plain text | linked path, when `SUPABASE_ACCESS_TOKEN` unset | +| `~/.supabase//linked-project.json` | JSON | linked path, to resolve the project ref | +| local Kong TLS cert/key | PEM | local + `api.enabled` + `api.tls.enabled` | +| `/supabase/.env*`, `/.env*` | dotenv | always, to resolve `SUPABASE_YES` (CLI-1878; Go's `loadNestedEnv`) | ## Files Written @@ -36,7 +37,9 @@ Auth: `apikey` always; `Authorization: Bearer ` unless the key is `sb_`-pre ## Environment Variables `SUPABASE_AUTH_SERVICE_ROLE_KEY`, `SUPABASE_AUTH_JWT_SECRET`, `SUPABASE_ACCESS_TOKEN`, -`SUPABASE_PROJECT_ID`, `SUPABASE_SERVICES_HOSTNAME`, plus `SUPABASE_YES` (auto-confirm). +`SUPABASE_PROJECT_ID`, `SUPABASE_SERVICES_HOSTNAME`, plus `SUPABASE_YES` (auto-confirm) — +read from the shell env OR the project `.env`/`.env.local`/`.env.[.local]` files +(shell wins; CLI-1878, matching Go's `loadNestedEnv` before `viper.GetBool("YES")`). `storage` is an experimental command (Go `root.go:63`): `rm` requires `--experimental` (or `SUPABASE_EXPERIMENTAL`), else it exits 1 with diff --git a/apps/cli/src/legacy/commands/storage/rm/rm.handler.ts b/apps/cli/src/legacy/commands/storage/rm/rm.handler.ts index dda8bc12fe..aa361c58e9 100644 --- a/apps/cli/src/legacy/commands/storage/rm/rm.handler.ts +++ b/apps/cli/src/legacy/commands/storage/rm/rm.handler.ts @@ -1,12 +1,13 @@ -import { Effect, Option } from "effect"; +import { Effect, FileSystem, Option, Path } from "effect"; import { LegacyCliConfig } from "../../../config/legacy-cli-config.service.ts"; import { LegacyProjectRefResolver } from "../../../config/legacy-project-ref.service.ts"; import { LegacyLinkedProjectCache } from "../../../telemetry/legacy-linked-project-cache.service.ts"; import { LegacyTelemetryState } from "../../../telemetry/legacy-telemetry-state.service.ts"; -import { legacyResolveYes } from "../../../../shared/legacy/global-flags.ts"; +import { legacyResolveYesWithProjectEnv } from "../../../../shared/legacy/global-flags.ts"; import { Output } from "../../../../shared/output/output.service.ts"; import { legacyBold } from "../../../shared/legacy-colors.ts"; +import { legacyLoadProjectEnv } from "../../../shared/legacy-db-config.toml-read.ts"; import { legacyPromptYesNo } from "../../../shared/legacy-prompt-yes-no.ts"; import { LEGACY_DELETE_OBJECTS_LIMIT, @@ -54,14 +55,27 @@ export const legacyStorageRm = Effect.fn("legacy.storage.rm")(function* ( const telemetryState = yield* LegacyTelemetryState; const linkedProjectCache = yield* LegacyLinkedProjectCache; const resolver = yield* LegacyProjectRefResolver; - // `--yes` OR `SUPABASE_YES` (Go's viper AutomaticEnv, root.go:318-320). - const yes = yield* legacyResolveYes; + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; let linkedRef = ""; yield* Effect.gen(function* () { + // Resolve the project ref BEFORE reading the project `.env`, matching Go's + // `ParseDatabaseConfig` `case linked:` (`db_url.go:87-93`), which calls + // `LoadProjectRef` strictly before `LoadConfig` (the `.env`/`loadNestedEnv` + // work). An unlinked workdir must fail fast with the not-linked guidance + // before a malformed/unreadable `supabase/.env` gets a chance to mask it + // with an env-parse error. const projectRef = flags.local ? "" : yield* resolver.loadProjectRef(Option.none()); linkedRef = projectRef; + // `--yes` OR `SUPABASE_YES` (Go's viper AutomaticEnv, root.go:318-320). Both the + // `--local` and (default) `--linked` branches of `ParseDatabaseConfig` call + // `LoadConfig` — which loads the project `.env` files — before `rm.Run`'s + // confirmation prompt (`root.go:118` → `db_url.go:78` (`local`) / `:91` (`linked`)), + // so a `SUPABASE_YES` set only in `supabase/.env` must auto-confirm here too. + const projectEnv = yield* legacyLoadProjectEnv(fs, path, cliConfig.workdir); + const yes = yield* legacyResolveYesWithProjectEnv(projectEnv); const loaded = yield* legacyLoadStorageConfig(cliConfig.workdir, projectRef); if (loaded.appliedRemote !== undefined) { yield* output.raw(`Loading config override: [remotes.${loaded.appliedRemote}]\n`, "stderr"); diff --git a/apps/cli/src/legacy/commands/storage/rm/rm.integration.test.ts b/apps/cli/src/legacy/commands/storage/rm/rm.integration.test.ts index c279b49460..858ea9953a 100644 --- a/apps/cli/src/legacy/commands/storage/rm/rm.integration.test.ts +++ b/apps/cli/src/legacy/commands/storage/rm/rm.integration.test.ts @@ -100,6 +100,57 @@ describe("legacy storage rm", () => { }); }); + it.live("auto-confirms from SUPABASE_YES in the project .env (Go loadNestedEnv)", () => { + // SUPABASE_YES lives only in supabase/.env, not the shell — both the `--local` and + // (default) `--linked` branches of Go's `ParseDatabaseConfig` load the project `.env` + // files before `rm.Run`'s confirmation prompt (root.go:118), so the deletion + // auto-confirms with no --yes flag and no env var set in the shell (CLI-1878). + const { layer, out, requests } = setupLegacyStorage(tmp.current, { + toml: 'project_id = "test"\n', + local: true, + files: { "supabase/.env": "SUPABASE_YES=true\n" }, + routes: [{ method: "DELETE", match: DELETE_OBJECT("private"), body: [{ name: "a.pdf" }] }], + }); + return Effect.gen(function* () { + const exit = yield* legacyStorageRm({ + files: ["ss:///private/a.pdf"], + recursive: false, + linked: true, + local: true, + }).pipe(Effect.provide(layer), Effect.exit); + expect(Exit.isSuccess(exit)).toBe(true); + expect(out.stderrText).toContain("[y/N] y"); + expect(requests.some((r) => r.method === "DELETE")).toBe(true); + }); + }); + + it.live( + "surfaces not-linked guidance before a malformed project .env (Go LoadProjectRef-before-LoadConfig)", + () => { + // Go's `ParseDatabaseConfig` `case linked:` (db_url.go:87-93) calls `LoadProjectRef` + // strictly before `LoadConfig` (which reads the project `.env` files), so an unlinked + // workdir must fail with the not-linked guidance even when `supabase/.env` is malformed + // — the malformed file must never be reached (CLI-1878). + const { layer, requests } = setupLegacyStorage(tmp.current, { + toml: 'project_id = "test"\n', + linkedFails: true, + files: { "supabase/.env": "!=\n" }, + }); + return Effect.gen(function* () { + const exit = yield* legacyStorageRm({ + files: ["ss:///private/a.pdf"], + recursive: false, + linked: true, + local: false, + }).pipe(Effect.provide(layer), Effect.exit); + expect(Exit.isFailure(exit)).toBe(true); + expect(JSON.stringify(exit)).toContain("Cannot find project ref"); + expect(JSON.stringify(exit)).not.toContain("failed to parse environment file"); + expect(requests).toHaveLength(0); + }); + }, + ); + it.live("skips the bucket when the confirmation is declined", () => { const { layer, requests } = setupLegacyStorage(tmp.current, { toml: 'project_id = "test"\n', diff --git a/apps/cli/src/legacy/shared/legacy-db-config.toml-read.ts b/apps/cli/src/legacy/shared/legacy-db-config.toml-read.ts index 639c1169ca..10fcddfcf8 100644 --- a/apps/cli/src/legacy/shared/legacy-db-config.toml-read.ts +++ b/apps/cli/src/legacy/shared/legacy-db-config.toml-read.ts @@ -279,6 +279,7 @@ const LEGACY_ENV_OVERRIDABLE_KEYS: ReadonlyArray = [ "db.migrations.enabled", "db.seed.enabled", "db.seed.sql_paths", + "auth.enabled", "edge_runtime.deno_version", "experimental.pgdelta.enabled", "experimental.pgdelta.declarative_schema_path", @@ -1267,13 +1268,16 @@ const readDbTomlCore = Effect.fnUntraced(function* ( // Go's config.Validate runs the full `if c.Auth.Enabled` block (`config.go:1036-1102`) after // the bucket/function checks. Gated on `auth.enabled` (default true); Go's viper AutomaticEnv // binds `auth.enabled` to `SUPABASE_AUTH_ENABLED` before Validate (`config.go:529-535`), so the - // env override decides whether the auth block is validated. + // env override decides whether the auth block is validated — UNLESS a matched `[remotes.*]` + // block supplies `auth.enabled` itself, in which case `mergeRemoteConfig`'s `v.Set` (override + // tier, above `AutomaticEnv`) wins, matching the same suppression every other + // `LEGACY_ENV_OVERRIDABLE_KEYS` entry gets below. const authEnabled = yield* resolveBoolOrFail( "auth.enabled", authRaw?.["enabled"], true, lookup, - envOverride("SUPABASE_AUTH_ENABLED"), + remoteOverrideKeys.has("auth.enabled") ? undefined : envOverride("SUPABASE_AUTH_ENABLED"), ); // Local helpers mirroring the deleted `legacyValidateAuthConfig`'s closures — its Go-parity diff --git a/apps/cli/src/legacy/shared/legacy-db-config.toml-read.unit.test.ts b/apps/cli/src/legacy/shared/legacy-db-config.toml-read.unit.test.ts index 673fd52d41..493f1f8276 100644 --- a/apps/cli/src/legacy/shared/legacy-db-config.toml-read.unit.test.ts +++ b/apps/cli/src/legacy/shared/legacy-db-config.toml-read.unit.test.ts @@ -453,6 +453,61 @@ describe("legacyReadDbToml", () => { ); }); + it.effect("an explicit remote auth.enabled beats its SUPABASE_AUTH_ENABLED env var", () => { + // Same v.Set-above-AutomaticEnv precedence as db.migrations.enabled / pgdelta.enabled + // (config.go:635-637), but for auth.enabled specifically (CLI-1878): a matched remote + // block's auth.enabled must win over SUPABASE_AUTH_ENABLED. + const ref = "abcdefghijklmnopqrst"; + const previous = process.env["SUPABASE_AUTH_ENABLED"]; + process.env["SUPABASE_AUTH_ENABLED"] = "true"; + const dir = withConfig( + [ + "[remotes.prod]", + `project_id = "${ref}"`, + "[remotes.prod.auth]", + "enabled = false", + "", + ].join("\n"), + ); + return readRef(dir, ref).pipe( + Effect.tap((v) => + Effect.sync(() => { + expect(v.baseline.authEnabled).toBe(false); + }), + ), + Effect.ensuring( + Effect.sync(() => { + if (previous === undefined) delete process.env["SUPABASE_AUTH_ENABLED"]; + else process.env["SUPABASE_AUTH_ENABLED"] = previous; + rmSync(dir, { recursive: true, force: true }); + }), + ), + ); + }); + + it.effect("SUPABASE_AUTH_ENABLED still wins when the remote block omits auth.enabled", () => { + // Control: the env override is suppressed only for keys the matched block explicitly + // set; a block that omits auth.enabled leaves the env override in force. + const ref = "abcdefghijklmnopqrst"; + const previous = process.env["SUPABASE_AUTH_ENABLED"]; + process.env["SUPABASE_AUTH_ENABLED"] = "false"; + const dir = withConfig(["[remotes.prod]", `project_id = "${ref}"`, ""].join("\n")); + return readRef(dir, ref).pipe( + Effect.tap((v) => + Effect.sync(() => { + expect(v.baseline.authEnabled).toBe(false); + }), + ), + Effect.ensuring( + Effect.sync(() => { + if (previous === undefined) delete process.env["SUPABASE_AUTH_ENABLED"]; + else process.env["SUPABASE_AUTH_ENABLED"] = previous; + rmSync(dir, { recursive: true, force: true }); + }), + ), + ); + }); + it.effect("matches a remote block by a SUPABASE_REMOTES__PROJECT_ID env override", () => { // Viper AutomaticEnv supplies/overrides remotes.prod.project_id, so the block merges // even with no TOML project_id (here it lifts major_version 15 over the base default). diff --git a/apps/cli/src/legacy/shared/legacy-seed-buckets.ts b/apps/cli/src/legacy/shared/legacy-seed-buckets.ts index 0ec17002b5..ff7754d5c5 100644 --- a/apps/cli/src/legacy/shared/legacy-seed-buckets.ts +++ b/apps/cli/src/legacy/shared/legacy-seed-buckets.ts @@ -8,9 +8,10 @@ import { FetchHttpClient } from "effect/unstable/http"; import type { PlatformError } from "effect/PlatformError"; import { Output } from "../../shared/output/output.service.ts"; -import { legacyResolveYes } from "../../shared/legacy/global-flags.ts"; +import { legacyResolveYesWithProjectEnv } from "../../shared/legacy/global-flags.ts"; import { LegacyCliConfig } from "../config/legacy-cli-config.service.ts"; import { legacyBold, legacyYellow } from "./legacy-colors.ts"; +import { legacyLoadProjectEnv } from "./legacy-db-config.toml-read.ts"; import { legacyPromptYesNo } from "./legacy-prompt-yes-no.ts"; import { legacyResolveStorageCredentials, @@ -127,9 +128,13 @@ export const legacySeedBucketsRun = Effect.fnUntraced(function* (opts: { /** * Pre-resolved auto-confirm value. `db reset` resolves `yes` with the nested project * `.env` loaded (Go's `loadNestedEnv` runs before `buckets.Run`), so pass it through here - * — the internal `legacyResolveYes` only sees the shell env and would skip the - * bucket/vector/analytics prune that a `SUPABASE_YES` in `supabase/.env` should confirm. - * When omitted (the standalone `seed buckets` command), fall back to `legacyResolveYes`. + * — the internal fallback below only loads whatever THIS command's own project would + * supply. When omitted (the standalone `seed buckets` command), fall back to + * `legacyResolveYesWithProjectEnv`, loading the project env ourselves — `seed buckets` + * defaults to `--local` (Go's `seedFlags.Bool("local", true, ...)`, `cmd/seed.go:31`), + * and root's `ParseDatabaseConfig` calls `LoadConfig` — loading the project `.env` files + * — before `buckets.Run`'s overwrite/prune prompts (`root.go:118`), so a `SUPABASE_YES` + * set only in `supabase/.env` must auto-confirm here too. */ readonly yes?: boolean; }) { @@ -138,7 +143,11 @@ export const legacySeedBucketsRun = Effect.fnUntraced(function* (opts: { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; // `--yes` OR `SUPABASE_YES` (Go's viper AutomaticEnv, root.go:318-320). - const yes = opts.yes ?? (yield* legacyResolveYes); + const yes = + opts.yes ?? + (yield* legacyResolveYesWithProjectEnv( + yield* legacyLoadProjectEnv(fs, path, cliConfig.workdir), + )); const { projectRef, emitSummary } = opts; const interactive = opts.interactive ?? true; From 31a10172b9071681e3ebec3f3d17784bf59bf2a4 Mon Sep 17 00:00:00 2001 From: Andrew Valleteau Date: Thu, 9 Jul 2026 16:07:27 +0200 Subject: [PATCH 35/79] ci: add contribution gate workflow for external PRs (#5843) Implement an automated contribution gate that enforces the Supabase CLI contribution workflow for external pull requests. The gate requires PRs from external contributors to link to an open GitHub issue carrying the `open-for-contribution` label, while exempting maintainers and bots. ## Changes - **Contribution gate script** (`contribution-gate.ts`): Pure decision logic and GitHub I/O for evaluating PRs against the gate policy. Supports two modes: - Single-PR mode: reacts to individual PRs on `pull_request_target` events - All-PRs sweep mode: evaluates every open PR on-demand via `workflow_dispatch` - **Gate tests** (`contribution-gate.test.ts`): Comprehensive unit tests for the decision logic and orchestration, with injected I/O for network-free testing - **Workflow** (`contribution-gate.yml`): GitHub Actions workflow that runs the gate reactively on PR open/reopen/edit and supports manual sweeps with dry-run capability - **Documentation**: - `MAINTAINERS.md`: Internal guide for maintainers on applying the `open-for-contribution` label and running manual sweeps - Updated `CONTRIBUTING.md`: Contributor-facing workflow requiring issues to be opened first and labeled before PR submission - Updated issue templates and PR template: Guidance on the new workflow - Updated issue config: Link to contribution workflow ## Implementation details - Non-conforming PRs are auto-closed with explanatory comments directing contributors to the workflow - Cross-repository closing keywords are rejected as a security measure (contributors cannot control external repos) - Repository name matching is case-insensitive - Internal authors (OWNER, MEMBER, COLLABORATOR) and bots are exempt from the gate - The workflow checks out the base branch, ensuring only trusted code executes https://claude.ai/code/session_01YY1sNQLXPxaeN6JX1NFSWj --------- Co-authored-by: Claude --- .github/ISSUE_TEMPLATE/bug-report.yml | 2 + .github/ISSUE_TEMPLATE/config.yml | 3 + .github/ISSUE_TEMPLATE/docs.yml | 5 + .github/ISSUE_TEMPLATE/feature-request.yml | 5 + .github/MAINTAINERS.md | 58 +++ .github/PULL_REQUEST_TEMPLATE.md | 22 ++ .github/scripts/contribution-gate.test.ts | 219 +++++++++++ .github/scripts/contribution-gate.ts | 418 +++++++++++++++++++++ .github/workflows/contribution-gate.yml | 74 ++++ CONTRIBUTING.md | 12 + README.md | 6 +- 11 files changed, 823 insertions(+), 1 deletion(-) create mode 100644 .github/MAINTAINERS.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/scripts/contribution-gate.test.ts create mode 100644 .github/scripts/contribution-gate.ts create mode 100644 .github/workflows/contribution-gate.yml diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 37fc048a0e..9304447079 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -8,6 +8,8 @@ body: value: | Thanks for helping improve the Supabase CLI. Please include the exact command and output whenever possible so maintainers can reproduce the issue quickly. + Open this issue **before** starting work — a maintainer will add the `open-for-contribution` label once it is triaged and ready. Pull requests from external contributors that don't link an open, `open-for-contribution` issue are closed automatically. See [CONTRIBUTING.md](../blob/develop/CONTRIBUTING.md). + - type: dropdown id: affected-area attributes: diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 8c49825527..00644d304d 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,5 +1,8 @@ blank_issues_enabled: false contact_links: + - name: 📖 Contribution workflow + url: https://github.com/supabase/cli/blob/develop/CONTRIBUTING.md + about: Read this before opening an issue or pull request — PRs must link a labeled, open issue. - name: Supabase support url: https://supabase.com/support about: Get help with your Supabase project, account, billing, or hosted services. diff --git a/.github/ISSUE_TEMPLATE/docs.yml b/.github/ISSUE_TEMPLATE/docs.yml index 57501fd9d3..aa04c9c2d3 100644 --- a/.github/ISSUE_TEMPLATE/docs.yml +++ b/.github/ISSUE_TEMPLATE/docs.yml @@ -3,6 +3,11 @@ description: Suggest an improvement to Supabase CLI documentation. labels: - 📘 Docs body: + - type: markdown + attributes: + value: | + Thanks for helping improve the docs! Open this issue **before** starting work — a maintainer will add the `open-for-contribution` label once it is triaged and ready. Pull requests from external contributors that don't link an open, `open-for-contribution` issue are closed automatically. See [CONTRIBUTING.md](../blob/develop/CONTRIBUTING.md). + - type: input id: link attributes: diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml index 12bb26e54e..fd3a0eda1d 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.yml +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -3,6 +3,11 @@ description: Suggest an improvement or new capability for the Supabase CLI. labels: - ✨ Feature body: + - type: markdown + attributes: + value: | + Thanks for the suggestion! Open this issue **before** starting work — a maintainer will add the `open-for-contribution` label once it is triaged and ready. Pull requests from external contributors that don't link an open, `open-for-contribution` issue are closed automatically. See [CONTRIBUTING.md](../blob/develop/CONTRIBUTING.md). + - type: checkboxes id: existing-issues attributes: diff --git a/.github/MAINTAINERS.md b/.github/MAINTAINERS.md new file mode 100644 index 0000000000..e8321dd3b9 --- /dev/null +++ b/.github/MAINTAINERS.md @@ -0,0 +1,58 @@ +# Maintainers guide + +Internal notes for maintaining the Supabase CLI contribution workflow. See +[`CONTRIBUTING.md`](../CONTRIBUTING.md) for the contributor-facing version. + +## The `open-for-contribution` gate + +External pull requests are only accepted when they link to an **open** GitHub issue +that carries the **`open-for-contribution`** label. This is enforced by the +[`Contribution Gate`](./workflows/contribution-gate.yml) workflow, whose decision logic +lives in [`scripts/contribution-gate.ts`](./scripts/contribution-gate.ts). + +The gate runs **reactively on each PR** so a non-conforming PR is closed right away, and +can also be **swept across every open PR on demand** via the workflow's *Run workflow* +button. + +A pull request is **auto-closed with an explanatory comment** when the author is external +and any of these is true: + +- no issue is linked (via a closing keyword such as `Closes #123`, or the PR's + Development sidebar), or +- the linked issue is closed, or +- the linked issue is missing the `open-for-contribution` label. + +Authors whose `author_association` is `OWNER`, `MEMBER`, or `COLLABORATOR`, and bot +accounts, are **exempt** — Supabase maintainers can keep working from Linear tickets that +aren't public on GitHub. + +### Running the gate manually + +Use **Actions → Contribution Gate → Run workflow** to sweep all open PRs on demand — for +example right after bulk-applying `open-for-contribution` labels, so the whole backlog is +re-evaluated at once instead of waiting for each PR's next edit. Set the **`dry_run`** +input to `true` first to log each PR's decision in the run output without commenting on or +closing anything; run again with `dry_run` unchecked to apply the decisions. + +## Triage: applying the label (manual) + +During triage: + +1. Categorize the issue with one of `✨ Feature`, `🐛 Bug`, or `📘 Docs`. Issues opened + via the templates start with their category label already applied. +2. When the issue is ready to be worked on, add the **`open-for-contribution`** label. + +The `open-for-contribution` label must exist as a repository label for this workflow to +function; create it once from **Issues → Labels** if it is missing. + +Applying `open-for-contribution` is currently a **manual step** — do it on the GitHub +issue directly (from the GitHub UI, or from the Linear-linked issue). + +## Deferred: automatic Linear → GitHub label sync + +We considered auto-applying `open-for-contribution` when a Linear issue moves out of +Triage/Backlog (e.g. to Todo). Linear's native GitHub automations are one-directional +(GitHub events update Linear status) and cannot push a GitHub label, so this would need an +external bridge (a scheduled job polling the Linear API, a Zapier/Make zap, or a Linear +webhook → relay). It is **out of scope for now** and tracked separately; until then, apply +the label manually as above. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000000..2c8e119819 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,22 @@ + + +## Summary + + + +## Linked issue + +Closes # + +- [ ] The linked issue is **open** and carries the `open-for-contribution` label (or I'm a Supabase maintainer). + +## Checklist + +- [ ] The PR title follows [Conventional Commits](https://www.conventionalcommits.org/) (e.g. `fix(cli): …`). +- [ ] Tests added or updated for the change. +- [ ] `pnpm check:all` and `pnpm test` pass for the workspace(s) I touched. diff --git a/.github/scripts/contribution-gate.test.ts b/.github/scripts/contribution-gate.test.ts new file mode 100644 index 0000000000..a8292a3a35 --- /dev/null +++ b/.github/scripts/contribution-gate.test.ts @@ -0,0 +1,219 @@ +import { describe, expect, test } from "bun:test"; +import { + evaluateAllOpenPrs, + evaluateGate, + GATE_LABEL, + type GateIo, + type LinkedIssue, + type OpenPr, +} from "./contribution-gate.ts"; + +const REPO = "supabase/cli"; + +describe("evaluateGate", () => { + test("skips bot authors", () => { + const result = evaluateGate({ + repository: REPO, + authorAssociation: "NONE", + isBot: true, + linkedIssues: [], + }); + expect(result.pass).toBe(true); + expect(result.reason).toBe("bot"); + }); + + test.each(["OWNER", "MEMBER", "COLLABORATOR"])( + "skips internal author association %s", + (authorAssociation) => { + const result = evaluateGate({ + repository: REPO, + authorAssociation, + isBot: false, + linkedIssues: [], + }); + expect(result.pass).toBe(true); + expect(result.reason).toBe("internal"); + }, + ); + + test("fails when no issue is linked", () => { + const result = evaluateGate({ + repository: REPO, + authorAssociation: "NONE", + isBot: false, + linkedIssues: [], + }); + expect(result.pass).toBe(false); + expect(result.reason).toBe("no-linked-issue"); + expect(result.message).toContain(GATE_LABEL); + expect(result.message).toContain("CONTRIBUTING.md"); + }); + + test("fails when the linked issue is open but not labeled", () => { + const result = evaluateGate({ + repository: REPO, + authorAssociation: "CONTRIBUTOR", + isBot: false, + linkedIssues: [ + { repository: REPO, number: 12, state: "OPEN", labels: ["🐛 Bug"] }, + ], + }); + expect(result.pass).toBe(false); + expect(result.reason).toBe("missing-label"); + expect(result.message).toContain(GATE_LABEL); + }); + + test("fails when the only labeled issue is closed", () => { + const result = evaluateGate({ + repository: REPO, + authorAssociation: "NONE", + isBot: false, + linkedIssues: [ + { repository: REPO, number: 7, state: "CLOSED", labels: [GATE_LABEL] }, + ], + }); + expect(result.pass).toBe(false); + expect(result.reason).toBe("issue-closed"); + }); + + test("passes when a linked issue is open and carries the gate label", () => { + const result = evaluateGate({ + repository: REPO, + authorAssociation: "NONE", + isBot: false, + linkedIssues: [ + { + repository: REPO, + number: 42, + state: "OPEN", + labels: [GATE_LABEL, "🐛 Bug"], + }, + ], + }); + expect(result.pass).toBe(true); + expect(result.reason).toBe("ok"); + }); + + test("passes when any one of several linked issues qualifies", () => { + const result = evaluateGate({ + repository: REPO, + authorAssociation: "FIRST_TIME_CONTRIBUTOR", + isBot: false, + linkedIssues: [ + { repository: REPO, number: 1, state: "CLOSED", labels: [GATE_LABEL] }, + { repository: REPO, number: 2, state: "OPEN", labels: ["✨ Feature"] }, + { repository: REPO, number: 3, state: "OPEN", labels: [GATE_LABEL] }, + ], + }); + expect(result.pass).toBe(true); + expect(result.reason).toBe("ok"); + }); + + test("ignores an open, labeled issue from a different repository", () => { + // Cross-repo closing keyword (e.g. `Closes attacker/repo#1`): the issue is + // controlled by the contributor, so it must not satisfy the gate. + const result = evaluateGate({ + repository: REPO, + authorAssociation: "NONE", + isBot: false, + linkedIssues: [ + { + repository: "attacker/repo", + number: 1, + state: "OPEN", + labels: [GATE_LABEL], + }, + ], + }); + expect(result.pass).toBe(false); + expect(result.reason).toBe("no-linked-issue"); + }); + + test("matches the repository case-insensitively", () => { + const result = evaluateGate({ + repository: REPO, + authorAssociation: "NONE", + isBot: false, + linkedIssues: [ + { + repository: "Supabase/CLI", + number: 5, + state: "OPEN", + labels: [GATE_LABEL], + }, + ], + }); + expect(result.pass).toBe(true); + expect(result.reason).toBe("ok"); + }); +}); + +describe("evaluateAllOpenPrs", () => { + function makeIo( + openPrs: OpenPr[], + linkedByPr: Record, + ): { io: GateIo; closed: Array<{ number: number; message: string }> } { + const closed: Array<{ number: number; message: string }> = []; + const io: GateIo = { + listOpenPrs: () => Promise.resolve(openPrs), + fetchLinkedIssues: (prNumber) => + Promise.resolve(linkedByPr[prNumber] ?? []), + closePr: (prNumber, message) => { + closed.push({ number: prNumber, message }); + return Promise.resolve(); + }, + }; + return { io, closed }; + } + + test("closes only non-conforming external PRs and leaves the rest", async () => { + const { io, closed } = makeIo( + [ + { number: 1, authorAssociation: "NONE", isBot: false }, // no issue -> close + { number: 2, authorAssociation: "MEMBER", isBot: false }, // internal -> skip + { number: 3, authorAssociation: "NONE", isBot: true }, // bot -> skip + { number: 4, authorAssociation: "CONTRIBUTOR", isBot: false }, // conforming -> keep + { number: 5, authorAssociation: "NONE", isBot: false }, // missing label -> close + ], + { + 4: [ + { repository: REPO, number: 40, state: "OPEN", labels: [GATE_LABEL] }, + ], + 5: [ + { repository: REPO, number: 50, state: "OPEN", labels: ["🐛 Bug"] }, + ], + }, + ); + + const entries = await evaluateAllOpenPrs(io, REPO); + + expect(closed.map((c) => c.number).sort((a, b) => a - b)).toEqual([1, 5]); + const byNumber = Object.fromEntries( + entries.map((entry) => [entry.number, entry.result]), + ); + expect(byNumber[1]?.reason).toBe("no-linked-issue"); + expect(byNumber[2]?.pass).toBe(true); + expect(byNumber[2]?.reason).toBe("internal"); + expect(byNumber[3]?.reason).toBe("bot"); + expect(byNumber[4]?.pass).toBe(true); + expect(byNumber[5]?.reason).toBe("missing-label"); + expect(closed.find((c) => c.number === 1)?.message).toContain(GATE_LABEL); + }); + + test("returns an entry per PR and closes none when all conform", async () => { + const { io, closed } = makeIo( + [{ number: 9, authorAssociation: "NONE", isBot: false }], + { + 9: [ + { repository: REPO, number: 90, state: "OPEN", labels: [GATE_LABEL] }, + ], + }, + ); + + const entries = await evaluateAllOpenPrs(io, REPO); + + expect(entries).toHaveLength(1); + expect(closed).toHaveLength(0); + expect(entries[0]?.result.pass).toBe(true); + }); +}); diff --git a/.github/scripts/contribution-gate.ts b/.github/scripts/contribution-gate.ts new file mode 100644 index 0000000000..ad599b3c81 --- /dev/null +++ b/.github/scripts/contribution-gate.ts @@ -0,0 +1,418 @@ +/** + * Contribution gate: enforces the Supabase CLI contribution workflow across all + * OPEN pull requests opened by external contributors. + * + * A PR passes only when it links to an OPEN GitHub issue that carries the + * `open-for-contribution` label. Members/collaborators/owners and bots are + * exempt (they work from Linear tickets or automation). PRs that do not follow + * the process are commented on and closed. + * + * Two modes, both driven from `main()`: + * - single-PR (default): reacts to one PR on `pull_request_target` + * (`opened`/`reopened`/`edited`), using the PR_* env vars from the event. + * - all-PRs sweep (`GATE_MODE=all`, or the `--all` flag): evaluates every + * open PR, for on-demand `workflow_dispatch` runs. Set `DRY_RUN=true` to + * log decisions without commenting/closing. + * + * In both modes the workflow checks out the base branch, so this only ever + * executes trusted repository code — it never runs a fork's code. + * + * Run in CI as: `bun .github/scripts/contribution-gate.ts`. + * The pure `evaluateGate` decision and the `evaluateAllOpenPrs` orchestrator + * (I/O injected) are unit-tested in `contribution-gate.test.ts`; `main()` wires + * up the real GitHub I/O. + */ + +export const GATE_LABEL = "open-for-contribution"; + +/** Author associations treated as internal (exempt from the gate). */ +const INTERNAL_ASSOCIATIONS = new Set(["OWNER", "MEMBER", "COLLABORATOR"]); + +export interface LinkedIssue { + /** `owner/name` of the repo the issue lives in (from GraphQL nameWithOwner). */ + repository: string; + number: number; + state: "OPEN" | "CLOSED"; + labels: string[]; +} + +export interface GateInput { + /** `owner/name` of this repository (from GITHUB_REPOSITORY). */ + repository: string; + authorAssociation: string; + isBot: boolean; + linkedIssues: LinkedIssue[]; +} + +export type GateReason = + | "bot" + | "internal" + | "ok" + | "no-linked-issue" + | "missing-label" + | "issue-closed"; + +export interface GateResult { + pass: boolean; + reason: GateReason; + /** Explanatory comment body, present only when `pass` is false. */ + message?: string; +} + +const DOCS_FOOTER = + `\nSee [CONTRIBUTING.md](../blob/develop/CONTRIBUTING.md) for the full workflow. ` + + `Once a maintainer adds the \`${GATE_LABEL}\` label to a linked open issue, ` + + `reopen or open a new pull request and it will be accepted.`; + +function buildMessage(reason: GateReason): string { + switch (reason) { + case "no-linked-issue": + return ( + `👋 Thanks for the contribution! This pull request isn't linked to a ` + + `tracked issue, so it's being closed automatically.\n\n` + + `Please open an issue first, wait for a maintainer to add the ` + + `\`${GATE_LABEL}\` label, then open a pull request that links the issue ` + + `with a closing keyword (e.g. \`Closes #123\`).${DOCS_FOOTER}` + ); + case "missing-label": + return ( + `👋 Thanks! The linked issue hasn't been marked \`${GATE_LABEL}\` yet, ` + + `so this pull request is being closed automatically.\n\n` + + `A maintainer adds that label once an issue is triaged and ready to be ` + + `worked on. Please wait for the label before opening a pull ` + + `request.${DOCS_FOOTER}` + ); + case "issue-closed": + return ( + `👋 The issue linked to this pull request is closed, so it's being ` + + `closed automatically.\n\n` + + `Please link an open issue that carries the \`${GATE_LABEL}\` ` + + `label.${DOCS_FOOTER}` + ); + default: + return ""; + } +} + +/** + * Pure decision function for the contribution gate. Given the PR author context + * and its linked issues, returns whether the PR is allowed and why. + */ +export function evaluateGate(input: GateInput): GateResult { + if (input.isBot) { + return { pass: true, reason: "bot" }; + } + if (INTERNAL_ASSOCIATIONS.has(input.authorAssociation)) { + return { pass: true, reason: "internal" }; + } + + // Only issues in THIS repository count. A cross-repository closing keyword + // (e.g. `Closes attacker/repo#1`) links an issue the contributor controls, + // so it must never satisfy the gate. + const repo = input.repository.toLowerCase(); + const repoIssues = input.linkedIssues.filter( + (issue) => issue.repository.toLowerCase() === repo, + ); + + if (repoIssues.length === 0) { + return { + pass: false, + reason: "no-linked-issue", + message: buildMessage("no-linked-issue"), + }; + } + + const qualifies = repoIssues.some( + (issue) => issue.state === "OPEN" && issue.labels.includes(GATE_LABEL), + ); + if (qualifies) { + return { pass: true, reason: "ok" }; + } + + const hasOpenIssue = repoIssues.some((issue) => issue.state === "OPEN"); + const reason: GateReason = hasOpenIssue ? "missing-label" : "issue-closed"; + return { pass: false, reason, message: buildMessage(reason) }; +} + +/** Minimal open-PR shape the gate needs to decide exemption. */ +export interface OpenPr { + number: number; + authorAssociation: string; + isBot: boolean; +} + +/** Injected GitHub I/O so the sweep can be unit-tested without the network. */ +export interface GateIo { + /** List every open pull request in the repository. */ + listOpenPrs: () => Promise; + /** Fetch the issues a PR closes (via `closingIssuesReferences`). */ + fetchLinkedIssues: (prNumber: number) => Promise; + /** Comment with `message` then close the PR. Called only for failing PRs. */ + closePr: (prNumber: number, message: string) => Promise; +} + +export interface SweepEntry { + number: number; + result: GateResult; +} + +/** + * Evaluate the gate against every open PR, closing the ones that fail. Pure + * orchestration over the injected `io`; returns each PR's decision so the + * caller can log a summary. + */ +export async function evaluateAllOpenPrs( + io: GateIo, + repository: string, +): Promise { + const openPrs = await io.listOpenPrs(); + const entries: SweepEntry[] = []; + for (const pr of openPrs) { + const linkedIssues = await io.fetchLinkedIssues(pr.number); + const result = evaluateGate({ + repository, + authorAssociation: pr.authorAssociation, + isBot: pr.isBot, + linkedIssues, + }); + if (!result.pass && result.message) { + await io.closePr(pr.number, result.message); + } + entries.push({ number: pr.number, result }); + } + return entries; +} + +// --- GitHub I/O (only runs when executed directly) --- + +interface GraphQLIssueNode { + number: number; + state: "OPEN" | "CLOSED"; + repository: { nameWithOwner: string }; + labels: { nodes: Array<{ name: string }> }; +} + +function requireEnv(name: string): string { + const value = process.env[name]; + if (!value) { + throw new Error(`Missing required environment variable: ${name}`); + } + return value; +} + +async function githubFetch( + url: string, + token: string, + init: Omit = {}, +): Promise { + const response = await fetch(url, { + ...init, + headers: { + Authorization: `Bearer ${token}`, + Accept: "application/vnd.github+json", + "X-GitHub-Api-Version": "2022-11-28", + "Content-Type": "application/json", + }, + }); + if (!response.ok) { + const body = await response.text(); + throw new Error( + `GitHub request failed (${response.status}) for ${url}: ${body}`, + ); + } + return response; +} + +async function fetchLinkedIssues( + token: string, + owner: string, + repo: string, + prNumber: number, +): Promise { + const query = ` + query($owner: String!, $repo: String!, $number: Int!) { + repository(owner: $owner, name: $repo) { + pullRequest(number: $number) { + closingIssuesReferences(first: 20) { + nodes { + number + state + repository { nameWithOwner } + labels(first: 50) { nodes { name } } + } + } + } + } + }`; + const response = await githubFetch("https://api.github.com/graphql", token, { + method: "POST", + body: JSON.stringify({ + query, + variables: { owner, repo, number: prNumber }, + }), + }); + const payload = (await response.json()) as { + errors?: Array<{ message: string }>; + data?: { + repository?: { + pullRequest?: { + closingIssuesReferences?: { nodes?: GraphQLIssueNode[] }; + }; + }; + }; + }; + if (payload.errors?.length) { + throw new Error( + `GraphQL errors: ${payload.errors.map((e) => e.message).join("; ")}`, + ); + } + const nodes = + payload.data?.repository?.pullRequest?.closingIssuesReferences?.nodes ?? []; + return nodes.map((node) => ({ + repository: node.repository.nameWithOwner, + number: node.number, + state: node.state, + labels: node.labels.nodes.map((label) => label.name), + })); +} + +interface RestPullRequest { + number: number; + author_association: string; + user: { type: string } | null; +} + +async function fetchOpenPullRequests( + token: string, + owner: string, + repo: string, +): Promise { + const prs: OpenPr[] = []; + for (let page = 1; ; page++) { + const url = + `https://api.github.com/repos/${owner}/${repo}/pulls` + + `?state=open&per_page=100&page=${page}`; + const response = await githubFetch(url, token); + const batch = (await response.json()) as RestPullRequest[]; + for (const pr of batch) { + prs.push({ + number: pr.number, + authorAssociation: pr.author_association, + isBot: pr.user?.type === "Bot", + }); + } + if (batch.length < 100) { + break; + } + } + return prs; +} + +/** + * Build the "comment then close" action for a repo. In dry-run mode it logs the + * intended action instead of mutating the PR. + */ +function makeCloser( + token: string, + base: string, + dryRun: boolean, +): (prNumber: number, message: string) => Promise { + return async (prNumber, message) => { + if (dryRun) { + console.log(`[dry-run] would comment on and close PR #${prNumber}`); + return; + } + await githubFetch(`${base}/issues/${prNumber}/comments`, token, { + method: "POST", + body: JSON.stringify({ body: message }), + }); + await githubFetch(`${base}/issues/${prNumber}`, token, { + method: "PATCH", + body: JSON.stringify({ state: "closed", state_reason: "not_planned" }), + }); + }; +} + +/** Single-PR mode: react to the PR carried by a `pull_request_target` event. */ +async function runSinglePr( + token: string, + owner: string, + repo: string, + repository: string, +): Promise { + const prNumber = Number(requireEnv("PR_NUMBER")); + const authorAssociation = process.env.PR_AUTHOR_ASSOCIATION ?? "NONE"; + const isBot = (process.env.PR_AUTHOR_TYPE ?? "User") === "Bot"; + + const linkedIssues = await fetchLinkedIssues(token, owner, repo, prNumber); + const result = evaluateGate({ + repository, + authorAssociation, + isBot, + linkedIssues, + }); + + console.log( + `Contribution gate for PR #${prNumber}: pass=${result.pass} reason=${result.reason} ` + + `(author_association=${authorAssociation}, bot=${isBot}, linked_issues=${linkedIssues.length})`, + ); + + if (result.pass || !result.message) { + return; + } + + const base = `https://api.github.com/repos/${owner}/${repo}`; + await makeCloser(token, base, false)(prNumber, result.message); + console.log(`Closed PR #${prNumber} (reason=${result.reason}).`); +} + +/** All-PRs mode: sweep every open PR, for on-demand `workflow_dispatch` runs. */ +async function runSweep( + token: string, + owner: string, + repo: string, + repository: string, + dryRun: boolean, +): Promise { + const base = `https://api.github.com/repos/${owner}/${repo}`; + const io: GateIo = { + listOpenPrs: () => fetchOpenPullRequests(token, owner, repo), + fetchLinkedIssues: (prNumber) => + fetchLinkedIssues(token, owner, repo, prNumber), + closePr: makeCloser(token, base, dryRun), + }; + + const entries = await evaluateAllOpenPrs(io, repository); + const failing = entries.filter((entry) => !entry.result.pass); + console.log( + `Contribution gate sweep: ${entries.length} open PR(s) evaluated, ` + + `${failing.length} ${dryRun ? "would be " : ""}closed.`, + ); + for (const entry of entries) { + console.log( + ` PR #${entry.number}: pass=${entry.result.pass} reason=${entry.result.reason}`, + ); + } +} + +async function main(): Promise { + const token = requireEnv("GITHUB_TOKEN"); + const repository = requireEnv("GITHUB_REPOSITORY"); + const [owner, repo] = repository.split("/"); + + const allMode = + process.env.GATE_MODE === "all" || process.argv.includes("--all"); + if (allMode) { + const dryRun = /^(1|true)$/i.test(process.env.DRY_RUN ?? ""); + await runSweep(token, owner!, repo!, repository, dryRun); + } else { + await runSinglePr(token, owner!, repo!, repository); + } +} + +if (import.meta.main) { + main().catch((error) => { + console.error(error); + process.exit(1); + }); +} diff --git a/.github/workflows/contribution-gate.yml b/.github/workflows/contribution-gate.yml new file mode 100644 index 0000000000..54582d7790 --- /dev/null +++ b/.github/workflows/contribution-gate.yml @@ -0,0 +1,74 @@ +name: Contribution Gate + +on: + pull_request_target: + types: + - opened + - reopened + - edited + # Manual sweep over every open PR — e.g. after bulk-applying the + # `open-for-contribution` label. Set `dry_run` to log decisions without + # commenting or closing. + workflow_dispatch: + inputs: + dry_run: + description: "Evaluate and log decisions only; do not comment or close" + type: boolean + default: false + +permissions: + pull-requests: write + issues: read + contents: read + +concurrency: + # Per-PR for pull_request_target; unique per run for manual sweeps (which + # carry no pull_request payload) so two sweeps never cancel each other. + group: contribution-gate-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + +jobs: + gate-single: + name: Contribution Gate + runs-on: ubuntu-latest + # Exempt maintainers/collaborators and bots. External contributors are gated. + if: > + github.event_name == 'pull_request_target' && + github.event.pull_request.author_association != 'OWNER' && + github.event.pull_request.author_association != 'MEMBER' && + github.event.pull_request.author_association != 'COLLABORATOR' && + github.event.pull_request.user.type != 'Bot' + steps: + # Checks out the base repo (default for pull_request_target), so the gate + # runs trusted code from the base branch, never the untrusted PR head. + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version: "1.3.13" + - name: Evaluate contribution gate + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REPOSITORY: ${{ github.repository }} + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_AUTHOR_ASSOCIATION: ${{ github.event.pull_request.author_association }} + PR_AUTHOR_TYPE: ${{ github.event.pull_request.user.type }} + run: bun .github/scripts/contribution-gate.ts + + gate-all: + name: Contribution Gate (all open PRs) + runs-on: ubuntu-latest + if: github.event_name == 'workflow_dispatch' + steps: + # Base-branch checkout only — the sweep makes API calls and never runs + # any PR's code. + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version: "1.3.13" + - name: Evaluate contribution gate across all open PRs + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REPOSITORY: ${{ github.repository }} + GATE_MODE: all + DRY_RUN: ${{ inputs.dry_run || 'false' }} + run: bun .github/scripts/contribution-gate.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4312076c56..cb8978cf1a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,6 +2,18 @@ Bun monorepo for exploring the next generation of the Supabase CLI and local development stack. +## Contribution workflow + +Before you open a pull request: + +1. **Open an issue first**, using one of the [issue templates](https://github.com/supabase/cli/issues/new/choose). +2. **Wait for maintainer triage.** A maintainer categorizes the issue (`✨ Feature`, `🐛 Bug`, or `📘 Docs`) and adds the **`open-for-contribution`** label once it is ready to be worked on. +3. **Open a pull request only after the `open-for-contribution` label is set**, and link the issue with a closing keyword (for example `Closes #123`). + +Until the `open-for-contribution` label is present, the issue is still in triage, so work should not start and a pull request should not be opened. + +Pull requests from external contributors that do not follow this workflow are commented on and closed automatically by the [Contribution Gate](.github/workflows/contribution-gate.yml). Supabase members are exempt, so they can work from Linear tickets that are not public on GitHub. Maintainers: see [`.github/MAINTAINERS.md`](.github/MAINTAINERS.md). + ## Setup ### Tool versions diff --git a/README.md b/README.md index 1e0b1d528e..9652d3738b 100644 --- a/README.md +++ b/README.md @@ -149,7 +149,11 @@ pnpm repos:install ## Contributing -We love focused pull requests with a clear problem, a small surface area, and tests that match the user-facing behavior. Before opening a PR, run the checks for the workspace you touched. +We love focused pull requests with a clear problem, a small surface area, and tests that match the user-facing behavior. + +Open an issue first and wait for a maintainer to add the `open-for-contribution` label before starting work — external pull requests that don't link a labeled, open issue are closed automatically. See [CONTRIBUTING.md](./CONTRIBUTING.md#contribution-workflow) for the full workflow. + +Before opening a PR, run the checks for the workspace you touched. ```sh pnpm check:all From 351d02529ed33846be30ecfe077a523660327e55 Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Thu, 9 Jul 2026 16:46:04 +0100 Subject: [PATCH 36/79] fix(config-push): decrypt dotenvx encrypted: secrets instead of skipping (#5842) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What kind of change does this PR introduce? Bug fix (Go-parity gap). ## What is the current behavior? `config push`'s secret-hashing logic (`config-sync/config-sync.secret.ts`) treats dotenvx `encrypted:` secret values (e.g. `[auth.captcha] secret = "encrypted:..."`) as unresolved: it hashes them to `""`, which silently gates them out of both the diff and the update-request body. The remote secret is left untouched with no error and no feedback — the user has no idea their encrypted secret was never pushed. Go decrypts every `config.Secret` field during `config.Load` (before any network call) and pushes the plaintext, or aborts the whole command with `failed to parse config: ` if it can't decrypt. Fixes [CLI-1881](https://linear.app/supabase/issue/CLI-1881/config-push-decrypt-dotenvx-encrypted-secrets-instead-of-skipping). ## What is the new behavior? - `config-sync.secret.ts` (`secretHash`/`secretPlaintext`) now decrypts an `encrypted:` value with the existing `legacy-vault-decrypt.ts` decryptor (already used by `db push`/`db reset`/`migration up|down`) before hashing it for the diff or sending it as plaintext in the auth update body — the ciphertext itself is never pushed. - `push.handler.ts` now runs a document-wide "assert every `config.Secret` is decryptable" pre-check immediately after loading `config.toml`, before the cost-matrix call or any other network request — mirroring Go's `config.Load` timing, where the decrypt hook runs over the *whole* document (not just `auth.*`) regardless of which fields the current command actually reads. An undecryptable secret anywhere (even one `config push` never itself pushes, e.g. `studio.openai_api_key`) aborts with Go's exact `failed to parse config: ` message and zero network calls. - Reused/exported `legacyAssertDecryptableSecrets` from `legacy-db-config.toml-read.ts` (previously private, used only by the db-config family) rather than duplicating the scan logic — its return type was generalized from a db-config-specific error class to a plain message string so each caller can wrap it in its own domain error. - Removed the now-stale "not implemented" note from `config push`'s `SIDE_EFFECTS.md` and documented the new `DOTENV_PRIVATE_KEY`(`_*`) env vars + the new abort exit condition. ## Test plan - New unit tests in `config-sync.secret.unit.test.ts` covering decrypt-before-hash, multi-key fallback, and the two failure messages (reusing the Go test vector from `apps/cli-go/pkg/config/secret_test.go` / `legacy-vault-decrypt.unit.test.ts`). - Three new `push.integration.test.ts` cases: - an `encrypted:` captcha secret decrypts and the **plaintext** (not ciphertext) reaches the `updateAuthServiceConfig` PATCH body; - an undecryptable `encrypted:` secret aborts with `failed to parse config: missing private key` and **zero** network calls, even with `auth.enabled = false` (proving the check isn't auth-gated); - an undecryptable `studio.openai_api_key` (a field `config push` never reads or pushes) also aborts before any network call, proving the pre-check is genuinely document-wide. - Full `apps/cli` `types:check`, `lint:check`, `fmt:check`, and the touched `unit`/`integration` Vitest projects (`config-sync.secret`, `auth.sync`, `legacy-vault-decrypt`, `legacy-db-config.toml-read`, `push.integration`) all pass. Parity confirmed against `apps/cli-go/pkg/config/secret.go` + `internal/config/push/push.go` via `go-parity-auditor` before implementation (abort timing before any network call, error-message format, `DOTENV_PRIVATE_KEY` env-loading order, and that Go always pushes decrypted plaintext, never ciphertext). Reviewed by `architect-reviewer`, `engineer-reviewer`, `security-reviewer`, and `supabase-dx-reviewer` (all APPROVE/SHIP IT) — every actionable finding from that pass is folded into this PR already (removed a dead defensive branch that broke 100%-branch-coverage, hardened a hex-decode error message so it can't echo malformed key fragments, clarified a shared helper's doc comment, added the `studio.openai_api_key` test, and tidied `SIDE_EFFECTS.md` wording). ## Judgement calls left open (deliberately, not blocking) - **Non-matching `[remotes.*]` blocks aren't covered by the document-wide pre-check.** `@supabase/config`'s `loadProjectConfig` strips the `remotes` key from its returned document once a block matches the target ref, so an undecryptable secret hiding in a *different, unused* remote block escapes the check (Go's decode hook would still abort on it, since it decodes the whole file). Narrow edge case — only matters with multiple remotes where the unused one has a broken secret — and safe-direction (we succeed where Go would abort; we never push ciphertext). Documented in `SIDE_EFFECTS.md`'s KNOWN GAPS and in the shared helper's doc comment. - **A secret reached via `env(VAR)` indirection where the referenced var only resolves through the wider env-file set `legacy-db-config.toml-read.ts`/Go's `loadNestedEnv` reads (not `@supabase/config`'s narrower `supabase/.env`/`.env.local`)** is a pre-existing, CLI-1489-adjacent asymmetry between the two env-resolution paths — out of scope here; the code takes the parity-correct (wider) source for the pre-check, consistent with the shared helper's other caller. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- .../commands/config/push/SIDE_EFFECTS.md | 23 ++- .../config/push/config-sync/auth.sync.ts | 94 ++++++--- .../push/config-sync/auth.sync.unit.test.ts | 9 +- .../push/config-sync/config-sync.secret.ts | 65 ++++-- .../config-sync.secret.unit.test.ts | 78 +++++-- .../commands/config/push/push.handler.ts | 60 +++++- .../config/push/push.integration.test.ts | 191 ++++++++++++++++++ .../shared/legacy-db-config.toml-read.ts | 55 +++-- .../src/legacy/shared/legacy-vault-decrypt.ts | 8 +- packages/config/src/io.ts | 39 +++- 10 files changed, 522 insertions(+), 100 deletions(-) diff --git a/apps/cli/src/legacy/commands/config/push/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/config/push/SIDE_EFFECTS.md index 374a785a22..3661eb0d7e 100644 --- a/apps/cli/src/legacy/commands/config/push/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/config/push/SIDE_EFFECTS.md @@ -10,7 +10,7 @@ local → if changed, print the unified diff and confirm → PATCH/PUT/POST. | Path | Format | When | | ---------------------------------------------- | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `/supabase/config.toml` | TOML | always, before any network call (parse error aborts, exit 1) | -| `/supabase/.env`, `.env.local` | dotenv | always, to resolve `env(VAR)` references inside `config.toml` | +| `/supabase/.env`, `.env.local` | dotenv | always, to resolve `env(VAR)` references inside `config.toml` and to collect `DOTENV_PRIVATE_KEY`(`_*`) values for decrypting `encrypted:` secrets | | Auth email template HTML (`content_path`) | HTML | when `auth.enabled`; paths resolved per Go rules (see below) | | `/supabase/.temp/project-ref` | plain text | project-ref fallback (flag → `SUPABASE_PROJECT_ID` → this file) | | `/supabase/.temp/linked-project.json` | JSON | existence check only, to decide whether the cache write below is skipped (mirrors Go's `ensureProjectGroupsCached` telemetry cache — see `db/lint`'s Notes for the full mechanism) | @@ -51,13 +51,14 @@ when its local gate is off. ## Environment Variables -| Variable | Purpose | Required? | -| ----------------------- | -------------------------------------------------------- | ------------------------------------------------------- | -| `SUPABASE_PROJECT_ID` | project ref (flag → this → `.temp/project-ref` → prompt) | no | -| `SUPABASE_YES` | auto-confirm prompts (`--yes`) | no | -| `SUPABASE_ACCESS_TOKEN` | auth token (bypasses credential file/keyring lookup) | no (falls back to keyring → `~/.supabase/access-token`) | -| `SUPABASE_PROFILE` | API profile selection | no | -| `env(VAR)` references | interpolated into `config.toml` values at load | no | +| Variable | Purpose | Required? | +| -------------------------------------------- | --------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `SUPABASE_PROJECT_ID` | project ref (flag → this → `.temp/project-ref` → prompt) | no | +| `SUPABASE_YES` | auto-confirm prompts (`--yes`) | no | +| `SUPABASE_ACCESS_TOKEN` | auth token (bypasses credential file/keyring lookup) | no (falls back to keyring → `~/.supabase/access-token`) | +| `SUPABASE_PROFILE` | API profile selection | no | +| `env(VAR)` references | interpolated into `config.toml` values at load | no | +| `DOTENV_PRIVATE_KEY`, `DOTENV_PRIVATE_KEY_*` | decrypt `encrypted:` (dotenvx) secret values before hashing/pushing; comma-split, first matching key wins | only if a `config.Secret`-typed field (see below) holds an `encrypted:` value — an `encrypted:`-looking string in a non-secret field (e.g. an email template `subject`) never needs a key | ## Exit Codes @@ -65,6 +66,7 @@ when its local gate is off. | ---- | ------------------------------------------------------------------------------------------ | | `0` | success, **including** declining a confirmation prompt (Go returns nil and continues) | | `1` | malformed `config.toml` | +| `1` | an `encrypted:` (dotenvx) secret anywhere in the document cannot be decrypted (see below) | | `1` | invalid `auth.email.*.content_path` (missing/unreadable template file when `auth.enabled`) | | `1` | two `[remotes.*]` blocks declare the same `project_id` as the target ref | | `1` | list-addons failure (network or non-200) | @@ -115,5 +117,8 @@ keys mirror `config.toml` paths. - Diff bytes are byte-for-byte identical to the Go CLI (BurntSushi TOML encoder + anchored diff ports). - Optional `*pointer` sections (`db.ssl_enforcement`, `storage.image_transformation`, `storage.s3_protocol`) are decoded as defaulted-present by `@supabase/config`; their true presence is recovered from the raw (merged) config document so they are skipped when absent, matching Go's nil-pointer behaviour. - **`[remotes.*]` overrides are merged before push.** When a `[remotes.]` block declares `project_id == `, `@supabase/config` merges that block's subtree over the base config at the raw (pre-decode) level — Go's `mergeRemoteConfig` (`apps/cli-go/pkg/config/config.go:550`) — so only the keys the block declares override the base. `Loading config override: [remotes.]` prints to stderr. Two remotes sharing the target `project_id` abort with Go's `duplicate project_id for [remotes.] and [remotes.]` message. +- **`encrypted:` (dotenvx) secrets are decrypted, hashed, and pushed as plaintext**, matching Go's `Secret.Decrypt`/`DecryptSecretHookFunc`. `DOTENV_PRIVATE_KEY`(`_*`) values from the shell + `supabase/.env` decrypt the ciphertext; the decrypted plaintext is what gets hashed for the diff and sent as `Secret.Value` in the update body. The ciphertext itself is never pushed. +- **An undecryptable secret aborts before any network call.** Before the cost-matrix list-addons request or any other service call, every `config.Secret`-typed value in the document is asserted decryptable — not just `auth.*` (the only fields `config push` actually sends), matching Go's document-wide decode hook, which runs the same check regardless of which fields a given command reads. This covers `[db.vault]` (a `map[string]Secret` in Go too, not just an `auth.*` field). An undecryptable value aborts with Go's `failed to parse config: ` message, exit code `1`. +- **Deprecated `[auth.external.{linkedin,slack}]` secrets are checked before they're stripped.** `@supabase/config` strips these deprecated blocks from the decoded document before returning it, but Go's decrypt hook runs at decode time — before its own later `external.validate()` deletes them — so `config push` re-checks the stripped-out sub-objects separately, matching Go's decode-before-delete order rather than missing a secret hiding in one of them. - KNOWN GAPS: - - **`encrypted:` (dotenvx) secret decryption is not reproduced.** The Go CLI decrypts `encrypted:` values before hashing and pushes the plaintext; we cannot decrypt here. Rather than push the ciphertext (which would overwrite the remote secret with garbage), `encrypted:` values are treated as unresolved — exactly like `env()` refs: they hash to `""`, so the empty hash gates them out of both the diff and the update body and the remote secret is left untouched. + - The document-wide decrypt-or-abort pre-check scans the config document `@supabase/config` hands back, which has the _matched_ `[remotes.]` block already merged in and its `remotes` key removed. An `encrypted:` secret that's undecryptable inside a **different, non-matching** `[remotes.*]` block is therefore not caught here (Go's decode hook runs over the whole parsed document, including every remote, so it would abort in that case too). Narrow edge case: it only matters when a project declares multiple remotes and the _unused_ one has a broken secret. diff --git a/apps/cli/src/legacy/commands/config/push/config-sync/auth.sync.ts b/apps/cli/src/legacy/commands/config/push/config-sync/auth.sync.ts index 2321f9d960..75e6d9d484 100644 --- a/apps/cli/src/legacy/commands/config/push/config-sync/auth.sync.ts +++ b/apps/cli/src/legacy/commands/config/push/config-sync/auth.sync.ts @@ -16,7 +16,7 @@ import { type TomlField, type TomlValue, encodeToml } from "./config-sync.toml.t import { intToUint } from "../../../../shared/legacy-size-units.ts"; import { durationString, parseDuration, secondsToDurationString } from "./config-sync.duration.ts"; import type { AuthEmailContent } from "./config-sync.auth-email-content.ts"; -import { secretHash } from "./config-sync.secret.ts"; +import { secretHash, secretPlaintext } from "./config-sync.secret.ts"; // --------------------------------------------------------------------------- // Sub-types @@ -858,9 +858,13 @@ function normalizeDurationStr(s: string | undefined): string { } } -function projectSecret(projectId: string, value: string | undefined): string { +function projectSecret( + projectId: string, + value: string | undefined, + dotenvPrivateKeys: ReadonlyArray, +): string { if (!value) return ""; - return secretHash(projectId, value); + return secretHash(projectId, value, dotenvPrivateKeys); } /** @@ -908,12 +912,21 @@ export interface AuthPresence { * Projects `config.auth` into the push subset, pre-computing all duration and * secret fields. `projectId` is the HMAC key for secret hashing. `presence` * carries raw-config presence for the optional sub-sections Go skips when nil. + * `dotenvPrivateKeys` (Go's `DOTENV_PRIVATE_KEY`/`DOTENV_PRIVATE_KEY_*` values) + * decrypt any `encrypted:` (dotenvx) secret before it's hashed or copied into + * {@link AuthSubset.rawSecrets} — see `config-sync.secret.ts`. + * + * @throws When an `encrypted:` secret cannot be decrypted with any key. The + * handler's document-wide pre-check (`legacyAssertDecryptableSecrets`) runs + * before this and is expected to have already aborted in that case, so this + * throw is a defensive backstop, not the primary abort path. */ export function authSubsetFromConfig( config: ProjectConfig, projectId: string, presence: AuthPresence, emailContent: AuthEmailContent = { template: {}, notification: {} }, + dotenvPrivateKeys: ReadonlyArray = [], ): AuthSubset { const a = config.auth; @@ -947,7 +960,7 @@ export function authSubsetFromConfig( : { enabled: captchaConfig.enabled ?? false, provider: captchaConfig.provider ?? "", - secret: projectSecret(projectId, captchaConfig.secret ?? ""), + secret: projectSecret(projectId, captchaConfig.secret ?? "", dotenvPrivateKeys), }; // Hooks — Go gates each on `hook. != nil`; absent in raw config → skip. @@ -960,7 +973,7 @@ export function authSubsetFromConfig( return { enabled: hc.enabled, uri: hc.uri ?? "", - secrets: projectSecret(projectId, hc.secrets ?? ""), + secrets: projectSecret(projectId, hc.secrets ?? "", dotenvPrivateKeys), }; } const hook: AuthSubset["hook"] = { @@ -1047,7 +1060,7 @@ export function authSubsetFromConfig( host: smtpConfig.host ?? "", port: smtpConfig.port ?? 0, user: smtpConfig.user ?? "", - pass: projectSecret(projectId, smtpConfig.pass ?? ""), + pass: projectSecret(projectId, smtpConfig.pass ?? "", dotenvPrivateKeys), admin_email: smtpConfig.admin_email ?? "", sender_name: smtpConfig.sender_name ?? "", }; @@ -1064,7 +1077,7 @@ export function authSubsetFromConfig( enabled: tc.enabled, account_sid: tc.account_sid ?? "", message_service_sid: tc.message_service_sid ?? "", - auth_token: projectSecret(projectId, tc.auth_token ?? ""), + auth_token: projectSecret(projectId, tc.auth_token ?? "", dotenvPrivateKeys), }; } const sms: AuthSubset["sms"] = { @@ -1077,18 +1090,18 @@ export function authSubsetFromConfig( messagebird: { enabled: s.messagebird.enabled, originator: s.messagebird.originator ?? "", - access_key: projectSecret(projectId, s.messagebird.access_key ?? ""), + access_key: projectSecret(projectId, s.messagebird.access_key ?? "", dotenvPrivateKeys), }, textlocal: { enabled: s.textlocal.enabled, sender: s.textlocal.sender ?? "", - api_key: projectSecret(projectId, s.textlocal.api_key ?? ""), + api_key: projectSecret(projectId, s.textlocal.api_key ?? "", dotenvPrivateKeys), }, vonage: { enabled: s.vonage.enabled, from: s.vonage.from ?? "", api_key: s.vonage.api_key ?? "", - api_secret: projectSecret(projectId, s.vonage.api_secret ?? ""), + api_secret: projectSecret(projectId, s.vonage.api_secret ?? "", dotenvPrivateKeys), }, test_otp: s.test_otp ?? {}, }; @@ -1105,7 +1118,7 @@ export function authSubsetFromConfig( external[k] = { enabled: p.enabled, client_id: p.client_id ?? "", - secret: projectSecret(projectId, p.secret ?? ""), + secret: projectSecret(projectId, p.secret ?? "", dotenvPrivateKeys), url: p.url ?? "", redirect_uri: p.redirect_uri ?? "", skip_nonce_check: p.skip_nonce_check ?? false, @@ -1193,33 +1206,52 @@ export function authSubsetFromConfig( allow_dynamic_registration: a.oauth_server.allow_dynamic_registration, authorization_url_path: a.oauth_server.authorization_url_path, }, - publishable_key: projectSecret(projectId, a.publishable_key ?? ""), - secret_key: projectSecret(projectId, a.secret_key ?? ""), - jwt_secret: projectSecret(projectId, a.jwt_secret ?? ""), - anon_key: projectSecret(projectId, a.anon_key ?? ""), - service_role_key: projectSecret(projectId, a.service_role_key ?? ""), + publishable_key: projectSecret(projectId, a.publishable_key ?? "", dotenvPrivateKeys), + secret_key: projectSecret(projectId, a.secret_key ?? "", dotenvPrivateKeys), + jwt_secret: projectSecret(projectId, a.jwt_secret ?? "", dotenvPrivateKeys), + anon_key: projectSecret(projectId, a.anon_key ?? "", dotenvPrivateKeys), + service_role_key: projectSecret(projectId, a.service_role_key ?? "", dotenvPrivateKeys), third_party, // Raw plaintext secrets for the update body (see AuthRawSecrets). Sourced - // from the same config accessors used for hashing above. + // from the same config accessors used for hashing above, decrypted the + // same way (`secretPlaintext`) so an `encrypted:` value is never sent + // as-is — Go always pushes `Secret.Value` (plaintext), never the ciphertext. rawSecrets: { - captcha: captchaConfig?.secret ?? "", + captcha: secretPlaintext(captchaConfig?.secret ?? "", dotenvPrivateKeys), hooks: { - mfa_verification_attempt: h.mfa_verification_attempt?.secrets ?? "", - password_verification_attempt: h.password_verification_attempt?.secrets ?? "", - custom_access_token: h.custom_access_token?.secrets ?? "", - send_sms: h.send_sms?.secrets ?? "", - send_email: h.send_email?.secrets ?? "", - before_user_created: h.before_user_created?.secrets ?? "", + mfa_verification_attempt: secretPlaintext( + h.mfa_verification_attempt?.secrets ?? "", + dotenvPrivateKeys, + ), + password_verification_attempt: secretPlaintext( + h.password_verification_attempt?.secrets ?? "", + dotenvPrivateKeys, + ), + custom_access_token: secretPlaintext( + h.custom_access_token?.secrets ?? "", + dotenvPrivateKeys, + ), + send_sms: secretPlaintext(h.send_sms?.secrets ?? "", dotenvPrivateKeys), + send_email: secretPlaintext(h.send_email?.secrets ?? "", dotenvPrivateKeys), + before_user_created: secretPlaintext( + h.before_user_created?.secrets ?? "", + dotenvPrivateKeys, + ), }, - smtp_pass: smtpConfig?.pass ?? "", + smtp_pass: secretPlaintext(smtpConfig?.pass ?? "", dotenvPrivateKeys), sms: { - twilio: s.twilio.auth_token ?? "", - twilio_verify: s.twilio_verify.auth_token ?? "", - messagebird: s.messagebird.access_key ?? "", - textlocal: s.textlocal.api_key ?? "", - vonage: s.vonage.api_secret ?? "", + twilio: secretPlaintext(s.twilio.auth_token ?? "", dotenvPrivateKeys), + twilio_verify: secretPlaintext(s.twilio_verify.auth_token ?? "", dotenvPrivateKeys), + messagebird: secretPlaintext(s.messagebird.access_key ?? "", dotenvPrivateKeys), + textlocal: secretPlaintext(s.textlocal.api_key ?? "", dotenvPrivateKeys), + vonage: secretPlaintext(s.vonage.api_secret ?? "", dotenvPrivateKeys), }, - providers: Object.fromEntries(Object.entries(ext ?? {}).map(([k, p]) => [k, p.secret ?? ""])), + providers: Object.fromEntries( + Object.entries(ext ?? {}).map(([k, p]) => [ + k, + secretPlaintext(p.secret ?? "", dotenvPrivateKeys), + ]), + ), }, }; } diff --git a/apps/cli/src/legacy/commands/config/push/config-sync/auth.sync.unit.test.ts b/apps/cli/src/legacy/commands/config/push/config-sync/auth.sync.unit.test.ts index 4083093f14..403cb01611 100644 --- a/apps/cli/src/legacy/commands/config/push/config-sync/auth.sync.unit.test.ts +++ b/apps/cli/src/legacy/commands/config/push/config-sync/auth.sync.unit.test.ts @@ -1418,10 +1418,11 @@ describe("authToUpdateBody secrets", () => { expect("security_captcha_secret" in body).toBe(false); }); - it("never pushes dotenvx ciphertext (encrypted: hashes to '' so the gate drops it)", () => { - // secretHash returns "" for `encrypted:` values, so the projected captcha - // secret is empty even though the raw (ciphertext) value is still present. - // The empty hash must gate the ciphertext out of the update body. + it("gates the raw secret out of the body when the hashed field is empty", () => { + // `authToUpdateBody` itself only knows the pre-computed hash/rawSecrets + // pair — this exercises the empty-hash gate directly (a decrypt failure + // for a real `encrypted:` value aborts earlier, in `authSubsetFromConfig` / + // the handler's document-wide pre-check; see push.integration.test.ts). const local = bareAuth({ enabled: true, captcha: { enabled: true, provider: "hcaptcha", secret: "" }, diff --git a/apps/cli/src/legacy/commands/config/push/config-sync/config-sync.secret.ts b/apps/cli/src/legacy/commands/config/push/config-sync/config-sync.secret.ts index 7b00046e6f..130bf6c48e 100644 --- a/apps/cli/src/legacy/commands/config/push/config-sync/config-sync.secret.ts +++ b/apps/cli/src/legacy/commands/config/push/config-sync/config-sync.secret.ts @@ -5,33 +5,72 @@ * Rules: * - Empty value → "" (no hash prefix). * - Value matching `^env\((.*)\)$` (unresolved env reference) → "" (no hash). - * - Value starting with `encrypted:` (dotenvx ciphertext) → "" (no hash). + * - Value starting with `encrypted:` (dotenvx ciphertext) → decrypt with + * `legacyDecryptSecret`, then hash the decrypted plaintext. * - Otherwise → "hash:" + sha256Hmac(projectId, value). * - * NOTE: `encrypted:` dotenvx decryption is not implemented. The Go CLI decrypts - * such values before hashing and pushes the plaintext; we cannot. Rather than - * hash and push the ciphertext — which would silently overwrite the remote - * secret with garbage — we treat `encrypted:` values as unresolved, exactly like - * `env()` refs: `secretHash` returns "", so the empty hash gates the value out of - * both the diff and the update body and the remote secret is left untouched. - * This is a documented residual gap for local dev use of dotenvx secrets - * (see SIDE_EFFECTS.md). + * `config push`'s handler runs a document-wide decrypt-or-abort pre-check + * (`push.handler.ts`, reusing `legacyAssertDecryptableSecrets`) immediately + * after loading `config.toml` and before any network call — mirroring Go's + * `config.Load` timing, where `DecryptSecretHookFunc` runs as part of the + * generic decode hook chain, before `internal/config/push.Run` ever reads the + * cost matrix or any service. By the time the functions below run (deep in + * the auth service's local/diff computation), decryption is therefore + * expected to always succeed. They still throw a `failed to parse config: + * ` error (Go's own wording) rather than silently gating the secret + * out, in case that invariant is ever violated by a future field addition. */ import { createHmac } from "node:crypto"; +import { legacyDecryptSecret } from "../../../../shared/legacy-vault-decrypt.ts"; + const ENV_PATTERN = /^env\((.*)\)$/; const ENCRYPTED_PREFIX = "encrypted:"; const HASHED_PREFIX = "hash:"; +/** Decrypts `value` when it's a dotenvx `encrypted:` ciphertext; otherwise returns it unchanged. */ +function decryptIfNeeded(value: string, dotenvPrivateKeys: ReadonlyArray): string { + if (!value.startsWith(ENCRYPTED_PREFIX)) return value; + const decrypted = legacyDecryptSecret(value, dotenvPrivateKeys); + if (!decrypted.ok) { + throw new Error(`failed to parse config: ${decrypted.error}`); + } + return decrypted.value; +} + /** * Returns the TOML serialisation of a Secret field, mirroring Go's - * `Secret.MarshalText`. The project ref is the HMAC key. + * `Secret.MarshalText`. The project ref is the HMAC key. `dotenvPrivateKeys` + * are Go's `DOTENV_PRIVATE_KEY`/`DOTENV_PRIVATE_KEY_*` values + * (`legacyCollectDotenvPrivateKeys`), used to decrypt an `encrypted:` value + * before hashing — Go always hashes the decrypted plaintext, never the + * ciphertext. + * + * @throws When an `encrypted:` value cannot be decrypted with any key. */ -export function secretHash(projectId: string, value: string): string { +export function secretHash( + projectId: string, + value: string, + dotenvPrivateKeys: ReadonlyArray, +): string { if (value.length === 0) return ""; if (ENV_PATTERN.test(value)) return ""; - if (value.startsWith(ENCRYPTED_PREFIX)) return ""; - const hmac = createHmac("sha256", projectId).update(value).digest("hex"); + const plaintext = decryptIfNeeded(value, dotenvPrivateKeys); + const hmac = createHmac("sha256", projectId).update(plaintext).digest("hex"); return HASHED_PREFIX + hmac; } + +/** + * Resolves a Secret field to the plaintext Go sends as `Secret.Value` in the + * update-request body — decrypting an `encrypted:` value with + * `dotenvPrivateKeys`, otherwise returning `value` unchanged. Callers gate on + * {@link secretHash}'s result being non-empty before using this (mirrors Go's + * `if len(Secret.SHA256) > 0`), so the empty/unresolved-`env()` cases never + * reach the request body regardless of what this returns for them. + * + * @throws When an `encrypted:` value cannot be decrypted with any key. + */ +export function secretPlaintext(value: string, dotenvPrivateKeys: ReadonlyArray): string { + return decryptIfNeeded(value, dotenvPrivateKeys); +} diff --git a/apps/cli/src/legacy/commands/config/push/config-sync/config-sync.secret.unit.test.ts b/apps/cli/src/legacy/commands/config/push/config-sync/config-sync.secret.unit.test.ts index 9766f1efbb..707ad52f28 100644 --- a/apps/cli/src/legacy/commands/config/push/config-sync/config-sync.secret.unit.test.ts +++ b/apps/cli/src/legacy/commands/config/push/config-sync/config-sync.secret.unit.test.ts @@ -1,6 +1,6 @@ /** * Unit tests for config-sync.secret.ts — golden parity with Go's - * `Secret.MarshalText` (`apps/cli-go/pkg/config/secret.go`). + * `Secret.MarshalText` + `DecryptSecretHookFunc` (`apps/cli-go/pkg/config/secret.go`). * * The HMAC keys/values below were captured from the same `createHmac` the port * uses; they lock the exact `hash:` serialisation Go emits. @@ -8,42 +8,86 @@ import { describe, expect, it } from "vitest"; -import { secretHash } from "./config-sync.secret.ts"; +import { secretHash, secretPlaintext } from "./config-sync.secret.ts"; + +// Go's test vector — `apps/cli-go/pkg/config/secret_test.go:9-19` (same one +// `legacy-vault-decrypt.unit.test.ts` uses). Decrypts to the plaintext "value". +const PRIVATE_KEY = "7fd7210cef8f331ee8c55897996aaaafd853a2b20a4dc73d6d75759f65d2a7eb"; +const ENCRYPTED_VALUE = + "encrypted:BKiXH15AyRzeohGyUrmB6cGjSklCrrBjdesQlX1VcXo/Xp20Bi2gGZ3AlIqxPQDmjVAALnhZamKnuY73l8Dz1P+BYiZUgxTSLzdCvdYUyVbNekj2UudbdUizBViERtZkuQwZHIv/"; +const WRONG_KEY = "11".repeat(32); describe("secretHash", () => { it("returns the hash: form for a plaintext secret", () => { - expect(secretHash("abcdefghijklmnopqrst", "my-secret")).toBe( + expect(secretHash("abcdefghijklmnopqrst", "my-secret", [])).toBe( "hash:64800db722cc0be9e1d816d5aed626805e91a939d2dbcbc5239cd31eeef763e9", ); - expect(secretHash("test", "topsecret")).toBe( + expect(secretHash("test", "topsecret", [])).toBe( "hash:8eed2826599c798e072951884ced30954f8322fa1c3648506634e8376a740d72", ); }); it("keys the HMAC on the project ref (same value, different ref → different hash)", () => { - expect(secretHash("ref-a", "same")).not.toBe(secretHash("ref-b", "same")); + expect(secretHash("ref-a", "same", [])).not.toBe(secretHash("ref-b", "same", [])); }); it("returns '' for an empty value", () => { - expect(secretHash("abcdefghijklmnopqrst", "")).toBe(""); + expect(secretHash("abcdefghijklmnopqrst", "", [])).toBe(""); }); it("returns '' for an unresolved env() reference", () => { - expect(secretHash("abcdefghijklmnopqrst", "env(MY_SECRET)")).toBe(""); - expect(secretHash("abcdefghijklmnopqrst", "env()")).toBe(""); - }); - - it("returns '' for a dotenvx encrypted: value (never hashes/pushes ciphertext)", () => { - // Regression: hashing/pushing the ciphertext would overwrite the remote - // secret with garbage. Treated as unresolved, like env(). - expect(secretHash("abcdefghijklmnopqrst", "encrypted:BvEYU1pXk9...")).toBe(""); + expect(secretHash("abcdefghijklmnopqrst", "env(MY_SECRET)", [])).toBe(""); + expect(secretHash("abcdefghijklmnopqrst", "env()", [])).toBe(""); }); it("hashes a value that merely contains (but does not start with) 'encrypted:'", () => { // Only the dotenvx prefix is special; an embedded substring is a real secret. - expect(secretHash("test", "not-encrypted:value")).toBe( - secretHash("test", "not-encrypted:value"), + expect(secretHash("test", "not-encrypted:value", [])).toBe( + secretHash("test", "not-encrypted:value", []), + ); + expect(secretHash("test", "not-encrypted:value", []).startsWith("hash:")).toBe(true); + }); + + describe("dotenvx encrypted: values", () => { + it("decrypts before hashing (hash matches the decrypted plaintext, not the ciphertext)", () => { + expect(secretHash("abcdefghijklmnopqrst", ENCRYPTED_VALUE, [PRIVATE_KEY])).toBe( + secretHash("abcdefghijklmnopqrst", "value", []), + ); + }); + + it("tries each key and the first working one wins", () => { + expect(secretHash("test", ENCRYPTED_VALUE, [WRONG_KEY, PRIVATE_KEY])).toBe( + secretHash("test", "value", []), + ); + }); + + it("throws 'failed to parse config: missing private key' with no keys", () => { + expect(() => secretHash("test", ENCRYPTED_VALUE, [])).toThrow( + "failed to parse config: missing private key", + ); + }); + + it("throws 'failed to parse config: failed to decrypt secret: ...' for a wrong key", () => { + expect(() => secretHash("test", ENCRYPTED_VALUE, [WRONG_KEY])).toThrow( + /^failed to parse config: failed to decrypt secret:/, + ); + }); + }); +}); + +describe("secretPlaintext", () => { + it("returns a plain value unchanged", () => { + expect(secretPlaintext("my-secret", [])).toBe("my-secret"); + expect(secretPlaintext("", [])).toBe(""); + }); + + it("decrypts a dotenvx encrypted: value to its plaintext", () => { + expect(secretPlaintext(ENCRYPTED_VALUE, [PRIVATE_KEY])).toBe("value"); + }); + + it("never returns the ciphertext when decryption fails — throws instead", () => { + expect(() => secretPlaintext(ENCRYPTED_VALUE, [])).toThrow( + "failed to parse config: missing private key", ); - expect(secretHash("test", "not-encrypted:value").startsWith("hash:")).toBe(true); }); }); diff --git a/apps/cli/src/legacy/commands/config/push/push.handler.ts b/apps/cli/src/legacy/commands/config/push/push.handler.ts index 60e1869c1e..e1fc8971f3 100644 --- a/apps/cli/src/legacy/commands/config/push/push.handler.ts +++ b/apps/cli/src/legacy/commands/config/push/push.handler.ts @@ -8,9 +8,13 @@ import { LegacyTelemetryState } from "../../../telemetry/legacy-telemetry-state. import { legacyResolveYesWithProjectEnv } from "../../../../shared/legacy/global-flags.ts"; import { Output } from "../../../../shared/output/output.service.ts"; import { RuntimeInfo } from "../../../../shared/runtime/runtime-info.service.ts"; -import { legacyLoadProjectEnv } from "../../../shared/legacy-db-config.toml-read.ts"; +import { + legacyAssertDecryptableSecrets, + legacyLoadProjectEnv, +} from "../../../shared/legacy-db-config.toml-read.ts"; import { mapLegacyHttpError } from "../../../shared/legacy-http-errors.ts"; import { legacyPromptYesNo } from "../../../shared/legacy-prompt-yes-no.ts"; +import { legacyCollectDotenvPrivateKeys } from "../../../shared/legacy-vault-decrypt.ts"; import { apiSubsetFromConfig, apiToUpdateBody, diffApiWithRemote } from "./config-sync/api.sync.ts"; import { applyRemoteAuthConfig, @@ -101,6 +105,19 @@ export const legacyConfigPush = Effect.fn("legacy.config.push")(function* ( const projectRoot = (yield* findProjectRoot(runtimeInfo.cwd)) ?? runtimeInfo.cwd; const projectEnv = yield* legacyLoadProjectEnv(fs, path, projectRoot); const yes = yield* legacyResolveYesWithProjectEnv(projectEnv); + // dotenvx private keys for decrypting `encrypted:` secrets (Go's + // `DecryptSecretHookFunc`), from the shell + project env — same source/precedence + // as `legacy-db-config.toml-read.ts` (`process.env` wins over `supabase/.env`). + const dotenvPrivateKeys = legacyCollectDotenvPrivateKeys({ ...projectEnv, ...process.env }); + // Only reached by `legacyAssertDecryptableSecrets` below for an `env(VAR)` literal that + // survives `loaded.document`'s own (`@supabase/config`) interpolation pass unresolved — i.e. + // when this wider env source (matching Go's `loadNestedEnv`) resolves `VAR` but + // `@supabase/config`'s narrower one (`supabase/.env`/`.env.local` only) didn't. Practically + // unreachable in the same narrow way the CLI-1489 comment below already documents for + // non-secret fields; kept for parity with the shared function's other caller + // (`legacy-db-config.toml-read.ts`, whose pre-interpolation document relies on this). + const secretEnvLookup = (name: string): string | undefined => + process.env[name] ?? projectEnv[name]; const ref = yield* resolver.resolve(flags.projectRef); @@ -144,6 +161,33 @@ export const legacyConfigPush = Effect.fn("legacy.config.push")(function* ( const projectId = ref; const config = loaded.config; + // 1b. Assert every `config.Secret`-typed `encrypted:` value in the document + // (not just auth.*) can be decrypted, mirroring Go's global + // `DecryptSecretHookFunc`, which runs as part of `config.Load` — before + // `internal/config/push.Run` ever reads the cost matrix (`push.go:16` vs. + // `push.go:25`) or touches any service. An undecryptable secret anywhere in + // the document (even one `config push` never itself pushes, e.g. + // `studio.openai_api_key`) aborts here with Go's own `failed to parse + // config: ` message, before any remote service is read or updated. + // + // `loaded.document` has already had Go's deprecated `auth.external.{linkedin,slack}` + // blocks stripped by `@supabase/config` (`normalizeDeprecatedExternalProviders`), but + // Go's decrypt hook runs at decode time — before its own later `external.validate()` + // deletes those blocks — so an `encrypted:` secret hiding in one of them still aborts + // Go's load. Fold `removedDeprecatedExternalProviders` back into a synthetic + // `auth.external` view and scan that too, reusing the same path list rather than a + // second scanner. + const secretError = + legacyAssertDecryptableSecrets(loaded.document, secretEnvLookup, dotenvPrivateKeys) ?? + legacyAssertDecryptableSecrets( + { auth: { external: loaded.removedDeprecatedExternalProviders ?? {} } }, + secretEnvLookup, + dotenvPrivateKeys, + ); + if (secretError !== undefined) { + return yield* new LegacyConfigPushLoadConfigError({ message: secretError }); + } + // Optional `*pointer` sections (ssl_enforcement, image_transformation, // s3_protocol) are defaulted-present by @supabase/config and cannot be // recovered from the decoded config, so we inspect the raw (merged) document @@ -363,7 +407,19 @@ export const legacyConfigPush = Effect.fn("legacy.config.push")(function* ( }), ), ); - let local = authSubsetFromConfig(config, projectId, presence.auth, authEmailContent); + // `dotenvPrivateKeys` decrypts any `encrypted:` auth secret before it's + // hashed/copied into the update body. The document-wide check above + // (step 1b) already scanned every `config.Secret` path — including every + // field `authSubsetFromConfig` reads — and would have aborted by now if + // any were undecryptable, so the decrypt calls inside it are unreachable + // failure paths here, not a real branch to guard with `Effect.try`. + let local = authSubsetFromConfig( + config, + projectId, + presence.auth, + authEmailContent, + dotenvPrivateKeys, + ); const projected = applyRemoteAuthConfig(local, remote); // MFA phone/webauthn are paid addons: confirm cost before enabling. if (mfaPhoneNewlyEnabled(local, projected) && !(yield* keep("auth_mfa_phone"))) { diff --git a/apps/cli/src/legacy/commands/config/push/push.integration.test.ts b/apps/cli/src/legacy/commands/config/push/push.integration.test.ts index 02ccd9d77c..0713bf7be0 100644 --- a/apps/cli/src/legacy/commands/config/push/push.integration.test.ts +++ b/apps/cli/src/legacy/commands/config/push/push.integration.test.ts @@ -29,6 +29,31 @@ function writeConfig(toml: string): void { writeFileSync(join(dir, "config.toml"), toml); } +// Go's test vector — `apps/cli-go/pkg/config/secret_test.go:9-19` (same one +// `legacy-vault-decrypt.unit.test.ts` and `config-sync.secret.unit.test.ts` use). +// Decrypts to the plaintext "value". +const DOTENVX_PRIVATE_KEY = "7fd7210cef8f331ee8c55897996aaaafd853a2b20a4dc73d6d75759f65d2a7eb"; +const DOTENVX_ENCRYPTED_VALUE = + "encrypted:BKiXH15AyRzeohGyUrmB6cGjSklCrrBjdesQlX1VcXo/Xp20Bi2gGZ3AlIqxPQDmjVAALnhZamKnuY73l8Dz1P+BYiZUgxTSLzdCvdYUyVbNekj2UudbdUizBViERtZkuQwZHIv/"; + +/** Save/restore `DOTENV_PRIVATE_KEY` around a test — mirrors the SUPABASE_YES pattern below. */ +function withDotenvPrivateKey( + value: string | undefined, + effect: Effect.Effect, +): Effect.Effect { + const prev = process.env["DOTENV_PRIVATE_KEY"]; + if (value === undefined) delete process.env["DOTENV_PRIVATE_KEY"]; + else process.env["DOTENV_PRIVATE_KEY"] = value; + return effect.pipe( + Effect.ensuring( + Effect.sync(() => { + if (prev === undefined) delete process.env["DOTENV_PRIVATE_KEY"]; + else process.env["DOTENV_PRIVATE_KEY"] = prev; + }), + ), + ); +} + // Schema-valid PostgREST GET response with the api disabled remotely (empty // schema). The real API client validates GET bodies against the generated // output schema, so every postgrest GET must carry these fields. @@ -724,6 +749,172 @@ secret = "my-plaintext-secret" }, ); + it.live( + "decrypts a dotenvx encrypted: captcha secret and pushes the plaintext (CLI-1881)", + () => { + const toml = `project_id = "test" +[storage] +enabled = false +[auth] +enabled = true +site_url = "http://localhost:3000" +[auth.captcha] +enabled = true +provider = "hcaptcha" +secret = "${DOTENVX_ENCRYPTED_VALUE}" +`; + const { layer, apiMock } = setupService({ + toml, + yes: true, + v1: { + getAuthServiceConfig: () => Effect.succeed({}), + updateAuthServiceConfig: () => Effect.succeed({}), + }, + }); + return withDotenvPrivateKey( + DOTENVX_PRIVATE_KEY, + Effect.gen(function* () { + yield* legacyConfigPush({ projectRef: Option.none() }); + const update = apiMock.requests.find((r) => r.method === "updateAuthServiceConfig"); + expect(update).toBeDefined(); + const input = update?.input as Record; + // Go decrypts before hashing/pushing — the plaintext goes to the API, + // never the dotenvx ciphertext. + expect(input["security_captcha_secret"]).toBe("value"); + }).pipe(Effect.provide(layer)), + ); + }, + ); + + it.live( + "aborts before any network call when an encrypted: secret cannot be decrypted (CLI-1881)", + () => { + // `auth.enabled = false` on purpose: Go's decrypt hook runs for every + // `config.Secret` field during `config.Load`, before any feature gate is + // consulted — an undecryptable secret aborts even when the section that + // contains it would otherwise be skipped entirely. + const toml = `project_id = "test" +[storage] +enabled = false +[auth] +enabled = false +[auth.captcha] +enabled = true +provider = "hcaptcha" +secret = "${DOTENVX_ENCRYPTED_VALUE}" +`; + const { layer, api } = setup({ toml, yes: true }); + return withDotenvPrivateKey( + undefined, + Effect.gen(function* () { + const message = yield* legacyConfigPush({ projectRef: Option.none() }).pipe( + Effect.catchTag("LegacyConfigPushLoadConfigError", (error) => + Effect.succeed(error.message), + ), + ); + expect(message).toBe("failed to parse config: missing private key"); + // The guard runs during config load, before any network call — not + // even the cost-matrix (list-addons) request that normally runs first. + expect(api.requests).toHaveLength(0); + }).pipe(Effect.provide(layer)), + ); + }, + ); + + it.live( + "aborts on an undecryptable secret config push never itself reads or pushes (CLI-1881)", + () => { + // `studio.openai_api_key` is a `config.Secret` field Go's `DecryptSecretHookFunc` + // still decrypts during `config.Load` — but no `config-sync/*.sync.ts` file (api, + // db, auth, storage, experimental) ever reads `studio.*`, so this proves the + // pre-check is genuinely document-wide, not merely reachable via `auth.*`. + const toml = `project_id = "test" +[storage] +enabled = false +[auth] +enabled = false +[studio] +openai_api_key = "${DOTENVX_ENCRYPTED_VALUE}" +`; + const { layer, api } = setup({ toml, yes: true }); + return withDotenvPrivateKey( + undefined, + Effect.gen(function* () { + const message = yield* legacyConfigPush({ projectRef: Option.none() }).pipe( + Effect.catchTag("LegacyConfigPushLoadConfigError", (error) => + Effect.succeed(error.message), + ), + ); + expect(message).toBe("failed to parse config: missing private key"); + expect(api.requests).toHaveLength(0); + }).pipe(Effect.provide(layer)), + ); + }, + ); + + it.live("aborts on an undecryptable [db.vault] secret (CLI-1881)", () => { + // `db.vault` decodes as `map[string]Secret` in Go (`pkg/config/db.go:96`), so Go's + // decrypt hook covers it during `config.Load` — but the shared + // `legacyAssertDecryptableSecrets` path list used to omit `db.vault` on the theory + // that only the db-config reader's own downstream vault loop needed it. `config push` + // never runs that downstream loop, so this proves the shared path list now covers + // `db.vault` for every caller. + const toml = `project_id = "test" +[storage] +enabled = false +[auth] +enabled = false +[db.vault] +my_secret = "${DOTENVX_ENCRYPTED_VALUE}" +`; + const { layer, api } = setup({ toml, yes: true }); + return withDotenvPrivateKey( + undefined, + Effect.gen(function* () { + const message = yield* legacyConfigPush({ projectRef: Option.none() }).pipe( + Effect.catchTag("LegacyConfigPushLoadConfigError", (error) => + Effect.succeed(error.message), + ), + ); + expect(message).toBe("failed to parse config: missing private key"); + expect(api.requests).toHaveLength(0); + }).pipe(Effect.provide(layer)), + ); + }); + + it.live( + "aborts on an undecryptable secret in a deprecated [auth.external.slack] block (CLI-1881)", + () => { + // `@supabase/config` strips `auth.external.{linkedin,slack}` from `loaded.document` + // before returning it (`normalizeDeprecatedExternalProviders`), matching Go's + // `external.validate()` — but Go's decrypt hook runs at DECODE time, strictly before + // that later validate-time deletion, so a `secret` hiding in one of these deprecated + // blocks still aborts Go's load. This proves the pre-check folds + // `removedDeprecatedExternalProviders` back in rather than missing it. + const toml = `project_id = "test" +[storage] +enabled = false +[auth] +enabled = false +[auth.external.slack] +secret = "${DOTENVX_ENCRYPTED_VALUE}" +`; + const { layer, api } = setup({ toml, yes: true }); + return withDotenvPrivateKey( + undefined, + Effect.gen(function* () { + const message = yield* legacyConfigPush({ projectRef: Option.none() }).pipe( + Effect.catchTag("LegacyConfigPushLoadConfigError", (error) => + Effect.succeed(error.message), + ), + ); + expect(message).toBe("failed to parse config: missing private key"); + expect(api.requests).toHaveLength(0); + }).pipe(Effect.provide(layer)), + ); + }, + ); + it.live("pushes storage when enabled and changed", () => { const toml = `project_id = "test" [auth] diff --git a/apps/cli/src/legacy/shared/legacy-db-config.toml-read.ts b/apps/cli/src/legacy/shared/legacy-db-config.toml-read.ts index 10fcddfcf8..0e11809a56 100644 --- a/apps/cli/src/legacy/shared/legacy-db-config.toml-read.ts +++ b/apps/cli/src/legacy/shared/legacy-db-config.toml-read.ts @@ -736,13 +736,20 @@ const resolveOptionalBoolOrFail = Effect.fnUntraced(function* ( * Dotted paths of every `config.Secret`-typed field Go decrypts via its global * `DecryptSecretHookFunc` (`pkg/config/secret.go`, `config.go:730`) — the hook only runs * while decoding INTO a `config.Secret`, never over arbitrary strings. `*` matches any map - * key (`auth.external.`, `auth.hook.`). `[db.vault]` (a `map[string]Secret`) - * is intentionally omitted — the reader decrypts it directly in the body with the same - * fail-on-undecryptable behaviour. Derived from the Go structs (`auth.go`, `db.go`, - * `config.go`); update alongside any new `Secret` field. + * key (`auth.external.`, `auth.hook.`, `db.vault.`). `[db.vault]` + * (a `map[string]Secret`, `pkg/config/db.go:96`) IS included — the db-config reader below + * also decrypts it directly in its own body ({@link legacyReadDbToml}'s `vault` loop) with + * the same fail-on-undecryptable behaviour, so for that caller this just detects the same + * failure a little earlier (no observable difference: same `legacyDecryptSecret` call, same + * `failed to parse config: ` message). For `config push` — the other caller of + * {@link legacyAssertDecryptableSecrets}, which has no such downstream vault pass — omitting + * `db.vault` here would let an undecryptable vault secret through to the API calls, unlike Go. + * Derived from the Go structs (`auth.go`, `db.go`, `config.go`); update alongside any new + * `Secret` field. */ const LEGACY_SECRET_PATHS: ReadonlyArray> = [ ["db", "root_key"], + ["db", "vault", "*"], ["auth", "publishable_key"], ["auth", "secret_key"], ["auth", "jwt_secret"], @@ -786,19 +793,17 @@ const legacyCollectSecretStrings = ( } }; -/** Fails when a single `encrypted:` secret value cannot be decrypted (Go's hook error). */ +/** Returns Go's hook-error message when a single `encrypted:` secret value cannot be decrypted. */ const legacyAssertSecretValue = ( value: string, lookup: EnvLookup, dotenvPrivateKeys: ReadonlyArray, -): LegacyDbConfigLoadError | undefined => { +): string | undefined => { const expanded = legacyExpandEnv(value, lookup); // Unset `env(...)` and plain strings are returned verbatim by Go's hook (no error). if (ENV_PATTERN.test(expanded) || !legacyIsEncryptedSecret(expanded)) return undefined; const decrypted = legacyDecryptSecret(expanded, dotenvPrivateKeys); - return decrypted.ok - ? undefined - : new LegacyDbConfigLoadError({ message: `failed to parse config: ${decrypted.error}` }); + return decrypted.ok ? undefined : `failed to parse config: ${decrypted.error}`; }; /** @@ -808,15 +813,27 @@ const legacyAssertSecretValue = ( * `Secret` field paths ({@link LEGACY_SECRET_PATHS}) are scanned — a non-secret string that * merely starts with `encrypted:` (e.g. an auth email-template `subject`) stays plain text * in Go and must not block the load. Go decodes every `[remotes.]` block into the same - * struct, so the same paths are checked under each remote too. Returns the failure (or - * `undefined`); the caller surfaces it via `Effect.fail`. + * struct, so the same paths are checked under each remote too. Returns Go's error message (or + * `undefined`); callers surface it as their own domain error (e.g. `Effect.fail`). + * + * Shared across command families: the db-config reader below uses it for `db push`/`db + * reset`/`migration up|down`/etc, and `config push`'s handler reuses it directly against its + * own (`@supabase/config`-decoded) document — both need the exact same "decrypt-or-abort before + * anything else runs" behaviour Go gets for free from `config.Load`. + * + * The "check every `[remotes.]` block too" part of that contract only holds when `doc` + * still has an intact `remotes` key. The db-config reader's own remote-merge keeps it (so this + * function really does check every declared remote there), but `@supabase/config`'s + * `loadProjectConfig` strips `remotes` from the document once a block matches the target ref — + * so for `config push`, an undecryptable secret hiding in a *different, non-matching* remote + * block goes unchecked (see that command's SIDE_EFFECTS.md KNOWN GAPS). */ -const legacyAssertDecryptableSecrets = ( +export const legacyAssertDecryptableSecrets = ( doc: unknown, lookup: EnvLookup, dotenvPrivateKeys: ReadonlyArray, -): LegacyDbConfigLoadError | undefined => { - const scan = (node: unknown): LegacyDbConfigLoadError | undefined => { +): string | undefined => { + const scan = (node: unknown): string | undefined => { for (const segs of LEGACY_SECRET_PATHS) { const values: Array = []; legacyCollectSecretStrings(node, segs, 0, values); @@ -990,11 +1007,13 @@ const readDbTomlCore = Effect.fnUntraced(function* ( // EVERY `config.Secret` field during `UnmarshalExact`, so an `encrypted:` secret anywhere // in the merged config that cannot be decrypted (e.g. no DOTENV_PRIVATE_KEY) aborts the // load with `failed to parse config: ` (secret.go:34,103; config.go:704) — before - // Validate and before connecting. The reader otherwise only decrypts `[db.vault]`, so - // assert decryptability across the whole document to match Go (a recursive scan tracks - // Go's "decode the entire config" better than a hand-listed set of Secret paths). + // Validate and before connecting. This also covers `[db.vault]` (see + // `LEGACY_SECRET_PATHS`), so the vault loop below never actually reaches an + // undecryptable value — it just decrypts-and-populates the already-asserted-valid ones. const secretError = legacyAssertDecryptableSecrets(effectiveDoc, lookup, dotenvPrivateKeys); - if (secretError !== undefined) return yield* Effect.fail(secretError); + if (secretError !== undefined) { + return yield* Effect.fail(new LegacyDbConfigLoadError({ message: secretError })); + } } // Go: `config.go:626` — read the linked pooler URL from `.temp/pooler-url` and diff --git a/apps/cli/src/legacy/shared/legacy-vault-decrypt.ts b/apps/cli/src/legacy/shared/legacy-vault-decrypt.ts index 5c1c0d53d2..f5826a5218 100644 --- a/apps/cli/src/legacy/shared/legacy-vault-decrypt.ts +++ b/apps/cli/src/legacy/shared/legacy-vault-decrypt.ts @@ -63,8 +63,12 @@ function decryptWithKey(keyHex: string, encryptedValue: string): LegacyDecrypted let privateKeyHex: string; try { privateKeyHex = PrivateKey.fromHex(keyHex).toHex(); - } catch (cause) { - return { ok: false, error: `failed to hex decode private key: ${errorMessage(cause)}` }; + } catch { + // Fixed message rather than the underlying error (Go's `ecies.NewPrivateKeyFromHex` + // returns the generic "cannot decode hex string" too): the fallback hex decoder some + // runtimes take for a malformed key can otherwise echo a fragment of the bad input + // (offending character + index) into this error, which reaches the user's stderr. + return { ok: false, error: "failed to hex decode private key: cannot decode hex string" }; } const encoded = encryptedValue.slice(ENCRYPTED_PREFIX.length); // Node's `Buffer.from(s, "base64")` silently drops invalid characters, unlike diff --git a/packages/config/src/io.ts b/packages/config/src/io.ts index 151ce2a373..8052399091 100644 --- a/packages/config/src/io.ts +++ b/packages/config/src/io.ts @@ -35,6 +35,18 @@ export interface LoadedProjectConfig { * `undefined` when no `projectRef` was requested or none matched. */ readonly appliedRemote?: string; + /** + * The top-level `auth.external.{linkedin,slack}` sub-objects that were stripped from + * {@link document} before it was returned (provider id → the removed object), keyed by + * provider id. Empty when neither deprecated block was present. See + * `normalizeDeprecatedExternalProviders`'s doc comment for why a caller doing its own + * Go-parity scan over `document` (e.g. a decrypt-or-abort secret check) may need to fold + * this back in — Go's decode-time decrypt hook sees these blocks before its later + * validate-time deletion, so `document` alone under-reports what Go would have decrypted. + * Present (possibly `{}`) whenever {@link document} is; absent from `saveProjectConfig`'s + * result, which has no document to strip from. + */ + readonly removedDeprecatedExternalProviders?: Readonly>; } /** @@ -441,6 +453,19 @@ interface NormalizedExternalProvidersDocument { readonly document: unknown; /** Provider ids (`"linkedin"` | `"slack"`) whose deprecated top-level block was `enabled` — drives the WARN. */ readonly deprecatedProviders: ReadonlyArray; + /** + * The removed top-level `auth.external.{linkedin,slack}` sub-objects (provider id → the + * object that was deleted), regardless of `enabled`. Go's global `DecryptSecretHookFunc` + * runs during decode — strictly BEFORE `Config.Validate()` → `external.validate()` (which + * this function mirrors) ever deletes these blocks (`config.go:753,775-783` decode vs. + * `config.go:882,1148,1419-1425` validate) — so an `encrypted:` secret hiding in one of + * these blocks still gets decrypted-or-aborted in Go. A caller that needs to reproduce that + * decrypt-or-abort check against the returned (already-stripped) {@link LoadedProjectConfig.document} + * (e.g. `config push`'s pre-check) can fold this back in. Only the top-level blocks are + * captured, not any surviving `remotes.*.auth.external.{linkedin,slack}` — that's the + * separate, already-documented "non-matching remote" gap. + */ + readonly removedProviders: Readonly>; } const DEPRECATED_EXTERNAL_PROVIDERS = ["linkedin", "slack"] as const; @@ -476,15 +501,17 @@ function normalizeDeprecatedExternalProviders( document: unknown, ): NormalizedExternalProvidersDocument { if (!isObject(document)) { - return { document, deprecatedProviders: [] }; + return { document, deprecatedProviders: [], removedProviders: {} }; } const normalized = { ...document }; const deprecatedProviders: Array = []; + const removedProviders: Record = {}; if (isObject(normalized.auth) && isObject(normalized.auth.external)) { const external = { ...normalized.auth.external }; for (const ext of DEPRECATED_EXTERNAL_PROVIDERS) { const provider = external[ext]; if (provider === undefined) continue; + removedProviders[ext] = provider; if (isObject(provider) && provider.enabled === true) { deprecatedProviders.push(ext); } @@ -506,7 +533,7 @@ function normalizeDeprecatedExternalProviders( }), ); } - return { document: normalized, deprecatedProviders }; + return { document: normalized, deprecatedProviders, removedProviders }; } /** @@ -740,8 +767,11 @@ export const loadProjectConfigFile = Effect.fnUntraced(function* ( // Strip Go's deprecated `auth.external.{linkedin,slack}` provider ids from // the POST-remote-merge document, matching `external.validate()` running // once on the final effective config (see `normalizeDeprecatedExternalProviders`). - const { document: normalizedForDecode, deprecatedProviders } = - normalizeDeprecatedExternalProviders(documentForDecode); + const { + document: normalizedForDecode, + deprecatedProviders, + removedProviders, + } = normalizeDeprecatedExternalProviders(documentForDecode); // Warn on stderr, matching Go's `external.validate()` (`config.go:1418-1423`). // Go's own format string is a raw string literal ending in a literal // backslash-n (raw string literals never process escapes, and `Fprintf` @@ -768,6 +798,7 @@ export const loadProjectConfigFile = Effect.fnUntraced(function* ( ignoredPaths: [], document: isObject(normalizedForDecode) ? normalizedForDecode : undefined, appliedRemote, + removedDeprecatedExternalProviders: removedProviders, } satisfies LoadedProjectConfig; }); From e83a6ccea65010ad6f0ca6a6bf5c20d68f521ed5 Mon Sep 17 00:00:00 2001 From: Andrew Valleteau Date: Thu, 9 Jul 2026 19:30:44 +0200 Subject: [PATCH 37/79] ci: exempt private org members from the contribution gate (#5848) The contribution gate identified internal maintainers solely from the PR's `author_association`, which GitHub only reports as `MEMBER` when a user's organization membership is public. A private org member (e.g. a `supabase/cli` team member who keeps membership private) is reported as `CONTRIBUTOR`/`NONE`, so the gate wrongly closed their PRs as "no-linked-issue" (see #5847). Resolve maintainer status from the author's effective repository permission (`admin`/`write`), which reflects team/org-granted access that `author_association` does not surface, falling back to it only when the cheap signals (bot, public internal association) are inconclusive. The permission endpoint needs just `Metadata: read`, already covered by the workflow's `contents: read`. Claude-Session: https://claude.ai/code/session_01D41gYiFBSU7adE4ppnUUKg --------- Co-authored-by: Claude --- .github/scripts/contribution-gate.test.ts | 180 ++++++++++++++++------ .github/scripts/contribution-gate.ts | 155 +++++++++++++++---- .github/workflows/contribution-gate.yml | 1 + 3 files changed, 251 insertions(+), 85 deletions(-) diff --git a/.github/scripts/contribution-gate.test.ts b/.github/scripts/contribution-gate.test.ts index a8292a3a35..7951de817a 100644 --- a/.github/scripts/contribution-gate.test.ts +++ b/.github/scripts/contribution-gate.test.ts @@ -1,20 +1,47 @@ -import { describe, expect, test } from "bun:test"; +import { afterEach, describe, expect, spyOn, test } from "bun:test"; import { evaluateAllOpenPrs, evaluateGate, + fetchAuthorPermission, GATE_LABEL, type GateIo, + isInternalAuthor, type LinkedIssue, type OpenPr, } from "./contribution-gate.ts"; const REPO = "supabase/cli"; +describe("isInternalAuthor", () => { + test.each(["OWNER", "MEMBER", "COLLABORATOR"])( + "treats internal association %s as internal without a permission lookup", + (authorAssociation) => { + expect(isInternalAuthor(authorAssociation, undefined)).toBe(true); + }, + ); + + test.each(["admin", "write"])("treats push-capable permission %s as internal", (permission) => { + // A private org member surfaces only as CONTRIBUTOR/NONE via + // author_association, but their effective repo permission gives them + // away as a maintainer. + expect(isInternalAuthor("CONTRIBUTOR", permission)).toBe(true); + expect(isInternalAuthor("NONE", permission)).toBe(true); + }); + + test.each(["read", "none", undefined])( + "treats external author with permission %s as external", + (permission) => { + expect(isInternalAuthor("CONTRIBUTOR", permission)).toBe(false); + expect(isInternalAuthor("NONE", permission)).toBe(false); + }, + ); +}); + describe("evaluateGate", () => { test("skips bot authors", () => { const result = evaluateGate({ repository: REPO, - authorAssociation: "NONE", + isInternal: false, isBot: true, linkedIssues: [], }); @@ -22,24 +49,21 @@ describe("evaluateGate", () => { expect(result.reason).toBe("bot"); }); - test.each(["OWNER", "MEMBER", "COLLABORATOR"])( - "skips internal author association %s", - (authorAssociation) => { - const result = evaluateGate({ - repository: REPO, - authorAssociation, - isBot: false, - linkedIssues: [], - }); - expect(result.pass).toBe(true); - expect(result.reason).toBe("internal"); - }, - ); + test("skips internal authors", () => { + const result = evaluateGate({ + repository: REPO, + isInternal: true, + isBot: false, + linkedIssues: [], + }); + expect(result.pass).toBe(true); + expect(result.reason).toBe("internal"); + }); test("fails when no issue is linked", () => { const result = evaluateGate({ repository: REPO, - authorAssociation: "NONE", + isInternal: false, isBot: false, linkedIssues: [], }); @@ -52,11 +76,9 @@ describe("evaluateGate", () => { test("fails when the linked issue is open but not labeled", () => { const result = evaluateGate({ repository: REPO, - authorAssociation: "CONTRIBUTOR", + isInternal: false, isBot: false, - linkedIssues: [ - { repository: REPO, number: 12, state: "OPEN", labels: ["🐛 Bug"] }, - ], + linkedIssues: [{ repository: REPO, number: 12, state: "OPEN", labels: ["🐛 Bug"] }], }); expect(result.pass).toBe(false); expect(result.reason).toBe("missing-label"); @@ -66,11 +88,9 @@ describe("evaluateGate", () => { test("fails when the only labeled issue is closed", () => { const result = evaluateGate({ repository: REPO, - authorAssociation: "NONE", + isInternal: false, isBot: false, - linkedIssues: [ - { repository: REPO, number: 7, state: "CLOSED", labels: [GATE_LABEL] }, - ], + linkedIssues: [{ repository: REPO, number: 7, state: "CLOSED", labels: [GATE_LABEL] }], }); expect(result.pass).toBe(false); expect(result.reason).toBe("issue-closed"); @@ -79,7 +99,7 @@ describe("evaluateGate", () => { test("passes when a linked issue is open and carries the gate label", () => { const result = evaluateGate({ repository: REPO, - authorAssociation: "NONE", + isInternal: false, isBot: false, linkedIssues: [ { @@ -97,7 +117,7 @@ describe("evaluateGate", () => { test("passes when any one of several linked issues qualifies", () => { const result = evaluateGate({ repository: REPO, - authorAssociation: "FIRST_TIME_CONTRIBUTOR", + isInternal: false, isBot: false, linkedIssues: [ { repository: REPO, number: 1, state: "CLOSED", labels: [GATE_LABEL] }, @@ -114,7 +134,7 @@ describe("evaluateGate", () => { // controlled by the contributor, so it must not satisfy the gate. const result = evaluateGate({ repository: REPO, - authorAssociation: "NONE", + isInternal: false, isBot: false, linkedIssues: [ { @@ -132,7 +152,7 @@ describe("evaluateGate", () => { test("matches the repository case-insensitively", () => { const result = evaluateGate({ repository: REPO, - authorAssociation: "NONE", + isInternal: false, isBot: false, linkedIssues: [ { @@ -152,61 +172,75 @@ describe("evaluateAllOpenPrs", () => { function makeIo( openPrs: OpenPr[], linkedByPr: Record, - ): { io: GateIo; closed: Array<{ number: number; message: string }> } { + permissionByLogin: Record = {}, + ): { + io: GateIo; + closed: Array<{ number: number; message: string }>; + permissionLookups: string[]; + } { const closed: Array<{ number: number; message: string }> = []; + const permissionLookups: string[] = []; const io: GateIo = { listOpenPrs: () => Promise.resolve(openPrs), - fetchLinkedIssues: (prNumber) => - Promise.resolve(linkedByPr[prNumber] ?? []), + fetchPermission: (login) => { + permissionLookups.push(login); + return Promise.resolve(permissionByLogin[login]); + }, + fetchLinkedIssues: (prNumber) => Promise.resolve(linkedByPr[prNumber] ?? []), closePr: (prNumber, message) => { closed.push({ number: prNumber, message }); return Promise.resolve(); }, }; - return { io, closed }; + return { io, closed, permissionLookups }; } test("closes only non-conforming external PRs and leaves the rest", async () => { - const { io, closed } = makeIo( + const { io, closed, permissionLookups } = makeIo( [ - { number: 1, authorAssociation: "NONE", isBot: false }, // no issue -> close - { number: 2, authorAssociation: "MEMBER", isBot: false }, // internal -> skip - { number: 3, authorAssociation: "NONE", isBot: true }, // bot -> skip - { number: 4, authorAssociation: "CONTRIBUTOR", isBot: false }, // conforming -> keep - { number: 5, authorAssociation: "NONE", isBot: false }, // missing label -> close + // no issue -> close + { number: 1, authorLogin: "ext-a", authorAssociation: "NONE", isBot: false }, + // public member -> skip (no permission lookup needed) + { number: 2, authorLogin: "maint", authorAssociation: "MEMBER", isBot: false }, + // bot -> skip + { number: 3, authorLogin: "dependabot", authorAssociation: "NONE", isBot: true }, + // conforming external -> keep + { number: 4, authorLogin: "ext-b", authorAssociation: "CONTRIBUTOR", isBot: false }, + // missing label -> close + { number: 5, authorLogin: "ext-c", authorAssociation: "NONE", isBot: false }, + // private-membership maintainer (CONTRIBUTOR + write access) -> skip + { number: 6, authorLogin: "hidden-maint", authorAssociation: "CONTRIBUTOR", isBot: false }, ], { - 4: [ - { repository: REPO, number: 40, state: "OPEN", labels: [GATE_LABEL] }, - ], - 5: [ - { repository: REPO, number: 50, state: "OPEN", labels: ["🐛 Bug"] }, - ], + 4: [{ repository: REPO, number: 40, state: "OPEN", labels: [GATE_LABEL] }], + 5: [{ repository: REPO, number: 50, state: "OPEN", labels: ["🐛 Bug"] }], }, + { "hidden-maint": "write" }, ); const entries = await evaluateAllOpenPrs(io, REPO); expect(closed.map((c) => c.number).sort((a, b) => a - b)).toEqual([1, 5]); - const byNumber = Object.fromEntries( - entries.map((entry) => [entry.number, entry.result]), - ); + const byNumber = Object.fromEntries(entries.map((entry) => [entry.number, entry.result])); expect(byNumber[1]?.reason).toBe("no-linked-issue"); expect(byNumber[2]?.pass).toBe(true); expect(byNumber[2]?.reason).toBe("internal"); expect(byNumber[3]?.reason).toBe("bot"); expect(byNumber[4]?.pass).toBe(true); expect(byNumber[5]?.reason).toBe("missing-label"); + expect(byNumber[6]?.pass).toBe(true); + expect(byNumber[6]?.reason).toBe("internal"); expect(closed.find((c) => c.number === 1)?.message).toContain(GATE_LABEL); + // Bots and public members are settled without a permission lookup. + expect(permissionLookups).not.toContain("maint"); + expect(permissionLookups).not.toContain("dependabot"); }); test("returns an entry per PR and closes none when all conform", async () => { const { io, closed } = makeIo( - [{ number: 9, authorAssociation: "NONE", isBot: false }], + [{ number: 9, authorLogin: "ext", authorAssociation: "NONE", isBot: false }], { - 9: [ - { repository: REPO, number: 90, state: "OPEN", labels: [GATE_LABEL] }, - ], + 9: [{ repository: REPO, number: 90, state: "OPEN", labels: [GATE_LABEL] }], }, ); @@ -217,3 +251,47 @@ describe("evaluateAllOpenPrs", () => { expect(entries[0]?.result.pass).toBe(true); }); }); + +describe("fetchAuthorPermission", () => { + const fetchSpy = spyOn(globalThis, "fetch"); + afterEach(() => { + fetchSpy.mockReset(); + }); + + function stubFetch(status: number, body: unknown): void { + // `typeof fetch` carries a static `preconnect`, so attach a no-op to keep + // the implementation assignable without casting. + fetchSpy.mockImplementation( + Object.assign(() => Promise.resolve(new Response(JSON.stringify(body), { status })), { + preconnect: () => {}, + }), + ); + } + + test("returns the effective permission for a collaborator", async () => { + stubFetch(200, { permission: "admin" }); + const permission = await fetchAuthorPermission("t", "supabase", "cli", "maint"); + expect(permission).toBe("admin"); + }); + + test("maps a 404 (non-collaborator fork author) to undefined", async () => { + // The endpoint 404s for users who are not collaborators — the common case + // for the external contributors the gate targets. It must map to external, + // not abort the run. + stubFetch(404, { message: "Not Found" }); + const permission = await fetchAuthorPermission("t", "supabase", "cli", "ext"); + expect(permission).toBeUndefined(); + }); + + test("throws on other API failures so the run aborts without closing PRs", async () => { + stubFetch(403, { message: "Forbidden" }); + await expect(fetchAuthorPermission("t", "supabase", "cli", "ext")).rejects.toThrow(/403/); + }); + + test("skips the network call for a blank login", async () => { + stubFetch(200, { permission: "admin" }); + const permission = await fetchAuthorPermission("t", "supabase", "cli", ""); + expect(permission).toBeUndefined(); + expect(fetchSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/.github/scripts/contribution-gate.ts b/.github/scripts/contribution-gate.ts index ad599b3c81..a2b62ee08f 100644 --- a/.github/scripts/contribution-gate.ts +++ b/.github/scripts/contribution-gate.ts @@ -3,9 +3,11 @@ * OPEN pull requests opened by external contributors. * * A PR passes only when it links to an OPEN GitHub issue that carries the - * `open-for-contribution` label. Members/collaborators/owners and bots are - * exempt (they work from Linear tickets or automation). PRs that do not follow - * the process are commented on and closed. + * `open-for-contribution` label. Maintainers (recognised by their effective + * repository permission, so private org members count too — not just the + * `author_association` GitHub exposes for public members) and bots are exempt + * (they work from Linear tickets or automation). PRs that do not follow the + * process are commented on and closed. * * Two modes, both driven from `main()`: * - single-PR (default): reacts to one PR on `pull_request_target` @@ -25,8 +27,43 @@ export const GATE_LABEL = "open-for-contribution"; -/** Author associations treated as internal (exempt from the gate). */ -const INTERNAL_ASSOCIATIONS = new Set(["OWNER", "MEMBER", "COLLABORATOR"]); +/** + * Author associations treated as internal (exempt from the gate). + * + * NOTE: `author_association` only reports `MEMBER` when a user's organization + * membership is *public*. A private org member (or a team member who keeps + * their membership private) is reported as `CONTRIBUTOR`/`NONE`, so this set + * alone is not enough to identify internal maintainers — see + * `isInternalAuthor`, which also consults the author's effective repository + * permission. + */ +export const INTERNAL_ASSOCIATIONS = new Set(["OWNER", "MEMBER", "COLLABORATOR"]); + +/** + * Effective repository permissions that mark an author as internal. A user who + * can push to the repository (directly, or via a team/org grant that + * `author_association` does not surface) is a trusted maintainer, not an + * external contributor. The legacy REST `permission` field collapses the + * `maintain` role to `write`, so `admin`/`write` covers every push-capable + * role. + */ +const WRITE_PERMISSIONS = new Set(["admin", "write"]); + +/** + * Decide whether a PR author is internal (exempt from the gate). Combines the + * cheap `author_association` signal with the author's effective repository + * `permission` (undefined when it could not be resolved), so private org + * members whose association is merely `CONTRIBUTOR` are still recognised. + */ +export function isInternalAuthor( + authorAssociation: string, + permission: string | undefined, +): boolean { + return ( + INTERNAL_ASSOCIATIONS.has(authorAssociation) || + (permission !== undefined && WRITE_PERMISSIONS.has(permission)) + ); +} export interface LinkedIssue { /** `owner/name` of the repo the issue lives in (from GraphQL nameWithOwner). */ @@ -39,7 +76,8 @@ export interface LinkedIssue { export interface GateInput { /** `owner/name` of this repository (from GITHUB_REPOSITORY). */ repository: string; - authorAssociation: string; + /** Whether the author is a trusted maintainer (see `isInternalAuthor`). */ + isInternal: boolean; isBot: boolean; linkedIssues: LinkedIssue[]; } @@ -102,7 +140,7 @@ export function evaluateGate(input: GateInput): GateResult { if (input.isBot) { return { pass: true, reason: "bot" }; } - if (INTERNAL_ASSOCIATIONS.has(input.authorAssociation)) { + if (input.isInternal) { return { pass: true, reason: "internal" }; } @@ -110,9 +148,7 @@ export function evaluateGate(input: GateInput): GateResult { // (e.g. `Closes attacker/repo#1`) links an issue the contributor controls, // so it must never satisfy the gate. const repo = input.repository.toLowerCase(); - const repoIssues = input.linkedIssues.filter( - (issue) => issue.repository.toLowerCase() === repo, - ); + const repoIssues = input.linkedIssues.filter((issue) => issue.repository.toLowerCase() === repo); if (repoIssues.length === 0) { return { @@ -137,6 +173,7 @@ export function evaluateGate(input: GateInput): GateResult { /** Minimal open-PR shape the gate needs to decide exemption. */ export interface OpenPr { number: number; + authorLogin: string; authorAssociation: string; isBot: boolean; } @@ -145,6 +182,12 @@ export interface OpenPr { export interface GateIo { /** List every open pull request in the repository. */ listOpenPrs: () => Promise; + /** + * Resolve an author's effective repository permission (`admin`/`write`/ + * `read`/`none`), or `undefined` when it could not be determined. Used to + * recognise maintainers whose org membership is private. + */ + fetchPermission: (login: string) => Promise; /** Fetch the issues a PR closes (via `closingIssuesReferences`). */ fetchLinkedIssues: (prNumber: number) => Promise; /** Comment with `message` then close the PR. Called only for failing PRs. */ @@ -161,17 +204,22 @@ export interface SweepEntry { * orchestration over the injected `io`; returns each PR's decision so the * caller can log a summary. */ -export async function evaluateAllOpenPrs( - io: GateIo, - repository: string, -): Promise { +export async function evaluateAllOpenPrs(io: GateIo, repository: string): Promise { const openPrs = await io.listOpenPrs(); const entries: SweepEntry[] = []; for (const pr of openPrs) { + // Only pay for a permission lookup when the cheap signals are inconclusive: + // bots are exempt outright, and a public internal association already + // proves membership. + let isInternal = pr.isBot || INTERNAL_ASSOCIATIONS.has(pr.authorAssociation); + if (!isInternal) { + const permission = await io.fetchPermission(pr.authorLogin); + isInternal = isInternalAuthor(pr.authorAssociation, permission); + } const linkedIssues = await io.fetchLinkedIssues(pr.number); const result = evaluateGate({ repository, - authorAssociation: pr.authorAssociation, + isInternal, isBot: pr.isBot, linkedIssues, }); @@ -204,6 +252,8 @@ async function githubFetch( url: string, token: string, init: Omit = {}, + /** Non-OK statuses to return to the caller instead of throwing on. */ + allowStatuses: readonly number[] = [], ): Promise { const response = await fetch(url, { ...init, @@ -214,11 +264,9 @@ async function githubFetch( "Content-Type": "application/json", }, }); - if (!response.ok) { + if (!response.ok && !allowStatuses.includes(response.status)) { const body = await response.text(); - throw new Error( - `GitHub request failed (${response.status}) for ${url}: ${body}`, - ); + throw new Error(`GitHub request failed (${response.status}) for ${url}: ${body}`); } return response; } @@ -262,12 +310,9 @@ async function fetchLinkedIssues( }; }; if (payload.errors?.length) { - throw new Error( - `GraphQL errors: ${payload.errors.map((e) => e.message).join("; ")}`, - ); + throw new Error(`GraphQL errors: ${payload.errors.map((e) => e.message).join("; ")}`); } - const nodes = - payload.data?.repository?.pullRequest?.closingIssuesReferences?.nodes ?? []; + const nodes = payload.data?.repository?.pullRequest?.closingIssuesReferences?.nodes ?? []; return nodes.map((node) => ({ repository: node.repository.nameWithOwner, number: node.number, @@ -279,7 +324,7 @@ async function fetchLinkedIssues( interface RestPullRequest { number: number; author_association: string; - user: { type: string } | null; + user: { login: string; type: string } | null; } async function fetchOpenPullRequests( @@ -297,6 +342,7 @@ async function fetchOpenPullRequests( for (const pr of batch) { prs.push({ number: pr.number, + authorLogin: pr.user?.login ?? "", authorAssociation: pr.author_association, isBot: pr.user?.type === "Bot", }); @@ -308,6 +354,38 @@ async function fetchOpenPullRequests( return prs; } +/** + * Resolve an author's effective permission on the repository. Reflects access + * granted directly or through a team/org membership, so it recognises private + * org members that `author_association` reports only as `CONTRIBUTOR`. This + * endpoint needs just `Metadata: read` (covered by the workflow's + * `contents: read`). + * + * A 404 means the author is not a collaborator at all — the common case for + * external fork contributors, who are exactly who the gate targets — so it maps + * to `undefined` (external) rather than throwing. Other failures still throw so + * a transient API error aborts the run without wrongly closing PRs. + */ +export async function fetchAuthorPermission( + token: string, + owner: string, + repo: string, + login: string, +): Promise { + if (!login) { + return undefined; + } + const url = + `https://api.github.com/repos/${owner}/${repo}/collaborators/` + + `${encodeURIComponent(login)}/permission`; + const response = await githubFetch(url, token, {}, [404]); + if (response.status === 404) { + return undefined; + } + const payload = (await response.json()) as { permission?: string }; + return payload.permission; +} + /** * Build the "comment then close" action for a repo. In dry-run mode it logs the * intended action instead of mutating the PR. @@ -342,19 +420,31 @@ async function runSinglePr( ): Promise { const prNumber = Number(requireEnv("PR_NUMBER")); const authorAssociation = process.env.PR_AUTHOR_ASSOCIATION ?? "NONE"; + const authorLogin = process.env.PR_AUTHOR_LOGIN ?? ""; const isBot = (process.env.PR_AUTHOR_TYPE ?? "User") === "Bot"; + // Resolve maintainer status from the effective repository permission unless a + // cheaper signal already settles it. `author_association` only exposes public + // org membership, so a private member must be confirmed via permission. + let permission: string | undefined; + let isInternal = isBot || INTERNAL_ASSOCIATIONS.has(authorAssociation); + if (!isInternal) { + permission = await fetchAuthorPermission(token, owner, repo, authorLogin); + isInternal = isInternalAuthor(authorAssociation, permission); + } + const linkedIssues = await fetchLinkedIssues(token, owner, repo, prNumber); const result = evaluateGate({ repository, - authorAssociation, + isInternal, isBot, linkedIssues, }); console.log( `Contribution gate for PR #${prNumber}: pass=${result.pass} reason=${result.reason} ` + - `(author_association=${authorAssociation}, bot=${isBot}, linked_issues=${linkedIssues.length})`, + `(author_association=${authorAssociation}, permission=${permission ?? "n/a"}, ` + + `bot=${isBot}, linked_issues=${linkedIssues.length})`, ); if (result.pass || !result.message) { @@ -377,8 +467,8 @@ async function runSweep( const base = `https://api.github.com/repos/${owner}/${repo}`; const io: GateIo = { listOpenPrs: () => fetchOpenPullRequests(token, owner, repo), - fetchLinkedIssues: (prNumber) => - fetchLinkedIssues(token, owner, repo, prNumber), + fetchPermission: (login) => fetchAuthorPermission(token, owner, repo, login), + fetchLinkedIssues: (prNumber) => fetchLinkedIssues(token, owner, repo, prNumber), closePr: makeCloser(token, base, dryRun), }; @@ -389,9 +479,7 @@ async function runSweep( `${failing.length} ${dryRun ? "would be " : ""}closed.`, ); for (const entry of entries) { - console.log( - ` PR #${entry.number}: pass=${entry.result.pass} reason=${entry.result.reason}`, - ); + console.log(` PR #${entry.number}: pass=${entry.result.pass} reason=${entry.result.reason}`); } } @@ -400,8 +488,7 @@ async function main(): Promise { const repository = requireEnv("GITHUB_REPOSITORY"); const [owner, repo] = repository.split("/"); - const allMode = - process.env.GATE_MODE === "all" || process.argv.includes("--all"); + const allMode = process.env.GATE_MODE === "all" || process.argv.includes("--all"); if (allMode) { const dryRun = /^(1|true)$/i.test(process.env.DRY_RUN ?? ""); await runSweep(token, owner!, repo!, repository, dryRun); diff --git a/.github/workflows/contribution-gate.yml b/.github/workflows/contribution-gate.yml index 54582d7790..6dfeee2bf0 100644 --- a/.github/workflows/contribution-gate.yml +++ b/.github/workflows/contribution-gate.yml @@ -51,6 +51,7 @@ jobs: GITHUB_REPOSITORY: ${{ github.repository }} PR_NUMBER: ${{ github.event.pull_request.number }} PR_AUTHOR_ASSOCIATION: ${{ github.event.pull_request.author_association }} + PR_AUTHOR_LOGIN: ${{ github.event.pull_request.user.login }} PR_AUTHOR_TYPE: ${{ github.event.pull_request.user.type }} run: bun .github/scripts/contribution-gate.ts From 58bd3c3099d99e7923cec79b8df3ff4856f0469e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Jul 2026 00:13:24 +0000 Subject: [PATCH 38/79] fix(deps): bump github.com/posthog/posthog-go from 1.17.2 to 1.17.4 in /apps/cli-go in the go-minor group across 1 directory (#5849) Bumps the go-minor group with 1 update in the /apps/cli-go directory: [github.com/posthog/posthog-go](https://github.com/posthog/posthog-go). Updates `github.com/posthog/posthog-go` from 1.17.2 to 1.17.4
Release notes

Sourced from github.com/posthog/posthog-go's releases.

1.17.4

Unreleased

1.17.3

Unreleased

Changelog

Sourced from github.com/posthog/posthog-go's changelog.

1.17.4

Patch Changes

  • 25e43f8: Stop duplicating distinct_id inside /flags person properties.

1.17.3

Patch Changes

  • dafed74: Retry remote feature flag requests after transient 502 and 504 responses.
Commits
  • 93b9681 chore: release v1.17.4 [version bump] [skip ci]
  • 25e43f8 fix: stop duplicating distinct_id in flags person properties (#250)
  • 01e60bf chore: release v1.17.3 [version bump] [skip ci]
  • dafed74 fix: Retry flags requests on 502 and 504 (#251)
  • ed70fae fix: make TCP drop recovery test deterministic (#248)
  • 2b6e187 ci: update SDK harness to 0.9.0 (#247)
  • 856a0d4 fix: satisfy SDK compliance harness 0.8.0 (#245)
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/posthog/posthog-go&package-manager=go_modules&previous-version=1.17.2&new-version=1.17.4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/cli-go/go.mod | 2 +- apps/cli-go/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/cli-go/go.mod b/apps/cli-go/go.mod index 507f7f885f..00a84266a6 100644 --- a/apps/cli-go/go.mod +++ b/apps/cli-go/go.mod @@ -42,7 +42,7 @@ require ( github.com/multigres/multigres v0.0.0-20260126223308-f5a52171bbc4 github.com/oapi-codegen/nullable v1.2.0 github.com/olekukonko/tablewriter v1.1.4 - github.com/posthog/posthog-go v1.17.2 + github.com/posthog/posthog-go v1.17.4 github.com/spf13/afero v1.15.0 github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 diff --git a/apps/cli-go/go.sum b/apps/cli-go/go.sum index bdde20104c..9d22c07d69 100644 --- a/apps/cli-go/go.sum +++ b/apps/cli-go/go.sum @@ -970,8 +970,8 @@ github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/posthog/posthog-go v1.17.2 h1:w0TaCAd+Z3WoEgVyR/nlcXlqNN2tpoBfIyxuGssDgCE= -github.com/posthog/posthog-go v1.17.2/go.mod h1://M430hNH3e8CDv4i8SJesb26816Mpa6GIZaiP4pNQU= +github.com/posthog/posthog-go v1.17.4 h1:7olsPzTGzuQvXJtH9n55u/Vgy58nB7V8T85AIpRjd8Q= +github.com/posthog/posthog-go v1.17.4/go.mod h1://M430hNH3e8CDv4i8SJesb26816Mpa6GIZaiP4pNQU= github.com/prometheus/client_golang v0.9.0-pre1.0.20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= From 1415b2f6c9d98910a253cc808687c78d00c141c5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Jul 2026 00:13:58 +0000 Subject: [PATCH 39/79] fix(deps): bump the npm-major group with 7 updates (#5850) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the npm-major group with 7 updates: | Package | From | To | | --- | --- | --- | | [undici](https://github.com/nodejs/undici) | `8.5.0` | `8.6.0` | | [@anthropic-ai/claude-agent-sdk](https://github.com/anthropics/claude-agent-sdk-typescript) | `0.3.198` | `0.3.199` | | [@anthropic-ai/sdk](https://github.com/anthropics/anthropic-sdk-typescript) | `0.109.1` | `0.110.0` | | [posthog-node](https://github.com/PostHog/posthog-js/tree/HEAD/packages/node) | `5.39.2` | `5.39.4` | | [@typescript/native-preview](https://github.com/microsoft/typescript-go) | `7.0.0-dev.20260701.1` | `7.0.0-dev.20260702.3` | | [knip](https://github.com/webpro-nl/knip/tree/HEAD/packages/knip) | `6.23.0` | `6.24.0` | | [tldts](https://github.com/remusao/tldts) | `6.1.86` | `7.4.6` | Updates `undici` from 8.5.0 to 8.6.0
Release notes

Sourced from undici's releases.

v8.6.0

What's Changed

New Contributors

Full Changelog: https://github.com/nodejs/undici/compare/v8.5.0...v8.6.0

Commits
  • 6f86196 Bumped v8.6.0 (#5491)
  • 61ddd8e fix: requeue h2 requests after goaway (#5473)
  • c5085ef feat: support HTTP QUERY method (RFC 10008) (#5459)
  • c144fd6 build(deps): bump github/codeql-action/analyze from 4.36.1 to 4.36.2 (#5477)
  • 782ad38 build(deps): bump github/codeql-action from 4.36.1 to 4.36.2 (#5484)
  • c4e045a build(deps): bump actions/checkout from 6.0.3 to 7.0.0 (#5482)
  • de960db build(deps): bump codecov/codecov-action from 6.0.1 to 7.0.0 (#5478)
  • 1132de8 build(deps): bump fastify/github-action-merge-dependabot (#5480)
  • 3acb7ba build(deps): bump github/codeql-action/upload-sarif (#5481)
  • 88c4254 build(deps): bump github/codeql-action/init from 4.36.1 to 4.36.2 (#5476)
  • Additional commits viewable in compare view

Updates `@anthropic-ai/claude-agent-sdk` from 0.3.198 to 0.3.199
Release notes

Sourced from @​anthropic-ai/claude-agent-sdk's releases.

v0.3.199

What's changed

  • Added requestId to canUseTool callback options for correlating out-of-band permission responses, and support for returning null to suppress the SDK's automatic control response
  • Added blocked field to workflow_agent progress events indicating when an agent was blocked by the auto-mode safety classifier
  • Added mode:"mask" and per-credential injectHosts to sandbox.credentials settings types for injecting masked credentials into sandboxed commands

Update

npm install @anthropic-ai/claude-agent-sdk@0.3.199
# or
yarn add @anthropic-ai/claude-agent-sdk@0.3.199
# or
pnpm add @anthropic-ai/claude-agent-sdk@0.3.199
# or
bun add @anthropic-ai/claude-agent-sdk@0.3.199
Changelog

Sourced from @​anthropic-ai/claude-agent-sdk's changelog.

0.3.199

  • Added requestId to canUseTool callback options for correlating out-of-band permission responses, and support for returning null to suppress the SDK's automatic control response
  • Added blocked field to workflow_agent progress events indicating when an agent was blocked by the auto-mode safety classifier
  • Added mode:"mask" and per-credential injectHosts to sandbox.credentials settings types for injecting masked credentials into sandboxed commands
Commits

Updates `@anthropic-ai/sdk` from 0.109.1 to 0.110.0
Release notes

Sourced from @​anthropic-ai/sdk's releases.

sdk: v0.110.0

0.110.0 (2026-07-02)

Full Changelog: sdk-v0.109.1...sdk-v0.110.0

Features

  • api: add agent-memory-2026-07-22 beta header (a470e10)
Changelog

Sourced from @​anthropic-ai/sdk's changelog.

0.110.0 (2026-07-02)

Full Changelog: sdk-v0.109.1...sdk-v0.110.0

Features

  • api: add agent-memory-2026-07-22 beta header (a470e10)
Commits

Updates `posthog-node` from 5.39.2 to 5.39.4
Release notes

Sourced from posthog-node's releases.

posthog-node@5.39.4

5.39.4

Patch Changes

posthog-node@5.39.3

5.39.3

Patch Changes

  • #4055 64e04ba Thanks @​marandaneto! - Retry /flags requests that receive HTTP 502 or 504 responses across SDKs that use the shared core flags client. (2026-07-02)
  • Updated dependencies [64e04ba]:
    • @​posthog/core@​1.39.4
Changelog

Sourced from posthog-node's changelog.

5.39.4

Patch Changes

5.39.3

Patch Changes

  • #4055 64e04ba Thanks @​marandaneto! - Retry /flags requests that receive HTTP 502 or 504 responses across SDKs that use the shared core flags client. (2026-07-02)
  • Updated dependencies [64e04ba]:
    • @​posthog/core@​1.39.4
Commits
  • 0ba87cb chore: update versions and lockfile [version bump]
  • 0c11747 fix: stop duplicating distinct_id in flags person properties (#4047)
  • 4eb8b21 chore: update versions and lockfile [version bump]
  • dd90e55 test(node): make local polling tests deterministic (#4043)
  • See full diff in compare view

Updates `@typescript/native-preview` from 7.0.0-dev.20260701.1 to 7.0.0-dev.20260702.3
Commits

Updates `knip` from 6.23.0 to 6.24.0
Release notes

Sourced from knip's releases.

Release 6.24.0

  • chore: update year in license (#1833) (32bc844dfd3895884b11fea5ef94bf3fa1974946) - thanks @​trueberryless!
  • ci: pin github actions (#1835) (82a8d0913105a637e9eeccfe3b785be90c873e2a) - thanks @​trueberryless!
  • Assume Node 24+/Bun and remove compilation step (d9ef038429bec06c37fdb520e5c7353c8cae6ce7)
  • Don't report working-directory scripts as unlisted binaries (resolve #1834) (aea7923438f0a8f459458578526f358b02497f77)
  • Add pnpm run lint to CI workflow (ec9aa1cabb58c97b8ecc4815e6cdd6421fe2a89e)
  • feat: add settings for Zed editor to use oxlint and oxfmt (#1836) (111f2e0a17999e3ffdf4c097cd42ba08acd48508) - thanks @​trueberryless!
  • fix: remove format_on_save: true settings from Zed settings to respect user settings (#1838) (dc2a64043035d426eb99a9d1e0eb873d02a09e7d) - thanks @​trueberryless!
  • feat: add Renovate for GitHub actions only (#1839) (ffce88c86f95822ee2a7cf8407e987a3ec79b097) - thanks @​trueberryless!
  • Ignore import() in JSDoc examples (#1844) (6f090f90f04e8202700ed68977faa1dc626ff235) - thanks @​cyphercodes!
  • Don't report types used only in module augmentations as unused (resolve #1843) (7901abd3c4b496f212445bff9768efc9548ded61)
  • Restore CI intent (0d739beab2e224385f449d62d6cc7d904107946e)
  • feat: add less, stylus compilers and astro, svelte, vue import resolution (#1845) (5525759f33a5664e4882aebe239e9c17eeb29f92) - thanks @​trueberryless!
  • Don't report built-in compiler dependencies as unused (3c9d4adf369f0064399d9da7b4f11f0467180bf5)
  • feat: add scss handling to stencil plugin (#1846) (acba6b85ab05f70385228872fafea2b08290d0f6) - thanks @​johnjenkins!
  • fix(reporters): always print the issue-type title for a single group (#1848) (cf997b2408cc0814c2d310d1e8c8680340153fa1) - thanks @​morgan-coded!
  • Export Issue, IssueRecords and IssueType types (resolve #1840) (260f19230f42c9dceaac97d55bf34d17902c5b38)
  • Squeeze every bit of perf out around compilers (bb0eeb6e33d050dab736134b5b692a3d6413f358)
Commits
  • 2979803 Release knip@6.24.0
  • bb0eeb6 Squeeze every bit of perf out around compilers
  • 260f192 Export Issue, IssueRecords and IssueType types (resolve #1840)
  • cf997b2 fix(reporters): always print the issue-type title for a single group (#1848)
  • acba6b8 feat: add scss handling to stencil plugin (#1846)
  • 3c9d4ad Don't report built-in compiler dependencies as unused
  • 5525759 feat: add less, stylus compilers and astro, svelte, vue import resolution (#1...
  • 7901abd Don't report types used only in module augmentations as unused (resolve #1843)
  • 6f090f9 Ignore import() in JSDoc examples (#1844)
  • aea7923 Don't report working-directory scripts as unlisted binaries (resolve #1834)
  • See full diff in compare view

Updates `tldts` from 6.1.86 to 7.4.6
Release notes

Sourced from tldts's releases.

v7.4.6

:scroll: Update Public Suffix List

  • tldts-experimental, tldts

:nut_and_bolt: Dependencies

Authors: 2

v7.4.5

:scroll: Update Public Suffix List

  • tldts-experimental, tldts

Authors: 1

v7.4.4

:scroll: Update Public Suffix List

  • tldts-experimental, tldts

Authors: 1

v7.4.3

:scroll: Update Public Suffix List

  • tldts-experimental, tldts

... (truncated)

Changelog

Sourced from tldts's changelog.

v7.4.6 (Thu Jul 02 2026)

:scroll: Update Public Suffix List

  • tldts-experimental, tldts

:nut_and_bolt: Dependencies

Authors: 2


v7.4.5 (Sun Jun 28 2026)

:scroll: Update Public Suffix List

  • tldts-experimental, tldts

Authors: 1


v7.4.4 (Tue Jun 23 2026)

:scroll: Update Public Suffix List

  • tldts-experimental, tldts

Authors: 1

... (truncated)

Commits
Maintainer changes

This version was pushed to npm by GitHub Actions, a new releaser for tldts since your current version.


Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/cli/package.json | 6 +- packages/api/package.json | 2 +- pnpm-lock.yaml | 252 +++++++++++++++++++------------------- pnpm-workspace.yaml | 6 +- 4 files changed, 133 insertions(+), 133 deletions(-) diff --git a/apps/cli/package.json b/apps/cli/package.json index c6c604f1ff..e7dd1a8292 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -43,8 +43,8 @@ "jose": "^6.2.3" }, "devDependencies": { - "@anthropic-ai/claude-agent-sdk": "^0.3.198", - "@anthropic-ai/sdk": "^0.109.1", + "@anthropic-ai/claude-agent-sdk": "^0.3.199", + "@anthropic-ai/sdk": "^0.110.0", "@clack/prompts": "^1.6.0", "@effect/atom-react": "catalog:", "@effect/platform-bun": "catalog:", @@ -76,7 +76,7 @@ "oxlint-tsgolint": "catalog:", "pg": "^8.22.0", "pg-copy-streams": "^7.0.0", - "posthog-node": "^5.39.2", + "posthog-node": "^5.39.4", "react": "^19.2.7", "react-devtools-core": "^7.0.1", "semantic-release": "^25.0.5", diff --git a/packages/api/package.json b/packages/api/package.json index 463c09af39..32bf5d93ce 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -23,7 +23,7 @@ "@effect/platform-bun": "catalog:", "@effect/platform-node": "catalog:", "effect": "catalog:", - "undici": "^8.5.0" + "undici": "^8.6.0" }, "devDependencies": { "@tsconfig/bun": "catalog:", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ef5928c0fa..00f08c3538 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -37,8 +37,8 @@ catalogs: specifier: ^1.3.14 version: 1.3.14 '@typescript/native-preview': - specifier: 7.0.0-dev.20260701.1 - version: 7.0.0-dev.20260701.1 + specifier: 7.0.0-dev.20260702.3 + version: 7.0.0-dev.20260702.3 '@vitest/coverage-istanbul': specifier: ^4.1.9 version: 4.1.9 @@ -46,8 +46,8 @@ catalogs: specifier: 4.0.0-beta.93 version: 4.0.0-beta.93 knip: - specifier: ^6.17.1 - version: 6.23.0 + specifier: ^6.24.0 + version: 6.25.0 nx: specifier: ^23.0.0 version: 23.0.1 @@ -61,8 +61,8 @@ catalogs: specifier: ^0.24.0 version: 0.24.0 tldts: - specifier: ^7.4.5 - version: 7.4.5 + specifier: ^7.4.6 + version: 7.4.8 vitest: specifier: ^4.1.9 version: 4.1.9 @@ -97,11 +97,11 @@ importers: version: 6.2.3 devDependencies: '@anthropic-ai/claude-agent-sdk': - specifier: ^0.3.198 - version: 0.3.198(@anthropic-ai/sdk@0.109.1(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(zod@4.4.3) + specifier: ^0.3.199 + version: 0.3.199(@anthropic-ai/sdk@0.110.0(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(zod@4.4.3) '@anthropic-ai/sdk': - specifier: ^0.109.1 - version: 0.109.1(zod@4.4.3) + specifier: ^0.110.0 + version: 0.110.0(zod@4.4.3) '@clack/prompts': specifier: ^1.6.0 version: 1.6.0 @@ -155,7 +155,7 @@ importers: version: 19.2.17 '@typescript/native-preview': specifier: 'catalog:' - version: 7.0.0-dev.20260701.1 + version: 7.0.0-dev.20260702.3 '@vercel/detect-agent': specifier: ^1.2.3 version: 1.2.3 @@ -179,7 +179,7 @@ importers: version: 5.0.0(ink@7.1.0(@types/react@19.2.17)(react-devtools-core@7.0.1)(react@19.2.7))(react@19.2.7) knip: specifier: 'catalog:' - version: 6.23.0 + version: 6.25.0 oxfmt: specifier: 'catalog:' version: 0.57.0 @@ -196,8 +196,8 @@ importers: specifier: ^7.0.0 version: 7.0.0 posthog-node: - specifier: ^5.39.2 - version: 5.39.2 + specifier: ^5.39.4 + version: 5.39.4 react: specifier: ^19.2.7 version: 19.2.7 @@ -212,7 +212,7 @@ importers: version: 1.7.0 tldts: specifier: 'catalog:' - version: 7.4.5 + version: 7.4.8 vitest: specifier: 'catalog:' version: 4.1.9(@types/node@26.1.1)(@vitest/coverage-istanbul@4.1.9)(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) @@ -259,13 +259,13 @@ importers: version: 1.3.14 '@typescript/native-preview': specifier: 'catalog:' - version: 7.0.0-dev.20260701.1 + version: 7.0.0-dev.20260702.3 '@vitest/coverage-istanbul': specifier: 'catalog:' version: 4.1.9(vitest@4.1.9) knip: specifier: 'catalog:' - version: 6.23.0 + version: 6.25.0 oxfmt: specifier: 'catalog:' version: 0.57.0 @@ -328,8 +328,8 @@ importers: specifier: 'catalog:' version: 4.0.0-beta.93 undici: - specifier: ^8.5.0 - version: 8.5.0 + specifier: ^8.6.0 + version: 8.6.0 devDependencies: '@tsconfig/bun': specifier: 'catalog:' @@ -339,13 +339,13 @@ importers: version: 1.3.14 '@typescript/native-preview': specifier: 'catalog:' - version: 7.0.0-dev.20260701.1 + version: 7.0.0-dev.20260702.3 '@vitest/coverage-istanbul': specifier: 'catalog:' version: 4.1.9(vitest@4.1.9) knip: specifier: 'catalog:' - version: 6.23.0 + version: 6.25.0 oxfmt: specifier: 'catalog:' version: 0.57.0 @@ -381,13 +381,13 @@ importers: version: 1.3.14 '@typescript/native-preview': specifier: 'catalog:' - version: 7.0.0-dev.20260701.1 + version: 7.0.0-dev.20260702.3 '@vitest/coverage-istanbul': specifier: 'catalog:' version: 4.1.9(vitest@4.1.9) knip: specifier: 'catalog:' - version: 6.23.0 + version: 6.25.0 oxfmt: specifier: 'catalog:' version: 0.57.0 @@ -431,13 +431,13 @@ importers: version: 1.3.14 '@typescript/native-preview': specifier: 'catalog:' - version: 7.0.0-dev.20260701.1 + version: 7.0.0-dev.20260702.3 '@vitest/coverage-istanbul': specifier: 'catalog:' version: 4.1.9(vitest@4.1.9) knip: specifier: 'catalog:' - version: 6.23.0 + version: 6.25.0 oxfmt: specifier: 'catalog:' version: 0.57.0 @@ -471,13 +471,13 @@ importers: version: 1.3.14 '@typescript/native-preview': specifier: 'catalog:' - version: 7.0.0-dev.20260701.1 + version: 7.0.0-dev.20260702.3 '@vitest/coverage-istanbul': specifier: 'catalog:' version: 4.1.9(vitest@4.1.9) knip: specifier: 'catalog:' - version: 6.23.0 + version: 6.25.0 oxfmt: specifier: 'catalog:' version: 0.57.0 @@ -523,13 +523,13 @@ importers: version: 1.3.14 '@typescript/native-preview': specifier: 'catalog:' - version: 7.0.0-dev.20260701.1 + version: 7.0.0-dev.20260702.3 '@vitest/coverage-istanbul': specifier: 'catalog:' version: 4.1.9(vitest@4.1.9) knip: specifier: 'catalog:' - version: 6.23.0 + version: 6.25.0 oxfmt: specifier: 'catalog:' version: 0.57.0 @@ -570,60 +570,60 @@ packages: resolution: {integrity: sha512-p+CMKJ93HFmLkjXKlXiVGlMQEuRb6H0MokBSwUsX+S6BRX8eV5naFZpQJFfJHjRZY0Hmnqy1/r6UWl3x+19zYA==} engines: {node: '>=18'} - '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.198': - resolution: {integrity: sha512-ZmiAybQKIKcP1qEAE/vfXvfxtKxG9CnJn98QTXC5Zxiwuy7Mllx2ALXh9dfmsf0V87CGEodlZQmMgUJotNIsUw==} + '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.199': + resolution: {integrity: sha512-0813IEsPlA3GQZ86CGmRPh/2DY/iEuBkXV7CRAj55sQ30oIm+N/BbXzI/jH7WiWsHh3pCsRx65dbswf6jA5Uuw==} cpu: [arm64] os: [darwin] - '@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.198': - resolution: {integrity: sha512-XwH5vgN46WSwg8aC1OagNofnJpV/G1ciEu118GEKer8ZhVkq/dvK/DqShxMkb6r1jV7u5IJ7zPXu9uKliyNJAw==} + '@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.199': + resolution: {integrity: sha512-iUenoE7UbWFfYjdfaeJkPl4qpXajPcRw8SzMYn0PK2LsBE5SJTfEdZyvtypNaKiWzsCCaU4MV1yzR8S7i/wZQg==} cpu: [x64] os: [darwin] - '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.198': - resolution: {integrity: sha512-Q7lKVNjIrUQ2B/AR77OvRf0zeOdEjonFVaR9FYrrwtzGeEqum69WSht5nM7Y7el3wjbNi0/eV0QTUM0DlsTEfw==} + '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.199': + resolution: {integrity: sha512-mcqHuNHsA2V589rt0JpCsweS8kZ8LkKZi0qmMLqN3oZz+ar4DsuDjLDaNI1C5f+W/nz5G/st+2W3ZCJubeOqVQ==} cpu: [arm64] os: [linux] libc: [musl] - '@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.198': - resolution: {integrity: sha512-qmz8dxEtDIlKntU5qYe0R4aWTxTue5S7zIQknatLX7aJ6HN/nq1aCNXWn5smTH2FViBkUPPR+sCIsNwSk6AT6Q==} + '@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.199': + resolution: {integrity: sha512-wJ4GJCwrdVv4TWTiEwh2gUjrddIg+oMhoUcfXhtZgZeqXfGzxVWJa3HudX5xsVq/wktvM65CfdYly2blVBVG8Q==} cpu: [arm64] os: [linux] libc: [glibc] - '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.198': - resolution: {integrity: sha512-h1SrWVIMjLInYNPlf+TxXuKTOdoiOfJLBSoQG97315Z2Nh0IpBfqWExlqYTtPCgKE7q2iga31U283QfHpIDlSQ==} + '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.199': + resolution: {integrity: sha512-wr7IIQ9d9H7Tt79Mn4ga8iNLzsvoPnAbBtsnUVBjgcAxKWrJ+QV2wCsowhhSc7DWNueiurdxNsLIwd07BzhfCA==} cpu: [x64] os: [linux] libc: [musl] - '@anthropic-ai/claude-agent-sdk-linux-x64@0.3.198': - resolution: {integrity: sha512-Zqxyz2AT1UM5WlOOoLJhLssZDgZo8rBK5ku6daveK12zp+UTJGZhGsjFghz1/ASxH08KqOTbUePNTORnPhHAEQ==} + '@anthropic-ai/claude-agent-sdk-linux-x64@0.3.199': + resolution: {integrity: sha512-nNLk8KcM33AYQ0P1cNK72kE14iTIuM5RTr0CrQm40FwdrPuVZ/se6KggpE3eje8hFYJu/1WrJqcZxtJSL6qnPg==} cpu: [x64] os: [linux] libc: [glibc] - '@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.198': - resolution: {integrity: sha512-mjIHf1HFiRuXefewWTaNZFlTZlCaEt/xsRjc1nSTCEEpFolZayVhrDKz+O2QFVcDtPl8x8GeYSL0kiikg1DZjQ==} + '@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.199': + resolution: {integrity: sha512-EQFMATdpp6XXYXw6Ih83BFCDj2PXqMYkM6nq3OrOVUF19xTe6o5dWhGzZ6TQfauvWf2BHFIfBDzZz82m8ffV8w==} cpu: [arm64] os: [win32] - '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.198': - resolution: {integrity: sha512-y3HLuCCz1kDwUrhd6OnqO+d5BUpTFSzNUsPT9kf3r1vk9HYKF+eMC9eIlcOhiW2kX491kxEvuEOfqgIkGx15cg==} + '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.199': + resolution: {integrity: sha512-6W2djU/NnOCY4NEdDjtZ3LIzFFQMT2OH9m5y5Hc4bko3KIXr0WHLrCJdgGiVycOWEd03OEsC4uYIIYIeueBAHw==} cpu: [x64] os: [win32] - '@anthropic-ai/claude-agent-sdk@0.3.198': - resolution: {integrity: sha512-xt469sSCyclTPtzpLAg0Aschy665GiRMgZKabSmESbGUA5/H56HcILVOiFxclXswkeMUk2fQxfHJUY9UZfiTnA==} + '@anthropic-ai/claude-agent-sdk@0.3.199': + resolution: {integrity: sha512-fET9rfYR2DgjVG4Yri02Q6YL+SWBlcrsd3Dx96CpZSM50W4Jqh1sEC06hXIEHL0uDZ/kS//Y2uPpCQ9GQqh8Bg==} engines: {node: '>=18.0.0'} peerDependencies: '@anthropic-ai/sdk': '>=0.93.0' '@modelcontextprotocol/sdk': ^1.29.0 zod: ^4.0.0 - '@anthropic-ai/sdk@0.109.1': - resolution: {integrity: sha512-q9OnEKLr5H9nxSuXdgDgJhxfYMiE+AaUEBze2Gk91UcaaLnsN+Lx5fbCYywiqurU/APLdwv23x03Wm6WN3EBsg==} + '@anthropic-ai/sdk@0.110.0': + resolution: {integrity: sha512-hOP4bNYXDFHDxxiEgzlILXrxZIYCDnhe8sry0RDRKD/QnsEpvZcQpablCdm9X/WuD/YgOiSIkkqsL1mLLlTqJw==} hasBin: true peerDependencies: zod: ^3.25.0 || ^4.0.0 @@ -2110,8 +2110,8 @@ packages: resolution: {integrity: sha512-//0sR/cow/s4ICQaYoAobOl4aU8cjU6x/V24V7XkKotb9+O+3zySIYp146vpaobYHnxa4pZX8NkV54Z5AwbDKA==} engines: {node: '>=12'} - '@posthog/core@1.40.0': - resolution: {integrity: sha512-oGDbIwlTquNwdHbEL5ZLEkuW4UFkkEanfx3QAxDgyVbISv+OAA6YGQwrvo0JD3MUJEbJZvyh8XsX+WYDGw9XHw==} + '@posthog/core@1.40.1': + resolution: {integrity: sha512-jXuMtZCwA7AMpYlo1wjm6GlC58YBlz/ZxpDIsF9hroUfqYoPPDmQbsQb4iiZAS43+o/kwloxdKJIlE0IiwQ5HQ==} '@posthog/types@1.393.0': resolution: {integrity: sha512-vzWeEJZ7ERQhFRoQYaP5jzN1JvIu46UJyHXsuv+dTGW2r3sMgREOhNxXLZjmFHwZ8/FOHQoyqqQmXTCXZSfMSg==} @@ -2867,50 +2867,50 @@ packages: '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} - '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260701.1': - resolution: {integrity: sha512-rm3v+3Mcrj91NPwG0mjaLfbJjydCDX/skF82cQw0K6XWsEK7wHmpLEF1xPOWGnV05RhTYJac9VoKUjgcADkbXA==} + '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260702.3': + resolution: {integrity: sha512-sswJ7XRSH17/jDv6eqlRyNzjQev4w+TH+16RgeoZkKbQfZnVuLchF4ORCecEuia15X2yE8pbiQGY596LVbGKdA==} engines: {node: '>=16.20.0'} cpu: [arm64] os: [darwin] - '@typescript/native-preview-darwin-x64@7.0.0-dev.20260701.1': - resolution: {integrity: sha512-jtbtV3A+cmkmDuqs56Qj3Hb+NmOAMHFX+FTLhCagJybjsng1lOl1h8vYaYg3F6EMgSiZI66hIpB9TMi3n8jFEw==} + '@typescript/native-preview-darwin-x64@7.0.0-dev.20260702.3': + resolution: {integrity: sha512-j8ZYT/chWtx3QT1Bghp0/+2g6aKLaxRVpXEHt6Fjdj4OXQTpH4pljOE0+LeAqaET88PUo/XM9pi0TJ5TJwHLMg==} engines: {node: '>=16.20.0'} cpu: [x64] os: [darwin] - '@typescript/native-preview-linux-arm64@7.0.0-dev.20260701.1': - resolution: {integrity: sha512-SPEfXZhCRff6kPYrqJIkKkKBy1rxPk0Wmam7U0AOuMQtXTNPmqCtx54J/F6nfGR89ATTDTDr69MujFAG+lFQVg==} + '@typescript/native-preview-linux-arm64@7.0.0-dev.20260702.3': + resolution: {integrity: sha512-FEBt9UER27yvn3o4quXZ9fQ/Hd39hxFPljJe8dxstCvyYLh0A9TwDuuwghWCisp5U9TDWWtl8m1GLjvqIKc6IA==} engines: {node: '>=16.20.0'} cpu: [arm64] os: [linux] - '@typescript/native-preview-linux-arm@7.0.0-dev.20260701.1': - resolution: {integrity: sha512-c22PWjLMecd2xsAZRlpC1mnr8GZUjnFS32URPd2NhKOEx/6Dp7bPZvI+7NNXDXTew/fkgAaZvhLTWHGG64gqGw==} + '@typescript/native-preview-linux-arm@7.0.0-dev.20260702.3': + resolution: {integrity: sha512-SM73Mp3oYUWkyHmym0MVfPnMeLGM6oo1vcPZTxVoL7BOKWkJKK+iM7D1tcqDSZNPwURSkWMxP17Dh+4JPqzikg==} engines: {node: '>=16.20.0'} cpu: [arm] os: [linux] - '@typescript/native-preview-linux-x64@7.0.0-dev.20260701.1': - resolution: {integrity: sha512-Y9NX6qGOCQD4zdCXJCABB8TL3IkFsWVlwMQ3OGtQ8A/OzyIFN01QyHWFgB5AJFZQoasryb+nnhu9r2IY5KpBwA==} + '@typescript/native-preview-linux-x64@7.0.0-dev.20260702.3': + resolution: {integrity: sha512-CY/r7gHjDnf3xWptpQVRQnagMdUDbh+zL01SF+cTJsg1WVY3TtVNrCo48dwAnJL+U97qx1SKaGVsi/8cSPwrZA==} engines: {node: '>=16.20.0'} cpu: [x64] os: [linux] - '@typescript/native-preview-win32-arm64@7.0.0-dev.20260701.1': - resolution: {integrity: sha512-7IRD34O2Kp7mIplQEC/HgLyehmKL1FGlTYhlAqxxZfKhSt694yytd2dCCnxMMA/aPHjcqBke04wNPOOhQ2ezcA==} + '@typescript/native-preview-win32-arm64@7.0.0-dev.20260702.3': + resolution: {integrity: sha512-9U/ahdJAWZdYxPdNry3VAhB9avaQCiqsEk2PfheeKrKSKCtfs5FoPXy/QXAaIGIBK/Lw4a7OqyVyEbd0i77TkQ==} engines: {node: '>=16.20.0'} cpu: [arm64] os: [win32] - '@typescript/native-preview-win32-x64@7.0.0-dev.20260701.1': - resolution: {integrity: sha512-zVfayHxvFZAQ0hJbegDcfhJamyQpLn6ahMDATAra+EkA3+/nbTW3VjCJ3h1V2PSNTY9rTPzGroFURV5cvErh3A==} + '@typescript/native-preview-win32-x64@7.0.0-dev.20260702.3': + resolution: {integrity: sha512-kPqfivHuzzEK2NFEl2QeCCiWJ4qkGMJYFX9TmJ4ylh784XICgQLX3PJe7OCqZcUicfeMIIDu6m3flcCi+4tW4Q==} engines: {node: '>=16.20.0'} cpu: [x64] os: [win32] - '@typescript/native-preview@7.0.0-dev.20260701.1': - resolution: {integrity: sha512-eXPtWsAj0s06kHWVDlBj7ABwoyNDHuW2gCbQ+bYGlyymDYteiAydelhyhyzUsenzGraPLdd/OPwEUB8/KUdpBw==} + '@typescript/native-preview@7.0.0-dev.20260702.3': + resolution: {integrity: sha512-j21laxUja23ex6qYGjwiSiQ6YpS/p5E7lCMDYKDBAdBv93Z1NzV96BZV3gdRwl+buH09EPuBAiqxKQqRO1grhw==} engines: {node: '>=16.20.0'} hasBin: true @@ -4633,8 +4633,8 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} - knip@6.23.0: - resolution: {integrity: sha512-2DvAOX2pZWiG4SLvRRxOAU0aWGEn1ZoVblI541xIoXFdHqq2THMZXy66/qcY5WGuW3TXhb9T1x1zd/Hd1u+yqg==} + knip@6.25.0: + resolution: {integrity: sha512-Q3n41VjOOB/aqsbxb8kallAcFKrUz3b2S5fD5pTODljVpP01t+rvAgy2x3j0Cq8yEpRRHNdar1vHuqFfGuIakQ==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -5665,8 +5665,8 @@ packages: postgres-range@1.1.4: resolution: {integrity: sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==} - posthog-node@5.39.2: - resolution: {integrity: sha512-5piMedjlQ2x+UKLvHWTC5ls5/T1dDZKE1Pu5AKkYh9EkbZOjvu0cac6lWFB7mgbGkKQ0I1bhbjDx1QAYRJ7Unw==} + posthog-node@5.39.4: + resolution: {integrity: sha512-+fCQ7htBFRQQFbIzl1T0TA7bDwYyaB9XP308ZFMCUoB5LzTzOFxBa6TYVrxdH/VQl43WXTp6sf0QsG2Z4XlNBg==} engines: {node: ^20.20.0 || >=22.22.0} peerDependencies: rxjs: ^7.0.0 @@ -6337,15 +6337,15 @@ packages: tldts-core@6.1.86: resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} - tldts-core@7.4.5: - resolution: {integrity: sha512-pGrwzZDvPwKe+7NNUqAunb6rqTfynr0VOUhCMdqbu5xlvNiszsAJygRzwvpVycdzejlbpY+SWJOn+s75Og7FEA==} + tldts-core@7.4.8: + resolution: {integrity: sha512-c1P7u0EhACHj7lPy4MJm8iTFEU8+nB0LCtddH0fhP7noaVoXAqafMtOOeX+ulpuPBqnrRgRhw494RICT3mbhnw==} tldts@6.1.86: resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} hasBin: true - tldts@7.4.5: - resolution: {integrity: sha512-RfEzKWcq5fHUOFq7J3rl3Oz6ylKGtcHqUznzj4EcXsxLSIjJcvpbXAQtWGeJQ0xKnimR5e0Cn+cn9TssfMzm+g==} + tldts@7.4.8: + resolution: {integrity: sha512-htwgN/8KRB3z3vnC0BOETVh2m499g5GmyTK9Wq5JBLX3FNz6tSBveAd+fQhzy9hkjif8vy2jwDMR1sGhLtZl2A==} hasBin: true tmp@0.2.7: @@ -6459,8 +6459,8 @@ packages: resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==} engines: {node: '>=20.18.1'} - undici@8.5.0: - resolution: {integrity: sha512-xamtWoB1EshgjpmlXd7GGm2VfdDtw1+rD8uhry8pSNW3If6S8E0m2T2+orSKeZXEn/aPJMviCpDBA65WJt8zhg==} + undici@8.6.0: + resolution: {integrity: sha512-l2FlC6I510GawyEd1qgcE/okihKrzy+BRTEBlu6T0fdbM9m5yxtIH5Oa3ysRsH0zC4EhmWUEaSDsy2QngBeRlw==} engines: {node: '>=22.19.0'} unicode-emoji-modifier-base@1.0.0: @@ -6852,46 +6852,46 @@ snapshots: ansi-styles: 6.2.3 is-fullwidth-code-point: 5.1.0 - '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.198': + '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.199': optional: true - '@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.198': + '@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.199': optional: true - '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.198': + '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.199': optional: true - '@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.198': + '@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.199': optional: true - '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.198': + '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.199': optional: true - '@anthropic-ai/claude-agent-sdk-linux-x64@0.3.198': + '@anthropic-ai/claude-agent-sdk-linux-x64@0.3.199': optional: true - '@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.198': + '@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.199': optional: true - '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.198': + '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.199': optional: true - '@anthropic-ai/claude-agent-sdk@0.3.198(@anthropic-ai/sdk@0.109.1(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(zod@4.4.3)': + '@anthropic-ai/claude-agent-sdk@0.3.199(@anthropic-ai/sdk@0.110.0(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(zod@4.4.3)': dependencies: - '@anthropic-ai/sdk': 0.109.1(zod@4.4.3) + '@anthropic-ai/sdk': 0.110.0(zod@4.4.3) '@modelcontextprotocol/sdk': 1.29.0(zod@4.4.3) zod: 4.4.3 optionalDependencies: - '@anthropic-ai/claude-agent-sdk-darwin-arm64': 0.3.198 - '@anthropic-ai/claude-agent-sdk-darwin-x64': 0.3.198 - '@anthropic-ai/claude-agent-sdk-linux-arm64': 0.3.198 - '@anthropic-ai/claude-agent-sdk-linux-arm64-musl': 0.3.198 - '@anthropic-ai/claude-agent-sdk-linux-x64': 0.3.198 - '@anthropic-ai/claude-agent-sdk-linux-x64-musl': 0.3.198 - '@anthropic-ai/claude-agent-sdk-win32-arm64': 0.3.198 - '@anthropic-ai/claude-agent-sdk-win32-x64': 0.3.198 + '@anthropic-ai/claude-agent-sdk-darwin-arm64': 0.3.199 + '@anthropic-ai/claude-agent-sdk-darwin-x64': 0.3.199 + '@anthropic-ai/claude-agent-sdk-linux-arm64': 0.3.199 + '@anthropic-ai/claude-agent-sdk-linux-arm64-musl': 0.3.199 + '@anthropic-ai/claude-agent-sdk-linux-x64': 0.3.199 + '@anthropic-ai/claude-agent-sdk-linux-x64-musl': 0.3.199 + '@anthropic-ai/claude-agent-sdk-win32-arm64': 0.3.199 + '@anthropic-ai/claude-agent-sdk-win32-x64': 0.3.199 - '@anthropic-ai/sdk@0.109.1(zod@4.4.3)': + '@anthropic-ai/sdk@0.110.0(zod@4.4.3)': dependencies: json-schema-to-ts: 3.1.1 standardwebhooks: 1.0.0 @@ -7069,7 +7069,7 @@ snapshots: effect: 4.0.0-beta.93 ioredis: 5.11.0 mime: 4.1.0 - undici: 8.5.0 + undici: 8.6.0 transitivePeerDependencies: - bufferutil - utf-8-validate @@ -8010,7 +8010,7 @@ snapshots: '@pnpm/network.ca-file': 1.0.2 config-chain: 1.1.13 - '@posthog/core@1.40.0': + '@posthog/core@1.40.1': dependencies: '@posthog/types': 1.393.0 @@ -8751,36 +8751,36 @@ snapshots: dependencies: '@types/node': 26.1.1 - '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260701.1': + '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260702.3': optional: true - '@typescript/native-preview-darwin-x64@7.0.0-dev.20260701.1': + '@typescript/native-preview-darwin-x64@7.0.0-dev.20260702.3': optional: true - '@typescript/native-preview-linux-arm64@7.0.0-dev.20260701.1': + '@typescript/native-preview-linux-arm64@7.0.0-dev.20260702.3': optional: true - '@typescript/native-preview-linux-arm@7.0.0-dev.20260701.1': + '@typescript/native-preview-linux-arm@7.0.0-dev.20260702.3': optional: true - '@typescript/native-preview-linux-x64@7.0.0-dev.20260701.1': + '@typescript/native-preview-linux-x64@7.0.0-dev.20260702.3': optional: true - '@typescript/native-preview-win32-arm64@7.0.0-dev.20260701.1': + '@typescript/native-preview-win32-arm64@7.0.0-dev.20260702.3': optional: true - '@typescript/native-preview-win32-x64@7.0.0-dev.20260701.1': + '@typescript/native-preview-win32-x64@7.0.0-dev.20260702.3': optional: true - '@typescript/native-preview@7.0.0-dev.20260701.1': + '@typescript/native-preview@7.0.0-dev.20260702.3': optionalDependencies: - '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260701.1 - '@typescript/native-preview-darwin-x64': 7.0.0-dev.20260701.1 - '@typescript/native-preview-linux-arm': 7.0.0-dev.20260701.1 - '@typescript/native-preview-linux-arm64': 7.0.0-dev.20260701.1 - '@typescript/native-preview-linux-x64': 7.0.0-dev.20260701.1 - '@typescript/native-preview-win32-arm64': 7.0.0-dev.20260701.1 - '@typescript/native-preview-win32-x64': 7.0.0-dev.20260701.1 + '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260702.3 + '@typescript/native-preview-darwin-x64': 7.0.0-dev.20260702.3 + '@typescript/native-preview-linux-arm': 7.0.0-dev.20260702.3 + '@typescript/native-preview-linux-arm64': 7.0.0-dev.20260702.3 + '@typescript/native-preview-linux-x64': 7.0.0-dev.20260702.3 + '@typescript/native-preview-win32-arm64': 7.0.0-dev.20260702.3 + '@typescript/native-preview-win32-x64': 7.0.0-dev.20260702.3 '@ungap/structured-clone@1.3.2': {} @@ -9958,9 +9958,9 @@ snapshots: dependencies: walk-up-path: 4.0.0 - fdir@6.5.0(picomatch@4.0.4): + fdir@6.5.0(picomatch@4.0.5): optionalDependencies: - picomatch: 4.0.4 + picomatch: 4.0.5 figures@2.0.0: dependencies: @@ -10745,15 +10745,15 @@ snapshots: dependencies: json-buffer: 3.0.1 - knip@6.23.0: + knip@6.25.0: dependencies: - fdir: 6.5.0(picomatch@4.0.4) + fdir: 6.5.0(picomatch@4.0.5) formatly: 0.3.0 get-tsconfig: 4.14.0 jiti: 2.7.0 oxc-parser: 0.137.0 oxc-resolver: 11.21.3 - picomatch: 4.0.4 + picomatch: 4.0.5 smol-toml: 1.7.0 strip-json-comments: 5.0.3 tinyglobby: 0.2.17 @@ -12071,9 +12071,9 @@ snapshots: postgres-range@1.1.4: {} - posthog-node@5.39.2: + posthog-node@5.39.4: dependencies: - '@posthog/core': 1.40.0 + '@posthog/core': 1.40.1 pretty-ms@9.3.0: dependencies: @@ -12901,8 +12901,8 @@ snapshots: tinyglobby@0.2.17: dependencies: - fdir: 6.5.0(picomatch@4.0.4) - picomatch: 4.0.4 + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 tinypool@2.1.0: {} @@ -12910,15 +12910,15 @@ snapshots: tldts-core@6.1.86: {} - tldts-core@7.4.5: {} + tldts-core@7.4.8: {} tldts@6.1.86: dependencies: tldts-core: 6.1.86 - tldts@7.4.5: + tldts@7.4.8: dependencies: - tldts-core: 7.4.5 + tldts-core: 7.4.8 tmp@0.2.7: {} @@ -13002,7 +13002,7 @@ snapshots: undici@7.28.0: {} - undici@8.5.0: {} + undici@8.6.0: {} unicode-emoji-modifier-base@1.0.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index c4b733934e..e5ca91b394 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -22,15 +22,15 @@ catalog: "@swc/core": "^1.15.43" "@tsconfig/bun": "^1.0.10" "@types/bun": "^1.3.14" - "@typescript/native-preview": "7.0.0-dev.20260701.1" + "@typescript/native-preview": "7.0.0-dev.20260702.3" "@vitest/coverage-istanbul": "^4.1.9" "effect": "4.0.0-beta.93" - "knip": "^6.17.1" + "knip": "^6.24.0" "nx": "^23.0.0" "oxfmt": "^0.57.0" "oxlint": "^1.72.0" "oxlint-tsgolint": "^0.24.0" - "tldts": "^7.4.5" + "tldts": "^7.4.6" "vitest": "^4.1.9" blockExoticSubdeps: true From b17d93826aa77c1436e31a31be5f99e85649b303 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Jul 2026 00:15:16 +0000 Subject: [PATCH 40/79] chore(ci): bump docker/login-action from 4.3.0 to 4.4.0 in the actions-major group (#5852) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the actions-major group with 1 update: [docker/login-action](https://github.com/docker/login-action). Updates `docker/login-action` from 4.3.0 to 4.4.0
Release notes

Sourced from docker/login-action's releases.

v4.4.0

Full Changelog: https://github.com/docker/login-action/compare/v4.3.0...v4.4.0

Commits
  • af1e73f Merge pull request #1034 from docker/dependabot/npm_and_yarn/aws-sdk-dependen...
  • da722bd [dependabot skip] chore: update generated content
  • 2916ad6 build(deps): bump the aws-sdk-dependencies group across 1 directory with 2 up...
  • ca0a662 Merge pull request #1035 from crazy-max/fix-registry-auth-empty-mask
  • c455755 chore: update generated content
  • 4835190 skip empty registry-auth secret mask
  • 992421c Merge pull request #1033 from docker/dependabot/github_actions/docker/bake-ac...
  • b249b43 Merge pull request #1032 from docker/dependabot/github_actions/docker/bake-ac...
  • 1b67977 build(deps): bump docker/bake-action from 7.2.0 to 7.3.0
  • 9d49d6a build(deps): bump docker/bake-action/subaction/matrix
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=docker/login-action&package-manager=github_actions&previous-version=4.3.0&new-version=4.4.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/cli-go-mirror-image.yml | 4 ++-- .github/workflows/cli-go-pg-prove.yml | 4 ++-- .github/workflows/cli-go-publish-migra.yml | 4 ++-- .github/workflows/mirror-template-images.yml | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/cli-go-mirror-image.yml b/.github/workflows/cli-go-mirror-image.yml index 18921bf791..d82bc05002 100644 --- a/.github/workflows/cli-go-mirror-image.yml +++ b/.github/workflows/cli-go-mirror-image.yml @@ -38,10 +38,10 @@ jobs: with: role-to-assume: ${{ secrets.PROD_AWS_ROLE }} aws-region: us-east-1 - - uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4.3.0 + - uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 with: registry: public.ecr.aws - - uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4.3.0 + - uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 with: registry: ghcr.io username: ${{ github.actor }} diff --git a/.github/workflows/cli-go-pg-prove.yml b/.github/workflows/cli-go-pg-prove.yml index 993e78bb0c..323758c1c0 100644 --- a/.github/workflows/cli-go-pg-prove.yml +++ b/.github/workflows/cli-go-pg-prove.yml @@ -45,7 +45,7 @@ jobs: - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 with: endpoint: builders - - uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4.3.0 + - uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} @@ -67,7 +67,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 - - uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4.3.0 + - uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} diff --git a/.github/workflows/cli-go-publish-migra.yml b/.github/workflows/cli-go-publish-migra.yml index 189e6beea4..11b6437666 100644 --- a/.github/workflows/cli-go-publish-migra.yml +++ b/.github/workflows/cli-go-publish-migra.yml @@ -45,7 +45,7 @@ jobs: - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 with: endpoint: builders - - uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4.3.0 + - uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} @@ -67,7 +67,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 - - uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4.3.0 + - uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} diff --git a/.github/workflows/mirror-template-images.yml b/.github/workflows/mirror-template-images.yml index ee7483d2e9..8f46d54288 100644 --- a/.github/workflows/mirror-template-images.yml +++ b/.github/workflows/mirror-template-images.yml @@ -50,7 +50,7 @@ jobs: dependency-firewall-token: ${{ secrets.DF_FIREWALL_TOKEN }} - name: Log in to ghcr.io - uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4.3.0 + uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 with: registry: ghcr.io username: ${{ github.actor }} From 43a8798b5eb2653af2b2720be84ee839b4a20ca9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Jul 2026 09:36:24 +0200 Subject: [PATCH 41/79] fix(docker): bump the docker-minor group in /apps/cli-go/pkg/config/templates with 3 updates (#5851) Bumps the docker-minor group in /apps/cli-go/pkg/config/templates with 3 updates: supabase/realtime, supabase/storage-api and supabase/logflare. Updates `supabase/realtime` from v2.112.9 to v2.112.10 Updates `supabase/storage-api` from v1.64.1 to v1.66.2 Updates `supabase/logflare` from 1.46.0 to 1.47.0 Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
--------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- apps/cli-go/pkg/config/templates/Dockerfile | 6 +++--- packages/stack/src/versions.ts | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/cli-go/pkg/config/templates/Dockerfile b/apps/cli-go/pkg/config/templates/Dockerfile index f285fe5ead..6b9c671d1d 100644 --- a/apps/cli-go/pkg/config/templates/Dockerfile +++ b/apps/cli-go/pkg/config/templates/Dockerfile @@ -11,9 +11,9 @@ FROM supabase/edge-runtime:v1.74.2 AS edgeruntime FROM timberio/vector:0.53.0-alpine AS vector FROM supabase/supavisor:2.9.7 AS supavisor FROM supabase/gotrue:v2.192.0 AS gotrue -FROM supabase/realtime:v2.112.9 AS realtime -FROM supabase/storage-api:v1.64.1 AS storage -FROM supabase/logflare:1.46.0 AS logflare +FROM supabase/realtime:v2.112.10 AS realtime +FROM supabase/storage-api:v1.66.2 AS storage +FROM supabase/logflare:1.47.0 AS logflare # Append to JobImages when adding new dependencies below FROM supabase/pgadmin-schema-diff:cli-0.0.5 AS differ FROM supabase/migra:3.0.1663481299 AS migra diff --git a/packages/stack/src/versions.ts b/packages/stack/src/versions.ts index e9196cea68..be3d7f5687 100644 --- a/packages/stack/src/versions.ts +++ b/packages/stack/src/versions.ts @@ -50,13 +50,13 @@ export const DEFAULT_VERSIONS: VersionManifest = { postgrest: "14.14", auth: "2.192.0", "edge-runtime": "1.74.2", - realtime: "2.112.9", - storage: "1.64.1", + realtime: "2.112.10", + storage: "1.66.2", imgproxy: "v3.8.0", mailpit: "v1.30.2", pgmeta: "0.96.6", studio: "2026.07.07-sha-a6a04f2", - analytics: "1.46.0", + analytics: "1.47.0", vector: "0.53.0-alpine", pooler: "2.9.7", } as const; From 68344977e5aab57c28ed8f99b10d09be98418828 Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Fri, 10 Jul 2026 10:39:29 +0100 Subject: [PATCH 42/79] fix(cli): suppress vendored effect CLI's stdout help-dump and duplicate error on parse failures (CLI-1901) (#5844) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What kind of change does this PR introduce? Bug fix (legacy shell shared CLI runtime). ## What is the current behavior? Fixes [CLI-1901](https://linear.app/supabase/issue/CLI-1901/legacy-cli-required-flagchoice-parse-errors-double-print-and-dump-help), originally surfaced as a side-finding during #5803's review (CLI-1859, `gen bearer-jwt --role required`). Any legacy command whose flag parsing fails with a `CliError.ShowHelp`-wrapped error carrying a non-empty `errors` array (missing required flag, invalid `Flag.choice` value, unrecognized flag, etc.) printed a broken, non-Go-parity error: ``` $ supabase sso add --project-ref DESCRIPTION Add and configure a new connection to a SSO identity provider... USAGE supabase sso add [flags] FLAGS ... [... 20-30 line help doc, printed to STDOUT ...] ERROR Missing required flag: --type Error: required flag(s) "type" not set Try rerunning the command with --debug to troubleshoot the error. ``` Root cause: the vendored `effect@4.0.0-beta.93` CLI library's `Command.runWith` (`.repos/effect/packages/effect/src/unstable/cli/Command.ts`) always dumps the full help doc via `Console.log` and, when `errors.length > 0`, also `Console.error`s the same errors — before this repo's own Go-parity renderer (`normalize-error.ts` + `run.ts`'s `handledProgram`) renders its own line for the same failure. This breaks stdout-piping scripts (e.g. `TOKEN=$(supabase gen bearer-jwt --role "$ROLE")` with an empty `$ROLE`) and violates this repo's own Go-parity contract (`apps/cli/CLAUDE.md`). ## What is the new behavior? `run.ts` gains `withoutParseErrorHelpDump`, which wraps `Command.runWith(rootCommand, ...)(args)` with a buffering `Console.Console` and, once the run's outcome is known, disposes of the buffered writes per `classifyParseErrorConsoleOutput` — a three-way split that mirrors Go cobra's *actual* behavior (verified directly against the built `apps/cli-go/supabase-go` binary, not just assumed): - **`drop`** — a missing required flag (`MissingOption`). Go's `PersistentPreRunE` sets `SilenceUsage = true` (`cmd/root.go:97`) *before* `ValidateRequiredFlags` runs, so this is a single clean stderr line, nothing on stdout. - **`flush-help-doc-to-stderr`** — every other genuine parse/validation failure (unrecognized flag, invalid `Flag.choice` value, missing positional argument, unknown subcommand). These are raised during `ParseFlags`/`ValidateArgs`, *before* `SilenceUsage` is set — Go still shows a usage block for these, always on stderr, never stdout. The library's help doc isn't byte-identical to cobra's shorter usage template (see judgement calls below), but it's now on the right stream with no duplicate. - **`flush-unchanged`** — success, `--help`, `--version`, `--completions`, and the bare-group-command help dump (`errors: []`, exit 0) — all untouched. In every genuine-failure case, the library's own duplicate `Console.error` render is dropped; this repo's own `handledProgram` + `normalizeCause` render the single Go-parity line instead. `normalize-error.ts`'s `ShowHelp`-envelope unwrap was also extended: since the library's duplicate render is now gone, a wrapped inner error with no Go-parity-specific mapping (`UnrecognizedOption`, `DuplicateOption`, `MissingArgument`, `UnknownSubcommand`, or a non-doubled-prefix `InvalidValue`) now falls back to that inner error's own `.message` instead of the useless generic "Help requested" envelope text — otherwise removing the duplicate would have silently regressed those tags from "informative, but printed twice" to "printed once, uselessly." Tests: pure-predicate unit tests for the three-way classifier (`run.unit.test.ts`), integration tests exercising the real buffering/flush wiring against `legacyBranchesCommand` and a synthetic required-flag command via a `Console.Console` test double (`run.integration.test.ts` — not `vi.spyOn`, which reliably breaks call detection when spying `console.log` and `console.error` in the same test under this repo's Bun+Vitest combination), and e2e tests proving the real subprocess stdout/stderr streams against `branches --bogus-flag` and `sso add` (missing/invalid `--type`, the issue's own named repro target). ### Judgement calls left open - **Wording, not structure**: some of this repo's existing error wording still differs from Go's exact text for these paths (e.g. Go's bare `required flag(s) "type" not set` vs this repo's `Error: required flag(s) "type" not set` with an extra `Error: ` prefix; Go's `invalid argument "bogus" for "-t, --type" flag: must be one of [ saml ]` vs this repo's `Invalid value for flag --type: "bogus". Expected "saml", got "bogus"`). Both pre-date this fix and are unrelated to the stdout-pollution/duplicate-print bug it targets — flagged by review as a possible follow-up, not fixed here to keep this PR's diff focused. - **Usage-block content, not stream**: the help/usage text now correctly lands on stderr (never stdout) for the `flush-help-doc-to-stderr` class, but its *content* is this library's own (longer) help doc, not cobra's shorter usage template. True byte-parity there would need a second, purpose-built "usage-only" formatter — out of scope for this fix. - **Pre-existing terminal-escape echo**: user-supplied argv tokens (e.g. an unrecognized flag name) are echoed verbatim into error messages with no control-character stripping. Unaffected in kind by this change (just relocated/de-duplicated); flagged by security review as low-priority and better suited to its own issue if the team wants to harden it. - The issue's secondary, explicitly-optional finding (`internal/command.ts:243` misreporting `Flag.optional` status in `--output-format json` help docs) is a separate, unrelated code path in the same vendored module — deliberately left unaddressed here to keep this PR scoped to the double-print/stdout-dump bug. --- apps/cli-e2e/src/tests/migrations.e2e.test.ts | 11 +- .../src/legacy/cli/agent-output.e2e.test.ts | 32 +- .../src/legacy/commands/sso/sso.e2e.test.ts | 49 +++ apps/cli/src/shared/cli/run.e2e.test.ts | 38 ++ .../src/shared/cli/run.integration.test.ts | 247 ++++++++++++- apps/cli/src/shared/cli/run.ts | 303 +++++++++++++++- apps/cli/src/shared/cli/run.unit.test.ts | 343 +++++++++++++++++- .../shared/cli/subcommand-flag-suggestions.ts | 65 ++++ apps/cli/src/shared/output/normalize-error.ts | 69 +++- .../output/normalize-error.unit.test.ts | 180 ++++++++- packages/config/src/io.ts | 15 +- 11 files changed, 1313 insertions(+), 39 deletions(-) diff --git a/apps/cli-e2e/src/tests/migrations.e2e.test.ts b/apps/cli-e2e/src/tests/migrations.e2e.test.ts index 0e3dbc932f..45b02ef3bc 100644 --- a/apps/cli-e2e/src/tests/migrations.e2e.test.ts +++ b/apps/cli-e2e/src/tests/migrations.e2e.test.ts @@ -45,7 +45,10 @@ describe("migrations", () => { testBehaviour("exits non-zero without name argument", async ({ run }) => { const result = await run(["migration", "new"]); expect(result.exitCode).not.toBe(0); - expect(result.stdout).toContain("migration name"); + // CLI-1901: a missing positional argument's usage block now prints to + // stderr (never stdout) instead of being duplicated across both. + expect(result.stdout).toBe(""); + expect(result.stderr).toContain("migration name"); }); }); @@ -90,7 +93,11 @@ describe("migrations", () => { testBehaviour("exits non-zero when --status flag is missing", async ({ run }) => { const result = await run(["migration", "repair", "--local", "20230101000000"]); expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("--status"); + // CLI-1901: a missing required flag now drops the vendored library's + // duplicate usage dump entirely (Go's cobra suppresses usage for this + // case too), leaving only this repo's existing Go-parity error line, + // which spells the flag name without its `--` prefix. + expect(result.stderr).toContain('"status" not set'); }); testBehaviour("exits non-zero on connection refused", async ({ run }) => { diff --git a/apps/cli/src/legacy/cli/agent-output.e2e.test.ts b/apps/cli/src/legacy/cli/agent-output.e2e.test.ts index d6625c88f7..15c124caa2 100644 --- a/apps/cli/src/legacy/cli/agent-output.e2e.test.ts +++ b/apps/cli/src/legacy/cli/agent-output.e2e.test.ts @@ -38,19 +38,18 @@ describe("legacy CLI agent output", () => { }); expect(exitCode).toBe(1); + // CLI-1901: the vendored effect CLI library's own duplicate JSON render + // (the old `{_tag:"Help"}` + `{_tag:"Error", error:{code:"ShowHelp"}}` + // pair on stdout, `{_tag:"Errors"}` on stderr) is gone. stdout carries + // exactly this repo's single Go-parity error line; the library's help + // doc is redirected to stderr instead of being dropped or duplicated. expect(parseJsonLines(stdout)).toEqual([ - expect.objectContaining({ _tag: "Help" }), expect.objectContaining({ _tag: "Error", - error: expect.objectContaining({ code: "ShowHelp" }), - }), - ]); - expect(parseJsonLines(stderr)).toEqual([ - expect.objectContaining({ - _tag: "Errors", - errors: [expect.objectContaining({ code: "UnknownSubcommand" })], + error: expect.objectContaining({ code: "UnknownSubcommand" }), }), ]); + expect(parseJsonLines(stderr)).toEqual([expect.objectContaining({ _tag: "Help" })]); }); test("keeps parse errors in text mode when --output-format=text is explicit", async () => { @@ -63,7 +62,9 @@ describe("legacy CLI agent output", () => { ); expect(exitCode).toBe(1); - expect(stdout).toContain("DESCRIPTION"); + // CLI-1901: the help doc no longer prints to stdout at all. + expect(stdout).toBe(""); + expect(stderr).toContain("DESCRIPTION"); expect(stderr).toContain('Unknown subcommand "definitely-not-a-command"'); }); @@ -77,7 +78,8 @@ describe("legacy CLI agent output", () => { ); expect(exitCode).toBe(1); - expect(stdout).toContain("DESCRIPTION"); + expect(stdout).toBe(""); + expect(stderr).toContain("DESCRIPTION"); expect(stderr).toContain('Unknown subcommand "definitely-not-a-command"'); }); @@ -92,18 +94,12 @@ describe("legacy CLI agent output", () => { expect(exitCode).toBe(1); expect(parseJsonLines(stdout)).toEqual([ - expect.objectContaining({ _tag: "Help" }), expect.objectContaining({ _tag: "Error", - error: expect.objectContaining({ code: "ShowHelp" }), - }), - ]); - expect(parseJsonLines(stderr)).toEqual([ - expect.objectContaining({ - _tag: "Errors", - errors: [expect.objectContaining({ code: "UnknownSubcommand" })], + error: expect.objectContaining({ code: "UnknownSubcommand" }), }), ]); + expect(parseJsonLines(stderr)).toEqual([expect.objectContaining({ _tag: "Help" })]); }); test("keeps built-in version and help in text mode for detected coding agents", async () => { diff --git a/apps/cli/src/legacy/commands/sso/sso.e2e.test.ts b/apps/cli/src/legacy/commands/sso/sso.e2e.test.ts index 795b71200a..b2b1636c51 100644 --- a/apps/cli/src/legacy/commands/sso/sso.e2e.test.ts +++ b/apps/cli/src/legacy/commands/sso/sso.e2e.test.ts @@ -69,4 +69,53 @@ describe("supabase sso (legacy)", () => { expect(`${stdout}${stderr}`).toContain(`identity provider ID "not-a-uuid" is not a UUID`); }, ); + + // CLI-1901: `add`'s `--type` has no `Flag.optional` (see `add.command.ts`) + // — Go marks it required via `MarkFlagRequired("type")` (`cmd/sso.go:65`) + // — so a missing/invalid `--type` used to dump the full help doc to + // stdout AND print the error twice on stderr. No auth/network call ever + // happens for either case: flag parsing fails before the handler runs. + // + // A missing required flag and an invalid choice value get different + // treatment, matching the real `apps/cli-go/supabase-go` binary (verified + // directly against it): Go's `PersistentPreRunE` sets `SilenceUsage = true` + // (`cmd/root.go:97`) BEFORE `ValidateRequiredFlags` runs, so a missing + // `--type` is a single clean stderr line with no usage block — but + // `Flag.choice` validation happens during `ParseFlags`, BEFORE that point, + // so Go still shows a usage block for an invalid `--type` value, always on + // stderr, never stdout. + test( + "add without --type: stdout stays clean, stderr is a single Go-parity line (no usage block)", + { timeout: E2E_TIMEOUT_MS }, + async () => { + const { exitCode, stdout, stderr } = await runSupabase( + ["sso", "add", "--project-ref", TEST_PROJECT_REF], + { entrypoint: "legacy" }, + ); + expect(exitCode).toBe(1); + expect(stdout).toBe(""); + expect(stderr).toContain(`required flag(s) "type" not set`); + expect(stderr).not.toContain("USAGE"); + expect(stderr.trim().split("\n")).toHaveLength(2); + }, + ); + + test( + "add with an invalid --type value: stdout stays clean, the usage content and the single error line land on stderr with no duplicate", + { timeout: E2E_TIMEOUT_MS }, + async () => { + const { exitCode, stdout, stderr } = await runSupabase( + ["sso", "add", "--type", "bogus", "--project-ref", TEST_PROJECT_REF], + { entrypoint: "legacy" }, + ); + expect(exitCode).toBe(1); + expect(stdout).toBe(""); + expect(stderr).toContain("USAGE"); + const occurrences = stderr.split(`Invalid value for flag --type: "bogus"`).length - 1; + expect(occurrences).toBe(1); + expect( + stderr.trim().endsWith("Try rerunning the command with --debug to troubleshoot the error."), + ).toBe(true); + }, + ); }); diff --git a/apps/cli/src/shared/cli/run.e2e.test.ts b/apps/cli/src/shared/cli/run.e2e.test.ts index 1f399eb37e..3c90257fe2 100644 --- a/apps/cli/src/shared/cli/run.e2e.test.ts +++ b/apps/cli/src/shared/cli/run.e2e.test.ts @@ -22,3 +22,41 @@ describe("legacy CLI process exit codes (CLI-1906)", () => { expect(exitCode).toBe(1); }); }); + +/** + * CLI-1901: a required-flag/choice parse failure used to dump the full help + * doc to **stdout** and print the error **twice** on stderr. The buffering + * mechanism that fixes this (`withoutParseErrorHelpDump` in `run.ts`) is + * already covered against a real command definition, in-process, by + * `run.integration.test.ts` — this is the one minimal case that observes the + * real subprocess boundary: whether the actual `stdout`/`stderr` streams of + * the shipped binary stay separated, and whether the error text is + * de-duplicated, matching the real `apps/cli-go/supabase-go` binary (Go + * still shows a usage block for an unrecognized flag — raised during + * `ParseFlags`, before `PersistentPreRunE` sets `SilenceUsage` + * (`apps/cli-go/cmd/root.go:97`) — always on stderr, never stdout; + * verified directly against the built Go binary). + */ +describe("legacy CLI required-flag/choice parse errors (CLI-1901)", () => { + test("an unrecognized flag: stdout stays clean, the help/usage content and the single error line land on stderr with no duplicate", async () => { + const { exitCode, stdout, stderr } = await runSupabase( + ["branches", "--this-flag-does-not-exist"], + { entrypoint: "legacy" }, + ); + expect(exitCode).toBe(1); + expect(stdout).toBe(""); + // Matches Go's still-shown usage block for this error class (see the + // describe-level comment) — this library's help doc isn't byte-identical + // to cobra's shorter usage template, but it's on the right stream now. + expect(stderr).toContain("USAGE"); + // The error text appears exactly once — before the fix, the library's own + // duplicate render put it on stderr a second time (on top of the stdout + // help dump this test doesn't even need to check for, since stdout is + // asserted empty above). + const occurrences = stderr.split("Unrecognized flag: --this-flag-does-not-exist").length - 1; + expect(occurrences).toBe(1); + expect( + stderr.trim().endsWith("Try rerunning the command with --debug to troubleshoot the error."), + ).toBe(true); + }); +}); diff --git a/apps/cli/src/shared/cli/run.integration.test.ts b/apps/cli/src/shared/cli/run.integration.test.ts index 9ff4f7fd4e..1b91a5567d 100644 --- a/apps/cli/src/shared/cli/run.integration.test.ts +++ b/apps/cli/src/shared/cli/run.integration.test.ts @@ -1,11 +1,54 @@ import { describe, expect, test } from "@effect/vitest"; import { BunServices } from "@effect/platform-bun"; -import { Effect, Exit, Layer } from "effect"; -import { CliOutput, Command } from "effect/unstable/cli"; +import { Console, Effect, Exit, Layer } from "effect"; +import { CliOutput, Command, Flag } from "effect/unstable/cli"; import { legacyBranchesCommand } from "../../legacy/commands/branches/branches.command.ts"; import { textCliOutputFormatter } from "../output/text-formatter.ts"; import { CliArgs } from "./cli-args.service.ts"; -import { exitCodeForFailure } from "./run.ts"; +import { exitCodeForFailure, withoutParseErrorHelpDump } from "./run.ts"; + +/** + * A `Console.Console` test double that records `log`/`error` calls into + * `calls` instead of writing anywhere, for asserting on writes through the + * `Console.Console` Effect service directly. Deliberately NOT `vi.spyOn`ing + * the global `console` object: under this repo's Bun + Vitest combination, + * spying on `console.log` and `console.error` in the SAME test reliably + * breaks call detection on both (reproduced in isolation, unrelated to this + * fix) — observing the `Console.Console` service `withoutParseErrorHelpDump` + * itself reads and overrides is both more precise and immune to that quirk. + */ +function fakeConsole(): { readonly console: Console.Console; readonly calls: Array } { + const calls: Array = []; + const unused = () => {}; + return { + calls, + console: { + assert: unused, + clear: unused, + count: unused, + countReset: unused, + debug: unused, + dir: unused, + dirxml: unused, + error: (...args: ReadonlyArray) => { + calls.push(`error:${args.join(" ")}`); + }, + group: unused, + groupCollapsed: unused, + groupEnd: unused, + info: unused, + log: (...args: ReadonlyArray) => { + calls.push(`log:${args.join(" ")}`); + }, + table: unused, + time: unused, + timeEnd: unused, + timeLog: unused, + trace: unused, + warn: unused, + }, + }; +} /** * CLI-1906: `supabase branches` (a legacy "group" command — subcommands, no @@ -64,3 +107,201 @@ describe("legacy group command exit codes (CLI-1906)", () => { expect(exitCodeForFailure(exit.cause)).toBe(1); }); }); + +/** + * CLI-1901: a required-flag/choice parse failure used to dump the full help + * doc to stdout (`console.log`) AND print the error twice — once from the + * vendored `effect` CLI library's own `showHelp()` (`console.error`), once + * from this repo's own Go-parity renderer (`handledProgram` + + * `normalizeCause` in `run.ts`, exercised downstream of this test, not + * here). These tests run real commands through `Command.runWith`, wrapped in + * `withoutParseErrorHelpDump`, and assert on the calls recorded by a fake + * `Console.Console` (see `fakeConsole` above) provided in place of the real + * one — that's the exact service `withoutParseErrorHelpDump` overrides and + * later replays through, so it's what actually matters here. + * + * Go only suppresses its own usage block for a MISSING REQUIRED FLAG + * (`ValidateRequiredFlags`, post-`PersistentPreRunE`) — verified against the + * real `apps/cli-go/supabase-go` binary. Every other parse-error tag + * (`UnrecognizedOption`, `InvalidValue`, `MissingArgument`, + * `UnknownSubcommand`) is raised during `ParseFlags`/`ValidateArgs`, BEFORE + * `PersistentPreRunE` sets `SilenceUsage` — Go still shows a usage block for + * those, always on stderr, never stdout. `classifyParseErrorConsoleOutput` + * (see `run.ts`) mirrors that split; these tests cover both branches. + * + * `legacyBranchesCommand` (no `Command.provide` of its own — see the + * sibling CLI-1906 suite above for why that matters) covers the + * `UnrecognizedOption` shape and proves the buffering/conditional-flush + * wiring end to end. `MissingOption`/`InvalidValue` specifically need a + * genuinely required flag / `Flag.choice`, which every shipped native + * command with one (e.g. `sso add`'s `--type`, see `add.command.ts`) also + * wraps in its own `Command.provide`d management-API runtime layer — + * exercising those tags against a real shipped command would mean + * providing or mocking that whole layer graph for services this parse-error + * path never consumes (same friction the CLI-1906 suite's own comment + * describes and avoids). A minimal `Command.make` with a required + * `Flag.string`/`Flag.choice` still runs through the exact same vendored + * `Command.runWith`/`showHelp()` machinery that produces the bug — the + * command is synthetic, but the `ShowHelp` cause shape it produces is not; a + * real subprocess run against `sso add` itself (the issue's own named + * repro target) is covered end to end by `sso.e2e.test.ts`. + */ +describe("withoutParseErrorHelpDump (CLI-1901)", () => { + const layerFor = (args: ReadonlyArray, console: Console.Console) => + Layer.mergeAll( + CliOutput.layer(textCliOutputFormatter()), + Layer.succeed(CliArgs, { args }), + Layer.succeed(Console.Console, console), + BunServices.layer, + ); + + const runBranches = (args: ReadonlyArray, console: Console.Console) => + withoutParseErrorHelpDump( + Command.runWith(legacyBranchesCommand, { version: "0.0.0-test" })(args), + { rootCommand: legacyBranchesCommand, args }, + ).pipe(Effect.provide(layerFor(args, console))); + + // `type`'s `-t` alias mirrors the real `sso add --type`/`-t` flag + // (`add.command.ts`) so the short-alias "present but missing its value" + // case below (Codex review finding, CLI-1901 follow-up) exercises the same + // shape end to end, without pulling in `sso add`'s own management-API + // runtime layer graph (see the suite-level comment above for why that's + // avoided here). + const requiredFlagCommand = Command.make("test-required-flag", { + type: Flag.choice("type", ["saml"] as const).pipe(Flag.withAlias("t")), + }); + + const runRequiredFlagCommand = (args: ReadonlyArray, console: Console.Console) => + withoutParseErrorHelpDump( + Command.runWith(requiredFlagCommand, { version: "0.0.0-test" })(args), + { rootCommand: requiredFlagCommand, args }, + ).pipe(Effect.provide(layerFor(args, console))); + + test("an unrecognized flag: replays the help dump to stderr (never stdout) and drops the duplicate error, but still fails with the original cause", async () => { + const { console, calls } = fakeConsole(); + const exit = await Effect.runPromiseExit(runBranches(["--this-flag-does-not-exist"], console)); + + // The library's own duplicate `Console.error` write is gone. Its help + // doc survives, but redirected to stderr (`error:`), never stdout + // (`log:`) — matching Go, which still shows usage for an unrecognized + // flag (raised during `ParseFlags`, before `SilenceUsage` is set), just + // on stderr. + expect(calls.length).toBeGreaterThan(0); + expect(calls.every((call) => call.startsWith("error:"))).toBe(true); + + // The original ShowHelp/UnrecognizedOption failure still propagates — + // the fix only suppresses the library's own console writes, it must not + // swallow or reshape the failure this repo's own `handledProgram` + + // `normalizeCause` still needs to render the single Go-parity line. + expect(Exit.isFailure(exit)).toBe(true); + if (!Exit.isFailure(exit)) return; + expect(exitCodeForFailure(exit.cause)).toBe(1); + }); + + test("`branches` bare (clean ShowHelp) still flushes its help dump to stdout and exits 0 (untouched)", async () => { + const { console, calls } = fakeConsole(); + const exit = await Effect.runPromiseExit(runBranches([], console)); + + expect(calls.length).toBeGreaterThan(0); + expect(calls.every((call) => call.startsWith("log:"))).toBe(true); + expect(Exit.isFailure(exit)).toBe(true); + if (!Exit.isFailure(exit)) return; + expect(exitCodeForFailure(exit.cause)).toBe(0); + }); + + test("missing a required flag: drops the help dump entirely and the duplicate error, but still fails with the original cause", async () => { + const { console, calls } = fakeConsole(); + const exit = await Effect.runPromiseExit(runRequiredFlagCommand([], console)); + + // Go's `SilenceUsage` is already active for a missing required flag + // (post-`PersistentPreRunE`) — nothing survives, not even on stderr. + expect(calls).toEqual([]); + expect(Exit.isFailure(exit)).toBe(true); + if (!Exit.isFailure(exit)) return; + expect(exitCodeForFailure(exit.cause)).toBe(1); + }); + + // CLI-1901 (Codex review finding): the vendored library can't tell "flag + // never given" apart from "flag given with no value following it" — both + // raise `MissingOption`. Go's pflag DOES distinguish these (a present-but- + // valueless flag is a `ParseFlags`-time error, before `SilenceUsage` is + // set) — verified against the real `apps/cli-go/supabase-go` binary, which + // still prints its usage block for this input. `--type` as the LAST token + // (no value token follows it) reproduces that "present but valueless" case. + test("a required flag present on argv but missing its value: replays the help dump to stderr instead of dropping it", async () => { + const { console, calls } = fakeConsole(); + const exit = await Effect.runPromiseExit(runRequiredFlagCommand(["--type"], console)); + + expect(calls.length).toBeGreaterThan(0); + expect(calls.every((call) => call.startsWith("error:"))).toBe(true); + expect(Exit.isFailure(exit)).toBe(true); + if (!Exit.isFailure(exit)) return; + expect(exitCodeForFailure(exit.cause)).toBe(1); + }); + + // Codex review finding (CLI-1901 follow-up): the same "present but missing + // its value" case, but supplied via the flag's SHORT ALIAS (`-t`) instead of + // its canonical long form. Go's pflag treats a present-but-valueless + // shorthand exactly like the long form (`parseSingleShortArg` raises the + // same `ValueRequiredError` as `parseLongArg`, same pre-`SilenceUsage` + // timing) — verified against the real `apps/cli-go/supabase-go` binary + // (`sso add -t`: full usage block on stderr, byte-parallel to `sso add + // --type`). Before this fix, `isMissingFlagTokenPresent` only recognized + // the canonical `--type` token and misclassified `-t` as absent, silently + // dropping the help dump instead. + test("a required flag present on argv by its short alias but missing its value: replays the help dump to stderr instead of dropping it", async () => { + const { console, calls } = fakeConsole(); + const exit = await Effect.runPromiseExit(runRequiredFlagCommand(["-t"], console)); + + expect(calls.length).toBeGreaterThan(0); + expect(calls.every((call) => call.startsWith("error:"))).toBe(true); + expect(Exit.isFailure(exit)).toBe(true); + if (!Exit.isFailure(exit)) return; + expect(exitCodeForFailure(exit.cause)).toBe(1); + }); + + test("an invalid Flag.choice value: replays the help dump to stderr (never stdout) and drops the duplicate error, but still fails with the original cause", async () => { + const { console, calls } = fakeConsole(); + const exit = await Effect.runPromiseExit(runRequiredFlagCommand(["--type", "bogus"], console)); + + expect(calls.length).toBeGreaterThan(0); + expect(calls.every((call) => call.startsWith("error:"))).toBe(true); + expect(Exit.isFailure(exit)).toBe(true); + if (!Exit.isFailure(exit)) return; + expect(exitCodeForFailure(exit.cause)).toBe(1); + }); + + test("`--help` on a command with a required flag still prints the full help doc to stdout and exits 0 (untouched)", async () => { + const { console, calls } = fakeConsole(); + const exit = await Effect.runPromiseExit(runRequiredFlagCommand(["--help"], console)); + + expect(calls.length).toBeGreaterThan(0); + expect(calls.every((call) => call.startsWith("log:"))).toBe(true); + expect(Exit.isSuccess(exit)).toBe(true); + }); + + // Effect's default logger (`Effect.log*`) resolves through this same + // `Console.Console` reference (`Logger.withConsoleLog`/`withConsoleError` + // in the vendored library) — see the "buffering scope" note on + // `withoutParseErrorHelpDump` in `run.ts`. No command handler in this + // codebase uses `Effect.log*` today, but this pins the invariant the doc + // comment describes: on a successful run, buffered logger output still + // reaches the user (deferred to end-of-run, not dropped). + test("Effect.log* output during a successful run is still flushed, not lost", async () => { + const { console, calls } = fakeConsole(); + const program = Effect.gen(function* () { + yield* Effect.logInfo("hello from a handler"); + return "done" as const; + }); + + const result = await Effect.runPromise( + withoutParseErrorHelpDump(program, { rootCommand: requiredFlagCommand, args: [] }).pipe( + Effect.provide(Layer.succeed(Console.Console, console)), + ), + ); + + expect(result).toBe("done"); + expect(calls.length).toBeGreaterThan(0); + expect(calls.some((call) => call.includes("hello from a handler"))).toBe(true); + }); +}); diff --git a/apps/cli/src/shared/cli/run.ts b/apps/cli/src/shared/cli/run.ts index b0db350a35..2fd8f1ea1d 100644 --- a/apps/cli/src/shared/cli/run.ts +++ b/apps/cli/src/shared/cli/run.ts @@ -1,8 +1,8 @@ import { BunServices } from "@effect/platform-bun"; import { ProjectConfigStore } from "@supabase/config"; import { unixHttpClientLayer } from "@supabase/stack"; -import { Cause, Effect, Exit, Fiber, Layer, Runtime, Stdio } from "effect"; -import { CliOutput, Command } from "effect/unstable/cli"; +import { Cause, Console, Effect, Exit, Fiber, Layer, Runtime, Stdio } from "effect"; +import { CliError, CliOutput, Command } from "effect/unstable/cli"; import { CLI_VERSION } from "./version.ts"; import { Credentials } from "../../next/auth/credentials.service.ts"; import { jsonCliOutputFormatter } from "../output/json-formatter.ts"; @@ -29,6 +29,8 @@ import { telemetryRuntimeLayer } from "../telemetry/runtime.layer.ts"; import { tracingLayer } from "../telemetry/tracing.layer.ts"; import { CliArgs } from "./cli-args.service.ts"; import { resolveAgentOutputFormatFromArgs } from "./agent-output.ts"; +import type { CliErrorSuggestionContext } from "./subcommand-flag-suggestions.ts"; +import { flagAliasesFor, isValueTakingFlagTokenFor } from "./subcommand-flag-suggestions.ts"; // Global flags that consume the following argv token as their value. Keep this in // sync with the value-taking global flags defined in `shared/cli/global-flags.ts` @@ -137,6 +139,290 @@ export function shouldReportFailure(cause: Cause.Cause, exitCode: numbe return !(Cause.squash(cause) instanceof LegacyGoChildExitError); } +/** + * A single `Console.log`/`Console.error` call captured while + * `withoutParseErrorHelpDump` runs, so it can be replayed once the run's + * outcome is known instead of being written immediately. + */ +interface BufferedConsoleWrite { + readonly method: "log" | "error"; + readonly args: ReadonlyArray; +} + +/** + * A `Console.Console` that captures `log`/`error` calls into `sink` instead + * of writing them, and forwards every other method straight through to the + * real console. The vendored `effect` CLI library's parser only ever calls + * `log`/`error` (`showHelp()` and the `Help`/`Version`/`Completions` + * `GlobalFlag.Action`s in `Command.ts`) — the rest are implemented so this + * stays a faithful `Console.Console` rather than a partial stand-in. + */ +function bufferingConsole(sink: Array): Console.Console { + const real = globalThis.console; + return { + assert: real.assert.bind(real), + clear: real.clear.bind(real), + count: real.count.bind(real), + countReset: real.countReset.bind(real), + debug: real.debug.bind(real), + dir: real.dir.bind(real), + dirxml: real.dirxml.bind(real), + error: (...args: ReadonlyArray) => { + sink.push({ method: "error", args }); + }, + group: real.group.bind(real), + groupCollapsed: real.groupCollapsed.bind(real), + groupEnd: real.groupEnd.bind(real), + info: real.info.bind(real), + log: (...args: ReadonlyArray) => { + sink.push({ method: "log", args }); + }, + table: real.table.bind(real), + time: real.time.bind(real), + timeEnd: real.timeEnd.bind(real), + timeLog: real.timeLog.bind(real), + trace: real.trace.bind(real), + warn: real.warn.bind(real), + }; +} + +/** + * How `withoutParseErrorHelpDump` should dispose of its buffered + * `Console.log`/`Console.error` writes, given how the wrapped effect failed: + * + * - `"flush-unchanged"` — success, or a "clean" `ShowHelp` (`errors: []` — + * an explicit `--help` or a bare group command with no subcommand, both + * of which map to exit `0` per `exitCodeForFailure` above), or any other + * failure. Those buffered writes (if any, there normally are none outside + * the two `ShowHelp` cases) are the actual intended output. + * - `"drop"` — a genuine parse/validation failure Go cobra's + * `PersistentPreRunE` already suppresses usage for: `ValidateRequiredFlags` + * (cobra `command.go:1007`), which sets `cmd.SilenceUsage = true` + * (`apps/cli-go/cmd/root.go:97`) BEFORE it runs. This library's + * `MissingOption` is the one tag that maps to that stage — see CLI-1901 — + * but ONLY when the flag was never given at all. A required flag that IS + * present on argv but missing its value (e.g. `sso add --type` with + * nothing after it) also raises `MissingOption` in this library (it has no + * distinct "value required" tag), yet Go's own pflag raises a DIFFERENT, + * earlier `ParseFlags`-time error for that input (`flag needs an + * argument: --type`) which does NOT get `SilenceUsage` treatment — verified + * against the real binary (`apps/cli-go/supabase-go sso add --type`: full + * usage block on stderr, vs `sso add --project-ref x` with `--type` never + * mentioned at all: bare `required flag(s) "type" not set`, no usage). See + * `isMissingFlagTokenPresent` below for how this case is distinguished from + * a genuinely-absent flag. + * - `"flush-help-doc-to-stderr"` — every other genuine parse/validation + * failure (`UnrecognizedOption`, `InvalidValue`, `MissingArgument`, + * `UnknownSubcommand`; multiple simultaneous errors also lands here). + * These map to cobra's `ParseFlags`/`ValidateArgs` (`command.go:919,968`), + * which run BEFORE `PersistentPreRunE` — Go still shows a usage block for + * these, just on stderr, never stdout (verified against the real + * `apps/cli-go/supabase-go` binary, e.g. `branches --bogus-flag` and + * `sso add --type bogus`). The help doc this library renders isn't + * byte-identical to cobra's shorter usage template (that would need a + * second formatter, out of scope for CLI-1901), but showing SOME usage + * content on the RIGHT stream is closer to Go than showing none at all. + * + * In every "genuine failure" case, the buffered `Console.error` write (the + * library's own duplicate render of the errors) is always dropped — this + * repo's own `handledProgram` + `normalizeCause` already render the single + * Go-parity line for it (see `withoutParseErrorHelpDump` below). + */ +export type ParseErrorConsoleDisposition = "flush-unchanged" | "drop" | "flush-help-doc-to-stderr"; + +/** + * Whether `option`'s canonical long-form flag token (`--option` or + * `--option=...`), or one of its short/long `aliases` (e.g. `-t`), appears + * anywhere in the raw argv this run was invoked with BEFORE the `--` + * operand terminator (if any) — used to tell a genuinely-absent required + * flag (Go: `SilenceUsage`-suppressed) apart from one that's present but + * missing its value (Go: a `ParseFlags`-time error, usage still shown). See + * the `"drop"` case on `ParseErrorConsoleDisposition` above for the full + * rationale. `aliases` come from `flagAliasesFor` (see + * `classifyParseErrorConsoleOutput` below), already formatted with their + * leading dash(es). + * + * Tokens after a literal `--` are always positional operands, never a flag + * occurrence for ANY option — this mirrors the vendored `effect` CLI + * library's own lexer, which treats `--` the same way (`internal/lexer.ts`, + * `argv.indexOf("--")`). Without this cutoff, a command like + * `migration repair -- 20230101000000 --status` (a required `Flag.choice`, + * `legacy/commands/migration/repair/repair.command.ts`) would have its + * trailing `--status` positional argument misread as evidence the `--status` + * flag was given, flipping a genuinely-absent-flag failure (Go: no usage + * shown) into a "present but missing its value" one (Go: usage shown) — see + * CLI-1901. + * + * That `--` cutoff is only genuine when the scan reaches `--` as a LIVE + * token, not when it was itself consumed as the VALUE of an immediately + * preceding value-taking flag. Go/pflag's `parseArgs` (`flag.go`) only + * recognizes `--` as the terminator when it's at the FRONT of the remaining + * args on a fresh iteration; `parseLongArg`'s value branch pops the very + * next raw token with no shape check at all, so a literal `--` right after a + * value-taking flag gets swallowed as that flag's value and never reaches + * the terminator check — e.g. `sso add --project-ref -- --type` hands the + * literal string `--` to `--project-ref`, and parsing resumes normally on + * `--type` (which then fails with pflag's OWN `ValueRequiredError` — a + * `ParseFlags`-time error, usage still shown — since nothing follows it). + * The scan below therefore folds the terminator check into the very same + * loop that already skips consumed-value tokens, rather than precomputing + * `args.indexOf("--")` up front — a Codex review finding on CLI-1901. + * + * `isValueTakingToken` (from `isValueTakingFlagTokenFor`, OR'd with + * `globalFlagsWithValues` at the `classifyParseErrorConsoleOutput` call site + * below) lets the scan skip a token immediately consumed as the VALUE of a + * preceding value-taking flag — local OR global — instead of mistaking that + * consumed token (or a consumed literal `--`, per above) for `option`'s own + * occurrence. Go/pflag's `parseLongArg` (`flag.go`) unconditionally consumes + * the very next argv entry as a value-taking flag's value, even when that + * entry itself looks like another flag — e.g. `sso add --project-ref --type` + * hands the literal string `--type` to `--project-ref`, so `--type` is never + * seen as its own occurrence in Go, and its `MissingOption` failure keeps + * Go's `SilenceUsage` treatment (no usage shown). The vendored `effect` + * parser does NOT replicate that eager consumption (it only treats a + * following token as a value when the lexer tags it `Value`, never a + * flag-shaped token — `internal/parser.ts`'s `consumeFlagValueWithTokens`), + * so without this skip the raw scan would find the literal `--type` token + * and wrongly flush the help doc for an input Go shows no usage for — a + * Codex review finding on CLI-1901. + */ +function isMissingFlagTokenPresent( + option: string, + args: ReadonlyArray, + aliases: ReadonlyArray = [], + isValueTakingToken: (token: string) => boolean = () => false, +): boolean { + const tokens = [`--${option}`, ...aliases]; + for (let index = 0; index < args.length; index++) { + const arg = args[index]; + if (arg === undefined) continue; + // A literal "--" only terminates flag parsing when reached as a LIVE + // token here — if the previous iteration already skipped it as a + // value-taking flag's consumed value (below), this line never runs for + // it, matching pflag's own ordering (see the doc comment above). + if (arg === "--") break; + if (tokens.some((token) => arg === token || arg.startsWith(`${token}=`))) return true; + const equalIndex = arg.indexOf("="); + const bareToken = equalIndex === -1 ? arg : arg.slice(0, equalIndex); + if (equalIndex === -1 && isValueTakingToken(bareToken)) { + // Go/pflag consumes the following argv entry as `bareToken`'s value + // unconditionally — even a literal "--" — so skip it here too, before + // the next iteration's terminator check ever sees it. + index++; + } + } + return false; +} + +export function classifyParseErrorConsoleOutput( + cause: Cause.Cause, + context: CliErrorSuggestionContext, +): ParseErrorConsoleDisposition { + const error = Cause.squash(cause); + if (!CliError.isCliError(error) || error._tag !== "ShowHelp" || error.errors.length === 0) { + return "flush-unchanged"; + } + // `isValueTakingFlagTokenFor` only inspects the resolved LEAF command's own + // flags, so it has no visibility into value-taking GLOBAL flags (`--network-id`, + // `--profile`, etc. — see `globalFlagsWithValues` above). Without OR-ing those + // in, a global value flag consuming the very next token (e.g. + // `migration repair --network-id --status --local ` handing the + // literal `--status` to `--network-id`, per pflag's `parseLongArg`) would + // leave the required `status` flag looking "present" to the scan below, even + // though Go/pflag never sees it as its own occurrence and suppresses usage + // for it (`SilenceUsage`) — a Codex review finding on CLI-1901. + const isLeafValueTakingToken = isValueTakingFlagTokenFor(context.rootCommand, error.commandPath); + const isValueTakingToken = (token: string) => + globalFlagsWithValues.has(token) || isLeafValueTakingToken(token); + const isSuppressedMissingFlag = (inner: (typeof error.errors)[number]) => + inner._tag === "MissingOption" && + !isMissingFlagTokenPresent( + inner.option, + context.args, + flagAliasesFor(context.rootCommand, error.commandPath, inner.option), + isValueTakingToken, + ); + return error.errors.every(isSuppressedMissingFlag) ? "drop" : "flush-help-doc-to-stderr"; +} + +/** + * Wraps `Command.runWith(rootCommand, ...)(args)` so the vendored `effect` + * CLI library's OWN `Console.log`/`Console.error` writes are captured + * instead of reaching the real console, then disposed of per + * `classifyParseErrorConsoleOutput` once the run's outcome is known: + * dropped entirely for a missing-required-flag failure, replayed to stderr + * (never stdout) for every other genuine parse/validation failure, and + * replayed unchanged for everything else. Either way, the library's own + * duplicate error render never survives — this repo's own `handledProgram` + * + `normalizeCause` already render the single Go-parity line for it. That + * fixes both halves of CLI-1901 (the stdout help dump and the duplicate + * error line) from this one call site, without patching the vendored + * library itself. + * + * TODO: remove this whole buffering/classification dance once upstream + * Effect-TS/effect#6313 is fixed — https://github.com/Effect-TS/effect/issues/6313. + * `runWith` has no supported way to opt out of, or redirect, its own + * `showHelp` console writes; everything below exists only to work around + * that gap from the outside. + * + * The "flush unchanged" outcome covers success, `--help`, `--version`, + * `--completions`, and the bare-group-command help dump, all of which stay + * untouched. + * + * Safe to wrap the entire `runWith` call — parsing AND the eventual command + * handler, not just the parse phase that can actually raise `ShowHelp`: no + * command handler in this codebase writes through `effect`'s `Console` + * service directly (they go through the `Output` service instead). One + * indirect exception is known — `@supabase/config`'s `loadProjectConfigFile` + * emits its deprecated-config-section warnings via `Console.error`, and is + * reachable from handlers through `ProjectConfigStore`/`loadProjectConfig` — + * so it pins itself to the real console (`Effect.provideService(Console.Console, + * globalThis.console)`) rather than relying on whatever `Console.Console` is + * ambient here; see CLI-1901 and that package's `io.ts` for why (a + * long-running command like `functions serve` would otherwise have the + * warning buffered for its entire session instead of shown at startup). + * Any other handler writing through `Console` directly would need the same + * treatment — buffering here never delays or drops real command output + * ONLY as long as that invariant holds. Note that Effect's OWN default + * logger (`Effect.log*`) DOES resolve through this same `Console` reference + * (`Logger.withConsoleLog`/`withConsoleError`) — this codebase has no + * `Effect.log*` call sites today, but if one is ever added to a handler, its + * output would be buffered too (deferred to end-of-run on the "flush + * unchanged" path, or dropped on a genuine parse failure — which never runs + * a handler in the first place, so that half is moot). + */ +export function withoutParseErrorHelpDump( + effect: Effect.Effect, + context: CliErrorSuggestionContext, +): Effect.Effect { + return Effect.gen(function* () { + const sink: Array = []; + const exit = yield* effect.pipe( + Effect.provideService(Console.Console, bufferingConsole(sink)), + Effect.exit, + ); + const disposition = Exit.isFailure(exit) + ? classifyParseErrorConsoleOutput(exit.cause, context) + : "flush-unchanged"; + if (disposition === "drop") { + return yield* exit; + } + for (const write of sink) { + // The library's own duplicate error render never survives a genuine + // parse failure — only its help-doc `log` write gets a second look, + // redirected to stderr instead of its original stdout-bound method. + if (disposition === "flush-help-doc-to-stderr" && write.method === "error") continue; + const method = disposition === "flush-help-doc-to-stderr" ? "error" : write.method; + yield* Console.consoleWith((console) => + Effect.sync(() => { + console[method](...write.args); + }), + ); + } + return yield* exit; + }); +} + function projectContextLayerFor(runtimeLayer: Layer.Layer) { return projectContextLayer.pipe(Layer.provide(runtimeLayer), Layer.provide(BunServices.layer)); } @@ -193,7 +479,10 @@ function cliProgramFor( }), ), ); - return Command.runWith(rootCommand, { version: CLI_VERSION })(args).pipe( + return withoutParseErrorHelpDump(Command.runWith(rootCommand, { version: CLI_VERSION })(args), { + rootCommand, + args, + }).pipe( Effect.provide(formatterLayerFor(rootCommand, args, outputFormat)), Effect.provide(options.analyticsLayer), Effect.provide(tracingLayer), @@ -218,6 +507,12 @@ export async function runCli(rootCommand: Command.Command.Any, options: RunCliOp }).pipe(Effect.provide(BunServices.layer)), ); + // Same `{ rootCommand, args }` shape `formatterLayerFor` builds below, so + // `normalizeCause`'s single-render fallback path (CLI-1901) can reuse + // `formatCliErrorsForDisplay` and surface the same subcommand-flag hint the + // text/json formatters would have shown before the vendored library's own + // duplicate render was suppressed. + const suggestionContext = { rootCommand, args }; const useGlobalSignalInterrupt = shouldUseGlobalSignalInterrupt(args); const outputFormat = await Effect.runPromise( Effect.gen(function* () { @@ -271,7 +566,7 @@ export async function runCli(rootCommand: Command.Command.Any, options: RunCliOp // below. See `exitCodeForFailure` for why a "clean" ShowHelp failure // (e.g. a bare group command with no subcommand) also maps to exit 0. if (shouldReportFailure(exit.cause, exitCode)) { - yield* output.fail(normalizeCause(exit.cause)); + yield* output.fail(normalizeCause(exit.cause, suggestionContext)); } return yield* processControl.exit(exitCode); } diff --git a/apps/cli/src/shared/cli/run.unit.test.ts b/apps/cli/src/shared/cli/run.unit.test.ts index 290c9e9557..687d556e7d 100644 --- a/apps/cli/src/shared/cli/run.unit.test.ts +++ b/apps/cli/src/shared/cli/run.unit.test.ts @@ -1,15 +1,26 @@ import { Cause } from "effect"; -import { CliError } from "effect/unstable/cli"; +import { CliError, Command } from "effect/unstable/cli"; import { describe, expect, it } from "vitest"; +import { legacyBranchesCommand } from "../../legacy/commands/branches/branches.command.ts"; +import { legacyMigrationCommand } from "../../legacy/commands/migration/migration.command.ts"; +import { legacySsoCommand } from "../../legacy/commands/sso/sso.command.ts"; import { LegacyGoChildExitError } from "../legacy/legacy-go-child-exit.error.ts"; import { + classifyParseErrorConsoleOutput, exitCodeForFailure, extractCommandPath, shouldReportFailure, shouldUseGlobalSignalInterrupt, } from "./run.ts"; +// Real command tree (not a hand-rolled stand-in) so `classifyParseErrorConsoleOutput`'s +// alias resolution (`flagAliasesFor`, via `context.rootCommand`) has real `Flag.withAlias` +// declarations to walk — e.g. `sso add`'s `type` flag aliases to `-t` (`add.command.ts`). +const testRoot = Command.make("supabase").pipe( + Command.withSubcommands([legacyBranchesCommand, legacyMigrationCommand, legacySsoCommand]), +); + describe("extractCommandPath", () => { it("returns positional command-path tokens", () => { expect(extractCommandPath(["functions", "serve"])).toEqual(["functions", "serve"]); @@ -145,3 +156,333 @@ describe("shouldReportFailure", () => { expect(shouldReportFailure(cause, 1)).toBe(true); }); }); + +// CLI-1901: a required-flag/choice parse failure used to dump the full help +// doc to stdout AND print the error twice (once from the vendored `effect` +// CLI library's own `showHelp()`, once from this repo's own Go-parity +// renderer). `withoutParseErrorHelpDump` fixes this by buffering the +// library's own `Console.log`/`Console.error` writes and disposing of them +// per this classifier's verdict — this suite covers the classifier; +// `run.integration.test.ts` covers the end-to-end buffering/flush behavior +// against real command definitions, and `run.e2e.test.ts` / +// `sso.e2e.test.ts` cover the real subprocess stdout/stderr streams. +describe("classifyParseErrorConsoleOutput", () => { + // Go cobra's `PersistentPreRunE` sets `SilenceUsage = true` + // (`apps/cli-go/cmd/root.go:97`) BEFORE `ValidateRequiredFlags` + // (`command.go:1007`) runs — verified against the real `supabase-go` + // binary (`sso add` without `--type`): a single clean stderr line, no + // usage block. `MissingOption` is the one tag that maps to that stage. + it("drops the help dump for a missing required flag", () => { + const cause = Cause.fail( + new CliError.ShowHelp({ + commandPath: ["supabase", "sso", "add"], + errors: [new CliError.MissingOption({ option: "type" })], + }), + ); + // `--type` (nor its `-t` alias) never appears on argv at all — genuinely absent. + expect( + classifyParseErrorConsoleOutput(cause, { + rootCommand: testRoot, + args: ["sso", "add", "--project-ref", "x"], + }), + ).toBe("drop"); + }); + + // Multiple simultaneously-missing required flags: still `ValidateRequiredFlags`, + // still post-`PersistentPreRunE` in Go — must still drop, not just for a lone error. + it("drops the help dump when every error is a missing required flag", () => { + const cause = Cause.fail( + new CliError.ShowHelp({ + commandPath: ["supabase", "sso", "add"], + errors: [ + new CliError.MissingOption({ option: "type" }), + new CliError.MissingOption({ option: "project-ref" }), + ], + }), + ); + expect( + classifyParseErrorConsoleOutput(cause, { rootCommand: testRoot, args: ["sso", "add"] }), + ).toBe("drop"); + }); + + // CLI-1901 (Codex review finding): the vendored library raises the SAME + // `MissingOption` tag whether a required flag was never given at all, or + // given with no value following it (e.g. `sso add --type` as the last + // token) — it has no distinct "value required" error. Go's own pflag does + // distinguish these: a present-but-valueless flag is a `ParseFlags`-time + // error (`flag needs an argument: --type`), raised BEFORE + // `PersistentPreRunE` sets `SilenceUsage` — verified against the real + // `apps/cli-go/supabase-go` binary, which still prints its full usage block + // to stderr for this input. `isMissingFlagTokenPresent` recovers this + // distinction from raw argv so this case flushes the help doc instead of + // silently dropping it like a genuinely-absent flag. + it("flushes the help dump to stderr for a required flag present on argv but missing its value", () => { + const cause = Cause.fail( + new CliError.ShowHelp({ + commandPath: ["supabase", "migration", "repair"], + errors: [new CliError.MissingOption({ option: "status" })], + }), + ); + expect( + classifyParseErrorConsoleOutput(cause, { + rootCommand: testRoot, + args: ["migration", "repair", "20230101000000", "--status"], + }), + ).toBe("flush-help-doc-to-stderr"); + }); + + // Codex review finding (CLI-1901 follow-up): `--` is the standard operand + // terminator — everything after it is positional, never a flag occurrence, + // for ANY option (mirrors the vendored `effect` lexer's own + // `argv.indexOf("--")` cutoff, `internal/lexer.ts`). Verified against the + // real `apps/cli-go/supabase-go` binary: + // `migration repair --local -- 20230101000000 --status` prints a bare + // `required flag(s) "status" not set`, no usage block — Go correctly parses + // the literal `--status` string as a second positional `version`, leaving + // the real `--status` flag genuinely unset. Without the `--` cutoff, + // `isMissingFlagTokenPresent` would find that trailing `--status` string + // anywhere in argv and wrongly conclude the flag was given but missing its + // value. + it("drops the help dump for a missing required flag whose token only appears after the -- terminator", () => { + const cause = Cause.fail( + new CliError.ShowHelp({ + commandPath: ["supabase", "migration", "repair"], + errors: [new CliError.MissingOption({ option: "status" })], + }), + ); + expect( + classifyParseErrorConsoleOutput(cause, { + rootCommand: testRoot, + args: ["migration", "repair", "--", "20230101000000", "--status"], + }), + ).toBe("drop"); + }); + + // Codex review finding (CLI-1901 follow-up): `sso add`'s `type` flag also has + // a short alias, `-t` (`add.command.ts`). Go's pflag treats a present-but- + // valueless SHORT flag exactly like the long form — `parseSingleShortArg` + // raises the same `ValueRequiredError` as `parseLongArg`, same timing, same + // "usage still shown" outcome — verified against the real + // `apps/cli-go/supabase-go` binary (`sso add -t`: full usage block on + // stderr, byte-parallel to `sso add --type`). `flagAliasesFor` resolves + // `type`'s aliases from the real command tree (via `context.rootCommand`) + // so `isMissingFlagTokenPresent` recognizes `-t` here too, instead of + // misclassifying it as a genuinely-absent flag. + it("flushes the help dump to stderr for a required flag present on argv by its short alias but missing its value", () => { + const cause = Cause.fail( + new CliError.ShowHelp({ + commandPath: ["supabase", "sso", "add"], + errors: [new CliError.MissingOption({ option: "type" })], + }), + ); + expect( + classifyParseErrorConsoleOutput(cause, { rootCommand: testRoot, args: ["sso", "add", "-t"] }), + ).toBe("flush-help-doc-to-stderr"); + }); + + // Codex review finding (CLI-1901 follow-up): `sso add --project-ref --type` + // omits `--type`'s value, but `--type` immediately follows `--project-ref` + // (a value-taking `Flag.string`, `add.command.ts`). Verified against the + // real `apps/cli-go/supabase-go` binary: pflag's `parseLongArg` + // (`flag.go`) unconditionally consumes the very next argv entry as + // `--project-ref`'s value, even though it looks like another flag — so + // `--type` is never seen as its own occurrence, and Go shows no usage at + // all (only its own "type" required-flag error, `SilenceUsage`-suppressed). + // The vendored `effect` parser does NOT eagerly consume a flag-shaped + // token as a value (`internal/parser.ts`'s `consumeFlagValueWithTokens` + // only consumes a following `Value`-tagged token), so `--type` remains its + // own token and raises its own `MissingOption` here too — but the raw scan + // must still recognize that `--type` was effectively consumed as + // `--project-ref`'s value, matching Go, instead of concluding `--type` is + // present but missing its value (which would wrongly flush the help doc). + it("drops the help dump when a required flag's own token is consumed as a preceding value-taking flag's value", () => { + const cause = Cause.fail( + new CliError.ShowHelp({ + commandPath: ["supabase", "sso", "add"], + errors: [new CliError.MissingOption({ option: "type" })], + }), + ); + expect( + classifyParseErrorConsoleOutput(cause, { + rootCommand: testRoot, + args: ["sso", "add", "--project-ref", "--type"], + }), + ).toBe("drop"); + }); + + // Codex review finding (CLI-1901 follow-up): `isValueTakingFlagTokenFor` + // only inspects the resolved LEAF command's own flags, so it doesn't know + // `--network-id` (a value-taking GLOBAL flag, `globalFlagsWithValues` in + // `run.ts`) consumes the very next argv entry. Verified against pflag's + // `parseLongArg` (`flag.go`): `--network-id --status` hands the literal + // string `--status` to `--network-id` as its value, so the required + // `status` flag (`migration repair`, `Flag.choice`) is never seen as its + // own occurrence and keeps Go's `SilenceUsage` treatment (no usage shown). + // Without OR-ing `globalFlagsWithValues` into the scan's value-taking + // predicate, the raw `--status` token would be found anyway and wrongly + // flush the help doc. + it("drops the help dump when a required flag's own token is consumed as a global value-taking flag's value", () => { + const cause = Cause.fail( + new CliError.ShowHelp({ + commandPath: ["supabase", "migration", "repair"], + errors: [new CliError.MissingOption({ option: "status" })], + }), + ); + expect( + classifyParseErrorConsoleOutput(cause, { + rootCommand: testRoot, + args: ["migration", "repair", "--network-id", "--status", "--local", "20230101000000"], + }), + ).toBe("drop"); + }); + + // Codex review finding (CLI-1901 follow-up): a literal `--` immediately + // after a value-taking flag is NOT a genuine operand terminator in Go — + // pflag's `parseLongArg` (`flag.go`) pops the very next raw token as the + // flag's value with no shape check at all, so `--project-ref --` hands the + // literal string `--` to `--project-ref`. Parsing then resumes normally on + // `--type`, which (nothing follows it) raises pflag's own + // `ValueRequiredError` — a `ParseFlags`-time error, usage still shown, NOT + // `SilenceUsage`-suppressed. Precomputing `args.indexOf("--")` before the + // value-consumption scan would wrongly treat that consumed `--` as the + // terminator and drop `--type` from the scan entirely, misclassifying + // `type` as genuinely absent. + it("flushes the help dump to stderr for a required flag whose own token follows a -- consumed as a preceding value-taking flag's value", () => { + const cause = Cause.fail( + new CliError.ShowHelp({ + commandPath: ["supabase", "sso", "add"], + errors: [new CliError.MissingOption({ option: "type" })], + }), + ); + expect( + classifyParseErrorConsoleOutput(cause, { + rootCommand: testRoot, + args: ["sso", "add", "--project-ref", "--", "--type"], + }), + ).toBe("flush-help-doc-to-stderr"); + }); + + it("still drops the help dump for a missing required flag even when an unrelated flag shares a substring of its name", () => { + const cause = Cause.fail( + new CliError.ShowHelp({ + commandPath: ["supabase", "sso", "add"], + errors: [new CliError.MissingOption({ option: "type" })], + }), + ); + // `--type-hint` is a different flag token — must not false-positive-match `--type`. + expect( + classifyParseErrorConsoleOutput(cause, { + rootCommand: testRoot, + args: ["sso", "add", "--type-hint", "x"], + }), + ).toBe("drop"); + }); + + // Go's `ParseFlags` (`command.go:919`) validates `Flag.choice` values BEFORE + // `PersistentPreRunE` runs — verified against the real binary (`sso add --type + // bogus`): Go still shows its usage block, on stderr. Same for an unrecognized + // flag and a missing positional argument (both also pre-`PersistentPreRunE`). + it("flushes the help dump to stderr for an invalid Flag.choice value", () => { + const cause = Cause.fail( + new CliError.ShowHelp({ + commandPath: ["supabase", "sso", "add"], + errors: [ + new CliError.InvalidValue({ + option: "type", + value: "bogus", + expected: 'Expected "saml", got "bogus"', + kind: "flag", + }), + ], + }), + ); + expect( + classifyParseErrorConsoleOutput(cause, { + rootCommand: testRoot, + args: ["sso", "add", "--type", "bogus"], + }), + ).toBe("flush-help-doc-to-stderr"); + }); + + it("flushes the help dump to stderr for an unrecognized flag", () => { + const cause = Cause.fail( + new CliError.ShowHelp({ + commandPath: ["supabase", "branches"], + errors: [new CliError.UnrecognizedOption({ option: "--bogus", suggestions: [] })], + }), + ); + expect( + classifyParseErrorConsoleOutput(cause, { + rootCommand: testRoot, + args: ["branches", "--bogus"], + }), + ).toBe("flush-help-doc-to-stderr"); + }); + + it("flushes the help dump to stderr for a missing positional argument", () => { + const cause = Cause.fail( + new CliError.ShowHelp({ + commandPath: ["supabase", "sso", "show"], + errors: [new CliError.MissingArgument({ argument: "id" })], + }), + ); + expect( + classifyParseErrorConsoleOutput(cause, { rootCommand: testRoot, args: ["sso", "show"] }), + ).toBe("flush-help-doc-to-stderr"); + }); + + // A mix (e.g. a missing required flag alongside an unrecognized flag) can't + // actually occur in practice — the library fails fast on the earlier + // `ParseFlags`-class error before `MissingOption` is ever checked — but the + // classifier must still not mistake a mix for the all-`MissingOption` case. + it("flushes the help dump to stderr for a mix of error tags", () => { + const cause = Cause.fail( + new CliError.ShowHelp({ + commandPath: ["supabase", "sso", "add"], + errors: [ + new CliError.MissingOption({ option: "type" }), + new CliError.UnrecognizedOption({ option: "--bogus", suggestions: [] }), + ], + }), + ); + expect( + classifyParseErrorConsoleOutput(cause, { + rootCommand: testRoot, + args: ["sso", "add", "--bogus"], + }), + ).toBe("flush-help-doc-to-stderr"); + }); + + it("flushes unchanged for a clean ShowHelp failure (bare group command / explicit --help)", () => { + const cause = Cause.fail( + new CliError.ShowHelp({ commandPath: ["supabase", "branches"], errors: [] }), + ); + expect( + classifyParseErrorConsoleOutput(cause, { rootCommand: testRoot, args: ["branches"] }), + ).toBe("flush-unchanged"); + }); + + it("flushes unchanged for a non-ShowHelp failure", () => { + expect( + classifyParseErrorConsoleOutput(Cause.fail(new Error("boom")), { + rootCommand: testRoot, + args: [], + }), + ).toBe("flush-unchanged"); + }); + + it("flushes unchanged for an interrupt", () => { + expect( + classifyParseErrorConsoleOutput(Cause.interrupt(), { rootCommand: testRoot, args: [] }), + ).toBe("flush-unchanged"); + }); + + it("flushes unchanged for a defect with no typed failure", () => { + expect( + classifyParseErrorConsoleOutput(Cause.die(new Error("unexpected crash")), { + rootCommand: testRoot, + args: [], + }), + ).toBe("flush-unchanged"); + }); +}); diff --git a/apps/cli/src/shared/cli/subcommand-flag-suggestions.ts b/apps/cli/src/shared/cli/subcommand-flag-suggestions.ts index c6da39c2e1..e1865a5273 100644 --- a/apps/cli/src/shared/cli/subcommand-flag-suggestions.ts +++ b/apps/cli/src/shared/cli/subcommand-flag-suggestions.ts @@ -102,6 +102,71 @@ function flagMatchesOption(flag: HelpDoc.FlagDoc, option: string): boolean { return flag.aliases.includes(option); } +/** + * Every argv token (e.g. `-t`, alongside the canonical `--type`) that also + * resolves to `option` for the command at `commandPath`, by walking the + * command tree the same way `buildSubcommandFlagHint` does. Returns `[]` if + * `commandPath` doesn't resolve to a real command or that command has no + * flag named `option` — callers should treat that as "no aliases", not an + * error, since a synthetic/test command path is a legitimate input. + * + * Used by `run.ts`'s `isMissingFlagTokenPresent` to recognize a required + * flag supplied by its short alias but missing its value (Go/pflag still + * shows usage for that case — see CLI-1901) instead of misclassifying it as + * genuinely absent (Go: `SilenceUsage`-suppressed, no usage shown). + */ +export function flagAliasesFor( + rootCommand: Command.Command.Any, + commandPath: ReadonlyArray, + option: string, +): ReadonlyArray { + const command = findCommand(rootCommand, commandPath.slice(1)); + if (!command) return []; + const flag = helpDocFor(command, commandPath)?.flags.find( + (candidate) => candidate.name === option, + ); + return flag?.aliases ?? []; +} + +/** + * Builds a lookup for whether an argv token (a canonical `--name` or one of + * its aliases, e.g. `-t`) at `commandPath` belongs to a *value-taking* flag + * (any flag whose `HelpDoc.FlagDoc.type !== "boolean"`) on the command + * resolved the same way `flagAliasesFor` does. Returns a function that + * answers `false` for every token when `commandPath` doesn't resolve to a + * real command, mirroring `flagAliasesFor`'s "no aliases" fallback. + * + * Used by `run.ts`'s `isMissingFlagTokenPresent` to recognize when a + * DIFFERENT flag's value-consumption ate the very token being scanned for. + * Go/pflag's `parseLongArg` (`flag.go`) unconditionally consumes the next + * argv entry as a value-taking flag's value, even when that entry itself + * looks like another flag — e.g. `sso add --project-ref --type` hands the + * literal string `--type` to `--project-ref`, so `--type` is never seen as + * its own occurrence and its `MissingOption` failure gets Go's + * `SilenceUsage` treatment (no usage shown). The vendored `effect` CLI + * library's own parser does NOT replicate that (`internal/parser.ts`'s + * `consumeFlagValueWithTokens` only consumes a following token when it's + * lexed as a plain `Value`, never a flag-shaped token), so without this + * lookup the raw argv scan in `isMissingFlagTokenPresent` would find the + * literal `--type` token and wrongly conclude the flag is present but + * missing its value — reintroducing the usage dump CLI-1901 suppresses. + */ +export function isValueTakingFlagTokenFor( + rootCommand: Command.Command.Any, + commandPath: ReadonlyArray, +): (token: string) => boolean { + const command = findCommand(rootCommand, commandPath.slice(1)); + const flags = command && helpDocFor(command, commandPath)?.flags; + if (!flags) return () => false; + const valueTakingTokens = new Set(); + for (const flag of flags) { + if (flag.type === "boolean") continue; + valueTakingTokens.add(`--${flag.name}`); + for (const alias of flag.aliases) valueTakingTokens.add(alias); + } + return (token: string) => valueTakingTokens.has(token); +} + function findPathEndIndex( args: ReadonlyArray, pathWithoutRoot: ReadonlyArray, diff --git a/apps/cli/src/shared/output/normalize-error.ts b/apps/cli/src/shared/output/normalize-error.ts index 19cc375913..7d0976dd05 100644 --- a/apps/cli/src/shared/output/normalize-error.ts +++ b/apps/cli/src/shared/output/normalize-error.ts @@ -1,5 +1,8 @@ import { Cause, Option } from "effect"; +import { CliError } from "effect/unstable/cli"; import { formatInvalidValueMessage } from "../cli/invalid-value-message.ts"; +import type { CliErrorSuggestionContext } from "../cli/subcommand-flag-suggestions.ts"; +import { formatCliErrorsForDisplay } from "../cli/subcommand-flag-suggestions.ts"; type NormalizedCliError = { readonly code: string; @@ -28,7 +31,10 @@ const readRawString = (value: ErrorRecord, key: string): string | undefined => { return typeof field === "string" ? field : undefined; }; -const mappedError = (error: ErrorRecord): NormalizedCliError | undefined => { +const mappedError = ( + error: ErrorRecord, + context?: CliErrorSuggestionContext, +): NormalizedCliError | undefined => { const tag = readString(error, "_tag"); switch (tag) { case "NoRunningStackError": @@ -123,21 +129,64 @@ const mappedError = (error: ErrorRecord): NormalizedCliError | undefined => { // message — otherwise the user sees a useless top-line above the real // problem. const errors = error["errors"]; - if (Array.isArray(errors) && errors.length === 1) { + if (!Array.isArray(errors) || errors.length === 0) return undefined; + + if (errors.length === 1) { const inner = errors[0]; if (isErrorRecord(inner)) { - const innerMapped = mappedError(inner); + const innerMapped = mappedError(inner, context); if (innerMapped) return innerMapped; } } + + // No Go-parity-specific single-error mapping applies (either more than + // one simultaneous error, e.g. a child flag placed before its + // subcommand — `UnrecognizedOption` plus the `UnknownSubcommand` its + // misplaced value gets parsed as — or a lone error with no known + // mapping: UnrecognizedOption, DuplicateOption, MissingArgument, + // UnknownSubcommand, UserError, or an InvalidValue that doesn't hit + // CLI-1898's doubled-"Expected"-prefix bug). Surface every inner + // error's own message — reusing the same `formatCliErrorsForDisplay` + // the text/json formatters use, so a subcommand-flag hint (e.g. "Hint: + // --foo is available on `branches create`. Pass it after the + // subcommand") survives — rather than falling through to ShowHelp's + // useless "Help requested" envelope message: since CLI-1901 (`run.ts`'s + // `withoutParseErrorHelpDump`) stopped the vendored library from also + // `Console.error`-ing this same text, this is now the ONLY place any + // of it reaches the user, for one error or many. + if (errors.every(CliError.isCliError)) { + const formatted = formatCliErrorsForDisplay(errors, context); + if (formatted.errors.length > 0) { + const [only] = formatted.errors; + return { + code: formatted.errors.length === 1 && only ? only._tag : "ShowHelp", + message: formatted.errors.map((formattedError) => formattedError.message).join("\n\n"), + }; + } + } + + // Defensive fallback for a single inner value that carries a usable + // `_tag`/`message` pair but isn't a real `CliError` instance (e.g. a + // hand-rolled test double) — real `ShowHelp.errors` entries always are. + if (errors.length === 1) { + const inner = errors[0]; + if (isErrorRecord(inner)) { + const code = readString(inner, "_tag"); + const message = readString(inner, "message"); + if (code && message) return { code, message }; + } + } return undefined; } } }; -export function normalizeCliError(error: unknown): NormalizedCliError { +export function normalizeCliError( + error: unknown, + context?: CliErrorSuggestionContext, +): NormalizedCliError { if (isErrorRecord(error)) { - const mapped = mappedError(error); + const mapped = mappedError(error, context); if (mapped) { return mapped; } @@ -174,9 +223,15 @@ export function normalizeCliError(error: unknown): NormalizedCliError { }; } -export function normalizeCause(cause: Cause.Cause): NormalizedCliError { +export function normalizeCause( + cause: Cause.Cause, + context?: CliErrorSuggestionContext, +): NormalizedCliError { const errorOption = Cause.findErrorOption(cause); - return normalizeCliError(Option.getOrElse(errorOption, () => Cause.squash(cause))); + return normalizeCliError( + Option.getOrElse(errorOption, () => Cause.squash(cause)), + context, + ); } export function formatCliError(error: NormalizedCliError): string { diff --git a/apps/cli/src/shared/output/normalize-error.unit.test.ts b/apps/cli/src/shared/output/normalize-error.unit.test.ts index 107600ac13..5f7c122ae3 100644 --- a/apps/cli/src/shared/output/normalize-error.unit.test.ts +++ b/apps/cli/src/shared/output/normalize-error.unit.test.ts @@ -1,8 +1,14 @@ import { describe, expect, test } from "vitest"; import { Cause } from "effect"; -import { CliError } from "effect/unstable/cli"; +import { CliError, Command } from "effect/unstable/cli"; +import { legacyBranchesCommand } from "../../legacy/commands/branches/branches.command.ts"; +import { legacyNetworkRestrictionsCommand } from "../../legacy/commands/network-restrictions/network-restrictions.command.ts"; import { formatCliError, normalizeCause, normalizeCliError } from "./normalize-error.ts"; +const testRoot = Command.make("supabase").pipe( + Command.withSubcommands([legacyBranchesCommand, legacyNetworkRestrictionsCommand]), +); + describe("normalizeCliError", () => { test("maps NoRunningStackError to a user-facing message", () => { const error = { @@ -157,6 +163,178 @@ describe("normalizeCliError", () => { }); }); + // CLI-1901: before this fix, the vendored `effect` CLI library's own + // `showHelp()` also printed this same message (via `Console.error`) as a + // duplicate of whatever `normalizeCause` rendered here — so a tag with no + // Go-parity-specific mapping (e.g. UnrecognizedOption) still had SOME + // informative text visible, just twice. Now that CLI-1901's `run.ts` fix + // suppresses the library's own duplicate print entirely, this generic + // fallback is the ONLY place the message reaches the user — it must not + // regress to the useless "Help requested" envelope message. + test("ShowHelp envelope unwraps a single UnrecognizedOption to its own message (no Go-parity mapping exists yet)", () => { + const error = { + _tag: "ShowHelp", + commandPath: ["branches"], + errors: [ + new CliError.UnrecognizedOption({ + option: "--bogus", + command: ["branches"], + suggestions: [], + }), + ], + }; + expect(normalizeCliError(error)).toEqual({ + code: "UnrecognizedOption", + message: "Unrecognized flag: --bogus in command branches", + }); + }); + + // Regression test for a Codex review finding on CLI-1901: `run.ts`'s + // `withoutParseErrorHelpDump` suppresses the vendored library's own + // `Console.error` render, which used to be the only place a subcommand-flag + // placement hint (`buildSubcommandFlagHint` / + // `subcommand-flag-suggestions.ts`) reached the user. When a + // `CliErrorSuggestionContext` is supplied, this fallback must reuse + // `formatCliErrorsForDisplay` — the same helper the text/json formatters + // use — so that hint still survives through the single-render path. + test("ShowHelp envelope unwraps a single UnrecognizedOption and preserves its subcommand-flag hint when a suggestion context is supplied", () => { + const error = { + _tag: "ShowHelp", + commandPath: ["branches"], + errors: [ + new CliError.UnrecognizedOption({ + option: "--persistent", + command: ["supabase", "branches"], + suggestions: [], + }), + ], + }; + + const result = normalizeCliError(error, { + rootCommand: testRoot, + args: ["branches", "--persistent", "create"], + }); + + expect(result.code).toBe("UnrecognizedOption"); + expect(result.message).toContain( + "Unrecognized flag: --persistent in command supabase branches", + ); + expect(result.message).toContain( + "Hint: --persistent is available on `supabase branches create` and `supabase branches update`. Pass it after the subcommand", + ); + }); + + // Regression test for a second Codex review finding on CLI-1901: a child + // flag placed before its subcommand, WITH a value (e.g. `network-restrictions + // --project-ref get`), makes Effect raise TWO simultaneous errors — + // `UnrecognizedOption` for the flag, plus `UnknownSubcommand` for the + // flag's own value being misread as the subcommand name. Before this fix, + // `mappedError`'s ShowHelp unwrap only ever looked at `errors.length === 1`, + // so a real two-error ShowHelp fell straight through to the generic + // "Help requested" envelope message — losing the hint (and the specific + // error) entirely now that CLI-1901 also suppresses the vendored library's + // own duplicate render for this case. + test("ShowHelp envelope unwraps multiple simultaneous errors and preserves the subcommand-flag hint (child flag with a value, before its subcommand)", () => { + const error = { + _tag: "ShowHelp", + commandPath: ["network-restrictions"], + errors: [ + new CliError.UnrecognizedOption({ + option: "--project-ref", + command: ["supabase", "network-restrictions"], + suggestions: [], + }), + new CliError.UnknownSubcommand({ + subcommand: "jacraenyzrorgjhsdvvf", + parent: ["supabase", "network-restrictions"], + suggestions: [], + }), + ], + }; + + const result = normalizeCliError(error, { + rootCommand: testRoot, + args: ["network-restrictions", "--project-ref", "jacraenyzrorgjhsdvvf", "get"], + }); + + expect(result.code).toBe("UnrecognizedOption"); + expect(result.message).toContain( + "Unrecognized flag: --project-ref in command supabase network-restrictions", + ); + expect(result.message).toContain( + "Hint: --project-ref is available on `supabase network-restrictions get` and `supabase network-restrictions update`.", + ); + expect(result.message).not.toContain("Help requested"); + }); + + // A genuine, unrelated multi-error case (no shared hint to collapse them + // into one) must still surface every error's own message, not just the + // first — and must not crash trying to pick a single `code`. + test("ShowHelp envelope unwraps multiple unrelated simultaneous errors into a joined message", () => { + const error = { + _tag: "ShowHelp", + commandPath: ["branches"], + errors: [ + new CliError.UnrecognizedOption({ + option: "--bogus-one", + command: ["supabase", "branches"], + suggestions: [], + }), + new CliError.UnrecognizedOption({ + option: "--bogus-two", + command: ["supabase", "branches"], + suggestions: [], + }), + ], + }; + + const result = normalizeCliError(error, { rootCommand: testRoot, args: ["branches"] }); + + expect(result.code).toBe("ShowHelp"); + expect(result.message).toContain("Unrecognized flag: --bogus-one in command supabase branches"); + expect(result.message).toContain("Unrecognized flag: --bogus-two in command supabase branches"); + }); + + // The same fallback also covers an InvalidValue that does NOT have the + // CLI-1898 doubled-"Expected"-prefix bug (e.g. a custom `Flag.mapTryCatch` + // validator like `sso add --domains`, whose `expected` text is already + // clean) — `mappedError`'s own InvalidValue case returns `undefined` for + // those (nothing to fix), so this generic fallback is what surfaces the + // message, not CLI-1898's specific rebuild. + test("ShowHelp envelope unwraps a single InvalidValue with an already-clean expected message (not the CLI-1898 doubled-prefix bug)", () => { + const error = { + _tag: "ShowHelp", + commandPath: ["sso", "add"], + errors: [ + new CliError.InvalidValue({ + option: "domains", + value: "unterminated-quote.com", + expected: "a comma-separated list (unterminated quote)", + kind: "flag", + }), + ], + }; + expect(normalizeCliError(error)).toEqual({ + code: "InvalidValue", + message: + 'Invalid value for flag --domains: "unterminated-quote.com". Expected: a comma-separated list (unterminated quote)', + }); + }); + + // If the single inner error has a `_tag` but no usable `message` (neither a + // string via its own getter nor otherwise), the fallback must not surface a + // blank/garbage message — it should fall through to ShowHelp's own generic + // handling, same as the pre-existing multiple-errors case below. + test("ShowHelp envelope with a single inner error carrying no message falls back to generic", () => { + const error = { + _tag: "ShowHelp", + commandPath: ["branches"], + errors: [{ _tag: "SomeUnmappedTag" }], + }; + const result = normalizeCliError(error); + expect(result.code).toBe("ShowHelp"); + }); + test("ShowHelp with multiple errors does not unwrap (falls back to generic)", () => { const error = { _tag: "ShowHelp", diff --git a/packages/config/src/io.ts b/packages/config/src/io.ts index 8052399091..ae427d5f67 100644 --- a/packages/config/src/io.ts +++ b/packages/config/src/io.ts @@ -694,12 +694,19 @@ export const loadProjectConfigFile = Effect.fnUntraced(function* ( }); const { document: normalized, deprecatedSections } = normalizeDeprecatedSMTPSections(document); // Warn on stderr (matching Go's normalizeDeprecatedSMTPConfig) so the notice - // never pollutes machine-readable stdout payloads. + // never pollutes machine-readable stdout payloads. Pinned to the real + // console (bypassing whatever `Console.Console` is ambient) so this always + // writes immediately, matching Go's synchronous `fmt.Fprintln(os.Stderr, ...)` + // (config.go:618,630) — a caller wrapping this in a deferred/buffered + // `Console.Console` (e.g. `apps/cli/src/shared/cli/run.ts`'s + // `withoutParseErrorHelpDump`, which only buffers the CLI parser's own + // duplicate-render writes and must never delay a handler's real output; see + // CLI-1901) must not silently swallow or delay it. for (const section of deprecatedSections) { const replacement = section.replace(/inbucket$/, "local_smtp"); yield* Console.error( `WARN: config section [${section}] is deprecated. Please use [${replacement}] instead.`, - ); + ).pipe(Effect.provideService(Console.Console, globalThis.console)); } // Substitute `env(VAR)` references against `.env`/`.env.local`/ambient env @@ -780,11 +787,13 @@ export const loadProjectConfigFile = Effect.fnUntraced(function* ( // artifact, not the parity-relevant part, same call already made for // `LegacyInvalidPortEnvOverrideError` in the legacy shell. Not reproduced // byte-for-byte; `Console.error` supplies a normal trailing newline instead. + // Pinned to the real console for the same reason as the `[inbucket]` + // warning above — see that comment. if (goViperCompat) { for (const ext of deprecatedProviders) { yield* Console.error( `WARN: disabling deprecated "${ext}" provider. Please use [auth.external.${ext}_oidc] instead`, - ); + ).pipe(Effect.provideService(Console.Console, globalThis.console)); } } From 7c047c59ac1e8e42b14b996b2a54d4f9ca1557d4 Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Fri, 10 Jul 2026 13:43:53 +0100 Subject: [PATCH 43/79] fix(cli): exempt global choice flags from telemetry redaction (Go isEnumFlag parity) (#5855) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What changed Go's `isEnumFlag` (`apps/cli-go/cmd/root_analytics.go:110-116`) unconditionally reports the value of any `*utils.EnumFlag`-backed pflag verbatim in `cli_command_executed` telemetry — no per-flag annotation needed. `--output`, `--dns-resolver`, and `--agent` are all registered as `*utils.EnumFlag` on `rootCmd.PersistentFlags()` (`cmd/root.go:330,331,333`), so Go always sends their real value. The TS port's `withLegacyCommandInstrumentation` already auto-detects `Flag.choice` flags declared in a command's own `config` (CLI-1866), and already resolves global/persistent flag values as a fallback when a command doesn't declare that CLI name locally (CLI-1896). But CLI-1896 deliberately scoped out the 3 global *choice* flags — nothing taught the global-fallback path that a resolved value might itself be a safe enum, so `--output`/`--dns-resolver`/`--agent` always fell through to `""`. This adds `GLOBAL_CHOICE_FLAG_NAMES` (`legacy-command-instrumentation.ts`), derived from `LEGACY_GLOBAL_FLAGS` by reusing the existing `getChoiceFlagNames` helper (so the choice-detection predicate has exactly one home), and consults it only on the global-fallback path — i.e. only when the invoking command's own `flags` record doesn't already declare that CLI name. A command that registers its own differently-typed local flag under the same CLI name (`db diff`'s local string `output: Flag.string("output")`, `cmd/db.go:622`) still shadows the global and stays redacted, matching Go's `isEnumFlag` type-asserting that command's own non-enum flag object instead of root's persistent `EnumFlag`. Verified this is the real wiring, not a hypothetical: `db diff` passes `output` in its own `flags` record with no matching `config` entry, so `isFromHandler` is structurally `true` for it and the new global set is never consulted. Also verified Go's separate `output_format` telemetry property (`PropOutputFormat`) is independent of the generic `flags.output` entry — both already fire in Go too, so no double-counting concern from un-redacting `flags.output`. Fixes CLI-1904. ## Why Found during CLI-1866's review as a pre-existing gap in the same problem class, tracked separately since it was out of scope for that PR's diff, then explicitly deferred again by CLI-1896 ("the 3 global choice flags ... remain redacted — that gap is the already-tracked CLI-1904, not this ticket"). ## Reviewer-relevant context - All 4 `review-changes` agents (architect, engineer, security, DX) reviewed this diff and approved with no blocking findings. Two converging nits were fixed in a follow-up commit: deduped the choice-detection predicate (`GLOBAL_CHOICE_FLAG_NAMES` now derives from `getChoiceFlagNames` instead of re-implementing it) and clarified the two `AGENTS.md` telemetry bullets. - One deliberately-deferred judgement call from engineer-reviewer: no command's own `*.integration.test.ts` today exercises `withLegacyCommandInstrumentation`'s PostHog-capture behavior for *any* flag (that coverage is entirely the job of `legacy-command-instrumentation.unit.test.ts`'s own scenario suite) — this is a pre-existing, repo-wide pattern, not something this PR narrows or regresses. Adding integration-tier telemetry assertions to `db diff`/`db query` specifically would set a new cross-cutting test precedent well beyond this fix's scope, so left as a possible future hardening rather than folded in here. --- apps/cli/AGENTS.md | 9 ++- .../legacy-command-instrumentation.ts | 48 ++++++++++-- ...egacy-command-instrumentation.unit.test.ts | 73 +++++++++++++++++-- 3 files changed, 117 insertions(+), 13 deletions(-) diff --git a/apps/cli/AGENTS.md b/apps/cli/AGENTS.md index da6b1bb60b..d4830ccc9e 100644 --- a/apps/cli/AGENTS.md +++ b/apps/cli/AGENTS.md @@ -285,8 +285,13 @@ The legacy shell sends the same PostHog events to the same product analytics pip - **Native legacy commands wrap with `withLegacyCommandInstrumentation`** (from `legacy/telemetry/legacy-command-instrumentation.ts`) — _not_ the shared `withCommandInstrumentation`. The legacy variant emits Go-shape properties: a single `flags` map (vs `flags_used`/`flag_values`), `is_agent: boolean` (vs `ai_tool: string`), and `env_signals`. - **Pass `flags` to the wrapper** so boolean flag values can be detected and logged verbatim: `handler(flags).pipe(withLegacyCommandInstrumentation({ flags }), ...)`. Sensitive values become the literal string `""` to match Go. - **Use `safeFlags: ["flag-name"]`** to whitelist flags that Go marks with `markFlagTelemetrySafe` (grep `apps/cli-go/cmd/*.go`). Today these are `--project-ref` (sso, branches, link, functions, projects/api-keys), `--project-id` (gen/types), `--org-id` (projects/create), and `--version` (migration/squash). -- **Pass `config` (the command's own flag config record) to the wrapper** if it has any `Flag.choice`/`Flag.choiceWithValue` flags: `withLegacyCommandInstrumentation({ flags, config })`. Every choice flag declared in that command's own `config` is auto-detected and treated as safe, mirroring Go's `isEnumFlag` (`cmd/root_analytics.go:110-116`), which checks `flag.Value.(*utils.EnumFlag)` unconditionally — no per-flag `safeFlags` entry needed, and it stays correct as choices are added or removed. This does NOT cover global/root flags (`--output`, `--dns-resolver`, `--agent` in `shared/legacy/global-flags.ts`) even though Go's equivalents are also `EnumFlag` — see CLI-1904. -- **Global/persistent flags (`shared/legacy/global-flags.ts`) resolve automatically** — the wrapper reads `legacyGlobalFlagValues` (via `Effect.serviceOption`, so it's a no-op outside the real CLI tree) and falls back to it whenever a changed flag name isn't in the handler's own `flags` record, mirroring Go's `changedFlags()` walking `cmd.Parent()`'s `PersistentFlags()` (`cmd/root_analytics.go:53-76`). No per-command wiring needed. Boolean globals (`--debug`, `--yes`, `--experimental`, `--create-ticket`) therefore already report their real value through the existing boolean-is-safe rule — but ONLY when a command's own `flags` record doesn't already declare that CLI name (a command's own flag always wins, e.g. `db diff`'s local `--output`); the three global choice flags (`--output`, `--dns-resolver`, `--agent`) still redact until CLI-1904 teaches the safety pipeline about global `EnumFlag`s. +- **Pass `config` (the command's own flag config record) to the wrapper** if it has any `Flag.choice`/`Flag.choiceWithValue` flags: `withLegacyCommandInstrumentation({ flags, config })`. Every choice flag declared in that command's own `config` is auto-detected and treated as safe, mirroring Go's `isEnumFlag` (`cmd/root_analytics.go:110-116`), which checks `flag.Value.(*utils.EnumFlag)` unconditionally — no per-flag `safeFlags` entry needed, and it stays correct as choices are added or removed. A command's own `config` only ever contains its own locally-declared flags, so this cannot cover the 3 global choice flags (`--output`, `--dns-resolver`, `--agent` in `shared/legacy/global-flags.ts`) — those are handled separately, see below. +- **Global/persistent flags (`shared/legacy/global-flags.ts`) resolve automatically** — the wrapper reads `legacyGlobalFlagValues` (via `Effect.serviceOption`, so it's a no-op outside the real CLI tree) and falls back to it whenever a changed flag name isn't in the handler's own `flags` record, mirroring Go's `changedFlags()` walking `cmd.Parent()`'s `PersistentFlags()` (`cmd/root_analytics.go:53-76`). No per-command wiring needed. This gives two flag families their real value automatically, via the existing boolean-is-safe rule and a new choice-is-safe rule (`GLOBAL_CHOICE_FLAG_NAMES` — CLI-1904) respectively: + - Boolean globals: `--debug`, `--yes`, `--experimental`, `--create-ticket`. + - Choice globals: `--output`, `--dns-resolver`, `--agent`. + + Both rules apply ONLY when a command's own `flags` record doesn't already declare that CLI name — a command's own flag always wins. Example: `db diff` declares its own local `output: Flag.string("output")` (a file path, not a choice) in its `flags` record, so `db diff --output diff.sql` stays redacted — matching Go, where `isEnumFlag` type-asserts `db diff`'s own non-enum local flag object instead of root's persistent `*utils.EnumFlag`. + - **Proxy handlers (`LegacyGoProxy.exec`) must NOT wrap with any instrumentation.** The Go subprocess fires its own telemetry; a TS wrapper would double-count `cli_command_executed`. - **When promoting a command from proxy to native, reproduce every `phtelemetry.*` call in the Go counterpart.** Grep `apps/cli-go/internal//` for `service.Capture`, `service.Alias`, `service.Identify`, `service.GroupIdentify`, and `TrackUpgradeSuggested`. The current Go custom events that legacy ports must reproduce when natively ported: diff --git a/apps/cli/src/legacy/telemetry/legacy-command-instrumentation.ts b/apps/cli/src/legacy/telemetry/legacy-command-instrumentation.ts index 8b38786463..ceae9c91be 100644 --- a/apps/cli/src/legacy/telemetry/legacy-command-instrumentation.ts +++ b/apps/cli/src/legacy/telemetry/legacy-command-instrumentation.ts @@ -45,7 +45,10 @@ interface LegacyCommandInstrumentationOptions; // The `-o`/`--output` values this command accepts, mirroring Go's per-command // `--output` enum (`internal/utils/enum.go`). Defaults to the resource-command @@ -295,6 +298,33 @@ const GLOBAL_SHORT_ALIASES: Readonly> = (() => { return aliases; })(); +// CLI-name set for every global/persistent flag that is itself a +// `Flag.choice`/`Flag.choiceWithValue` — today `output`, `dns-resolver`, and +// `agent` (`shared/legacy/global-flags.ts`). Reuses `getChoiceFlagNames` +// itself (keyed by `.id` rather than CLI name — `getChoiceFlagNames` only +// ever reads `single.name` off the unwrapped param, so the record key is +// irrelevant) rather than re-implementing the choice-detection predicate, so +// the two can never silently drift apart. Derived from `LEGACY_GLOBAL_FLAGS` +// the same way `GLOBAL_SHORT_ALIASES` is, so this never drifts from that +// single source of truth either. Mirrors Go's `isEnumFlag` +// (`cmd/root_analytics.go:110-116`) checked against the actual +// `*utils.EnumFlag`-backed persistent flag object registered on root +// (`cmd/root.go:330,331,333`). Applied ONLY to the global-fallback path in +// `buildFlagsMap` below (`!isFromHandler`): when a command registers its OWN +// differently-typed local flag under the same CLI name (e.g. `db diff`'s +// local string `--output`, `cmd/db.go:622`), Go's own `isEnumFlag` check runs +// against THAT command's local flag object instead — which fails the type +// assertion, so it stays redacted — exactly mirrored by +// `choiceFlagNames`/`isFromHandler` continuing to govern the handler-owned +// path. Invariant this depends on: any command whose Go counterpart locally +// shadows one of these three with a NON-enum flag (only `db diff`'s `output` +// today) must pass that flag in its own `flags` record, so `isFromHandler` +// is true and this global set is never consulted for it — otherwise the +// fallback would report verbatim here even though Go redacts it. +const GLOBAL_CHOICE_FLAG_NAMES: ReadonlySet = getChoiceFlagNames( + Object.fromEntries(LEGACY_GLOBAL_FLAGS.map((globalFlag) => [globalFlag.id, globalFlag.flag])), +); + function buildFlagsMap>(options: { readonly flags: Flags | undefined; // Live global/persistent flag values (`legacyGlobalFlagValues`), keyed by @@ -330,13 +360,19 @@ function buildFlagsMap>(options: { // `safeFlagSet`/`choiceFlagNames` classify a flag as safe by CLI NAME, // sourced from this command's own `safeFlags`/`config` options — they may // only vouch for a value that actually came from this command's own - // `flags` record. A value resolved from the global-flag fallback is safe - // solely because it's boolean (Go's `isBooleanFlag` branch applies - // unconditionally); it must never inherit a *different* command's - // per-flag safe/choice annotation just because the CLI name matches. + // `flags` record; a value resolved from the global-flag fallback must + // never inherit a *different* command's per-flag safe/choice annotation + // just because the CLI name matches. The global-fallback path instead + // consults `GLOBAL_CHOICE_FLAG_NAMES` (CLI-1904) — the global flag's OWN + // choice-ness, exactly mirroring Go's `isEnumFlag` type-asserting the + // actual persistent flag object rather than any per-command annotation. + // Boolean values are safe unconditionally regardless of source (Go's + // `isBooleanFlag` branch applies unconditionally too). const isSafe = typeof value === "boolean" || - (isFromHandler && (safeFlagSet.has(cliName) || choiceFlagNames.has(cliName))); + (isFromHandler + ? safeFlagSet.has(cliName) || choiceFlagNames.has(cliName) + : GLOBAL_CHOICE_FLAG_NAMES.has(cliName)); result[cliName] = isSafe ? (value ?? REDACTED_VALUE) : REDACTED_VALUE; } diff --git a/apps/cli/src/legacy/telemetry/legacy-command-instrumentation.unit.test.ts b/apps/cli/src/legacy/telemetry/legacy-command-instrumentation.unit.test.ts index f546c5e536..eb5be95e3d 100644 --- a/apps/cli/src/legacy/telemetry/legacy-command-instrumentation.unit.test.ts +++ b/apps/cli/src/legacy/telemetry/legacy-command-instrumentation.unit.test.ts @@ -3,6 +3,7 @@ import { Effect, Layer, Option, Stdio } from "effect"; import { Flag } from "effect/unstable/cli"; import { commandRuntimeLayer } from "../../shared/runtime/command-runtime.layer.ts"; import { + LegacyAgentFlag, LegacyDebugFlag, LegacyDnsResolverFlag, LegacyOutputFlag, @@ -1072,7 +1073,7 @@ describe("withLegacyCommandInstrumentation", () => { ); it.live( - "still redacts a changed global choice flag like --dns-resolver (global EnumFlag safety is CLI-1904's scope, not this fix's)", + "passes a changed global choice flag like --dns-resolver through verbatim (Go parity: isEnumFlag, CLI-1904)", () => { const analytics = mockContextualAnalytics(); @@ -1091,7 +1092,68 @@ describe("withLegacyCommandInstrumentation", () => { Effect.tap(() => Effect.sync(() => { const event = analytics.captured[0]; - expect(event?.properties.flags).toEqual({ "dns-resolver": "" }); + expect(event?.properties.flags).toEqual({ "dns-resolver": "https" }); + }), + ), + ); + }, + ); + + it.live( + "passes a changed global choice flag like --agent through verbatim (Go parity: isEnumFlag, CLI-1904)", + () => { + const analytics = mockContextualAnalytics(); + + return Effect.void.pipe( + withLegacyCommandInstrumentation({ flags: {} }), + Effect.provide(analytics.layer), + Effect.provide(mockProcessControl().layer), + Effect.provide(mockOutput({ format: "text" }).layer), + Effect.provide( + Stdio.layerTest({ + args: Effect.succeed(["backups", "list", "--agent", "yes"]), + }), + ), + Effect.provide(commandRuntimeLayer(["backups", "list"])), + Effect.provide(Layer.succeed(LegacyAgentFlag, "yes" as const)), + Effect.tap(() => + Effect.sync(() => { + const event = analytics.captured[0]; + expect(event?.properties.flags).toEqual({ agent: "yes" }); + }), + ), + ); + }, + ); + + it.live( + "still redacts a global choice flag shadowed by a command's own differently-typed local flag (db diff's local string --output, Go parity)", + () => { + // `db diff` declares its own local `output: Flag.string("output")` (a + // file path, `cmd/db.go:622`) rather than a `Flag.choice` — mirroring + // Go, where that command's own non-enum flag object governs + // `isEnumFlag`, not root's persistent `*utils.EnumFlag`. Simulate that + // shape here: `output` is declared in the handler's own `flags` record + // (so `isFromHandler` is true) but absent from `config`, so it must NOT + // inherit safety from `GLOBAL_CHOICE_FLAG_NAMES` just because the CLI + // name collides with the global `--output` choice flag. + const analytics = mockContextualAnalytics(); + + return Effect.void.pipe( + withLegacyCommandInstrumentation({ flags: { output: "diff.sql" } }), + Effect.provide(analytics.layer), + Effect.provide(mockProcessControl().layer), + Effect.provide(mockOutput({ format: "text" }).layer), + Effect.provide( + Stdio.layerTest({ + args: Effect.succeed(["db", "diff", "--output", "diff.sql"]), + }), + ), + Effect.provide(commandRuntimeLayer(["db", "diff"])), + Effect.tap(() => + Effect.sync(() => { + const event = analytics.captured[0]; + expect(event?.properties.flags).toEqual({ output: "" }); }), ), ); @@ -1170,9 +1232,10 @@ describe("withLegacyCommandInstrumentation", () => { Effect.tap(() => Effect.sync(() => { const event = analytics.captured[0]; - // `output` is a global choice flag — still redacted (CLI-1904's - // scope), but it must be PRESENT, not silently dropped. - expect(event?.properties.flags).toEqual({ output: "" }); + // `output` is a global choice flag — passed through verbatim + // (Go parity: isEnumFlag, CLI-1904), and it must be PRESENT, not + // silently dropped. + expect(event?.properties.flags).toEqual({ output: "json" }); }), ), ); From 3fa4bb094ca75c867d59e880b500b3869c014b6b Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Fri, 10 Jul 2026 15:23:22 +0100 Subject: [PATCH 44/79] fix(cli): match Go path.Match character-class negation in legacy seed globbing (#5856) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Current Behavior `legacyMatchPattern` (the glob matcher backing `[db.seed].sql_paths` and `db reset --version`) treated a leading `!` inside a `[...]` bracket class the same as `^` — shell/fnmatch-style negation. Go's `path.Match` (which `apps/cli-go`'s `fs.Glob`/`afero.Glob` actually use for this) only negates on `^`; `!` is an ordinary literal class member. So a pattern like `[!a].sql` matched `b.sql` in the TS port where the Go CLI would not, meaning `db push --include-seed` / `db reset` could read, hash, and execute seed SQL outside the set the user's Go-compatible config intended. Verified directly against the real Go stdlib source (`$GOROOT/src/path/match.go`) available on this machine, and against `apps/cli-go/pkg/config/config.go` (`fs.Glob`) and `apps/cli-go/internal/migration/repair/repair.go` (`GetMigrationFile` / `afero.Glob`) — both go through the same `path.Match` grammar. ## New Behavior Rather than patching the bug in place, this fixes it by deleting the duplicate: `apps/cli/src/legacy/shared/legacy-path-match.ts` (`legacyPathMatch`) is already a byte-faithful, already-tested port of Go's `path.Match` — introduced earlier for the sibling `db new`/migration seed pipeline (`legacy-seed.ts`) — with correct `^`-only negation, escapes, and malformed-pattern (`ErrBadPattern`) detection. `legacyMatchPattern` and its `matchClass`/`match` helpers are removed, and both call sites are rewired onto `legacyPathMatch`: - `legacy-seed-ops.ts`'s own seed glob (`globOne`) - `reset.handler.ts`'s `--version` migration-file glob (behavior-identical here — `v` is validated as digits-only before the glob ever runs, so no bracket syntax is reachable on this path) A follow-up commit closes a related gap all four `review-changes` reviewers independently flagged: `legacyGlobSeedFiles` was discarding `legacyPathMatch`'s `badPattern` signal, so a malformed pattern (e.g. an unterminated `[` class) fell through to the generic `no files matched pattern` warning instead of Go's `failed to glob files: syntax error in pattern` (`fs.Glob`'s up-front `Match(pattern, "")` validation). The sibling pipeline (`legacy-seed.ts:resolveSeedFiles`) already did this correctly — mirrored it here now that both pipelines share `legacyPathMatch`. **Behavior change worth calling out for release notes:** any existing `[db.seed].sql_paths` config relying on `[!x]` as shell-style negation will now select a different (Go-correct) set of seed files with no error — e.g. `[!0-9]` now means "the literal characters `!`, `0`-`9`", not "not a digit". Use `[^x]` for negation, matching Go/the real Go CLI. This is a correctness fix (the legacy shell's contract is strict Go parity), but it is a silent behavior change for anyone who had unknowingly depended on the old, non-Go-compatible negation. ## Related Issue(s) Fixes https://linear.app/supabase/issue/CLI-1880 ## Notes for reviewers `review-changes` (architect/engineer/security/DX, run against the first commit) all returned APPROVE/SHIP IT. Judgement calls deliberately left open rather than expanded into this PR: - The two seed pipelines (`legacy-seed-ops.ts` and `legacy-seed.ts`) still duplicate the outer `fs.Glob`-style directory-walk/dedup logic (both now delegate the leaf matcher to `legacyPathMatch`, but the surrounding glob-resolution code is still two separate ports). Flagged by the architect reviewer as a good candidate for a follow-up consolidation, out of scope for this fix. - `sql_paths`' glob dialect (Go `path.Match` semantics, `^`-only negation, no POSIX classes) isn't documented anywhere user-facing (`packages/config/src/db.ts`). Flagged by the DX reviewer as worth a docs follow-up, not blocking. --- .../legacy/commands/db/reset/reset.handler.ts | 11 ++- .../commands/db/shared/legacy-seed-ops.ts | 84 +++--------------- .../db/shared/legacy-seed-ops.unit.test.ts | 85 +++++++++++-------- 3 files changed, 69 insertions(+), 111 deletions(-) diff --git a/apps/cli/src/legacy/commands/db/reset/reset.handler.ts b/apps/cli/src/legacy/commands/db/reset/reset.handler.ts index f1637df50a..4b5fc92782 100644 --- a/apps/cli/src/legacy/commands/db/reset/reset.handler.ts +++ b/apps/cli/src/legacy/commands/db/reset/reset.handler.ts @@ -30,11 +30,8 @@ import { LegacyTelemetryState } from "../../../telemetry/legacy-telemetry-state. import { legacyDropUserSchemas } from "../shared/legacy-drop-schemas.ts"; import { LegacyDbBootstrapSeam } from "../shared/legacy-db-bootstrap.seam.service.ts"; import { legacyListLocalMigrations } from "../shared/legacy-pgdelta.cache.ts"; -import { - legacyGetPendingSeeds, - legacyMatchPattern, - legacySeedData, -} from "../shared/legacy-seed-ops.ts"; +import { legacyGetPendingSeeds, legacySeedData } from "../shared/legacy-seed-ops.ts"; +import { legacyPathMatch } from "../../../shared/legacy-path-match.ts"; import { legacyUpsertVaultSecrets } from "../../../shared/legacy-vault.ts"; import { legacySeedBucketsRun } from "../../../shared/legacy-seed-buckets.ts"; import type { LegacyDbResetFlags } from "./reset.command.ts"; @@ -206,7 +203,9 @@ export const legacyDbReset = Effect.fn("legacy.db.reset")(function* (flags: Lega const entries = yield* fs .readDirectory(migrationsDir) .pipe(Effect.orElseSucceed(() => [] as ReadonlyArray)); - const found = entries.some((name) => legacyMatchPattern(`${v}_*.sql`, path.basename(name))); + const found = entries.some( + (name) => legacyPathMatch(`${v}_*.sql`, path.basename(name)).matched, + ); if (!found) { return yield* Effect.fail( new LegacyDbResetMigrationFileError({ diff --git a/apps/cli/src/legacy/commands/db/shared/legacy-seed-ops.ts b/apps/cli/src/legacy/commands/db/shared/legacy-seed-ops.ts index 5caba63a87..2617aee0c3 100644 --- a/apps/cli/src/legacy/commands/db/shared/legacy-seed-ops.ts +++ b/apps/cli/src/legacy/commands/db/shared/legacy-seed-ops.ts @@ -5,6 +5,7 @@ import { Output } from "../../../../shared/output/output.service.ts"; import type { LegacyDbExecError } from "../../../shared/legacy-db-connection.errors.ts"; import type { LegacyDbSession } from "../../../shared/legacy-db-connection.service.ts"; import { legacyCreateSeedTable } from "../../../shared/legacy-migration-history.ts"; +import { LEGACY_BAD_PATTERN_MESSAGE, legacyPathMatch } from "../../../shared/legacy-path-match.ts"; import { legacySplitAndTrim } from "../../../shared/legacy-sql-split.ts"; /** @@ -27,74 +28,6 @@ export interface LegacySeedFile { const META_CHARS = /[*?[\\]/u; -/** - * Go's `path.Match` for a single filename (no `/`). Supports `*` (any run of - * non-separator chars), `?` (one char), `[...]` classes with ranges and a - * leading `^`/`!` negation, and `\` escapes. Filenames never contain `/`, so the - * separator subtlety in Go's matcher does not apply here. - */ -export function legacyMatchPattern(pattern: string, name: string): boolean { - const matchClass = (cls: string, ch: string): boolean => { - let negated = false; - let body = cls; - if (body.startsWith("^") || body.startsWith("!")) { - negated = true; - body = body.slice(1); - } - let matched = false; - for (let k = 0; k < body.length; k++) { - if (body[k + 1] === "-" && k + 2 < body.length) { - if (ch >= body[k]! && ch <= body[k + 2]!) matched = true; - k += 2; - } else if (body[k] === ch) { - matched = true; - } - } - return matched !== negated; - }; - - const match = (p: number, n: number): boolean => { - while (p < pattern.length) { - const pc = pattern[p]!; - if (pc === "*") { - // Collapse consecutive stars, then try to match the rest at every offset. - while (pattern[p] === "*") p++; - if (p === pattern.length) return true; - for (let k = n; k <= name.length; k++) { - if (match(p, k)) return true; - } - return false; - } - if (n >= name.length) return false; - if (pc === "?") { - p++; - n++; - continue; - } - if (pc === "[") { - const end = pattern.indexOf("]", p + 1); - if (end === -1) return false; - if (!matchClass(pattern.slice(p + 1, end), name[n]!)) return false; - p = end + 1; - n++; - continue; - } - if (pc === "\\" && p + 1 < pattern.length) { - if (pattern[p + 1] !== name[n]) return false; - p += 2; - n++; - continue; - } - if (pc !== name[n]) return false; - p++; - n++; - } - return n === name.length; - }; - - return match(0, 0); -} - /** Result of resolving `[db.seed].sql_paths` against the workspace. */ interface LegacyGlobResult { /** Workdir-relative, forward-slashed matches, deduplicated in pattern order. */ @@ -108,8 +41,10 @@ interface LegacyGlobResult { * over `fs.Glob` (`pkg/config/config.go:102-124`). Each pattern is first joined * under the `supabase/` directory (Go resolves `sql_paths` at config load, * `config.go:884`). Matches per pattern are sorted; the overall result preserves - * first-seen order across patterns. A pattern that matches nothing contributes a - * `no files matched pattern: ` warning but is not fatal. + * first-seen order across patterns. A pattern that matches nothing, or is malformed + * (Go's `path.ErrBadPattern`, e.g. an unterminated `[` class), contributes a warning + * but is not fatal — mirroring `fs.Glob`'s up-front `Match(pattern, "")` validation + * (`io/fs/glob.go`) and the sibling seed pipeline's `legacy-seed.ts:resolveSeedFiles`. */ const legacyGlobSeedFiles = Effect.fnUntraced(function* ( fs: FileSystem.FileSystem, @@ -128,6 +63,13 @@ const legacyGlobSeedFiles = Effect.fnUntraced(function* ( // globs those resolved paths without re-prefixing (`config.go:102-124`), so only // normalize separators here; re-joining `supabase/` would double-prefix. const pattern = toSlash(rawPattern); + // Go's `fs.Glob` validates the whole pattern up front (`Match(pattern, "")`); a + // malformed glob is reported as `failed to glob files: ` and + // contributes no matches, rather than the misleading "no files matched" below. + if (legacyPathMatch(pattern, "").badPattern) { + errors.push(`failed to glob files: ${LEGACY_BAD_PATTERN_MESSAGE}`); + continue; + } const matches = yield* globOne(fs, path, workdir, pattern); if (matches.length === 0) { errors.push(`no files matched pattern: ${pattern}`); @@ -202,7 +144,7 @@ const globOne = ( .readDirectory(absDir) .pipe(Effect.orElseSucceed((): ReadonlyArray => [])); for (const name of names) { - if (legacyMatchPattern(file, name)) { + if (legacyPathMatch(file, name).matched) { result.push(d === "" ? name : `${d}/${name}`); } } diff --git a/apps/cli/src/legacy/commands/db/shared/legacy-seed-ops.unit.test.ts b/apps/cli/src/legacy/commands/db/shared/legacy-seed-ops.unit.test.ts index 3c50f914c8..6f0a1dcb3f 100644 --- a/apps/cli/src/legacy/commands/db/shared/legacy-seed-ops.unit.test.ts +++ b/apps/cli/src/legacy/commands/db/shared/legacy-seed-ops.unit.test.ts @@ -7,7 +7,7 @@ import { Data, Effect, Exit, FileSystem, Path } from "effect"; import { mockOutput } from "../../../../../tests/helpers/mocks.ts"; import type { LegacyDbSession } from "../../../shared/legacy-db-connection.service.ts"; -import { legacyMatchPattern, legacySeedData } from "./legacy-seed-ops.ts"; +import { legacyGetPendingSeeds, legacySeedData } from "./legacy-seed-ops.ts"; class TestError extends Data.TaggedError("TestError")<{ readonly message: string }> {} @@ -29,40 +29,57 @@ function fakeSeedSession() { return { session, calls }; } -describe("legacyMatchPattern", () => { - it("matches a literal filename", () => { - expect(legacyMatchPattern("seed.sql", "seed.sql")).toBe(true); - expect(legacyMatchPattern("seed.sql", "other.sql")).toBe(false); - }); - - it("matches `*` against any run of characters", () => { - expect(legacyMatchPattern("*.sql", "seed.sql")).toBe(true); - expect(legacyMatchPattern("*.sql", "0001_init.sql")).toBe(true); - expect(legacyMatchPattern("*.sql", "seed.txt")).toBe(false); - expect(legacyMatchPattern("seed.*", "seed.sql")).toBe(true); - }); - - it("matches `?` against exactly one character", () => { - expect(legacyMatchPattern("seed?.sql", "seed1.sql")).toBe(true); - expect(legacyMatchPattern("seed?.sql", "seed12.sql")).toBe(false); - expect(legacyMatchPattern("seed?.sql", "seed.sql")).toBe(false); - }); - - it("matches character classes with ranges and negation", () => { - expect(legacyMatchPattern("seed[0-9].sql", "seed5.sql")).toBe(true); - expect(legacyMatchPattern("seed[0-9].sql", "seedx.sql")).toBe(false); - expect(legacyMatchPattern("seed[!0-9].sql", "seedx.sql")).toBe(true); - expect(legacyMatchPattern("seed[!0-9].sql", "seed5.sql")).toBe(false); - }); - - it("honors backslash escapes", () => { - expect(legacyMatchPattern("seed\\*.sql", "seed*.sql")).toBe(true); - expect(legacyMatchPattern("seed\\*.sql", "seedx.sql")).toBe(false); - }); +// Glob matching itself is `legacyPathMatch` (`../../../shared/legacy-path-match.ts`), +// a faithful port of Go's `path.Match` already covered by +// `legacy-path-match.unit.test.ts` (including the `^`-only negation / `!`-is-literal +// rule this file used to duplicate — and get wrong — in a local `legacyMatchPattern`). +// This exercises that the seed pipeline's own glob resolution (`legacyGetPendingSeeds`) +// actually uses it end to end, per Go's `config.Glob.Files` → `fs.Glob` → `path.Match`. +describe("legacyGetPendingSeeds (glob character classes)", () => { + it.effect( + "treats a leading `!` in a bracket class as literal, not negation (Go path.Match parity)", + () => { + const dir = mkdtempSync(join(tmpdir(), "legacy-seed-glob-")); + writeFileSync(join(dir, "a.sql"), "select 1;"); + writeFileSync(join(dir, "b.sql"), "select 2;"); + const { session } = fakeSeedSession(); + return Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + // Go's `[!a]` is a positive class of the literal members `!` and `a` — only a + // leading `^` negates. So this pattern matches `a.sql`, not `b.sql` (the old + // shell-style bug negated on `!` too, and would have matched `b.sql` instead). + const pending = yield* legacyGetPendingSeeds(session, fs, path, ["[!a].sql"], dir); + expect(pending.map((seed) => seed.path)).toEqual(["a.sql"]); + rmSync(dir, { recursive: true, force: true }); + }).pipe( + Effect.provide(mockOutput({ format: "text" }).layer), + Effect.provide(BunServices.layer), + ); + }, + ); - it("collapses consecutive stars", () => { - expect(legacyMatchPattern("**.sql", "seed.sql")).toBe(true); - }); + it.effect( + "warns Go's bad-pattern message for an unterminated bracket class, not a bogus no-match", + () => { + // An unclosed `[` is malformed per Go's `path.Match` grammar (`ErrBadPattern`), which + // `fs.Glob` reports as `failed to glob files: syntax error in pattern` — not the + // generic `no files matched pattern` a same-shaped but well-formed glob would get. + const dir = mkdtempSync(join(tmpdir(), "legacy-seed-glob-")); + const { session } = fakeSeedSession(); + const out = mockOutput({ format: "text" }); + return Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const pending = yield* legacyGetPendingSeeds(session, fs, path, ["seed[.sql"], dir); + expect(pending).toEqual([]); + expect(out.rawChunks.map((c) => c.text).join("")).toContain( + "failed to glob files: syntax error in pattern", + ); + rmSync(dir, { recursive: true, force: true }); + }).pipe(Effect.provide(out.layer), Effect.provide(BunServices.layer)); + }, + ); }); const runSeed = ( From bba5486d66f45f4bdf292bce127dedb768a1ab00 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 11 Jul 2026 00:10:25 +0000 Subject: [PATCH 45/79] fix(deps): bump github.com/stripe/pg-schema-diff from 1.0.5 to 1.0.7 in /apps/cli-go in the go-minor group across 1 directory (#5858) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the go-minor group with 1 update in the /apps/cli-go directory: [github.com/stripe/pg-schema-diff](https://github.com/stripe/pg-schema-diff). Updates `github.com/stripe/pg-schema-diff` from 1.0.5 to 1.0.7
Release notes

Sourced from github.com/stripe/pg-schema-diff's releases.

v1.0.7

Security fixes

Fixes two additional SQL injection sinks identified via the same root cause as v1.0.6 (enum labels, #295). Schema-derived values were re-emitted into generated DDL without proper escaping.

Policy role names (policy_sql_generator.go)

AppliesTo role names (sourced from pg_roles.rolname) were interpolated raw into CREATE POLICY ... TO and ALTER POLICY ... TO statements. A user with CREATEROLE privilege could plant a role whose name contains an embedded double-quote to inject arbitrary SQL during plan execution.

Fix: Added escapeRoleNames() helper that applies EscapeIdentifier to each role name, preserving PUBLIC as an unquoted SQL keyword.

Function/procedure names (schema.go buildProcName)

The function used hand-rolled quoting (fmt.Sprintf("\"%s\"(%s)", name, ...)) that did not double embedded double-quotes. A user with CREATE FUNCTION privilege could create a function with " in its name to inject SQL when DROP FUNCTION/DROP PROCEDURE statements are generated.

Fix: Replaced the hand-rolled quoting with EscapeIdentifier(name).

What's Changed

  • fix: escape policy role names and function/procedure names in generated SQL (#296)

Full Changelog: https://github.com/stripe/pg-schema-diff/compare/v1.0.6...v1.0.7

v1.0.6

Security fix

Escapes enum labels in generated SQL to prevent a second-order SQL injection.

Enum labels were interpolated into generated DDL using raw fmt.Sprintf("'%s'", val). A label containing a single quote could break out of the string literal and inject arbitrary SQL, which then executes with the plan runner's (often superuser) privileges when pg-schema-diff generates migration SQL — enabling RCE via COPY ... TO PROGRAM.

All three enum sinks (CREATE TYPE ... AS ENUM, ALTER TYPE ... ADD VALUE, and the BEFORE ordering clause) now route through a new EscapeLiteral helper that doubles single quotes and strips null bytes.

See #295 for details and test evidence.

Recommendation: upgrade to v1.0.6, especially if you run pg-schema-diff against databases where lower-privileged users can create enum types.

Commits
  • 6208f8f fix: escape policy role names and function names in generated SQL (#296)
  • 4ff84db fix: escape enum labels in generated SQL to prevent SQL injection (#295)
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/stripe/pg-schema-diff&package-manager=go_modules&previous-version=1.0.5&new-version=1.0.7)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/cli-go/go.mod | 2 +- apps/cli-go/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/cli-go/go.mod b/apps/cli-go/go.mod index 00a84266a6..875cf3c268 100644 --- a/apps/cli-go/go.mod +++ b/apps/cli-go/go.mod @@ -48,7 +48,7 @@ require ( github.com/spf13/pflag v1.0.10 github.com/spf13/viper v1.21.0 github.com/stretchr/testify v1.11.1 - github.com/stripe/pg-schema-diff v1.0.5 + github.com/stripe/pg-schema-diff v1.0.7 github.com/supabase/cli/pkg v1.0.0 github.com/tidwall/jsonc v0.3.3 github.com/withfig/autocomplete-tools/packages/cobra v1.2.0 diff --git a/apps/cli-go/go.sum b/apps/cli-go/go.sum index 9d22c07d69..676a7cb287 100644 --- a/apps/cli-go/go.sum +++ b/apps/cli-go/go.sum @@ -1111,8 +1111,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/stripe/pg-schema-diff v1.0.5 h1:TNHkiRNMn7ttiBd+YBypAbx9v0SfVls+NQZFtamy1K4= -github.com/stripe/pg-schema-diff v1.0.5/go.mod h1:3IctPaAqm+0LtWw/GiwyRoRlU1/N/+00+eXVk0KZIHs= +github.com/stripe/pg-schema-diff v1.0.7 h1:aVMFqjsnPSeh46hlJnexGPm28nZJUvZK0bEyk6rdFVE= +github.com/stripe/pg-schema-diff v1.0.7/go.mod h1:3IctPaAqm+0LtWw/GiwyRoRlU1/N/+00+eXVk0KZIHs= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/tenntenn/modver v1.0.1 h1:2klLppGhDgzJrScMpkj9Ujy3rXPUspSjAcev9tSEBgA= From 6201e4fac7bbcfc0ef29c80720457e7d7eb8b09c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 11 Jul 2026 00:12:55 +0000 Subject: [PATCH 46/79] fix(deps): bump the npm-major group with 3 updates (#5859) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the npm-major group with 3 updates: [@anthropic-ai/claude-agent-sdk](https://github.com/anthropics/claude-agent-sdk-typescript), [@clack/prompts](https://github.com/bombshell-dev/clack/tree/HEAD/packages/prompts) and [@typescript/native-preview](https://github.com/microsoft/typescript-go). Updates `@anthropic-ai/claude-agent-sdk` from 0.3.199 to 0.3.201
Release notes

Sourced from @​anthropic-ai/claude-agent-sdk's releases.

v0.3.201

What's changed

  • Updated to parity with Claude Code v2.1.201

Update

npm install @anthropic-ai/claude-agent-sdk@0.3.201
# or
yarn add @anthropic-ai/claude-agent-sdk@0.3.201
# or
pnpm add @anthropic-ai/claude-agent-sdk@0.3.201
# or
bun add @anthropic-ai/claude-agent-sdk@0.3.201

v0.3.200

What's changed

  • Added 'manual' as an accepted alias for the 'default' permission mode in SDK inputs
  • Fixed onSetPermissionMode callback not firing for SDK-hosted Remote Control sessions
  • Fixed set_model control request accepting unrecognized model strings; invalid models are now rejected before latching

Update

npm install @anthropic-ai/claude-agent-sdk@0.3.200
# or
yarn add @anthropic-ai/claude-agent-sdk@0.3.200
# or
pnpm add @anthropic-ai/claude-agent-sdk@0.3.200
# or
bun add @anthropic-ai/claude-agent-sdk@0.3.200
Changelog

Sourced from @​anthropic-ai/claude-agent-sdk's changelog.

0.3.201

  • Updated to parity with Claude Code v2.1.201

0.3.200

  • Added 'manual' as an accepted alias for the 'default' permission mode in SDK inputs
  • Fixed onSetPermissionMode callback not firing for SDK-hosted Remote Control sessions
  • Fixed set_model control request accepting unrecognized model strings; invalid models are now rejected before latching
Commits

Updates `@clack/prompts` from 1.6.0 to 1.7.0
Release notes

Sourced from @​clack/prompts's releases.

@​clack/prompts@​1.7.0

Minor Changes

  • #574 8f1c380 Thanks @​dreyfus92! - Add showInstructions option to select, multiselect, and groupMultiselect. Keyboard hints remain shown by default; pass showInstructions: false to hide them.

Patch Changes

Changelog

Sourced from @​clack/prompts's changelog.

1.7.0

Minor Changes

  • #574 8f1c380 Thanks @​dreyfus92! - Add showInstructions option to select, multiselect, and groupMultiselect. Keyboard hints remain shown by default; pass showInstructions: false to hide them.

Patch Changes

Commits

Updates `@typescript/native-preview` from 7.0.0-dev.20260702.3 to 7.0.0-dev.20260703.1
Commits

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/cli/package.json | 4 +- pnpm-lock.yaml | 506 +++++++++++++++++++++++++++++++----------- pnpm-workspace.yaml | 2 +- 3 files changed, 378 insertions(+), 134 deletions(-) diff --git a/apps/cli/package.json b/apps/cli/package.json index e7dd1a8292..4d458e4441 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -43,9 +43,9 @@ "jose": "^6.2.3" }, "devDependencies": { - "@anthropic-ai/claude-agent-sdk": "^0.3.199", + "@anthropic-ai/claude-agent-sdk": "^0.3.201", "@anthropic-ai/sdk": "^0.110.0", - "@clack/prompts": "^1.6.0", + "@clack/prompts": "^1.7.0", "@effect/atom-react": "catalog:", "@effect/platform-bun": "catalog:", "@effect/sql-pg": "catalog:", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 00f08c3538..fb2200064f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -37,8 +37,8 @@ catalogs: specifier: ^1.3.14 version: 1.3.14 '@typescript/native-preview': - specifier: 7.0.0-dev.20260702.3 - version: 7.0.0-dev.20260702.3 + specifier: 7.0.0-dev.20260703.1 + version: 7.0.0-dev.20260703.1 '@vitest/coverage-istanbul': specifier: ^4.1.9 version: 4.1.9 @@ -97,14 +97,14 @@ importers: version: 6.2.3 devDependencies: '@anthropic-ai/claude-agent-sdk': - specifier: ^0.3.199 - version: 0.3.199(@anthropic-ai/sdk@0.110.0(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(zod@4.4.3) + specifier: ^0.3.201 + version: 0.3.201(@anthropic-ai/sdk@0.110.0(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(zod@4.4.3) '@anthropic-ai/sdk': specifier: ^0.110.0 version: 0.110.0(zod@4.4.3) '@clack/prompts': - specifier: ^1.6.0 - version: 1.6.0 + specifier: ^1.7.0 + version: 1.7.0 '@effect/atom-react': specifier: 'catalog:' version: 4.0.0-beta.93(effect@4.0.0-beta.93)(react@19.2.7)(scheduler@0.27.0) @@ -155,7 +155,7 @@ importers: version: 19.2.17 '@typescript/native-preview': specifier: 'catalog:' - version: 7.0.0-dev.20260702.3 + version: 7.0.0-dev.20260703.1 '@vercel/detect-agent': specifier: ^1.2.3 version: 1.2.3 @@ -259,7 +259,7 @@ importers: version: 1.3.14 '@typescript/native-preview': specifier: 'catalog:' - version: 7.0.0-dev.20260702.3 + version: 7.0.0-dev.20260703.1 '@vitest/coverage-istanbul': specifier: 'catalog:' version: 4.1.9(vitest@4.1.9) @@ -339,7 +339,7 @@ importers: version: 1.3.14 '@typescript/native-preview': specifier: 'catalog:' - version: 7.0.0-dev.20260702.3 + version: 7.0.0-dev.20260703.1 '@vitest/coverage-istanbul': specifier: 'catalog:' version: 4.1.9(vitest@4.1.9) @@ -381,7 +381,7 @@ importers: version: 1.3.14 '@typescript/native-preview': specifier: 'catalog:' - version: 7.0.0-dev.20260702.3 + version: 7.0.0-dev.20260703.1 '@vitest/coverage-istanbul': specifier: 'catalog:' version: 4.1.9(vitest@4.1.9) @@ -431,7 +431,7 @@ importers: version: 1.3.14 '@typescript/native-preview': specifier: 'catalog:' - version: 7.0.0-dev.20260702.3 + version: 7.0.0-dev.20260703.1 '@vitest/coverage-istanbul': specifier: 'catalog:' version: 4.1.9(vitest@4.1.9) @@ -471,7 +471,7 @@ importers: version: 1.3.14 '@typescript/native-preview': specifier: 'catalog:' - version: 7.0.0-dev.20260702.3 + version: 7.0.0-dev.20260703.1 '@vitest/coverage-istanbul': specifier: 'catalog:' version: 4.1.9(vitest@4.1.9) @@ -523,7 +523,7 @@ importers: version: 1.3.14 '@typescript/native-preview': specifier: 'catalog:' - version: 7.0.0-dev.20260702.3 + version: 7.0.0-dev.20260703.1 '@vitest/coverage-istanbul': specifier: 'catalog:' version: 4.1.9(vitest@4.1.9) @@ -547,7 +547,7 @@ importers: dependencies: '@nx/devkit': specifier: 'catalog:' - version: 23.0.1(nx@23.0.1(@swc-node/register@1.11.1(@swc/core@1.15.43)(@swc/types@0.1.27)(typescript@6.0.3))(@swc/core@1.15.43)) + version: 23.0.1(nx@23.0.2(@swc-node/register@1.11.1(@swc/core@1.15.43)(@swc/types@0.1.27)(typescript@6.0.3))(@swc/core@1.15.43)) vitest: specifier: 'catalog:' version: 4.1.9(@types/node@26.1.1)(@vitest/coverage-istanbul@4.1.9)(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) @@ -570,52 +570,52 @@ packages: resolution: {integrity: sha512-p+CMKJ93HFmLkjXKlXiVGlMQEuRb6H0MokBSwUsX+S6BRX8eV5naFZpQJFfJHjRZY0Hmnqy1/r6UWl3x+19zYA==} engines: {node: '>=18'} - '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.199': - resolution: {integrity: sha512-0813IEsPlA3GQZ86CGmRPh/2DY/iEuBkXV7CRAj55sQ30oIm+N/BbXzI/jH7WiWsHh3pCsRx65dbswf6jA5Uuw==} + '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.201': + resolution: {integrity: sha512-8Mcb3BDyKUGfJWFFTWwt+at37lbDH3ZwVtUNPWGG1toZ75RDCJry5U4kXRvQ2xokvJQlA0E+eNp6keWe5ZH22Q==} cpu: [arm64] os: [darwin] - '@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.199': - resolution: {integrity: sha512-iUenoE7UbWFfYjdfaeJkPl4qpXajPcRw8SzMYn0PK2LsBE5SJTfEdZyvtypNaKiWzsCCaU4MV1yzR8S7i/wZQg==} + '@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.201': + resolution: {integrity: sha512-TFR2bu0+ml3RHoMrtsgD0qDK5Oknw8kYGBV7qpQHn+IWmE96gnHhogG1LpJwpHtni08XkJIjfWk1DdlsUYtRkQ==} cpu: [x64] os: [darwin] - '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.199': - resolution: {integrity: sha512-mcqHuNHsA2V589rt0JpCsweS8kZ8LkKZi0qmMLqN3oZz+ar4DsuDjLDaNI1C5f+W/nz5G/st+2W3ZCJubeOqVQ==} + '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.201': + resolution: {integrity: sha512-EiqbpfJIpChfkn+8Uj061Qjyw0eaRcOXtdrvVuHANyj8ZErVOr8HlH6op9PSeIUa9TX0m2+tNgKPQvOGseQckA==} cpu: [arm64] os: [linux] libc: [musl] - '@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.199': - resolution: {integrity: sha512-wJ4GJCwrdVv4TWTiEwh2gUjrddIg+oMhoUcfXhtZgZeqXfGzxVWJa3HudX5xsVq/wktvM65CfdYly2blVBVG8Q==} + '@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.201': + resolution: {integrity: sha512-mShTo3MwF0gkN4dDw78wWJiB6aBDVRkl81cnApvoBofpdyUBYgm9Gw16CCjDTgelMKeBFqN6ErJpwjI3wbP00A==} cpu: [arm64] os: [linux] libc: [glibc] - '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.199': - resolution: {integrity: sha512-wr7IIQ9d9H7Tt79Mn4ga8iNLzsvoPnAbBtsnUVBjgcAxKWrJ+QV2wCsowhhSc7DWNueiurdxNsLIwd07BzhfCA==} + '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.201': + resolution: {integrity: sha512-IbxnzO5UCbqbm2TnzCHkSyJorAFw2isdKdIsFCTxJJjSs3ZC+v3LC1QSUiVCx0qi+CV6w3MKx6mLI11mrvhbbQ==} cpu: [x64] os: [linux] libc: [musl] - '@anthropic-ai/claude-agent-sdk-linux-x64@0.3.199': - resolution: {integrity: sha512-nNLk8KcM33AYQ0P1cNK72kE14iTIuM5RTr0CrQm40FwdrPuVZ/se6KggpE3eje8hFYJu/1WrJqcZxtJSL6qnPg==} + '@anthropic-ai/claude-agent-sdk-linux-x64@0.3.201': + resolution: {integrity: sha512-jrJBrRWrSuoFKIgjyqxHqmfd6Pb3Bs5Bvakg0knXCTC4fbUXGnC9Q6u7gdDwgXohUNP6/DD+s8U7bivvvVv0dg==} cpu: [x64] os: [linux] libc: [glibc] - '@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.199': - resolution: {integrity: sha512-EQFMATdpp6XXYXw6Ih83BFCDj2PXqMYkM6nq3OrOVUF19xTe6o5dWhGzZ6TQfauvWf2BHFIfBDzZz82m8ffV8w==} + '@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.201': + resolution: {integrity: sha512-UsoytRJ/037uHpb3ATrIoe+AgwTf+PwKuFLGjddHAV/11wERJs0hlrnSmcnp43kf0PFxoSNinngme96YYASmQg==} cpu: [arm64] os: [win32] - '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.199': - resolution: {integrity: sha512-6W2djU/NnOCY4NEdDjtZ3LIzFFQMT2OH9m5y5Hc4bko3KIXr0WHLrCJdgGiVycOWEd03OEsC4uYIIYIeueBAHw==} + '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.201': + resolution: {integrity: sha512-PhalN/0cWcqDfbx7iwoLNR2gurjTiqhBk1G6K+NRScxEcQjWuu5xKXCcdbX8ePVpT+nbEMmFEFpn2y+8V8hIdA==} cpu: [x64] os: [win32] - '@anthropic-ai/claude-agent-sdk@0.3.199': - resolution: {integrity: sha512-fET9rfYR2DgjVG4Yri02Q6YL+SWBlcrsd3Dx96CpZSM50W4Jqh1sEC06hXIEHL0uDZ/kS//Y2uPpCQ9GQqh8Bg==} + '@anthropic-ai/claude-agent-sdk@0.3.201': + resolution: {integrity: sha512-InT1XLmf2QpldWdtznKDWEoGJT4p+sXh24yxbeBQ++lMJCzMrI0W27MEmmmDWx0otpa+ubdHCF5YQ6oiNt7cmg==} engines: {node: '>=18.0.0'} peerDependencies: '@anthropic-ai/sdk': '>=0.93.0' @@ -702,12 +702,12 @@ packages: resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} engines: {node: '>=6.9.0'} - '@clack/core@1.4.2': - resolution: {integrity: sha512-0Ty/1Gfm+Kb07sXcuESjyKfwEhSy4Ns1AgeEisHb/bDY5fWme0tTeTkU14T1Gmcs17YIjB/teiDe4uaCghbYqQ==} + '@clack/core@1.4.3': + resolution: {integrity: sha512-/kr3UWNtdJfxZtPgDqUOmG2pvwlmcLGheex5yiZKdwbzZJxhV+HMNR9QNmyY5cGwTNV6LrR7Jtp+KjhUAP1qBQ==} engines: {node: '>= 20.12.0'} - '@clack/prompts@1.6.0': - resolution: {integrity: sha512-EYlRokl8szrP9Z25qT5aepMdBjzBvHF9ZEhzIiUBc9guz/T31EqRgvD0QSgZcpE93xiwrr+OkB4nz0BZyF6fSA==} + '@clack/prompts@1.7.0': + resolution: {integrity: sha512-y7/yvZ2TPAnR9+jnc00klvNNLkJiXFFrQA/hlLCcxA9a2A4zQIOimyFQ9XfwYKiGD1fb5GY8vbKIIgO8d5Tb2A==} engines: {node: '>= 20.12.0'} '@colors/colors@1.5.0': @@ -1396,55 +1396,109 @@ packages: cpu: [arm64] os: [darwin] + '@nx/nx-darwin-arm64@23.0.2': + resolution: {integrity: sha512-9sqhZMVFpF+qM7hq6y2xA4gVK+6RdxRioAwHxorhOZRSXdW7Y7NESs5fm8vOmdddlG07QB7sMefOKLrqCV3zGg==} + cpu: [arm64] + os: [darwin] + '@nx/nx-darwin-x64@23.0.1': resolution: {integrity: sha512-e/lvzHKN6gpuD7MqEtfH1fOfnR75E55ytYNt8jaRxKI6EvpCq+Q3MunDuh9GQYAkqDrUqE7AhHrHc+eKATVEHw==} cpu: [x64] os: [darwin] + '@nx/nx-darwin-x64@23.0.2': + resolution: {integrity: sha512-p6L3AvRhRRaR8Bl3jr76/9H04RdWUQbSgB7agK7GB7vqaLI8RifP2lqeaXcAngzjDAjw2EAf0TjOBP+T67hhcg==} + cpu: [x64] + os: [darwin] + '@nx/nx-freebsd-x64@23.0.1': resolution: {integrity: sha512-f582OhSYN9qHpA9Ox9qnr3kZSZ7gQHs7crmBUutmbXmZQB2TDS/TlhvYSNnxudpwHR/tuWGi2IOQqa7zGOZj1Q==} cpu: [x64] os: [freebsd] + '@nx/nx-freebsd-x64@23.0.2': + resolution: {integrity: sha512-/py4I8Rp2UURses9H/+SQmgPVnHVSJgPimJLhXIfsRavKGu4RS7Ddu1OyNqSkCT3Otic6ImMTtkufURW22KiEQ==} + cpu: [x64] + os: [freebsd] + '@nx/nx-linux-arm-gnueabihf@23.0.1': resolution: {integrity: sha512-VjhqPc6E7aiI0e+lowrkVbdyulsmP9fgMdcX1mCzXCEu/XZDcUbZ5qveR964cMhvm5qKn0ILJtJOUqZgmOT3Xg==} cpu: [arm] os: [linux] + '@nx/nx-linux-arm-gnueabihf@23.0.2': + resolution: {integrity: sha512-xv2IzeiWJFWi4WjK0ocMkP+ze1lDeoPVCg0xOTqVs40gM66V1wVw3EK077gTqU4m0Bq1wUxe6/I8WaIGlkLgug==} + cpu: [arm] + os: [linux] + '@nx/nx-linux-arm64-gnu@23.0.1': resolution: {integrity: sha512-zX2JdHQejZWB3DRgNsh77qOVYaSSjSLuBP2qIqc7EWVlCUnR7Aj3e65PTIps4LxMMmUp4twZA2ezS0rtyK2A4w==} cpu: [arm64] os: [linux] libc: [glibc] + '@nx/nx-linux-arm64-gnu@23.0.2': + resolution: {integrity: sha512-ckA6hTXST+agxt/HzPGqMss9qFCZhO9b07o8usygb7QFBYQRXFgcYzhTblq4yiTL5ibJmXAGGh98011fLA2MVw==} + cpu: [arm64] + os: [linux] + libc: [glibc] + '@nx/nx-linux-arm64-musl@23.0.1': resolution: {integrity: sha512-9lyhxRNBgNYwHt6paq0OLzoKNoEGF5LnNW2YYrgFY8Cjtsg/Q4pcfZ1vB5o9FX9OmUgUQs3t2d4tU8YDukRUWg==} cpu: [arm64] os: [linux] libc: [musl] + '@nx/nx-linux-arm64-musl@23.0.2': + resolution: {integrity: sha512-PAxBxy7m//cKxUeIb6Sk2X5MJ/wjcJcqCx7/L0p8omTt/y/+q1TGpVy6qmJMPUWzNgAULXtsVRSOK4rmiBcrQQ==} + cpu: [arm64] + os: [linux] + libc: [musl] + '@nx/nx-linux-x64-gnu@23.0.1': resolution: {integrity: sha512-kVszY2xRyyrCXgdCdM1qG1WUhDjNPZxtdWq86a0TyIRJjfJTP9NHqpyhmvj9c2RdZxKVWHotx6fBJzY6Vn2ZrA==} cpu: [x64] os: [linux] libc: [glibc] + '@nx/nx-linux-x64-gnu@23.0.2': + resolution: {integrity: sha512-gR146Mo+BjhrIU2fNOJeVjX8HEAHrtmc7IyrD0qD9yqyq0l9Mdx92JnMW3yVQaYIMpPJIqsOvHTDIUbTLFrmTA==} + cpu: [x64] + os: [linux] + libc: [glibc] + '@nx/nx-linux-x64-musl@23.0.1': resolution: {integrity: sha512-co71K2n4zcS1SYR8EBRlCvIko7M1YycO2tZL0nrCrga87AF5dzCwx+wEclpyCR/4tNOY3FrACk4gIkVskh3CdA==} cpu: [x64] os: [linux] libc: [musl] + '@nx/nx-linux-x64-musl@23.0.2': + resolution: {integrity: sha512-L7JkaoAI0p+DpAi2CNCqPq8nHWkApQw2td0Cyt+stOx/OJlmEteyc/MXyTHBoMPoopNj1wWANJJQm/G1anOLhQ==} + cpu: [x64] + os: [linux] + libc: [musl] + '@nx/nx-win32-arm64-msvc@23.0.1': resolution: {integrity: sha512-7oma7iy5fbnn+x5AP7SFGMuleAA2R5RZm26dn+faikyQ4PXjoRAikWJJNiOWAeCA0BaMAeVedI6fJeAsVeDUKg==} cpu: [arm64] os: [win32] + '@nx/nx-win32-arm64-msvc@23.0.2': + resolution: {integrity: sha512-B3ePaYeu31ivP3i72Vou5RBYFVrDKCMZgz7Jsax+RvEJa8dNfI+Ynh/PSoXzHudN0YsrekkKbjlxNvp/D6fWFA==} + cpu: [arm64] + os: [win32] + '@nx/nx-win32-x64-msvc@23.0.1': resolution: {integrity: sha512-TE/wvBa2cpkVXmk/AXUQAneong4JReS2hyNpAUONKG1yXU7TDKe0wvn1xQXxAbyspudT9NuCnVtpVuEkRz8S+Q==} cpu: [x64] os: [win32] + '@nx/nx-win32-x64-msvc@23.0.2': + resolution: {integrity: sha512-/NiB9w8nYrw7LUkcmwAJ9wis5O+kh3ahSZXMDHVYgFnD8yN7kLql39MNJD+/DGstjNSDvWh45ftqr7mZMi6wpQ==} + cpu: [x64] + os: [win32] + '@octokit/auth-token@6.0.0': resolution: {integrity: sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==} engines: {node: '>= 20'} @@ -2867,50 +2921,50 @@ packages: '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} - '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260702.3': - resolution: {integrity: sha512-sswJ7XRSH17/jDv6eqlRyNzjQev4w+TH+16RgeoZkKbQfZnVuLchF4ORCecEuia15X2yE8pbiQGY596LVbGKdA==} + '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260703.1': + resolution: {integrity: sha512-EwJEd6hfaHrrbSLat6+xX8fZiAvG+/Ae1gqvJEayTNdDbkrZxmeLS23YdjUmDMIOKI2wa7zXQDid8WtrvDfMTQ==} engines: {node: '>=16.20.0'} cpu: [arm64] os: [darwin] - '@typescript/native-preview-darwin-x64@7.0.0-dev.20260702.3': - resolution: {integrity: sha512-j8ZYT/chWtx3QT1Bghp0/+2g6aKLaxRVpXEHt6Fjdj4OXQTpH4pljOE0+LeAqaET88PUo/XM9pi0TJ5TJwHLMg==} + '@typescript/native-preview-darwin-x64@7.0.0-dev.20260703.1': + resolution: {integrity: sha512-tcS3gpivMq+BAiaupFqM5vuERykogJlfD4CjoxkSCksmmsKgWV96S2U/LjrKgll8R6/OEkg2VL2ycRG9+tIuHw==} engines: {node: '>=16.20.0'} cpu: [x64] os: [darwin] - '@typescript/native-preview-linux-arm64@7.0.0-dev.20260702.3': - resolution: {integrity: sha512-FEBt9UER27yvn3o4quXZ9fQ/Hd39hxFPljJe8dxstCvyYLh0A9TwDuuwghWCisp5U9TDWWtl8m1GLjvqIKc6IA==} + '@typescript/native-preview-linux-arm64@7.0.0-dev.20260703.1': + resolution: {integrity: sha512-m8IJTOneLXRtq6prLz8uuhp463kEt+AHV5Ceqp7G0o3eAvbKP059OwzK6WXCS5J0B3ZxX+BwBf9wDXSNidWJCw==} engines: {node: '>=16.20.0'} cpu: [arm64] os: [linux] - '@typescript/native-preview-linux-arm@7.0.0-dev.20260702.3': - resolution: {integrity: sha512-SM73Mp3oYUWkyHmym0MVfPnMeLGM6oo1vcPZTxVoL7BOKWkJKK+iM7D1tcqDSZNPwURSkWMxP17Dh+4JPqzikg==} + '@typescript/native-preview-linux-arm@7.0.0-dev.20260703.1': + resolution: {integrity: sha512-nluwnKcdGo6laWWYtWF3zFUDfi7nNrcD5S/T5382fVmtKmNqIdzFm9ANgSSGAavuzfdu9YJLaVWpx72Oe40AEQ==} engines: {node: '>=16.20.0'} cpu: [arm] os: [linux] - '@typescript/native-preview-linux-x64@7.0.0-dev.20260702.3': - resolution: {integrity: sha512-CY/r7gHjDnf3xWptpQVRQnagMdUDbh+zL01SF+cTJsg1WVY3TtVNrCo48dwAnJL+U97qx1SKaGVsi/8cSPwrZA==} + '@typescript/native-preview-linux-x64@7.0.0-dev.20260703.1': + resolution: {integrity: sha512-5ga94rso68kaxJUzaufDVhka1zPaSbPgNXaemQO8faPW0crvRIkbv0g8bpG4pdWMV0tTylmluDles4ZcAzOprg==} engines: {node: '>=16.20.0'} cpu: [x64] os: [linux] - '@typescript/native-preview-win32-arm64@7.0.0-dev.20260702.3': - resolution: {integrity: sha512-9U/ahdJAWZdYxPdNry3VAhB9avaQCiqsEk2PfheeKrKSKCtfs5FoPXy/QXAaIGIBK/Lw4a7OqyVyEbd0i77TkQ==} + '@typescript/native-preview-win32-arm64@7.0.0-dev.20260703.1': + resolution: {integrity: sha512-YeRqOUuSyhbtryBSUE073OLpReIQXWJVyjP45gPLJ+0KAGvkd1VFz2FY1JEo++LyHoqXsGD2+qvdPpnTfSAIJg==} engines: {node: '>=16.20.0'} cpu: [arm64] os: [win32] - '@typescript/native-preview-win32-x64@7.0.0-dev.20260702.3': - resolution: {integrity: sha512-kPqfivHuzzEK2NFEl2QeCCiWJ4qkGMJYFX9TmJ4ylh784XICgQLX3PJe7OCqZcUicfeMIIDu6m3flcCi+4tW4Q==} + '@typescript/native-preview-win32-x64@7.0.0-dev.20260703.1': + resolution: {integrity: sha512-WYNcbTaxCPgiDLrL2aPJ3BJA05nJU8DK0fUFGkGAER0oM+KcJcQUBeUkXg/7A4HBTNYcj7zIxqNgkJU1TRa/Kw==} engines: {node: '>=16.20.0'} cpu: [x64] os: [win32] - '@typescript/native-preview@7.0.0-dev.20260702.3': - resolution: {integrity: sha512-j21laxUja23ex6qYGjwiSiQ6YpS/p5E7lCMDYKDBAdBv93Z1NzV96BZV3gdRwl+buH09EPuBAiqxKQqRO1grhw==} + '@typescript/native-preview@7.0.0-dev.20260703.1': + resolution: {integrity: sha512-qyEHkEeRWCSGLa6a8oArnMPdn3Vcl1AZj8YHLO7lK0VjEEF/YJfz1FvxmpEeuhy0ZDfvJ7GtgXbvbjcU3zpPjQ==} engines: {node: '>=16.20.0'} hasBin: true @@ -3179,6 +3233,9 @@ packages: axios@1.16.0: resolution: {integrity: sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==} + axios@1.16.1: + resolution: {integrity: sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==} + b4a@1.8.1: resolution: {integrity: sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==} peerDependencies: @@ -5313,6 +5370,18 @@ packages: '@swc/core': optional: true + nx@23.0.2: + resolution: {integrity: sha512-e5H6ceqj0Z8ovAmtsiHXswwpiNPrr1DRhvJMTpc2AW8G1za9PKxk3bP5josShsIrmGEsOlBNZZsxszXA2+Q2dw==} + hasBin: true + peerDependencies: + '@swc-node/register': ^1.11.1 + '@swc/core': ^1.15.8 + peerDependenciesMeta: + '@swc-node/register': + optional: true + '@swc/core': + optional: true + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -6852,44 +6921,44 @@ snapshots: ansi-styles: 6.2.3 is-fullwidth-code-point: 5.1.0 - '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.199': + '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.201': optional: true - '@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.199': + '@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.201': optional: true - '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.199': + '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.201': optional: true - '@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.199': + '@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.201': optional: true - '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.199': + '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.201': optional: true - '@anthropic-ai/claude-agent-sdk-linux-x64@0.3.199': + '@anthropic-ai/claude-agent-sdk-linux-x64@0.3.201': optional: true - '@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.199': + '@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.201': optional: true - '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.199': + '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.201': optional: true - '@anthropic-ai/claude-agent-sdk@0.3.199(@anthropic-ai/sdk@0.110.0(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(zod@4.4.3)': + '@anthropic-ai/claude-agent-sdk@0.3.201(@anthropic-ai/sdk@0.110.0(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(zod@4.4.3)': dependencies: '@anthropic-ai/sdk': 0.110.0(zod@4.4.3) '@modelcontextprotocol/sdk': 1.29.0(zod@4.4.3) zod: 4.4.3 optionalDependencies: - '@anthropic-ai/claude-agent-sdk-darwin-arm64': 0.3.199 - '@anthropic-ai/claude-agent-sdk-darwin-x64': 0.3.199 - '@anthropic-ai/claude-agent-sdk-linux-arm64': 0.3.199 - '@anthropic-ai/claude-agent-sdk-linux-arm64-musl': 0.3.199 - '@anthropic-ai/claude-agent-sdk-linux-x64': 0.3.199 - '@anthropic-ai/claude-agent-sdk-linux-x64-musl': 0.3.199 - '@anthropic-ai/claude-agent-sdk-win32-arm64': 0.3.199 - '@anthropic-ai/claude-agent-sdk-win32-x64': 0.3.199 + '@anthropic-ai/claude-agent-sdk-darwin-arm64': 0.3.201 + '@anthropic-ai/claude-agent-sdk-darwin-x64': 0.3.201 + '@anthropic-ai/claude-agent-sdk-linux-arm64': 0.3.201 + '@anthropic-ai/claude-agent-sdk-linux-arm64-musl': 0.3.201 + '@anthropic-ai/claude-agent-sdk-linux-x64': 0.3.201 + '@anthropic-ai/claude-agent-sdk-linux-x64-musl': 0.3.201 + '@anthropic-ai/claude-agent-sdk-win32-arm64': 0.3.201 + '@anthropic-ai/claude-agent-sdk-win32-x64': 0.3.201 '@anthropic-ai/sdk@0.110.0(zod@4.4.3)': dependencies: @@ -6919,7 +6988,7 @@ snapshots: '@babel/types': 7.29.7 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -6991,7 +7060,7 @@ snapshots: '@babel/parser': 7.29.7 '@babel/template': 7.29.7 '@babel/types': 7.29.7 - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) transitivePeerDependencies: - supports-color @@ -7000,14 +7069,14 @@ snapshots: '@babel/helper-string-parser': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 - '@clack/core@1.4.2': + '@clack/core@1.4.3': dependencies: fast-wrap-ansi: 0.2.2 sisteransi: 1.0.5 - '@clack/prompts@1.6.0': + '@clack/prompts@1.7.0': dependencies: - '@clack/core': 1.4.2 + '@clack/core': 1.4.3 fast-string-width: 3.0.2 fast-wrap-ansi: 0.2.2 sisteransi: 1.0.5 @@ -7572,13 +7641,13 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 - '@nx/devkit@23.0.1(nx@23.0.1(@swc-node/register@1.11.1(@swc/core@1.15.43)(@swc/types@0.1.27)(typescript@6.0.3))(@swc/core@1.15.43))': + '@nx/devkit@23.0.1(nx@23.0.2(@swc-node/register@1.11.1(@swc/core@1.15.43)(@swc/types@0.1.27)(typescript@6.0.3))(@swc/core@1.15.43))': dependencies: '@zkochan/js-yaml': 0.0.7 ejs: 5.0.1 enquirer: 2.3.6 minimatch: 10.2.5 - nx: 23.0.1(@swc-node/register@1.11.1(@swc/core@1.15.43)(@swc/types@0.1.27)(typescript@6.0.3))(@swc/core@1.15.43) + nx: 23.0.2(@swc-node/register@1.11.1(@swc/core@1.15.43)(@swc/types@0.1.27)(typescript@6.0.3))(@swc/core@1.15.43) semver: 7.8.5 tslib: 2.8.1 yargs-parser: 21.1.1 @@ -7586,33 +7655,63 @@ snapshots: '@nx/nx-darwin-arm64@23.0.1': optional: true + '@nx/nx-darwin-arm64@23.0.2': + optional: true + '@nx/nx-darwin-x64@23.0.1': optional: true + '@nx/nx-darwin-x64@23.0.2': + optional: true + '@nx/nx-freebsd-x64@23.0.1': optional: true + '@nx/nx-freebsd-x64@23.0.2': + optional: true + '@nx/nx-linux-arm-gnueabihf@23.0.1': optional: true + '@nx/nx-linux-arm-gnueabihf@23.0.2': + optional: true + '@nx/nx-linux-arm64-gnu@23.0.1': optional: true + '@nx/nx-linux-arm64-gnu@23.0.2': + optional: true + '@nx/nx-linux-arm64-musl@23.0.1': optional: true + '@nx/nx-linux-arm64-musl@23.0.2': + optional: true + '@nx/nx-linux-x64-gnu@23.0.1': optional: true + '@nx/nx-linux-x64-gnu@23.0.2': + optional: true + '@nx/nx-linux-x64-musl@23.0.1': optional: true + '@nx/nx-linux-x64-musl@23.0.2': + optional: true + '@nx/nx-win32-arm64-msvc@23.0.1': optional: true + '@nx/nx-win32-arm64-msvc@23.0.2': + optional: true + '@nx/nx-win32-x64-msvc@23.0.1': optional: true + '@nx/nx-win32-x64-msvc@23.0.2': + optional: true + '@octokit/auth-token@6.0.0': {} '@octokit/core@7.0.6': @@ -8424,7 +8523,7 @@ snapshots: conventional-changelog-writer: 8.4.0 conventional-commits-filter: 5.0.0 conventional-commits-parser: 6.4.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) import-from-esm: 2.0.0 lodash-es: 4.18.1 micromatch: 4.0.8 @@ -8442,7 +8541,7 @@ snapshots: '@octokit/plugin-throttling': 11.0.3(@octokit/core@7.0.6) '@semantic-release/error': 4.0.0 aggregate-error: 5.0.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) dir-glob: 3.0.1 http-proxy-agent: 9.1.0 https-proxy-agent: 9.1.0 @@ -8483,7 +8582,7 @@ snapshots: conventional-changelog-writer: 8.4.0 conventional-commits-filter: 5.0.0 conventional-commits-parser: 6.4.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) import-from-esm: 2.0.0 lodash-es: 4.18.1 read-package-up: 11.0.0 @@ -8584,7 +8683,7 @@ snapshots: '@swc-node/sourcemap-support': 0.6.1 '@swc/core': 1.15.43 colorette: 2.0.20 - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) oxc-resolver: 11.21.3 pirates: 4.0.7 tslib: 2.8.1 @@ -8751,36 +8850,36 @@ snapshots: dependencies: '@types/node': 26.1.1 - '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260702.3': + '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260703.1': optional: true - '@typescript/native-preview-darwin-x64@7.0.0-dev.20260702.3': + '@typescript/native-preview-darwin-x64@7.0.0-dev.20260703.1': optional: true - '@typescript/native-preview-linux-arm64@7.0.0-dev.20260702.3': + '@typescript/native-preview-linux-arm64@7.0.0-dev.20260703.1': optional: true - '@typescript/native-preview-linux-arm@7.0.0-dev.20260702.3': + '@typescript/native-preview-linux-arm@7.0.0-dev.20260703.1': optional: true - '@typescript/native-preview-linux-x64@7.0.0-dev.20260702.3': + '@typescript/native-preview-linux-x64@7.0.0-dev.20260703.1': optional: true - '@typescript/native-preview-win32-arm64@7.0.0-dev.20260702.3': + '@typescript/native-preview-win32-arm64@7.0.0-dev.20260703.1': optional: true - '@typescript/native-preview-win32-x64@7.0.0-dev.20260702.3': + '@typescript/native-preview-win32-x64@7.0.0-dev.20260703.1': optional: true - '@typescript/native-preview@7.0.0-dev.20260702.3': + '@typescript/native-preview@7.0.0-dev.20260703.1': optionalDependencies: - '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260702.3 - '@typescript/native-preview-darwin-x64': 7.0.0-dev.20260702.3 - '@typescript/native-preview-linux-arm': 7.0.0-dev.20260702.3 - '@typescript/native-preview-linux-arm64': 7.0.0-dev.20260702.3 - '@typescript/native-preview-linux-x64': 7.0.0-dev.20260702.3 - '@typescript/native-preview-win32-arm64': 7.0.0-dev.20260702.3 - '@typescript/native-preview-win32-x64': 7.0.0-dev.20260702.3 + '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260703.1 + '@typescript/native-preview-darwin-x64': 7.0.0-dev.20260703.1 + '@typescript/native-preview-linux-arm': 7.0.0-dev.20260703.1 + '@typescript/native-preview-linux-arm64': 7.0.0-dev.20260703.1 + '@typescript/native-preview-linux-x64': 7.0.0-dev.20260703.1 + '@typescript/native-preview-win32-arm64': 7.0.0-dev.20260703.1 + '@typescript/native-preview-win32-x64': 7.0.0-dev.20260703.1 '@ungap/structured-clone@1.3.2': {} @@ -8792,7 +8891,7 @@ snapshots: '@verdaccio/core': 8.1.2 '@verdaccio/loaders': 8.0.3 '@verdaccio/signature': 8.0.3 - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) lodash: 4.18.1 verdaccio-htpasswd: 13.0.3 transitivePeerDependencies: @@ -8801,7 +8900,7 @@ snapshots: '@verdaccio/config@8.1.2': dependencies: '@verdaccio/core': 8.1.2 - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) js-yaml: 4.1.1 lodash: 4.18.1 transitivePeerDependencies: @@ -8824,7 +8923,7 @@ snapshots: dependencies: '@verdaccio/core': 8.1.2 '@verdaccio/logger': 8.0.3 - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) got-cjs: 12.5.4 handlebars: 4.7.9 transitivePeerDependencies: @@ -8833,7 +8932,7 @@ snapshots: '@verdaccio/loaders@8.0.3': dependencies: '@verdaccio/core': 8.1.2 - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) lodash: 4.18.1 transitivePeerDependencies: - supports-color @@ -8843,7 +8942,7 @@ snapshots: '@verdaccio/core': 8.1.2 '@verdaccio/file-locking': 13.0.1 '@verdaccio/streams': 10.2.5 - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) globby: 11.1.0 lodash: 4.18.1 lowdb: 1.0.0 @@ -8857,7 +8956,7 @@ snapshots: '@verdaccio/core': 8.1.2 '@verdaccio/logger-prettify': 8.0.1 colorette: 2.0.20 - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) transitivePeerDependencies: - supports-color @@ -8882,7 +8981,7 @@ snapshots: '@verdaccio/config': 8.1.2 '@verdaccio/core': 8.1.2 '@verdaccio/url': 13.0.3 - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) express: 4.22.1 express-rate-limit: 5.5.1 lodash: 4.18.1 @@ -8893,14 +8992,14 @@ snapshots: '@verdaccio/package-filter@13.0.3': dependencies: '@verdaccio/core': 8.1.2 - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) semver: 7.7.4 transitivePeerDependencies: - supports-color '@verdaccio/search-indexer@8.0.2': dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) fuse.js: 7.3.0 transitivePeerDependencies: - supports-color @@ -8909,7 +9008,7 @@ snapshots: dependencies: '@verdaccio/config': 8.1.2 '@verdaccio/core': 8.1.2 - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) jsonwebtoken: 9.0.3 transitivePeerDependencies: - supports-color @@ -8920,7 +9019,7 @@ snapshots: dependencies: '@verdaccio/core': 8.1.2 '@verdaccio/url': 13.0.3 - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) gunzip-maybe: 1.4.2 tar-stream: 3.1.7 transitivePeerDependencies: @@ -8930,14 +9029,14 @@ snapshots: '@verdaccio/ui-theme@9.0.0-next-9.20': dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) transitivePeerDependencies: - supports-color '@verdaccio/url@13.0.3': dependencies: '@verdaccio/core': 8.1.2 - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) validator: 13.15.26 transitivePeerDependencies: - supports-color @@ -9036,9 +9135,9 @@ snapshots: acorn@8.17.0: {} - agent-base@6.0.2: + agent-base@6.0.2(supports-color@7.2.0): dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) transitivePeerDependencies: - supports-color @@ -9122,12 +9221,22 @@ snapshots: axios@1.16.0: dependencies: - follow-redirects: 1.16.0 + follow-redirects: 1.16.0(debug@4.4.3(supports-color@7.2.0)) form-data: 4.0.6 proxy-from-env: 2.1.0 transitivePeerDependencies: - debug + axios@1.16.1(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0): + dependencies: + follow-redirects: 1.16.0(debug@4.4.3(supports-color@7.2.0)) + form-data: 4.0.6 + https-proxy-agent: 5.0.1(supports-color@7.2.0) + proxy-from-env: 2.1.0 + transitivePeerDependencies: + - debug + - supports-color + b4a@1.8.1: {} bail@2.0.2: {} @@ -9177,7 +9286,7 @@ snapshots: dependencies: bytes: 3.1.2 content-type: 1.0.5 - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) http-errors: 2.0.1 iconv-lite: 0.7.2 on-finished: 2.4.1 @@ -9505,9 +9614,11 @@ snapshots: dependencies: ms: 2.0.0 - debug@4.4.3: + debug@4.4.3(supports-color@7.2.0): dependencies: ms: 2.1.3 + optionalDependencies: + supports-color: 7.2.0 decode-named-character-reference@1.3.0: dependencies: @@ -9891,7 +10002,7 @@ snapshots: content-type: 1.0.5 cookie: 0.7.2 cookie-signature: 1.2.2 - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) depd: 2.0.0 encodeurl: 2.0.0 escape-html: 1.0.3 @@ -9992,7 +10103,7 @@ snapshots: finalhandler@2.1.1: dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) encodeurl: 2.0.0 escape-html: 1.0.3 on-finished: 2.4.1 @@ -10016,7 +10127,9 @@ snapshots: flat@5.0.2: {} - follow-redirects@1.16.0: {} + follow-redirects@1.16.0(debug@4.4.3(supports-color@7.2.0)): + optionalDependencies: + debug: 4.4.3(supports-color@7.2.0) forever-agent@0.6.1: {} @@ -10429,7 +10542,7 @@ snapshots: http-proxy-agent@9.1.0: dependencies: agent-base: 9.0.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) proxy-agent-negotiate: 1.1.0 transitivePeerDependencies: - kerberos @@ -10448,17 +10561,17 @@ snapshots: quick-lru: 5.1.1 resolve-alpn: 1.2.1 - https-proxy-agent@5.0.1: + https-proxy-agent@5.0.1(supports-color@7.2.0): dependencies: - agent-base: 6.0.2 - debug: 4.4.3 + agent-base: 6.0.2(supports-color@7.2.0) + debug: 4.4.3(supports-color@7.2.0) transitivePeerDependencies: - supports-color https-proxy-agent@9.1.0: dependencies: agent-base: 9.0.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) proxy-agent-negotiate: 1.1.0 transitivePeerDependencies: - kerberos @@ -10491,7 +10604,7 @@ snapshots: import-from-esm@2.0.0: dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) import-meta-resolve: 4.2.0 transitivePeerDependencies: - supports-color @@ -10555,7 +10668,7 @@ snapshots: dependencies: '@ioredis/commands': 1.10.0 cluster-key-slot: 1.1.1 - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) denque: 2.1.0 redis-errors: 1.2.0 redis-parser: 3.0.0 @@ -11350,7 +11463,7 @@ snapshots: micromark@4.0.2: dependencies: '@types/debug': 4.1.13 - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) decode-named-character-reference: 1.3.0 devlop: 1.1.0 micromark-core-commonmark: 2.0.3 @@ -11594,7 +11707,7 @@ snapshots: escape-string-regexp: 1.0.5 figures: 3.2.0 flat: 5.0.2 - follow-redirects: 1.16.0 + follow-redirects: 1.16.0(debug@4.4.3(supports-color@7.2.0)) form-data: 4.0.6 fs-constants: 1.0.0 function-bind: 1.1.2 @@ -11675,6 +11788,137 @@ snapshots: transitivePeerDependencies: - debug + nx@23.0.2(@swc-node/register@1.11.1(@swc/core@1.15.43)(@swc/types@0.1.27)(typescript@6.0.3))(@swc/core@1.15.43): + dependencies: + '@emnapi/core': 1.4.5 + '@emnapi/runtime': 1.4.5 + '@emnapi/wasi-threads': 1.0.4 + '@jest/diff-sequences': 30.0.1 + '@napi-rs/wasm-runtime': 0.2.4 + '@tybys/wasm-util': 0.9.0 + '@yarnpkg/lockfile': 1.1.0 + '@zkochan/js-yaml': 0.0.7 + agent-base: 6.0.2(supports-color@7.2.0) + ansi-colors: 4.1.3 + ansi-regex: 5.0.1 + ansi-styles: 4.3.0 + argparse: 2.0.1 + asynckit: 0.4.0 + axios: 1.16.1(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0) + balanced-match: 4.0.3 + base64-js: 1.5.1 + bl: 4.1.0 + brace-expansion: 5.0.6 + buffer: 5.7.1 + call-bind-apply-helpers: 1.0.2 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-spinners: 2.6.1 + cliui: 8.0.1 + clone: 1.0.4 + color-convert: 2.0.1 + color-name: 1.1.4 + combined-stream: 1.0.8 + debug: 4.4.3(supports-color@7.2.0) + defaults: 1.0.4 + define-lazy-prop: 2.0.0 + delayed-stream: 1.0.0 + dotenv: 16.4.7 + dotenv-expand: 12.0.3 + dunder-proto: 1.0.1 + ejs: 5.0.1 + emoji-regex: 8.0.0 + end-of-stream: 1.4.5 + enquirer: 2.3.6 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-set-tostringtag: 2.1.0 + escalade: 3.2.0 + escape-string-regexp: 1.0.5 + figures: 3.2.0 + flat: 5.0.2 + follow-redirects: 1.16.0(debug@4.4.3(supports-color@7.2.0)) + form-data: 4.0.6 + fs-constants: 1.0.0 + function-bind: 1.1.2 + get-caller-file: 2.0.5 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + has-flag: 4.0.0 + has-symbols: 1.1.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + https-proxy-agent: 5.0.1(supports-color@7.2.0) + ieee754: 1.2.1 + ignore: 7.0.5 + inherits: 2.0.4 + is-docker: 2.2.1 + is-fullwidth-code-point: 3.0.0 + is-interactive: 1.0.0 + is-unicode-supported: 0.1.0 + is-wsl: 2.2.0 + isexe: 2.0.0 + json5: 2.2.3 + jsonc-parser: 3.3.1 + lines-and-columns: 2.0.3 + log-symbols: 4.1.0 + math-intrinsics: 1.1.0 + mime-db: 1.52.0 + mime-types: 2.1.35 + mimic-fn: 2.1.0 + minimatch: 10.2.5 + minimist: 1.2.8 + ms: 2.1.3 + npm-run-path: 4.0.1 + once: 1.4.0 + onetime: 5.1.2 + open: 8.4.2 + ora: 5.4.1 + path-key: 3.1.1 + picocolors: 1.1.1 + proxy-from-env: 2.1.0 + readable-stream: 3.6.2 + require-directory: 2.1.1 + resolve.exports: 2.0.3 + restore-cursor: 3.1.0 + safe-buffer: 5.2.1 + semver: 7.7.4 + signal-exit: 3.0.7 + smol-toml: 1.6.1 + string-width: 4.2.3 + string_decoder: 1.3.0 + strip-ansi: 6.0.1 + strip-bom: 3.0.0 + supports-color: 7.2.0 + tar-stream: 2.2.0 + tmp: 0.2.7 + tsconfig-paths: 4.2.0 + tslib: 2.8.1 + util-deprecate: 1.0.2 + wcwidth: 1.0.1 + which: 3.0.1 + wrap-ansi: 7.0.0 + wrappy: 1.0.2 + y18n: 5.0.8 + yaml: 2.9.0 + yargs: 17.7.2 + yargs-parser: 21.1.1 + optionalDependencies: + '@nx/nx-darwin-arm64': 23.0.2 + '@nx/nx-darwin-x64': 23.0.2 + '@nx/nx-freebsd-x64': 23.0.2 + '@nx/nx-linux-arm-gnueabihf': 23.0.2 + '@nx/nx-linux-arm64-gnu': 23.0.2 + '@nx/nx-linux-arm64-musl': 23.0.2 + '@nx/nx-linux-x64-gnu': 23.0.2 + '@nx/nx-linux-x64-musl': 23.0.2 + '@nx/nx-win32-arm64-msvc': 23.0.2 + '@nx/nx-win32-x64-msvc': 23.0.2 + '@swc-node/register': 1.11.1(@swc/core@1.15.43)(@swc/types@0.1.27)(typescript@6.0.3) + '@swc/core': 1.15.43 + object-assign@4.1.1: {} object-inspect@1.13.4: {} @@ -12425,7 +12669,7 @@ snapshots: router@2.2.0: dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) depd: 2.0.0 is-promise: 4.0.0 parseurl: 1.3.3 @@ -12464,7 +12708,7 @@ snapshots: '@semantic-release/release-notes-generator': 14.1.1(semantic-release@25.0.5(typescript@6.0.3)) aggregate-error: 5.0.0 cosmiconfig: 9.0.2(typescript@6.0.3) - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) env-ci: 11.2.0 execa: 9.6.1 figures: 6.1.0 @@ -12522,7 +12766,7 @@ snapshots: send@1.2.1: dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 @@ -13113,7 +13357,7 @@ snapshots: '@verdaccio/config': 8.1.2 '@verdaccio/core': 8.1.2 express: 4.22.1 - https-proxy-agent: 5.0.1 + https-proxy-agent: 5.0.1(supports-color@7.2.0) node-fetch: 2.6.7 transitivePeerDependencies: - encoding @@ -13125,7 +13369,7 @@ snapshots: '@verdaccio/file-locking': 13.0.1 apache-md5: 1.1.8 bcryptjs: 2.4.3 - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) http-errors: 2.0.1 unix-crypt-td-js: 1.1.4 transitivePeerDependencies: @@ -13155,7 +13399,7 @@ snapshots: clipanion: 4.0.0-rc.4(typanion@3.14.0) compression: 1.8.1 cors: 2.8.6 - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) envinfo: 7.21.0 express: 4.22.2 lodash: 4.18.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index e5ca91b394..1c109e8892 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -22,7 +22,7 @@ catalog: "@swc/core": "^1.15.43" "@tsconfig/bun": "^1.0.10" "@types/bun": "^1.3.14" - "@typescript/native-preview": "7.0.0-dev.20260702.3" + "@typescript/native-preview": "7.0.0-dev.20260703.1" "@vitest/coverage-istanbul": "^4.1.9" "effect": "4.0.0-beta.93" "knip": "^6.24.0" From 79379d9a68740d939ab131ccb3fca28a80d600e7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2026 00:15:10 +0000 Subject: [PATCH 47/79] fix(deps): bump github.com/posthog/posthog-go from 1.17.4 to 1.17.5 in /apps/cli-go in the go-minor group across 1 directory (#5865) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the go-minor group with 1 update in the /apps/cli-go directory: [github.com/posthog/posthog-go](https://github.com/posthog/posthog-go). Updates `github.com/posthog/posthog-go` from 1.17.4 to 1.17.5
Release notes

Sourced from github.com/posthog/posthog-go's releases.

1.17.5

Unreleased

Changelog

Sourced from github.com/posthog/posthog-go's changelog.

1.17.5

Patch Changes

  • fe66557: Change the default capture delivery budget from 10 attempts to 4 (DefaultMaxAttempts) when Config.MaxRetries is unset, aligning with the cross-SDK Capture V1 parity standard (posthog-rs uses the same envelope). This affects both the v0 (/batch/) and v1 send paths, since they share the attempt budget. Callers that set MaxRetries explicitly are unaffected.
  • 3d8404a: Unify the capture retry backoff ceiling at 30s. DefaultBackoff's cap changes from 10s to 30s (default only — override via Config.RetryAfter), and the Capture V1 send now clamps a server Retry-After to the same 30s so a hostile or buggy header cannot park a batch goroutine. Retry-After still acts as a minimum; the configured backoff is never truncated. This aligns the default retry behavior with posthog-rs and posthog-python.
Commits
  • d7f8a22 chore: release v1.17.5 [version bump] [skip ci]
  • fe66557 feat(capture): default to 4 delivery attempts (down from 10) (#256)
  • 3d8404a fix(capture): unify retry backoff ceiling at 30s (#255)
  • 6affc15 ci: Standardize SDK release failure telemetry (#252)
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/posthog/posthog-go&package-manager=go_modules&previous-version=1.17.4&new-version=1.17.5)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/cli-go/go.mod | 2 +- apps/cli-go/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/cli-go/go.mod b/apps/cli-go/go.mod index 875cf3c268..ede19cb805 100644 --- a/apps/cli-go/go.mod +++ b/apps/cli-go/go.mod @@ -42,7 +42,7 @@ require ( github.com/multigres/multigres v0.0.0-20260126223308-f5a52171bbc4 github.com/oapi-codegen/nullable v1.2.0 github.com/olekukonko/tablewriter v1.1.4 - github.com/posthog/posthog-go v1.17.4 + github.com/posthog/posthog-go v1.17.5 github.com/spf13/afero v1.15.0 github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 diff --git a/apps/cli-go/go.sum b/apps/cli-go/go.sum index 676a7cb287..a9cb686621 100644 --- a/apps/cli-go/go.sum +++ b/apps/cli-go/go.sum @@ -970,8 +970,8 @@ github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/posthog/posthog-go v1.17.4 h1:7olsPzTGzuQvXJtH9n55u/Vgy58nB7V8T85AIpRjd8Q= -github.com/posthog/posthog-go v1.17.4/go.mod h1://M430hNH3e8CDv4i8SJesb26816Mpa6GIZaiP4pNQU= +github.com/posthog/posthog-go v1.17.5 h1:mrLiAdyiQpl8Yeyg23iShyAztJMaUrYzsBjKeH52Aak= +github.com/posthog/posthog-go v1.17.5/go.mod h1://M430hNH3e8CDv4i8SJesb26816Mpa6GIZaiP4pNQU= github.com/prometheus/client_golang v0.9.0-pre1.0.20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= From 249b9ec577f178350c686e1a581469d680e054ba Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 Jul 2026 00:14:31 +0000 Subject: [PATCH 48/79] chore(ci): bump aws-actions/configure-aws-credentials from 6.2.1 to 6.2.2 in the actions-major group (#5871) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the actions-major group with 1 update: [aws-actions/configure-aws-credentials](https://github.com/aws-actions/configure-aws-credentials). Updates `aws-actions/configure-aws-credentials` from 6.2.1 to 6.2.2
Release notes

Sourced from aws-actions/configure-aws-credentials's releases.

v6.2.2

6.2.2 (2026-07-07)

Miscellaneous Chores

Changelog

Sourced from aws-actions/configure-aws-credentials's changelog.

Changelog

All notable changes to this project will be documented in this file. See standard-version for commit guidelines.

6.2.2 (2026-07-07)

Miscellaneous Chores

6.2.1 (2026-06-26)

Bug Fixes

  • enforce allowed-account-ids on all auth paths (#1847) (4d281fb)

6.2.0 (2026-06-01)

Features

Bug Fixes

  • skip credential check on output-env-credentials: false (#1778) (58e7c47)
  • assumeRole failing from session tag size too large (#1808) (d6f5dc3)

6.1.3 (2026-05-28)

Bug Fixes

  • fix: allow kubelet token symlink in #1805

6.1.2 (2026-05-26)

Bug Fixes

6.1.1 (2026-05-05)

Miscellaneous Chores

... (truncated)

Commits
  • 517a711 chore(main): release 6.2.2 (#1876)
  • d01d678 chore: release 6.2.2
  • 8efa52b chore(deps-dev): bump vitest dependencies (#1874)
  • 8e1eed5 chore(deps-dev): bump @​smithy/property-provider from 4.4.4 to 4.4.6 (#1869)
  • 112421a chore(deps-dev): bump @​biomejs/biome from 2.5.1 to 2.5.2 (#1868)
  • fbc01c6 chore(deps-dev): bump @​types/node from 26.0.1 to 26.1.0 (#1871)
  • b12ca87 chore(deps-dev): bump memfs from 4.57.8 to 4.58.0 (#1873)
  • d314f7f chore: Update dist
  • a53b65b chore(deps): bump @​aws-sdk/client-sts from 3.1076.0 to 3.1080.0 (#1867)
  • 338d2c1 chore(deps-dev): bump sigstore from 4.1.0 to 4.1.1 (#1864)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=aws-actions/configure-aws-credentials&package-manager=github_actions&previous-version=6.2.1&new-version=6.2.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/cli-go-mirror-image.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cli-go-mirror-image.yml b/.github/workflows/cli-go-mirror-image.yml index d82bc05002..2844079487 100644 --- a/.github/workflows/cli-go-mirror-image.yml +++ b/.github/workflows/cli-go-mirror-image.yml @@ -34,7 +34,7 @@ jobs: run: | echo "image=${TAG##*/}" >> $GITHUB_OUTPUT - name: configure aws credentials - uses: aws-actions/configure-aws-credentials@254c19bd240aabef8777f48595e9d2d7b972184b # v6.2.1 + uses: aws-actions/configure-aws-credentials@517a711dbcd0e402f90c77e7e2f81e849156e31d # v6.2.2 with: role-to-assume: ${{ secrets.PROD_AWS_ROLE }} aws-region: us-east-1 From dac162506e8a3a760745698268eb44f926c26623 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 00:16:24 +0000 Subject: [PATCH 49/79] chore(ci): bump the actions-major group with 2 updates (#5877) Bumps the actions-major group with 2 updates: [github/codeql-action/init](https://github.com/github/codeql-action) and [github/codeql-action/analyze](https://github.com/github/codeql-action). Updates `github/codeql-action/init` from 4.36.3 to 4.37.0
Release notes

Sourced from github/codeql-action/init's releases.

v4.37.0

  • Update default CodeQL bundle version to 2.26.0. #3995
  • In addition to the existing input format, the config-file input for the codeql-action/init step will soon support a new [owner/]repo[@ref][:path] format. All components except the repository name are optional. If omitted, owner defaults to the same owner as the repository the analysis is running for, ref to main, and path to .github/codeql-action.yaml. Support for this format ships in this version of the CodeQL Action, but will only be enabled over the coming weeks. #3973
Changelog

Sourced from github/codeql-action/init's changelog.

CodeQL Action Changelog

See the releases page for the relevant changes to the CodeQL CLI and language packs.

[UNRELEASED]

  • Upcoming breaking change: Add a deprecation warning for customers using CodeQL version 2.20.6 and earlier. These versions of CodeQL were discontinued on 1 July 2026 alongside GitHub Enterprise Server 3.16, and will be unsupported by the next minor release of the CodeQL Action. #3956

4.37.0 - 08 Jul 2026

  • Update default CodeQL bundle version to 2.26.0. #3995
  • In addition to the existing input format, the config-file input for the codeql-action/init step will soon support a new [owner/]repo[@ref][:path] format. All components except the repository name are optional. If omitted, owner defaults to the same owner as the repository the analysis is running for, ref to main, and path to .github/codeql-action.yaml. Support for this format ships in this version of the CodeQL Action, but will only be enabled over the coming weeks. #3973

4.36.3 - 01 Jul 2026

No user facing changes.

4.36.2 - 04 Jun 2026

  • Cache CodeQL CLI version information across Actions steps. #3943
  • Reduce requests while waiting for analysis processing by using exponential backoff when polling SARIF processing status. #3937
  • Update default CodeQL bundle version to 2.25.6. #3948

4.36.1 - 02 Jun 2026

No user facing changes.

4.36.0 - 22 May 2026

  • Breaking change: Bump the minimum required CodeQL bundle version to 2.19.4. #3894
  • Add support for SHA-256 Git object IDs. #3893
  • Update default CodeQL bundle version to 2.25.5. #3926

4.35.5 - 15 May 2026

  • We have improved how the JavaScript bundles for the CodeQL Action are generated to avoid duplication across bundles and reduce the size of the repository by around 70%. This should have no effect on the runtime behaviour of the CodeQL Action. #3899
  • For performance and accuracy reasons, improved incremental analysis will now only be enabled on a pull request when diff-informed analysis is also enabled for that run. If diff-informed analysis is unavailable (for example, because the PR diff ranges could not be computed), the action will fall back to a full analysis. #3791
  • If multiple inputs are provided for the GitHub-internal analysis-kinds input, only code-scanning will be enabled. The analysis-kinds input is experimental, for GitHub-internal use only, and may change without notice at any time. #3892
  • Added an experimental change which, when running a Code Scanning analysis for a PR with improved incremental analysis enabled, prefers CodeQL CLI versions that have a cached overlay-base database for the configured languages. This speeds up analysis for a repository when there is not yet a cached overlay-base database for the latest CLI version. We expect to roll this change out to everyone in May. #3880

4.35.4 - 07 May 2026

  • Update default CodeQL bundle version to 2.25.4. #3881

4.35.3 - 01 May 2026

  • Upcoming breaking change: Add a deprecation warning for customers using CodeQL version 2.19.3 and earlier. These versions of CodeQL were discontinued on 9 April 2026 alongside GitHub Enterprise Server 3.15, and will be unsupported by the next minor release of the CodeQL Action. #3837
  • Configurations for private registries that use Cloudsmith or GCP OIDC are now accepted. #3850
  • Best-effort connection tests for private registries now use GET requests instead of HEAD for better compatibility with various registry implementations. For NuGet feeds, the test is now always performed against the service index. #3853
  • Fixed a bug where two diagnostics produced within the same millisecond could overwrite each other on disk, causing one of them to be lost. #3852

... (truncated)

Commits
  • 99df26d Merge pull request #3996 from github/update-v4.37.0-c7c896d71
  • 31c2707 Add changenote for #3973
  • 72df218 Update changelog for v4.37.0
  • c7c896d Merge pull request #3995 from github/update-bundle/codeql-bundle-v2.26.0
  • 3f34ff0 Add changelog note
  • 43bec09 Update default bundle to codeql-bundle-v2.26.0
  • f58f0d1 Merge pull request #3973 from github/mbg/repo-props/config-file-shorthands
  • 7dc37cb Merge remote-tracking branch 'origin/main' into mbg/repo-props/config-file-sh...
  • 8e22350 Thread ActionState to initConfig
  • 69c9e8c Mark some status-report imports as type-only to avoid circular dependencies
  • Additional commits viewable in compare view

Updates `github/codeql-action/analyze` from 4.36.3 to 4.37.0
Release notes

Sourced from github/codeql-action/analyze's releases.

v4.37.0

  • Update default CodeQL bundle version to 2.26.0. #3995
  • In addition to the existing input format, the config-file input for the codeql-action/init step will soon support a new [owner/]repo[@ref][:path] format. All components except the repository name are optional. If omitted, owner defaults to the same owner as the repository the analysis is running for, ref to main, and path to .github/codeql-action.yaml. Support for this format ships in this version of the CodeQL Action, but will only be enabled over the coming weeks. #3973
Changelog

Sourced from github/codeql-action/analyze's changelog.

CodeQL Action Changelog

See the releases page for the relevant changes to the CodeQL CLI and language packs.

[UNRELEASED]

  • Upcoming breaking change: Add a deprecation warning for customers using CodeQL version 2.20.6 and earlier. These versions of CodeQL were discontinued on 1 July 2026 alongside GitHub Enterprise Server 3.16, and will be unsupported by the next minor release of the CodeQL Action. #3956

4.37.0 - 08 Jul 2026

  • Update default CodeQL bundle version to 2.26.0. #3995
  • In addition to the existing input format, the config-file input for the codeql-action/init step will soon support a new [owner/]repo[@ref][:path] format. All components except the repository name are optional. If omitted, owner defaults to the same owner as the repository the analysis is running for, ref to main, and path to .github/codeql-action.yaml. Support for this format ships in this version of the CodeQL Action, but will only be enabled over the coming weeks. #3973

4.36.3 - 01 Jul 2026

No user facing changes.

4.36.2 - 04 Jun 2026

  • Cache CodeQL CLI version information across Actions steps. #3943
  • Reduce requests while waiting for analysis processing by using exponential backoff when polling SARIF processing status. #3937
  • Update default CodeQL bundle version to 2.25.6. #3948

4.36.1 - 02 Jun 2026

No user facing changes.

4.36.0 - 22 May 2026

  • Breaking change: Bump the minimum required CodeQL bundle version to 2.19.4. #3894
  • Add support for SHA-256 Git object IDs. #3893
  • Update default CodeQL bundle version to 2.25.5. #3926

4.35.5 - 15 May 2026

  • We have improved how the JavaScript bundles for the CodeQL Action are generated to avoid duplication across bundles and reduce the size of the repository by around 70%. This should have no effect on the runtime behaviour of the CodeQL Action. #3899
  • For performance and accuracy reasons, improved incremental analysis will now only be enabled on a pull request when diff-informed analysis is also enabled for that run. If diff-informed analysis is unavailable (for example, because the PR diff ranges could not be computed), the action will fall back to a full analysis. #3791
  • If multiple inputs are provided for the GitHub-internal analysis-kinds input, only code-scanning will be enabled. The analysis-kinds input is experimental, for GitHub-internal use only, and may change without notice at any time. #3892
  • Added an experimental change which, when running a Code Scanning analysis for a PR with improved incremental analysis enabled, prefers CodeQL CLI versions that have a cached overlay-base database for the configured languages. This speeds up analysis for a repository when there is not yet a cached overlay-base database for the latest CLI version. We expect to roll this change out to everyone in May. #3880

4.35.4 - 07 May 2026

  • Update default CodeQL bundle version to 2.25.4. #3881

4.35.3 - 01 May 2026

  • Upcoming breaking change: Add a deprecation warning for customers using CodeQL version 2.19.3 and earlier. These versions of CodeQL were discontinued on 9 April 2026 alongside GitHub Enterprise Server 3.15, and will be unsupported by the next minor release of the CodeQL Action. #3837
  • Configurations for private registries that use Cloudsmith or GCP OIDC are now accepted. #3850
  • Best-effort connection tests for private registries now use GET requests instead of HEAD for better compatibility with various registry implementations. For NuGet feeds, the test is now always performed against the service index. #3853
  • Fixed a bug where two diagnostics produced within the same millisecond could overwrite each other on disk, causing one of them to be lost. #3852

... (truncated)

Commits
  • 99df26d Merge pull request #3996 from github/update-v4.37.0-c7c896d71
  • 31c2707 Add changenote for #3973
  • 72df218 Update changelog for v4.37.0
  • c7c896d Merge pull request #3995 from github/update-bundle/codeql-bundle-v2.26.0
  • 3f34ff0 Add changelog note
  • 43bec09 Update default bundle to codeql-bundle-v2.26.0
  • f58f0d1 Merge pull request #3973 from github/mbg/repo-props/config-file-shorthands
  • 7dc37cb Merge remote-tracking branch 'origin/main' into mbg/repo-props/config-file-sh...
  • 8e22350 Thread ActionState to initConfig
  • 69c9e8c Mark some status-report imports as type-only to avoid circular dependencies
  • Additional commits viewable in compare view

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/cli-go-codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cli-go-codeql.yml b/.github/workflows/cli-go-codeql.yml index 9759f2f7e6..e55cfffdfd 100644 --- a/.github/workflows/cli-go-codeql.yml +++ b/.github/workflows/cli-go-codeql.yml @@ -67,7 +67,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3 + uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -95,7 +95,7 @@ jobs: exit 1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3 + uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 with: category: "/language:${{matrix.language}}" defaults: From e0f7b8adacee392cb1e84d556f6b94afa45bbfc9 Mon Sep 17 00:00:00 2001 From: Vaibhav <117663341+7ttp@users.noreply.github.com> Date: Wed, 15 Jul 2026 16:56:29 +0530 Subject: [PATCH 50/79] fix(cli): restore Edge Function docker bundling (#5875) ## TL;DR docker-based deploys could finish bundling and then fail with `failed to open eszip: ENOENT ... output.eszip`
the [Go CLI wrote bundle output to project-local `supabase/.temp`]() but the [TypeScript port moved it to the host OS temporary directory]()
typically under `/var/folders/...`, which may not be shared with Docker Desktop's VM this restores the Go CLI's project local output behavior while retaining unique bundle directories & cleanup... ## ref: * towards: supabase/cli#5735 * follow up to:
[https://github.com/supabase/cli/pull/5755]()
[https://github.com/supabase/cli/pull/5747]() --- .../deploy/deploy.integration.test.ts | 38 +++++++++++++++++++ apps/cli/src/shared/functions/deploy.ts | 7 ++-- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/apps/cli/src/next/commands/functions/deploy/deploy.integration.test.ts b/apps/cli/src/next/commands/functions/deploy/deploy.integration.test.ts index edf7c3304f..60f5bfe80d 100644 --- a/apps/cli/src/next/commands/functions/deploy/deploy.integration.test.ts +++ b/apps/cli/src/next/commands/functions/deploy/deploy.integration.test.ts @@ -1549,6 +1549,44 @@ describe("functions deploy", () => { }).pipe(Effect.ensuring(cleanupTempDir(tempDir))); }); + it.live("bundles when Docker only shares the project directory", () => { + const tempDir = makeTempDir(); + const sharedOutputRoot = join(tempDir, "supabase", ".temp"); + let outputPath = ""; + const child = mockChildProcessSpawner({ + exitCode: 0, + onSpawn: (record) => { + if (record.command !== "docker" || record.args[0] !== "run") { + return; + } + outputPath = resolveDockerOutputPath(record.args); + if (!outputPath.startsWith(`${sharedOutputRoot}${sep}`)) { + return; + } + mkdirSync(dirname(outputPath), { recursive: true }); + writeFileSync(outputPath, "eszip-test-output"); + }, + }); + + return Effect.gen(function* () { + yield* Effect.promise(() => writeProjectConfig(tempDir)); + yield* Effect.promise(() => writeLocalFunction(tempDir, "hello-world")); + + const { layer } = setup(tempDir, { + rawArgs: ["functions", "deploy", "hello-world", "--use-docker"], + childLayer: child.layer, + }); + + yield* functionsDeploy({ + ...BASE_FLAGS, + functionNames: ["hello-world"], + useDocker: true, + }).pipe(Effect.provide(layer)); + + expect(outputPath.startsWith(`${sharedOutputRoot}${sep}`)).toBe(true); + }).pipe(Effect.ensuring(cleanupTempDir(tempDir))); + }); + it.live("forwards npm auth environment to the Docker bundler", () => { const tempDir = makeTempDir(); const previousRegistry = process.env["NPM_CONFIG_REGISTRY"]; diff --git a/apps/cli/src/shared/functions/deploy.ts b/apps/cli/src/shared/functions/deploy.ts index 51f0d9a9a8..6783c7a7f5 100644 --- a/apps/cli/src/shared/functions/deploy.ts +++ b/apps/cli/src/shared/functions/deploy.ts @@ -1,7 +1,6 @@ import { brotliCompressSync, constants as zlibConstants } from "node:zlib"; -import { chmod, mkdtemp, readFile, readdir, realpath, rm, stat } from "node:fs/promises"; +import { chmod, mkdir, mkdtemp, readFile, readdir, realpath, rm, stat } from "node:fs/promises"; import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path"; -import { tmpdir } from "node:os"; import { URL } from "node:url"; import { FunctionResponse, operationDefinitions, type ApiClient } from "@supabase/api/effect"; import { @@ -1334,8 +1333,10 @@ const bundleFunctionWithDocker = Effect.fnUntraced(function* ( const output = yield* Output; yield* output.raw(`Bundling Function: ${config.slug}\n`, "stderr"); + const outputRoot = resolve(functionsDir, "..", ".temp"); + yield* Effect.tryPromise(() => mkdir(outputRoot, { recursive: true })); const outputDir = yield* Effect.tryPromise(() => - mkdtemp(join(tmpdir(), `.supabase-output-${config.slug}-`)), + mkdtemp(join(outputRoot, `.supabase-output-${config.slug}-`)), ); try { yield* Effect.tryPromise(() => chmod(outputDir, 0o777)); From 51836e43ed5fb9ca1c1e76d7354134ec55946612 Mon Sep 17 00:00:00 2001 From: Vaibhav <117663341+7ttp@users.noreply.github.com> Date: Wed, 15 Jul 2026 20:17:03 +0530 Subject: [PATCH 51/79] fix(cli): restore configless type generation (#5881) ## TL;DR `supabase gen types typescript --local` started requiring `supabase/config.toml` after the ts port, although the previous one used embedded defaults when the file was absent now it again uses the config schema defaults for the config-less `--local` path, restoring the previous behavior.. ## ref - Closes #5879 - Follow-up to #5514 --- .../legacy/commands/gen/types/SIDE_EFFECTS.md | 10 +- .../commands/gen/types/types.handler.ts | 25 ++-- .../gen/types/types.integration.test.ts | 107 +++++++++++++++--- .../shared/legacy-db-config.toml-read.ts | 40 ++++++- .../legacy-db-config.toml-read.unit.test.ts | 5 +- 5 files changed, 158 insertions(+), 29 deletions(-) diff --git a/apps/cli/src/legacy/commands/gen/types/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/gen/types/SIDE_EFFECTS.md index c7accf16f1..4ef5a89369 100644 --- a/apps/cli/src/legacy/commands/gen/types/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/gen/types/SIDE_EFFECTS.md @@ -5,7 +5,8 @@ | Path | Format | When | | ----------------------------------------- | ---------- | ---------------------------------------------------------------------------------------- | | `~/.supabase/access-token` | plain text | when `SUPABASE_ACCESS_TOKEN` unset and `--linked` or `--project-id` | -| `/supabase/config.toml` | TOML | when selecting schemas from config; required for `--local`, best-effort otherwise | +| `/supabase/config.toml` | TOML | when selecting schemas; `--local` uses embedded defaults when the file is missing | +| `{/supabase}/.env*` | dotenv | `--local`; resolves the same nested environment overrides as the legacy CLI | | `/supabase/.temp/rest-version` | plain text | `--local` only, when `db.major_version > 14` — forces v9 compat if the tag contains `v9` | | `/supabase/.temp/pgmeta-version` | plain text | `--local` only — overrides the pg-meta docker image tag | @@ -59,6 +60,11 @@ default 10s pg-delta probe timeout. | Variable | Purpose | Required? | | ---------------------------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | | `SUPABASE_ACCESS_TOKEN` | auth token for linked/project-id mode | no (falls back to keyring → `~/.supabase/access-token`) | +| `SUPABASE_PROJECT_ID` | local Docker container and network project ID | no (falls back to the workdir name) | +| `SUPABASE_DB_PORT` | local database probe port | no (defaults to `54322`) | +| `SUPABASE_DB_MAJOR_VERSION` | local PostgreSQL major version | no (defaults to `17`) | +| `SUPABASE_API_SCHEMAS` | local schemas used when `--schema` is omitted | no (defaults to `public,graphql_public`) | +| `SUPABASE_ENV` | selects nested dotenv files for local generation | no (defaults to `development`) | | `SUPABASE_PROFILE` | built-in profile name or YAML file path | no (falls back to `~/.supabase/profile` -> `supabase`) | | `SUPABASE_DB_PASSWORD` | database password for `--local` and the `--linked` workdir project | no (defaults to `postgres`; **ignored** for ad-hoc `--project-id`, which always mints a temporary login role) | | `SUPABASE_SERVICES_HOSTNAME` | host used for the local TLS probe | no (defaults to `127.0.0.1`) | @@ -95,6 +101,8 @@ Not applicable. ## Notes - Exactly one of `--local`, `--linked`, `--project-id`, or `--db-url` must be specified. +- With `--local`, a missing `supabase/config.toml` uses the embedded config defaults plus + shell and nested dotenv overrides, matching the legacy CLI. - `--lang` flag accepts `typescript` (default), `go`, `swift`, or `python`. Project-ref paths use the Management API for TypeScript, and use a project database host + temporary login role + pg-meta for other languages. diff --git a/apps/cli/src/legacy/commands/gen/types/types.handler.ts b/apps/cli/src/legacy/commands/gen/types/types.handler.ts index 87113c6a42..3f090e6090 100644 --- a/apps/cli/src/legacy/commands/gen/types/types.handler.ts +++ b/apps/cli/src/legacy/commands/gen/types/types.handler.ts @@ -26,6 +26,10 @@ import { mapLegacyHttpError } from "../../../shared/legacy-http-errors.ts"; import { LegacyDbConfigResolver } from "../../../shared/legacy-db-config.service.ts"; import type { LegacyDbConfigFlags } from "../../../shared/legacy-db-config.types.ts"; import { legacyPoolerConfigFromConnectionString } from "../../../shared/legacy-db-config.parse.ts"; +import { + legacyApplyProjectEnv, + legacyReadDbToml, +} from "../../../shared/legacy-db-config.toml-read.ts"; import type { LegacyPgConnInput } from "../../../shared/legacy-db-connection.service.ts"; import { legacyToPostgresURL } from "../../../shared/legacy-postgres-url.ts"; import { legacyTempPaths } from "../../../shared/legacy-temp-paths.ts"; @@ -527,12 +531,12 @@ export const legacyGenTypes = Effect.fn("legacy.gen.types")(function* (flags: Le yield* Effect.gen(function* () { if (flags.local) { - const loaded = yield* loadConfig(); - if (loaded === null) { - return yield* Effect.fail( - new Error("failed to load config: supabase/config.toml not found"), - ); - } + const config = yield* legacyReadDbToml(fs, path, cliConfig.workdir); + yield* legacyApplyProjectEnv( + config.projectEnv, + Object.keys(config.projectEnv).filter((key) => key !== "SUPABASE_DB_PASSWORD"), + ); + const projectId = Option.getOrElse(config.projectId, () => path.basename(cliConfig.workdir)); const paths = legacyTempPaths(path, cliConfig.workdir); // Go resolves Config.Api.Image from the rest-version file only when @@ -540,7 +544,7 @@ export const legacyGenTypes = Effect.fn("legacy.gen.types")(function* (flags: Le // (pkg/config/config.go:657-666, internal/gen/types/types.go:69). Gate and trim // identically so we don't force v9 on older databases. const restVersion = - loaded.config.db.major_version > 14 + config.majorVersion > 14 ? (yield* fs .readFileString(paths.restVersion) .pipe(Effect.orElseSucceed(() => ""))).trim() @@ -551,9 +555,8 @@ export const legacyGenTypes = Effect.fn("legacy.gen.types")(function* (flags: Le .pipe(Effect.orElseSucceed(() => "")); const includedSchemas = ( - schemas.length > 0 ? schemas : defaultSchemas(loaded.config.api.schemas) + schemas.length > 0 ? schemas : defaultSchemas(config.apiSchemas) ).join(","); - const projectId = loaded.config.project_id ?? path.basename(cliConfig.workdir); yield* assertLocalDbRunning(projectId); yield* runPgMeta({ @@ -567,7 +570,7 @@ export const legacyGenTypes = Effect.fn("legacy.gen.types")(function* (flags: Le host: "db", port: 5432, probeHost: legacyGetHostname(), - probePort: loaded.config.db.port, + probePort: config.port, networkMode: localNetworkId(projectId), includedSchemas, postgrestV9Compat: flags.postgrestV9Compat || forcedV9, @@ -637,5 +640,5 @@ export const legacyGenTypes = Effect.fn("legacy.gen.types")(function* (flags: Le schemas.length > 0 ? schemas : schemasFromConfig(loaded?.config.api.schemas), false, ); - }).pipe(Effect.ensuring(telemetryState.flush)); + }).pipe(Effect.scoped, Effect.ensuring(telemetryState.flush)); }); diff --git a/apps/cli/src/legacy/commands/gen/types/types.integration.test.ts b/apps/cli/src/legacy/commands/gen/types/types.integration.test.ts index 67015337f6..5d75f2caa9 100644 --- a/apps/cli/src/legacy/commands/gen/types/types.integration.test.ts +++ b/apps/cli/src/legacy/commands/gen/types/types.integration.test.ts @@ -1,7 +1,7 @@ import { existsSync, mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; import { createServer } from "node:net"; import { tmpdir } from "node:os"; -import { join } from "node:path"; +import { basename, join } from "node:path"; import { describe, expect, it } from "@effect/vitest"; import { BunServices } from "@effect/platform-bun"; import type { @@ -62,7 +62,12 @@ import type { import { legacyGenCommand } from "../gen.command.ts"; import type { LegacyGenTypesFlags } from "./types.command.ts"; import { legacyGenTypes } from "./types.handler.ts"; -import { parseQueryTimeoutSeconds, resolvePgmetaImage } from "./types.shared.ts"; +import { + localDbContainerId, + localNetworkId, + parseQueryTimeoutSeconds, + resolvePgmetaImage, +} from "./types.shared.ts"; function writeConfig(workdir: string, contents: string) { const supabaseDir = join(workdir, "supabase"); @@ -2631,22 +2636,98 @@ describe("legacy gen types", () => { }, ); - it.live("fails local generation when supabase/config.toml is missing", () => { + it.live("generates locally with Go defaults when supabase/config.toml is missing", () => { const workdir = mkdtempSync(join(tmpdir(), "supabase-gen-types-local-no-config-")); - const { layer } = setup({ workdir, skipConfig: true }); + const docker = captureDockerRun(); + const probes: Array<{ host: string; port: number }> = []; + const { layer, out, child } = setup({ + workdir, + skipConfig: true, + childStdout: ["generated"], + onSpawn: docker.onSpawn, + sslProbeLayer: Layer.succeed(LegacyPgDeltaSslProbe, { + requireSsl: () => Effect.succeed(false), + requireSslForHost: (host, port) => + Effect.sync(() => { + probes.push({ host, port }); + return false; + }), + }), + }); return Effect.gen(function* () { - const exit = yield* legacyGenTypes(defaultFlags({ local: true })).pipe( - Effect.provide(layer), - Effect.exit, + yield* legacyGenTypes(defaultFlags({ local: true })).pipe(Effect.provide(layer)); + + const projectId = basename(workdir); + expect(child.spawned[0]).toEqual({ + command: "docker", + args: ["container", "inspect", localDbContainerId(projectId)], + }); + expect(child.spawned[1]?.args).toContain(localNetworkId(projectId)); + expect(probes).toEqual([{ host: "127.0.0.1", port: 54322 }]); + expect(docker.env.has("PG_META_GENERATE_TYPES_INCLUDED_SCHEMAS=public,graphql_public")).toBe( + true, ); + expect(out.stdoutText).toContain("generated"); + }); + }); - expect(Exit.isFailure(exit)).toBe(true); - if (Exit.isFailure(exit)) { - expect(String(exit.cause)).toContain( - "failed to load config: supabase/config.toml not found", - ); - } + it.live("honors local dotenv overrides when supabase/config.toml is missing", () => { + const workdir = mkdtempSync(join(tmpdir(), "supabase-gen-types-local-no-config-env-")); + const supabaseDir = join(workdir, "supabase"); + mkdirSync(supabaseDir, { recursive: true }); + writeFileSync( + join(supabaseDir, ".env"), + [ + "SUPABASE_PROJECT_ID=configless-env-project", + "SUPABASE_DB_PORT=55432", + "SUPABASE_DB_PASSWORD=remote-password", + "SUPABASE_API_SCHEMAS=private,graphql_public", + "SUPABASE_SERVICES_HOSTNAME=host.docker.internal", + "SUPABASE_INTERNAL_IMAGE_REGISTRY=mirror.example.com", + "", + ].join("\n"), + ); + const docker = captureDockerRun(); + const probes: Array<{ host: string; port: number }> = []; + const { layer, out, child } = setup({ + workdir, + skipConfig: true, + childStdout: ["generated"], + onSpawn: docker.onSpawn, + sslProbeLayer: Layer.succeed(LegacyPgDeltaSslProbe, { + requireSsl: () => Effect.succeed(false), + requireSslForHost: (host, port) => + Effect.sync(() => { + probes.push({ host, port }); + return false; + }), + }), + }); + + return Effect.gen(function* () { + yield* legacyGenTypes(defaultFlags({ local: true })).pipe(Effect.provide(layer)); + + expect(child.spawned[0]).toEqual({ + command: "docker", + args: ["container", "inspect", localDbContainerId("configless-env-project")], + }); + expect(child.spawned[1]?.args).toContain(localNetworkId("configless-env-project")); + expect(probes).toEqual([{ host: "host.docker.internal", port: 55432 }]); + expect( + docker.env.has("PG_META_GENERATE_TYPES_INCLUDED_SCHEMAS=public,private,graphql_public"), + ).toBe(true); + expect( + docker.env.has( + "PG_META_DB_URL=postgresql://postgres:postgres@db:5432/postgres?connect_timeout=10", + ), + ).toBe(true); + expect( + child.spawned[1]?.args.some((arg) => + arg.startsWith("mirror.example.com/supabase/postgres-meta:"), + ), + ).toBe(true); + expect(out.stdoutText).toContain("generated"); }); }); diff --git a/apps/cli/src/legacy/shared/legacy-db-config.toml-read.ts b/apps/cli/src/legacy/shared/legacy-db-config.toml-read.ts index 0e11809a56..e17dd5f5be 100644 --- a/apps/cli/src/legacy/shared/legacy-db-config.toml-read.ts +++ b/apps/cli/src/legacy/shared/legacy-db-config.toml-read.ts @@ -43,6 +43,7 @@ type EnvLookup = (name: string) => string | undefined; * and aborts the command rather than running against the default local database). */ interface LegacyDbTomlValues { + readonly projectEnv: Readonly>; /** * Resolves a `SUPABASE_*` env var with Go's precedence: shell env (non-empty) * wins, then the loaded project `.env*` files (non-empty), else undefined. @@ -51,6 +52,7 @@ interface LegacyDbTomlValues { * rather than `process.env` alone (e.g. `SUPABASE_EXPERIMENTAL_PG_DELTA`). */ readonly envLookup: (name: string) => string | undefined; + readonly apiSchemas: ReadonlyArray; /** `[db] port`, default 54322 (`packages/config/src/db.ts`). */ readonly port: number; /** `[db] shadow_port`, default 54320. */ @@ -169,6 +171,7 @@ const DEFAULT_PORT = 54322; const DEFAULT_SHADOW_PORT = 54320; const DEFAULT_MAJOR_VERSION = 17; const DEFAULT_PASSWORD = "postgres"; +const DEFAULT_API_SCHEMAS = ["public", "graphql_public"] as const; /** `[edge_runtime] deno_version` default (`config.toml` template). 2 → the current edge-runtime image. */ const DEFAULT_DENO_VERSION = 2; @@ -273,6 +276,7 @@ function legacyResolveValidatedRemoteProjectId( * `AutomaticEnv` — `config.go:635-637`), so the block value must beat the env override. */ const LEGACY_ENV_OVERRIDABLE_KEYS: ReadonlyArray = [ + "api.schemas", "db.port", "db.shadow_port", "db.major_version", @@ -470,6 +474,22 @@ function resolveConfigInt(value: unknown, lookup: EnvLookup): number | "absent" return "invalid"; } +function resolveStringSlice( + value: unknown, + fallback: ReadonlyArray, + lookup: EnvLookup, +): ReadonlyArray | undefined { + if (value === undefined) return fallback; + if (typeof value === "string") { + const expanded = legacyExpandEnv(value, lookup); + return expanded.length === 0 ? [] : expanded.split(","); + } + if (!Array.isArray(value) || !value.every((item): item is string => typeof item === "string")) { + return undefined; + } + return value.map((item) => legacyExpandEnv(item, lookup)); +} + /** * Replicates Go's `path.Join("supabase", pattern)` for a relative seed `sql_paths` * entry (`pkg/config/config.go:881-886`). Go's `path.Join` runs `path.Clean`, which @@ -603,9 +623,12 @@ export const legacyLoadProjectEnv = Effect.fnUntraced(function* ( * re-checks). The `acquireRelease` finalizer deletes only the keys it set when the * scope closes, so in-process test workers don't leak env between cases. */ -export const legacyApplyProjectEnv = (loaded: Record) => +export const legacyApplyProjectEnv = ( + loaded: Readonly>, + keys: ReadonlyArray = LEGACY_PROCESS_ENV_APPLY_KEYS, +) => Effect.forEach( - LEGACY_PROCESS_ENV_APPLY_KEYS, + keys, (key) => { const value = loaded[key]; if (value === undefined || process.env[key] !== undefined) { @@ -1834,9 +1857,22 @@ const readDbTomlCore = Effect.fnUntraced(function* ( apiRaw?.["auto_expose_new_tables"], lookup, ); + const apiSchemas = resolveStringSlice( + (remoteOverrideKeys.has("api.schemas") ? undefined : envOverride("SUPABASE_API_SCHEMAS")) ?? + apiRaw?.["schemas"], + DEFAULT_API_SCHEMAS, + lookup, + ); + if (apiSchemas === undefined) { + return yield* Effect.fail( + new LegacyDbConfigLoadError({ message: "failed to parse config: invalid api.schemas." }), + ); + } const values: LegacyDbTomlValues = { + projectEnv, envLookup: envOverride, + apiSchemas, port, shadowPort, password: passwordRaw !== undefined ? legacyExpandEnv(passwordRaw, lookup) : DEFAULT_PASSWORD, diff --git a/apps/cli/src/legacy/shared/legacy-db-config.toml-read.unit.test.ts b/apps/cli/src/legacy/shared/legacy-db-config.toml-read.unit.test.ts index 493f1f8276..4919c78ef8 100644 --- a/apps/cli/src/legacy/shared/legacy-db-config.toml-read.unit.test.ts +++ b/apps/cli/src/legacy/shared/legacy-db-config.toml-read.unit.test.ts @@ -929,11 +929,12 @@ describe("legacyReadDbToml", () => { ); }); - it.effect("keeps [api] auto_expose_new_tables tri-state None when absent", () => { - const dir = withConfig("[api]\n"); + it.effect("decodes empty api schemas while keeping auto_expose_new_tables absent", () => { + const dir = withConfig('[api]\nschemas = ""\n'); return read(dir).pipe( Effect.tap((v) => Effect.sync(() => { + expect(v.apiSchemas).toEqual([]); expect(Option.isNone(v.baseline.apiAutoExposeNewTables)).toBe(true); rmSync(dir, { recursive: true, force: true }); }), From b62f3e0c837732eaae903b4e61ef17c2507343dd Mon Sep 17 00:00:00 2001 From: "supabase-cli-releaser[bot]" <246109035+supabase-cli-releaser[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 15:46:23 +0000 Subject: [PATCH 52/79] chore: sync API types from infrastructure (#5883) This PR was automatically created to sync API types from the infrastructure repository. Changes were detected in the generated API code after syncing with the latest spec from infrastructure. Co-authored-by: supabase-cli-releaser[bot] <246109035+supabase-cli-releaser[bot]@users.noreply.github.com> --- apps/cli-go/pkg/api/types.gen.go | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/cli-go/pkg/api/types.gen.go b/apps/cli-go/pkg/api/types.gen.go index 743af52366..245586c705 100644 --- a/apps/cli-go/pkg/api/types.gen.go +++ b/apps/cli-go/pkg/api/types.gen.go @@ -511,6 +511,7 @@ const ( // Defines values for JitStateResponse1UnavailableReason. const ( PostgresUpgradeRequired JitStateResponse1UnavailableReason = "postgres_upgrade_required" + SslEnforcementRequired JitStateResponse1UnavailableReason = "ssl_enforcement_required" TemporarilyUnavailable JitStateResponse1UnavailableReason = "temporarily_unavailable" ) From 112f005cc7a352ba8bf6069d16dd899d2f0ad790 Mon Sep 17 00:00:00 2001 From: "supabase-cli-releaser[bot]" <246109035+supabase-cli-releaser[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 20:49:29 +0000 Subject: [PATCH 53/79] chore: sync API types from infrastructure (#5884) This PR was automatically created to sync API types from the infrastructure repository. Changes were detected in the generated API code after syncing with the latest spec from infrastructure. Co-authored-by: supabase-cli-releaser[bot] <246109035+supabase-cli-releaser[bot]@users.noreply.github.com> --- apps/cli-go/pkg/api/client.gen.go | 98 +++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/apps/cli-go/pkg/api/client.gen.go b/apps/cli-go/pkg/api/client.gen.go index 7ff5b0a26c..1fb6625479 100644 --- a/apps/cli-go/pkg/api/client.gen.go +++ b/apps/cli-go/pkg/api/client.gen.go @@ -221,6 +221,9 @@ type ClientInterface interface { // V1GetProjectLogsAll request V1GetProjectLogsAll(ctx context.Context, ref string, params *V1GetProjectLogsAllParams, reqEditors ...RequestEditorFn) (*http.Response, error) + // V1ScrapeProjectMetrics request + V1ScrapeProjectMetrics(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) + // V1GetProjectUsageApiCount request V1GetProjectUsageApiCount(ctx context.Context, ref string, params *V1GetProjectUsageApiCountParams, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -1282,6 +1285,18 @@ func (c *Client) V1GetProjectLogsAll(ctx context.Context, ref string, params *V1 return c.Client.Do(req) } +func (c *Client) V1ScrapeProjectMetrics(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewV1ScrapeProjectMetricsRequest(c.Server, ref) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) V1GetProjectUsageApiCount(ctx context.Context, ref string, params *V1GetProjectUsageApiCountParams, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewV1GetProjectUsageApiCountRequest(c.Server, ref, params) if err != nil { @@ -5491,6 +5506,40 @@ func NewV1GetProjectLogsAllRequest(server string, ref string, params *V1GetProje return req, nil } +// NewV1ScrapeProjectMetricsRequest generates requests for V1ScrapeProjectMetrics +func NewV1ScrapeProjectMetricsRequest(server string, ref string) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/v1/projects/%s/analytics/endpoints/metrics", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + // NewV1GetProjectUsageApiCountRequest generates requests for V1GetProjectUsageApiCount func NewV1GetProjectUsageApiCountRequest(server string, ref string, params *V1GetProjectUsageApiCountParams) (*http.Request, error) { var err error @@ -11668,6 +11717,9 @@ type ClientWithResponsesInterface interface { // V1GetProjectLogsAllWithResponse request V1GetProjectLogsAllWithResponse(ctx context.Context, ref string, params *V1GetProjectLogsAllParams, reqEditors ...RequestEditorFn) (*V1GetProjectLogsAllResponse, error) + // V1ScrapeProjectMetricsWithResponse request + V1ScrapeProjectMetricsWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1ScrapeProjectMetricsResponse, error) + // V1GetProjectUsageApiCountWithResponse request V1GetProjectUsageApiCountWithResponse(ctx context.Context, ref string, params *V1GetProjectUsageApiCountParams, reqEditors ...RequestEditorFn) (*V1GetProjectUsageApiCountResponse, error) @@ -12972,6 +13024,27 @@ func (r V1GetProjectLogsAllResponse) StatusCode() int { return 0 } +type V1ScrapeProjectMetricsResponse struct { + Body []byte + HTTPResponse *http.Response +} + +// Status returns HTTPResponse.Status +func (r V1ScrapeProjectMetricsResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r V1ScrapeProjectMetricsResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type V1GetProjectUsageApiCountResponse struct { Body []byte HTTPResponse *http.Response @@ -16256,6 +16329,15 @@ func (c *ClientWithResponses) V1GetProjectLogsAllWithResponse(ctx context.Contex return ParseV1GetProjectLogsAllResponse(rsp) } +// V1ScrapeProjectMetricsWithResponse request returning *V1ScrapeProjectMetricsResponse +func (c *ClientWithResponses) V1ScrapeProjectMetricsWithResponse(ctx context.Context, ref string, reqEditors ...RequestEditorFn) (*V1ScrapeProjectMetricsResponse, error) { + rsp, err := c.V1ScrapeProjectMetrics(ctx, ref, reqEditors...) + if err != nil { + return nil, err + } + return ParseV1ScrapeProjectMetricsResponse(rsp) +} + // V1GetProjectUsageApiCountWithResponse request returning *V1GetProjectUsageApiCountResponse func (c *ClientWithResponses) V1GetProjectUsageApiCountWithResponse(ctx context.Context, ref string, params *V1GetProjectUsageApiCountParams, reqEditors ...RequestEditorFn) (*V1GetProjectUsageApiCountResponse, error) { rsp, err := c.V1GetProjectUsageApiCount(ctx, ref, params, reqEditors...) @@ -18730,6 +18812,22 @@ func ParseV1GetProjectLogsAllResponse(rsp *http.Response) (*V1GetProjectLogsAllR return response, nil } +// ParseV1ScrapeProjectMetricsResponse parses an HTTP response from a V1ScrapeProjectMetricsWithResponse call +func ParseV1ScrapeProjectMetricsResponse(rsp *http.Response) (*V1ScrapeProjectMetricsResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &V1ScrapeProjectMetricsResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + return response, nil +} + // ParseV1GetProjectUsageApiCountResponse parses an HTTP response from a V1GetProjectUsageApiCountWithResponse call func ParseV1GetProjectUsageApiCountResponse(rsp *http.Response) (*V1GetProjectUsageApiCountResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) From 7f1d939c8f395b207028e89a04059943540446be Mon Sep 17 00:00:00 2001 From: "supabase-cli-releaser[bot]" <246109035+supabase-cli-releaser[bot]@users.noreply.github.com> Date: Thu, 16 Jul 2026 08:53:14 +0000 Subject: [PATCH 54/79] chore: sync API types from infrastructure (#5888) This PR was automatically created to sync API types from the infrastructure repository. Changes were detected in the generated API code after syncing with the latest spec from infrastructure. Co-authored-by: supabase-cli-releaser[bot] <246109035+supabase-cli-releaser[bot]@users.noreply.github.com> --- apps/cli-go/pkg/api/types.gen.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/cli-go/pkg/api/types.gen.go b/apps/cli-go/pkg/api/types.gen.go index 245586c705..308eafac63 100644 --- a/apps/cli-go/pkg/api/types.gen.go +++ b/apps/cli-go/pkg/api/types.gen.go @@ -3432,6 +3432,7 @@ type OrganizationResponseV1 struct { // PgsodiumConfigResponse defines model for PgsodiumConfigResponse. type PgsodiumConfigResponse struct { + // RootKey The pgsodium root key: 32 bytes, hex-encoded (64 characters). RootKey string `json:"root_key"` } @@ -4373,6 +4374,7 @@ type UpdateJitAccessBody struct { // UpdatePgsodiumConfigBody defines model for UpdatePgsodiumConfigBody. type UpdatePgsodiumConfigBody struct { + // RootKey The pgsodium root key: 32 bytes, hex-encoded (64 characters). RootKey string `json:"root_key"` } From 338321430422d32d2de30903a6af5047ec67a35a Mon Sep 17 00:00:00 2001 From: Vaibhav <117663341+7ttp@users.noreply.github.com> Date: Thu, 16 Jul 2026 15:02:02 +0530 Subject: [PATCH 55/79] fix(cli): detect local API exposure lints (#5887) ## TL;DR `db advisors --local` silently missed API exposure lints when `pgrst.db_schemas` was unset in its direct database session This defaults the missing setting to PostgREST's `public` schema while preserving explicitly configured and empty schema values. ## ref - closes #5868 - follows supabase/splinter#168 --- .../internal/db/advisors/templates/lints.sql | 14 +++++++------- .../db/advisors/advisors.format.unit.test.ts | 8 ++++++++ .../commands/db/advisors/advisors.lints-sql.ts | 2 +- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/apps/cli-go/internal/db/advisors/templates/lints.sql b/apps/cli-go/internal/db/advisors/templates/lints.sql index c8a54a3a7f..c8c96de64c 100644 --- a/apps/cli-go/internal/db/advisors/templates/lints.sql +++ b/apps/cli-go/internal/db/advisors/templates/lints.sql @@ -117,7 +117,7 @@ where pg_catalog.has_table_privilege('anon', c.oid, 'SELECT') or pg_catalog.has_table_privilege('authenticated', c.oid, 'SELECT') ) - and n.nspname = any(array(select trim(unnest(string_to_array(current_setting('pgrst.db_schemas', 't'), ','))))) + and n.nspname = any(array(select trim(unnest(string_to_array(coalesce(current_setting('pgrst.db_schemas', 't'), 'public'), ','))))) -- Exclude self and c.relname <> '0002_auth_users_exposed' -- There are 3 insecure configurations @@ -610,7 +610,7 @@ where or pg_catalog.has_table_privilege('authenticated', c.oid, 'SELECT') ) and substring(pg_catalog.version() from 'PostgreSQL ([0-9]+)') >= '15' -- security invoker was added in pg15 - and n.nspname = any(array(select trim(unnest(string_to_array(current_setting('pgrst.db_schemas', 't'), ','))))) + and n.nspname = any(array(select trim(unnest(string_to_array(coalesce(current_setting('pgrst.db_schemas', 't'), 'public'), ','))))) and n.nspname not in ( '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault' ) @@ -705,7 +705,7 @@ where pg_catalog.has_table_privilege('anon', c.oid, 'SELECT') or pg_catalog.has_table_privilege('authenticated', c.oid, 'SELECT') ) - and n.nspname = any(array(select trim(unnest(string_to_array(current_setting('pgrst.db_schemas', 't'), ','))))) + and n.nspname = any(array(select trim(unnest(string_to_array(coalesce(current_setting('pgrst.db_schemas', 't'), 'public'), ','))))) and n.nspname not in ( '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault' )) @@ -836,7 +836,7 @@ where pg_catalog.has_table_privilege('anon', c.oid, 'SELECT') or pg_catalog.has_table_privilege('authenticated', c.oid, 'SELECT') ) - and n.nspname = any(array(select trim(unnest(string_to_array(current_setting('pgrst.db_schemas', 't'), ','))))) + and n.nspname = any(array(select trim(unnest(string_to_array(coalesce(current_setting('pgrst.db_schemas', 't'), 'public'), ','))))) and n.nspname not in ( '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault' ) @@ -879,7 +879,7 @@ where pg_catalog.has_table_privilege('anon', c.oid, 'SELECT') or pg_catalog.has_table_privilege('authenticated', c.oid, 'SELECT') ) - and n.nspname = any(array(select trim(unnest(string_to_array(current_setting('pgrst.db_schemas', 't'), ','))))) + and n.nspname = any(array(select trim(unnest(string_to_array(coalesce(current_setting('pgrst.db_schemas', 't'), 'public'), ','))))) and n.nspname not in ( '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault' ) @@ -966,7 +966,7 @@ where and n.nspname = 'pgmq' -- tables in the pgmq schema and c.relname like 'q_%' -- only queue tables -- Constant requirements - and 'pgmq_public' = any(array(select trim(unnest(string_to_array(current_setting('pgrst.db_schemas', 't'), ',')))))) + and 'pgmq_public' = any(array(select trim(unnest(string_to_array(coalesce(current_setting('pgrst.db_schemas', 't'), 'public'), ',')))))) union all ( with constants as ( @@ -1198,7 +1198,7 @@ exposed_tables as ( pg_catalog.has_table_privilege('anon', c.oid, 'SELECT') or pg_catalog.has_table_privilege('authenticated', c.oid, 'SELECT') ) - and n.nspname = any(array(select trim(unnest(string_to_array(current_setting('pgrst.db_schemas', 't'), ','))))) + and n.nspname = any(array(select trim(unnest(string_to_array(coalesce(current_setting('pgrst.db_schemas', 't'), 'public'), ','))))) and n.nspname not in ( '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault' ) diff --git a/apps/cli/src/legacy/commands/db/advisors/advisors.format.unit.test.ts b/apps/cli/src/legacy/commands/db/advisors/advisors.format.unit.test.ts index ef64d7fdb4..e293152d44 100644 --- a/apps/cli/src/legacy/commands/db/advisors/advisors.format.unit.test.ts +++ b/apps/cli/src/legacy/commands/db/advisors/advisors.format.unit.test.ts @@ -385,4 +385,12 @@ describe("splitLegacyLintsSql", () => { expect(setup).toBe("set local search_path = ''"); expect(query.startsWith("(")).toBe(true); }); + + it("defaults API-exposure lints to public when pgrst.db_schemas is unset", () => { + const [, query] = splitLegacyLintsSql(); + expect(query).not.toContain("string_to_array(current_setting('pgrst.db_schemas', 't'), ',')"); + expect(query).toContain( + "string_to_array(coalesce(current_setting('pgrst.db_schemas', 't'), 'public'), ',')", + ); + }); }); diff --git a/apps/cli/src/legacy/commands/db/advisors/advisors.lints-sql.ts b/apps/cli/src/legacy/commands/db/advisors/advisors.lints-sql.ts index 97d8947c15..5cf4bfb06a 100644 --- a/apps/cli/src/legacy/commands/db/advisors/advisors.lints-sql.ts +++ b/apps/cli/src/legacy/commands/db/advisors/advisors.lints-sql.ts @@ -10,7 +10,7 @@ * (`advisors.go:154-160`). */ const LEGACY_ADVISORS_LINTS_SQL = - "set local search_path = '';\n\n(\nwith foreign_keys as (\n select\n cl.relnamespace::regnamespace::text as schema_name,\n cl.relname as table_name,\n cl.oid as table_oid,\n ct.conname as fkey_name,\n ct.conkey as col_attnums\n from\n pg_catalog.pg_constraint ct\n join pg_catalog.pg_class cl -- fkey owning table\n on ct.conrelid = cl.oid\n left join pg_catalog.pg_depend d\n on d.objid = cl.oid\n and d.deptype = 'e'\n where\n ct.contype = 'f' -- foreign key constraints\n and d.objid is null -- exclude tables that are dependencies of extensions\n and cl.relnamespace::regnamespace::text not in (\n 'pg_catalog', 'information_schema', 'auth', 'storage', 'vault', 'extensions'\n )\n),\nindex_ as (\n select\n pi.indrelid as table_oid,\n indexrelid::regclass as index_,\n string_to_array(indkey::text, ' ')::smallint[] as col_attnums\n from\n pg_catalog.pg_index pi\n where\n indisvalid\n)\nselect\n 'unindexed_foreign_keys' as name,\n 'Unindexed foreign keys' as title,\n 'INFO' as level,\n 'EXTERNAL' as facing,\n array['PERFORMANCE'] as categories,\n 'Identifies foreign key constraints without a covering index, which can impact database performance.' as description,\n format(\n 'Table `%s.%s` has a foreign key `%s` without a covering index. This can lead to suboptimal query performance.',\n fk.schema_name,\n fk.table_name,\n fk.fkey_name\n ) as detail,\n 'https://supabase.com/docs/guides/database/database-linter?lint=0001_unindexed_foreign_keys' as remediation,\n jsonb_build_object(\n 'schema', fk.schema_name,\n 'name', fk.table_name,\n 'type', 'table',\n 'fkey_name', fk.fkey_name,\n 'fkey_columns', fk.col_attnums\n ) as metadata,\n format('unindexed_foreign_keys_%s_%s_%s', fk.schema_name, fk.table_name, fk.fkey_name) as cache_key\nfrom\n foreign_keys fk\n left join index_ idx\n on fk.table_oid = idx.table_oid\n and fk.col_attnums = idx.col_attnums[1:array_length(fk.col_attnums, 1)]\n left join pg_catalog.pg_depend dep\n on idx.table_oid = dep.objid\n and dep.deptype = 'e'\nwhere\n idx.index_ is null\n and fk.schema_name not in (\n '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n )\n and dep.objid is null -- exclude tables owned by extensions\norder by\n fk.schema_name,\n fk.table_name,\n fk.fkey_name)\nunion all\n(\nselect\n 'auth_users_exposed' as name,\n 'Exposed Auth Users' as title,\n 'ERROR' as level,\n 'EXTERNAL' as facing,\n array['SECURITY'] as categories,\n 'Detects if auth.users is exposed to anon or authenticated roles via a view or materialized view in schemas exposed to PostgREST, potentially compromising user data security.' as description,\n format(\n 'View/Materialized View \"%s\" in the public schema may expose `auth.users` data to anon or authenticated roles.',\n c.relname\n ) as detail,\n 'https://supabase.com/docs/guides/database/database-linter?lint=0002_auth_users_exposed' as remediation,\n jsonb_build_object(\n 'schema', n.nspname,\n 'name', c.relname,\n 'type', 'view',\n 'exposed_to', array_remove(array_agg(DISTINCT case when pg_catalog.has_table_privilege('anon', c.oid, 'SELECT') then 'anon' when pg_catalog.has_table_privilege('authenticated', c.oid, 'SELECT') then 'authenticated' end), null)\n ) as metadata,\n format('auth_users_exposed_%s_%s', n.nspname, c.relname) as cache_key\nfrom\n -- Identify the oid for auth.users\n pg_catalog.pg_class auth_users_pg_class\n join pg_catalog.pg_namespace auth_users_pg_namespace\n on auth_users_pg_class.relnamespace = auth_users_pg_namespace.oid\n and auth_users_pg_class.relname = 'users'\n and auth_users_pg_namespace.nspname = 'auth'\n -- Depends on auth.users\n join pg_catalog.pg_depend d\n on d.refobjid = auth_users_pg_class.oid\n join pg_catalog.pg_rewrite r\n on r.oid = d.objid\n join pg_catalog.pg_class c\n on c.oid = r.ev_class\n join pg_catalog.pg_namespace n\n on n.oid = c.relnamespace\n join pg_catalog.pg_class pg_class_auth_users\n on d.refobjid = pg_class_auth_users.oid\nwhere\n d.deptype = 'n'\n and (\n pg_catalog.has_table_privilege('anon', c.oid, 'SELECT')\n or pg_catalog.has_table_privilege('authenticated', c.oid, 'SELECT')\n )\n and n.nspname = any(array(select trim(unnest(string_to_array(current_setting('pgrst.db_schemas', 't'), ',')))))\n -- Exclude self\n and c.relname <> '0002_auth_users_exposed'\n -- There are 3 insecure configurations\n and\n (\n -- Materialized views don't support RLS so this is insecure by default\n (c.relkind in ('m')) -- m for materialized view\n or\n -- Standard View, accessible to anon or authenticated that is security_definer\n (\n c.relkind = 'v' -- v for view\n -- Exclude security invoker views\n and not (\n lower(coalesce(c.reloptions::text,'{}'))::text[]\n && array[\n 'security_invoker=1',\n 'security_invoker=true',\n 'security_invoker=yes',\n 'security_invoker=on'\n ]\n )\n )\n or\n -- Standard View, security invoker, but no RLS enabled on auth.users\n (\n c.relkind in ('v') -- v for view\n -- is security invoker\n and (\n lower(coalesce(c.reloptions::text,'{}'))::text[]\n && array[\n 'security_invoker=1',\n 'security_invoker=true',\n 'security_invoker=yes',\n 'security_invoker=on'\n ]\n )\n and not pg_class_auth_users.relrowsecurity\n )\n )\ngroup by\n n.nspname,\n c.relname,\n c.oid)\nunion all\n(\nwith policies as (\n select\n nsp.nspname as schema_name,\n pb.tablename as table_name,\n pc.relrowsecurity as is_rls_active,\n polname as policy_name,\n polpermissive as is_permissive, -- if not, then restrictive\n (select array_agg(r::regrole) from unnest(polroles) as x(r)) as roles,\n case polcmd\n when 'r' then 'SELECT'\n when 'a' then 'INSERT'\n when 'w' then 'UPDATE'\n when 'd' then 'DELETE'\n when '*' then 'ALL'\n end as command,\n qual,\n with_check\n from\n pg_catalog.pg_policy pa\n join pg_catalog.pg_class pc\n on pa.polrelid = pc.oid\n join pg_catalog.pg_namespace nsp\n on pc.relnamespace = nsp.oid\n join pg_catalog.pg_policies pb\n on pc.relname = pb.tablename\n and nsp.nspname = pb.schemaname\n and pa.polname = pb.policyname\n)\nselect\n 'auth_rls_initplan' as name,\n 'Auth RLS Initialization Plan' as title,\n 'WARN' as level,\n 'EXTERNAL' as facing,\n array['PERFORMANCE'] as categories,\n 'Detects if calls to `current_setting()` and `auth.()` in RLS policies are being unnecessarily re-evaluated for each row' as description,\n format(\n 'Table `%s.%s` has a row level security policy `%s` that re-evaluates current_setting() or auth.() for each row. This produces suboptimal query performance at scale. Resolve the issue by replacing `auth.()` with `(select auth.())`. See [docs](https://supabase.com/docs/guides/database/postgres/row-level-security#call-functions-with-select) for more info.',\n schema_name,\n table_name,\n policy_name\n ) as detail,\n 'https://supabase.com/docs/guides/database/database-linter?lint=0003_auth_rls_initplan' as remediation,\n jsonb_build_object(\n 'schema', schema_name,\n 'name', table_name,\n 'type', 'table'\n ) as metadata,\n format('auth_rls_init_plan_%s_%s_%s', schema_name, table_name, policy_name) as cache_key\nfrom\n policies\nwhere\n is_rls_active\n -- NOTE: does not include realtime in support of monitoring policies on realtime.messages\n and schema_name not in (\n '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n )\n and (\n -- Example: auth.uid()\n (\n qual like '%auth.uid()%'\n and lower(qual) not like '%select auth.uid()%'\n )\n or (\n qual like '%auth.jwt()%'\n and lower(qual) not like '%select auth.jwt()%'\n )\n or (\n qual like '%auth.role()%'\n and lower(qual) not like '%select auth.role()%'\n )\n or (\n qual like '%auth.email()%'\n and lower(qual) not like '%select auth.email()%'\n )\n or (\n qual like '%current\\_setting(%)%'\n and lower(qual) not like '%select current\\_setting(%)%'\n )\n or (\n with_check like '%auth.uid()%'\n and lower(with_check) not like '%select auth.uid()%'\n )\n or (\n with_check like '%auth.jwt()%'\n and lower(with_check) not like '%select auth.jwt()%'\n )\n or (\n with_check like '%auth.role()%'\n and lower(with_check) not like '%select auth.role()%'\n )\n or (\n with_check like '%auth.email()%'\n and lower(with_check) not like '%select auth.email()%'\n )\n or (\n with_check like '%current\\_setting(%)%'\n and lower(with_check) not like '%select current\\_setting(%)%'\n )\n ))\nunion all\n(\nselect\n 'no_primary_key' as name,\n 'No Primary Key' as title,\n 'INFO' as level,\n 'EXTERNAL' as facing,\n array['PERFORMANCE'] as categories,\n 'Detects if a table does not have a primary key. Tables without a primary key can be inefficient to interact with at scale.' as description,\n format(\n 'Table `%s.%s` does not have a primary key',\n pgns.nspname,\n pgc.relname\n ) as detail,\n 'https://supabase.com/docs/guides/database/database-linter?lint=0004_no_primary_key' as remediation,\n jsonb_build_object(\n 'schema', pgns.nspname,\n 'name', pgc.relname,\n 'type', 'table'\n ) as metadata,\n format(\n 'no_primary_key_%s_%s',\n pgns.nspname,\n pgc.relname\n ) as cache_key\nfrom\n pg_catalog.pg_class pgc\n join pg_catalog.pg_namespace pgns\n on pgns.oid = pgc.relnamespace\n left join pg_catalog.pg_index pgi\n on pgi.indrelid = pgc.oid\n left join pg_catalog.pg_depend dep\n on pgc.oid = dep.objid\n and dep.deptype = 'e'\nwhere\n pgc.relkind = 'r' -- regular tables\n and pgns.nspname not in (\n '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n )\n and dep.objid is null -- exclude tables owned by extensions\ngroup by\n pgc.oid,\n pgns.nspname,\n pgc.relname\nhaving\n max(coalesce(pgi.indisprimary, false)::int) = 0)\nunion all\n(\nselect\n 'unused_index' as name,\n 'Unused Index' as title,\n 'INFO' as level,\n 'EXTERNAL' as facing,\n array['PERFORMANCE'] as categories,\n 'Detects if an index has never been used and may be a candidate for removal.' as description,\n format(\n 'Index `%s` on table `%s.%s` has not been used',\n psui.indexrelname,\n psui.schemaname,\n psui.relname\n ) as detail,\n 'https://supabase.com/docs/guides/database/database-linter?lint=0005_unused_index' as remediation,\n jsonb_build_object(\n 'schema', psui.schemaname,\n 'name', psui.relname,\n 'type', 'table'\n ) as metadata,\n format(\n 'unused_index_%s_%s_%s',\n psui.schemaname,\n psui.relname,\n psui.indexrelname\n ) as cache_key\n\nfrom\n pg_catalog.pg_stat_user_indexes psui\n join pg_catalog.pg_index pi\n on psui.indexrelid = pi.indexrelid\n left join pg_catalog.pg_depend dep\n on psui.relid = dep.objid\n and dep.deptype = 'e'\nwhere\n psui.idx_scan = 0\n and not pi.indisunique\n and not pi.indisprimary\n and dep.objid is null -- exclude tables owned by extensions\n and psui.schemaname not in (\n '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n ))\nunion all\n(\nselect\n 'multiple_permissive_policies' as name,\n 'Multiple Permissive Policies' as title,\n 'WARN' as level,\n 'EXTERNAL' as facing,\n array['PERFORMANCE'] as categories,\n 'Detects if multiple permissive row level security policies are present on a table for the same `role` and `action` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query.' as description,\n format(\n 'Table `%s.%s` has multiple permissive policies for role `%s` for action `%s`. Policies include `%s`',\n n.nspname,\n c.relname,\n r.rolname,\n act.cmd,\n array_agg(p.polname order by p.polname)\n ) as detail,\n 'https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies' as remediation,\n jsonb_build_object(\n 'schema', n.nspname,\n 'name', c.relname,\n 'type', 'table'\n ) as metadata,\n format(\n 'multiple_permissive_policies_%s_%s_%s_%s',\n n.nspname,\n c.relname,\n r.rolname,\n act.cmd\n ) as cache_key\nfrom\n pg_catalog.pg_policy p\n join pg_catalog.pg_class c\n on p.polrelid = c.oid\n join pg_catalog.pg_namespace n\n on c.relnamespace = n.oid\n join pg_catalog.pg_roles r\n on p.polroles @> array[r.oid]\n or p.polroles = array[0::oid]\n left join pg_catalog.pg_depend dep\n on c.oid = dep.objid\n and dep.deptype = 'e',\n lateral (\n select x.cmd\n from unnest((\n select\n case p.polcmd\n when 'r' then array['SELECT']\n when 'a' then array['INSERT']\n when 'w' then array['UPDATE']\n when 'd' then array['DELETE']\n when '*' then array['SELECT', 'INSERT', 'UPDATE', 'DELETE']\n else array['ERROR']\n end as actions\n )) x(cmd)\n ) act(cmd)\nwhere\n c.relkind = 'r' -- regular tables\n and p.polpermissive -- policy is permissive\n and n.nspname not in (\n '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n )\n and r.rolname not like 'pg_%'\n and r.rolname not like 'supabase%admin'\n and not r.rolbypassrls\n and dep.objid is null -- exclude tables owned by extensions\ngroup by\n n.nspname,\n c.relname,\n r.rolname,\n act.cmd\nhaving\n count(1) > 1)\nunion all\n(\nselect\n 'policy_exists_rls_disabled' as name,\n 'Policy Exists RLS Disabled' as title,\n 'ERROR' as level,\n 'EXTERNAL' as facing,\n array['SECURITY'] as categories,\n 'Detects cases where row level security (RLS) policies have been created, but RLS has not been enabled for the underlying table.' as description,\n format(\n 'Table `%s.%s` has RLS policies but RLS is not enabled on the table. Policies include %s.',\n n.nspname,\n c.relname,\n array_agg(p.polname order by p.polname)\n ) as detail,\n 'https://supabase.com/docs/guides/database/database-linter?lint=0007_policy_exists_rls_disabled' as remediation,\n jsonb_build_object(\n 'schema', n.nspname,\n 'name', c.relname,\n 'type', 'table'\n ) as metadata,\n format(\n 'policy_exists_rls_disabled_%s_%s',\n n.nspname,\n c.relname\n ) as cache_key\nfrom\n pg_catalog.pg_policy p\n join pg_catalog.pg_class c\n on p.polrelid = c.oid\n join pg_catalog.pg_namespace n\n on c.relnamespace = n.oid\n left join pg_catalog.pg_depend dep\n on c.oid = dep.objid\n and dep.deptype = 'e'\nwhere\n c.relkind = 'r' -- regular tables\n and n.nspname not in (\n '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n )\n -- RLS is disabled\n and not c.relrowsecurity\n and dep.objid is null -- exclude tables owned by extensions\ngroup by\n n.nspname,\n c.relname)\nunion all\n(\nselect\n 'rls_enabled_no_policy' as name,\n 'RLS Enabled No Policy' as title,\n 'INFO' as level,\n 'EXTERNAL' as facing,\n array['SECURITY'] as categories,\n 'Detects cases where row level security (RLS) has been enabled on a table but no RLS policies have been created.' as description,\n format(\n 'Table `%s.%s` has RLS enabled, but no policies exist',\n n.nspname,\n c.relname\n ) as detail,\n 'https://supabase.com/docs/guides/database/database-linter?lint=0008_rls_enabled_no_policy' as remediation,\n jsonb_build_object(\n 'schema', n.nspname,\n 'name', c.relname,\n 'type', 'table'\n ) as metadata,\n format(\n 'rls_enabled_no_policy_%s_%s',\n n.nspname,\n c.relname\n ) as cache_key\nfrom\n pg_catalog.pg_class c\n left join pg_catalog.pg_policy p\n on p.polrelid = c.oid\n join pg_catalog.pg_namespace n\n on c.relnamespace = n.oid\n left join pg_catalog.pg_depend dep\n on c.oid = dep.objid\n and dep.deptype = 'e'\nwhere\n c.relkind = 'r' -- regular tables\n and n.nspname not in (\n '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n )\n -- RLS is enabled\n and c.relrowsecurity\n and p.polname is null\n and dep.objid is null -- exclude tables owned by extensions\ngroup by\n n.nspname,\n c.relname)\nunion all\n(\nselect\n 'duplicate_index' as name,\n 'Duplicate Index' as title,\n 'WARN' as level,\n 'EXTERNAL' as facing,\n array['PERFORMANCE'] as categories,\n 'Detects cases where two ore more identical indexes exist.' as description,\n format(\n 'Table `%s.%s` has identical indexes %s. Drop all except one of them',\n n.nspname,\n c.relname,\n array_agg(pi.indexname order by pi.indexname)\n ) as detail,\n 'https://supabase.com/docs/guides/database/database-linter?lint=0009_duplicate_index' as remediation,\n jsonb_build_object(\n 'schema', n.nspname,\n 'name', c.relname,\n 'type', case\n when c.relkind = 'r' then 'table'\n when c.relkind = 'm' then 'materialized view'\n else 'ERROR'\n end,\n 'indexes', array_agg(pi.indexname order by pi.indexname)\n ) as metadata,\n format(\n 'duplicate_index_%s_%s_%s',\n n.nspname,\n c.relname,\n array_agg(pi.indexname order by pi.indexname)\n ) as cache_key\nfrom\n pg_catalog.pg_indexes pi\n join pg_catalog.pg_namespace n\n on n.nspname = pi.schemaname\n join pg_catalog.pg_class c\n on pi.tablename = c.relname\n and n.oid = c.relnamespace\n left join pg_catalog.pg_depend dep\n on c.oid = dep.objid\n and dep.deptype = 'e'\nwhere\n c.relkind in ('r', 'm') -- tables and materialized views\n and n.nspname not in (\n '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n )\n and dep.objid is null -- exclude tables owned by extensions\ngroup by\n n.nspname,\n c.relkind,\n c.relname,\n replace(pi.indexdef, pi.indexname, '')\nhaving\n count(*) > 1)\nunion all\n(\nselect\n 'security_definer_view' as name,\n 'Security Definer View' as title,\n 'ERROR' as level,\n 'EXTERNAL' as facing,\n array['SECURITY'] as categories,\n 'Detects views defined with the SECURITY DEFINER property. These views enforce Postgres permissions and row level security policies (RLS) of the view creator, rather than that of the querying user' as description,\n format(\n 'View `%s.%s` is defined with the SECURITY DEFINER property',\n n.nspname,\n c.relname\n ) as detail,\n 'https://supabase.com/docs/guides/database/database-linter?lint=0010_security_definer_view' as remediation,\n jsonb_build_object(\n 'schema', n.nspname,\n 'name', c.relname,\n 'type', 'view'\n ) as metadata,\n format(\n 'security_definer_view_%s_%s',\n n.nspname,\n c.relname\n ) as cache_key\nfrom\n pg_catalog.pg_class c\n join pg_catalog.pg_namespace n\n on n.oid = c.relnamespace\n left join pg_catalog.pg_depend dep\n on c.oid = dep.objid\n and dep.deptype = 'e'\nwhere\n c.relkind = 'v'\n and (\n pg_catalog.has_table_privilege('anon', c.oid, 'SELECT')\n or pg_catalog.has_table_privilege('authenticated', c.oid, 'SELECT')\n )\n and substring(pg_catalog.version() from 'PostgreSQL ([0-9]+)') >= '15' -- security invoker was added in pg15\n and n.nspname = any(array(select trim(unnest(string_to_array(current_setting('pgrst.db_schemas', 't'), ',')))))\n and n.nspname not in (\n '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n )\n and dep.objid is null -- exclude views owned by extensions\n and not (\n lower(coalesce(c.reloptions::text,'{}'))::text[]\n && array[\n 'security_invoker=1',\n 'security_invoker=true',\n 'security_invoker=yes',\n 'security_invoker=on'\n ]\n ))\nunion all\n(\nselect\n 'function_search_path_mutable' as name,\n 'Function Search Path Mutable' as title,\n 'WARN' as level,\n 'EXTERNAL' as facing,\n array['SECURITY'] as categories,\n 'Detects functions where the search_path parameter is not set.' as description,\n format(\n 'Function `%s.%s` has a role mutable search_path',\n n.nspname,\n p.proname\n ) as detail,\n 'https://supabase.com/docs/guides/database/database-linter?lint=0011_function_search_path_mutable' as remediation,\n jsonb_build_object(\n 'schema', n.nspname,\n 'name', p.proname,\n 'type', 'function'\n ) as metadata,\n format(\n 'function_search_path_mutable_%s_%s_%s',\n n.nspname,\n p.proname,\n md5(p.prosrc) -- required when function is polymorphic\n ) as cache_key\nfrom\n pg_catalog.pg_proc p\n join pg_catalog.pg_namespace n\n on p.pronamespace = n.oid\n left join pg_catalog.pg_depend dep\n on p.oid = dep.objid\n and dep.deptype = 'e'\nwhere\n n.nspname not in (\n '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n )\n and dep.objid is null -- exclude functions owned by extensions\n -- Search path not set\n and not exists (\n select 1\n from unnest(coalesce(p.proconfig, '{}')) as config\n where config like 'search_path=%'\n ))\nunion all\n(\nselect\n 'rls_disabled_in_public' as name,\n 'RLS Disabled in Public' as title,\n 'ERROR' as level,\n 'EXTERNAL' as facing,\n array['SECURITY'] as categories,\n 'Detects cases where row level security (RLS) has not been enabled on tables in schemas exposed to PostgREST' as description,\n format(\n 'Table `%s.%s` is public, but RLS has not been enabled.',\n n.nspname,\n c.relname\n ) as detail,\n 'https://supabase.com/docs/guides/database/database-linter?lint=0013_rls_disabled_in_public' as remediation,\n jsonb_build_object(\n 'schema', n.nspname,\n 'name', c.relname,\n 'type', 'table'\n ) as metadata,\n format(\n 'rls_disabled_in_public_%s_%s',\n n.nspname,\n c.relname\n ) as cache_key\nfrom\n pg_catalog.pg_class c\n join pg_catalog.pg_namespace n\n on c.relnamespace = n.oid\nwhere\n c.relkind = 'r' -- regular tables\n -- RLS is disabled\n and not c.relrowsecurity\n and (\n pg_catalog.has_table_privilege('anon', c.oid, 'SELECT')\n or pg_catalog.has_table_privilege('authenticated', c.oid, 'SELECT')\n )\n and n.nspname = any(array(select trim(unnest(string_to_array(current_setting('pgrst.db_schemas', 't'), ',')))))\n and n.nspname not in (\n '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n ))\nunion all\n(\nselect\n 'extension_in_public' as name,\n 'Extension in Public' as title,\n 'WARN' as level,\n 'EXTERNAL' as facing,\n array['SECURITY'] as categories,\n 'Detects extensions installed in the `public` schema.' as description,\n format(\n 'Extension `%s` is installed in the public schema. Move it to another schema.',\n pe.extname\n ) as detail,\n 'https://supabase.com/docs/guides/database/database-linter?lint=0014_extension_in_public' as remediation,\n jsonb_build_object(\n 'schema', pe.extnamespace::regnamespace,\n 'name', pe.extname,\n 'type', 'extension'\n ) as metadata,\n format(\n 'extension_in_public_%s',\n pe.extname\n ) as cache_key\nfrom\n pg_catalog.pg_extension pe\nwhere\n -- plpgsql is installed by default in public and outside user control\n -- confirmed safe\n pe.extname not in ('plpgsql')\n -- Scoping this to public is not optimal. Ideally we would use the postgres\n -- search path. That currently isn't available via SQL. In other lints\n -- we have used has_schema_privilege('anon', 'extensions', 'USAGE') but that\n -- is not appropriate here as it would evaluate true for the extensions schema\n and pe.extnamespace::regnamespace::text = 'public')\nunion all\n(\nwith policies as (\n select\n nsp.nspname as schema_name,\n pb.tablename as table_name,\n polname as policy_name,\n qual,\n with_check\n from\n pg_catalog.pg_policy pa\n join pg_catalog.pg_class pc\n on pa.polrelid = pc.oid\n join pg_catalog.pg_namespace nsp\n on pc.relnamespace = nsp.oid\n join pg_catalog.pg_policies pb\n on pc.relname = pb.tablename\n and nsp.nspname = pb.schemaname\n and pa.polname = pb.policyname\n)\nselect\n 'rls_references_user_metadata' as name,\n 'RLS references user metadata' as title,\n 'ERROR' as level,\n 'EXTERNAL' as facing,\n array['SECURITY'] as categories,\n 'Detects when Supabase Auth user_metadata is referenced insecurely in a row level security (RLS) policy.' as description,\n format(\n 'Table `%s.%s` has a row level security policy `%s` that references Supabase Auth `user_metadata`. `user_metadata` is editable by end users and should never be used in a security context.',\n schema_name,\n table_name,\n policy_name\n ) as detail,\n 'https://supabase.com/docs/guides/database/database-linter?lint=0015_rls_references_user_metadata' as remediation,\n jsonb_build_object(\n 'schema', schema_name,\n 'name', table_name,\n 'type', 'table'\n ) as metadata,\n format('rls_references_user_metadata_%s_%s_%s', schema_name, table_name, policy_name) as cache_key\nfrom\n policies\nwhere\n schema_name not in (\n '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n )\n and (\n -- Example: auth.jwt() -> 'user_metadata'\n -- False positives are possible, but it isn't practical to string match\n -- If false positive rate is too high, this expression can iterate\n qual like '%auth.jwt()%user_metadata%'\n or qual like '%current_setting(%request.jwt.claims%)%user_metadata%'\n or with_check like '%auth.jwt()%user_metadata%'\n or with_check like '%current_setting(%request.jwt.claims%)%user_metadata%'\n ))\nunion all\n(\nselect\n 'materialized_view_in_api' as name,\n 'Materialized View in API' as title,\n 'WARN' as level,\n 'EXTERNAL' as facing,\n array['SECURITY'] as categories,\n 'Detects materialized views that are accessible over the Data APIs.' as description,\n format(\n 'Materialized view `%s.%s` is selectable by anon or authenticated roles',\n n.nspname,\n c.relname\n ) as detail,\n 'https://supabase.com/docs/guides/database/database-linter?lint=0016_materialized_view_in_api' as remediation,\n jsonb_build_object(\n 'schema', n.nspname,\n 'name', c.relname,\n 'type', 'materialized view'\n ) as metadata,\n format(\n 'materialized_view_in_api_%s_%s',\n n.nspname,\n c.relname\n ) as cache_key\nfrom\n pg_catalog.pg_class c\n join pg_catalog.pg_namespace n\n on n.oid = c.relnamespace\n left join pg_catalog.pg_depend dep\n on c.oid = dep.objid\n and dep.deptype = 'e'\nwhere\n c.relkind = 'm'\n and (\n pg_catalog.has_table_privilege('anon', c.oid, 'SELECT')\n or pg_catalog.has_table_privilege('authenticated', c.oid, 'SELECT')\n )\n and n.nspname = any(array(select trim(unnest(string_to_array(current_setting('pgrst.db_schemas', 't'), ',')))))\n and n.nspname not in (\n '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n )\n and dep.objid is null)\nunion all\n(\nselect\n 'foreign_table_in_api' as name,\n 'Foreign Table in API' as title,\n 'WARN' as level,\n 'EXTERNAL' as facing,\n array['SECURITY'] as categories,\n 'Detects foreign tables that are accessible over APIs. Foreign tables do not respect row level security policies.' as description,\n format(\n 'Foreign table `%s.%s` is accessible over APIs',\n n.nspname,\n c.relname\n ) as detail,\n 'https://supabase.com/docs/guides/database/database-linter?lint=0017_foreign_table_in_api' as remediation,\n jsonb_build_object(\n 'schema', n.nspname,\n 'name', c.relname,\n 'type', 'foreign table'\n ) as metadata,\n format(\n 'foreign_table_in_api_%s_%s',\n n.nspname,\n c.relname\n ) as cache_key\nfrom\n pg_catalog.pg_class c\n join pg_catalog.pg_namespace n\n on n.oid = c.relnamespace\n left join pg_catalog.pg_depend dep\n on c.oid = dep.objid\n and dep.deptype = 'e'\nwhere\n c.relkind = 'f'\n and (\n pg_catalog.has_table_privilege('anon', c.oid, 'SELECT')\n or pg_catalog.has_table_privilege('authenticated', c.oid, 'SELECT')\n )\n and n.nspname = any(array(select trim(unnest(string_to_array(current_setting('pgrst.db_schemas', 't'), ',')))))\n and n.nspname not in (\n '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n )\n and dep.objid is null)\nunion all\n(\nselect\n 'unsupported_reg_types' as name,\n 'Unsupported reg types' as title,\n 'WARN' as level,\n 'EXTERNAL' as facing,\n array['SECURITY'] as categories,\n 'Identifies columns using unsupported reg* types outside pg_catalog schema, which prevents database upgrades using pg_upgrade.' as description,\n format(\n 'Table `%s.%s` has a column `%s` with unsupported reg* type `%s`.',\n n.nspname,\n c.relname,\n a.attname,\n t.typname\n ) as detail,\n 'https://supabase.com/docs/guides/database/database-linter?lint=unsupported_reg_types' as remediation,\n jsonb_build_object(\n 'schema', n.nspname,\n 'name', c.relname,\n 'column', a.attname,\n 'type', 'table'\n ) as metadata,\n format(\n 'unsupported_reg_types_%s_%s_%s',\n n.nspname,\n c.relname,\n a.attname\n ) AS cache_key\nfrom\n pg_catalog.pg_attribute a\n join pg_catalog.pg_class c\n on a.attrelid = c.oid\n join pg_catalog.pg_namespace n\n on c.relnamespace = n.oid\n join pg_catalog.pg_type t\n on a.atttypid = t.oid\n join pg_catalog.pg_namespace tn\n on t.typnamespace = tn.oid\nwhere\n tn.nspname = 'pg_catalog'\n and t.typname in ('regcollation', 'regconfig', 'regdictionary', 'regnamespace', 'regoper', 'regoperator', 'regproc', 'regprocedure')\n and n.nspname not in ('pg_catalog', 'information_schema', 'pgsodium'))\nunion all\n(\nselect\n 'insecure_queue_exposed_in_api' as name,\n 'Insecure Queue Exposed in API' as title,\n 'ERROR' as level,\n 'EXTERNAL' as facing,\n array['SECURITY'] as categories,\n 'Detects cases where an insecure Queue is exposed over Data APIs' as description,\n format(\n 'Table `%s.%s` is public, but RLS has not been enabled.',\n n.nspname,\n c.relname\n ) as detail,\n 'https://supabase.com/docs/guides/database/database-linter?lint=0019_insecure_queue_exposed_in_api' as remediation,\n jsonb_build_object(\n 'schema', n.nspname,\n 'name', c.relname,\n 'type', 'table'\n ) as metadata,\n format(\n 'rls_disabled_in_public_%s_%s',\n n.nspname,\n c.relname\n ) as cache_key\nfrom\n pg_catalog.pg_class c\n join pg_catalog.pg_namespace n\n on c.relnamespace = n.oid\nwhere\n c.relkind in ('r', 'I') -- regular or partitioned tables\n and not c.relrowsecurity -- RLS is disabled\n and (\n pg_catalog.has_table_privilege('anon', c.oid, 'SELECT')\n or pg_catalog.has_table_privilege('authenticated', c.oid, 'SELECT')\n )\n and n.nspname = 'pgmq' -- tables in the pgmq schema\n and c.relname like 'q_%' -- only queue tables\n -- Constant requirements\n and 'pgmq_public' = any(array(select trim(unnest(string_to_array(current_setting('pgrst.db_schemas', 't'), ','))))))\nunion all\n(\nwith constants as (\n select current_setting('block_size')::numeric as bs, 23 as hdr, 4 as ma\n),\n\nbloat_info as (\n select\n ma,\n bs,\n schemaname,\n tablename,\n (datawidth + (hdr + ma - (case when hdr % ma = 0 then ma else hdr % ma end)))::numeric as datahdr,\n (maxfracsum * (nullhdr + ma - (case when nullhdr % ma = 0 then ma else nullhdr % ma end))) as nullhdr2\n from (\n select\n schemaname,\n tablename,\n hdr,\n ma,\n bs,\n sum((1 - null_frac) * avg_width) as datawidth,\n max(null_frac) as maxfracsum,\n hdr + (\n select 1 + count(*) / 8\n from pg_stats s2\n where\n null_frac <> 0\n and s2.schemaname = s.schemaname\n and s2.tablename = s.tablename\n ) as nullhdr\n from pg_stats s, constants\n group by 1, 2, 3, 4, 5\n ) as foo\n),\n\ntable_bloat as (\n select\n schemaname,\n tablename,\n cc.relpages,\n bs,\n ceil((cc.reltuples * ((datahdr + ma -\n (case when datahdr % ma = 0 then ma else datahdr % ma end)) + nullhdr2 + 4)) / (bs - 20::float)) as otta\n from\n bloat_info\n join pg_class cc\n on cc.relname = bloat_info.tablename\n join pg_namespace nn\n on cc.relnamespace = nn.oid\n and nn.nspname = bloat_info.schemaname\n and nn.nspname <> 'information_schema'\n where\n cc.relkind = 'r'\n and cc.relam = (select oid from pg_am where amname = 'heap')\n),\n\nbloat_data as (\n select\n 'table' as type,\n schemaname,\n tablename as object_name,\n round(case when otta = 0 then 0.0 else table_bloat.relpages / otta::numeric end, 1) as bloat,\n case when relpages < otta then 0 else (bs * (table_bloat.relpages - otta)::bigint)::bigint end as raw_waste\n from\n table_bloat\n)\n\nselect\n 'table_bloat' as name,\n 'Table Bloat' as title,\n 'INFO' as level,\n 'EXTERNAL' as facing,\n array['PERFORMANCE'] as categories,\n 'Detects if a table has excess bloat and may benefit from maintenance operations like vacuum full or cluster.' as description,\n format(\n 'Table `%s`.`%s` has excessive bloat',\n bloat_data.schemaname,\n bloat_data.object_name\n ) as detail,\n 'Consider running vacuum full (WARNING: incurs downtime) and tweaking autovacuum settings to reduce bloat.' as remediation,\n jsonb_build_object(\n 'schema', bloat_data.schemaname,\n 'name', bloat_data.object_name,\n 'type', bloat_data.type\n ) as metadata,\n format(\n 'table_bloat_%s_%s',\n bloat_data.schemaname,\n bloat_data.object_name\n ) as cache_key\nfrom\n bloat_data\nwhere\n bloat > 70.0\n and raw_waste > (20 * 1024 * 1024) -- filter for waste > 200 MB\norder by\n schemaname,\n object_name)\nunion all\n(\nselect\n 'fkey_to_auth_unique' as name,\n 'Foreign Key to Auth Unique Constraint' as title,\n 'ERROR' as level,\n 'EXTERNAL' as facing,\n array['SECURITY'] as categories,\n 'Detects user defined foreign keys to unique constraints in the auth schema.' as description,\n format(\n 'Table `%s`.`%s` has a foreign key `%s` referencing an auth unique constraint',\n n.nspname, -- referencing schema\n c_rel.relname, -- referencing table\n c.conname -- fkey name\n ) as detail,\n 'Drop the foreign key constraint that references the auth schema.' as remediation,\n jsonb_build_object(\n 'schema', n.nspname,\n 'name', c_rel.relname,\n 'foreign_key', c.conname\n ) as metadata,\n format(\n 'fkey_to_auth_unique_%s_%s_%s',\n n.nspname, -- referencing schema\n c_rel.relname, -- referencing table\n c.conname\n ) as cache_key\nfrom\n pg_catalog.pg_constraint c\n join pg_catalog.pg_class c_rel\n on c.conrelid = c_rel.oid\n join pg_catalog.pg_namespace n\n on c_rel.relnamespace = n.oid\n join pg_catalog.pg_class ref_rel\n on c.confrelid = ref_rel.oid\n join pg_catalog.pg_namespace cn\n on ref_rel.relnamespace = cn.oid\n join pg_catalog.pg_index i\n on c.conindid = i.indexrelid\nwhere c.contype = 'f'\n and cn.nspname = 'auth'\n and i.indisunique\n and not i.indisprimary)\nunion all\n(\nselect\n 'extension_versions_outdated' as name,\n 'Extension Versions Outdated' as title,\n 'WARN' as level,\n 'EXTERNAL' as facing,\n array['SECURITY'] as categories,\n 'Detects extensions that are not using the default (recommended) version.' as description,\n format(\n 'Extension `%s` is using version `%s` but version `%s` is available. Using outdated extension versions may expose the database to security vulnerabilities.',\n ext.name,\n ext.installed_version,\n ext.default_version\n ) as detail,\n 'https://supabase.com/docs/guides/database/database-linter?lint=0022_extension_versions_outdated' as remediation,\n jsonb_build_object(\n 'extension_name', ext.name,\n 'installed_version', ext.installed_version,\n 'default_version', ext.default_version\n ) as metadata,\n format(\n 'extension_versions_outdated_%s_%s',\n ext.name,\n ext.installed_version\n ) as cache_key\nfrom\n pg_catalog.pg_available_extensions ext\njoin\n -- ignore versions not in pg_available_extension_versions\n -- e.g. residue of pg_upgrade\n pg_catalog.pg_available_extension_versions extv\n on extv.name = ext.name and extv.installed\nwhere\n ext.installed_version is not null\n and ext.default_version is not null\n and ext.installed_version != ext.default_version\norder by\n ext.name)\nunion all\n(\n-- Detects tables exposed via API that contain columns with sensitive names\n-- Inspired by patterns from security scanners that detect PII/credential exposure\nwith sensitive_patterns as (\n select unnest(array[\n -- Authentication & Credentials\n 'password', 'passwd', 'pwd', 'passphrase',\n 'secret', 'secret_key', 'private_key', 'api_key', 'apikey',\n 'auth_key', 'token', 'jwt', 'access_token', 'refresh_token',\n 'oauth_token', 'session_token', 'bearer_token', 'auth_code',\n 'session_id', 'session_key', 'session_secret',\n 'recovery_code', 'backup_code', 'verification_code',\n 'otp', 'two_factor', '2fa_secret', '2fa_code',\n -- Personal Identifiers\n 'ssn', 'social_security', 'social_security_number',\n 'driver_license', 'drivers_license', 'license_number',\n 'passport_number', 'passport_id', 'national_id', 'tax_id',\n -- Financial Information\n 'credit_card', 'card_number', 'cvv', 'cvc', 'cvn',\n 'bank_account', 'account_number', 'routing_number',\n 'iban', 'swift_code', 'bic',\n -- Health & Medical\n 'health_record', 'medical_record', 'patient_id',\n 'insurance_number', 'health_insurance', 'medical_insurance',\n 'treatment',\n -- Device Identifiers\n 'mac_address', 'macaddr', 'imei', 'device_uuid',\n -- Digital Keys & Certificates\n 'pgp_key', 'gpg_key', 'ssh_key', 'certificate',\n 'license_key', 'activation_key',\n -- Biometric Data\n 'facial_recognition'\n ]) as pattern\n),\nexposed_tables as (\n select\n n.nspname as schema_name,\n c.relname as table_name,\n c.oid as table_oid\n from\n pg_catalog.pg_class c\n join pg_catalog.pg_namespace n\n on c.relnamespace = n.oid\n where\n c.relkind = 'r' -- regular tables\n and (\n pg_catalog.has_table_privilege('anon', c.oid, 'SELECT')\n or pg_catalog.has_table_privilege('authenticated', c.oid, 'SELECT')\n )\n and n.nspname = any(array(select trim(unnest(string_to_array(current_setting('pgrst.db_schemas', 't'), ',')))))\n and n.nspname not in (\n '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n )\n -- Only flag tables without RLS enabled\n and not c.relrowsecurity\n),\nsensitive_columns as (\n select\n et.schema_name,\n et.table_name,\n a.attname as column_name,\n sp.pattern as matched_pattern\n from\n exposed_tables et\n join pg_catalog.pg_attribute a\n on a.attrelid = et.table_oid\n and a.attnum > 0\n and not a.attisdropped\n cross join sensitive_patterns sp\n where\n -- Match column name against sensitive patterns (case insensitive), allowing '-'/'_' variants\n replace(lower(a.attname), '-', '_') = sp.pattern\n)\nselect\n 'sensitive_columns_exposed' as name,\n 'Sensitive Columns Exposed' as title,\n 'ERROR' as level,\n 'EXTERNAL' as facing,\n array['SECURITY'] as categories,\n 'Detects tables exposed via API that contain columns with potentially sensitive data (PII, credentials, financial info) without RLS protection.' as description,\n format(\n 'Table `%s.%s` is exposed via API without RLS and contains potentially sensitive column(s): %s. This may lead to data exposure.',\n schema_name,\n table_name,\n string_agg(distinct column_name, ', ' order by column_name)\n ) as detail,\n 'https://supabase.com/docs/guides/database/database-linter?lint=0023_sensitive_columns_exposed' as remediation,\n jsonb_build_object(\n 'schema', schema_name,\n 'name', table_name,\n 'type', 'table',\n 'sensitive_columns', array_agg(distinct column_name order by column_name),\n 'matched_patterns', array_agg(distinct matched_pattern order by matched_pattern)\n ) as metadata,\n format(\n 'sensitive_columns_exposed_%s_%s',\n schema_name,\n table_name\n ) as cache_key\nfrom\n sensitive_columns\ngroup by\n schema_name,\n table_name\norder by\n schema_name,\n table_name)\nunion all\n(\n-- Detects RLS policies that are overly permissive (e.g., USING (true), USING (1=1))\n-- These policies effectively disable row-level security while giving a false sense of security\nwith policies as (\n select\n nsp.nspname as schema_name,\n pb.tablename as table_name,\n pc.relrowsecurity as is_rls_active,\n pa.polname as policy_name,\n pa.polpermissive as is_permissive,\n pa.polroles as role_oids,\n (select array_agg(r::regrole::text) from unnest(pa.polroles) as x(r)) as roles,\n case pa.polcmd\n when 'r' then 'SELECT'\n when 'a' then 'INSERT'\n when 'w' then 'UPDATE'\n when 'd' then 'DELETE'\n when '*' then 'ALL'\n end as command,\n pb.qual,\n pb.with_check,\n -- Normalize expressions by removing whitespace and lowercasing\n replace(replace(replace(lower(coalesce(pb.qual, '')), ' ', ''), E'\\n', ''), E'\\t', '') as normalized_qual,\n replace(replace(replace(lower(coalesce(pb.with_check, '')), ' ', ''), E'\\n', ''), E'\\t', '') as normalized_with_check\n from\n pg_catalog.pg_policy pa\n join pg_catalog.pg_class pc\n on pa.polrelid = pc.oid\n join pg_catalog.pg_namespace nsp\n on pc.relnamespace = nsp.oid\n join pg_catalog.pg_policies pb\n on pc.relname = pb.tablename\n and nsp.nspname = pb.schemaname\n and pa.polname = pb.policyname\n where\n pc.relkind = 'r' -- regular tables\n and nsp.nspname not in (\n '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n )\n),\npermissive_patterns as (\n select\n p.*,\n -- Check for always-true USING clause patterns\n -- Note: SELECT with (true) is often intentional and documented, so we only flag UPDATE/DELETE\n case when (\n command in ('UPDATE', 'DELETE', 'ALL')\n and (\n normalized_qual in ('true', '(true)', '1=1', '(1=1)')\n -- Empty or null qual on permissive policy means allow all\n or (qual is null and is_permissive)\n )\n ) then true else false end as has_permissive_using,\n -- Check for always-true WITH CHECK clause patterns\n case when (\n normalized_with_check in ('true', '(true)', '1=1', '(1=1)')\n -- Empty with_check on INSERT means allow all (INSERT has no USING to fall back on)\n or (with_check is null and is_permissive and command = 'INSERT')\n -- Empty with_check on UPDATE/ALL with permissive USING means allow all writes\n or (with_check is null and is_permissive and command in ('UPDATE', 'ALL')\n and normalized_qual in ('true', '(true)', '1=1', '(1=1)'))\n ) then true else false end as has_permissive_with_check\n from\n policies p\n where\n -- Only check tables with RLS enabled (otherwise it's a different lint)\n is_rls_active\n -- Only check permissive policies (restrictive policies with true are less dangerous)\n and is_permissive\n -- Only flag policies that apply to anon or authenticated roles (or public/all roles)\n and (\n role_oids = array[0::oid] -- public (all roles)\n or exists (\n select 1\n from unnest(role_oids) as r\n where r::regrole::text in ('anon', 'authenticated')\n )\n )\n)\nselect\n 'rls_policy_always_true' as name,\n 'RLS Policy Always True' as title,\n 'WARN' as level,\n 'EXTERNAL' as facing,\n array['SECURITY'] as categories,\n 'Detects RLS policies that use overly permissive expressions like `USING (true)` or `WITH CHECK (true)` for UPDATE, DELETE, or INSERT operations. SELECT policies with `USING (true)` are intentionally excluded as this pattern is often used deliberately for public read access.' as description,\n format(\n 'Table `%s.%s` has an RLS policy `%s` for `%s` that allows unrestricted access%s. This effectively bypasses row-level security for %s.',\n schema_name,\n table_name,\n policy_name,\n command,\n case\n when has_permissive_using and has_permissive_with_check then ' (both USING and WITH CHECK are always true)'\n when has_permissive_using then ' (USING clause is always true)'\n when has_permissive_with_check then ' (WITH CHECK clause is always true)'\n else ''\n end,\n array_to_string(roles, ', ')\n ) as detail,\n 'https://supabase.com/docs/guides/database/database-linter?lint=0024_permissive_rls_policy' as remediation,\n jsonb_build_object(\n 'schema', schema_name,\n 'name', table_name,\n 'type', 'table',\n 'policy_name', policy_name,\n 'command', command,\n 'roles', roles,\n 'qual', qual,\n 'with_check', with_check,\n 'permissive_using', has_permissive_using,\n 'permissive_with_check', has_permissive_with_check\n ) as metadata,\n format(\n 'rls_policy_always_true_%s_%s_%s',\n schema_name,\n table_name,\n policy_name\n ) as cache_key\nfrom\n permissive_patterns\nwhere\n has_permissive_using or has_permissive_with_check\norder by\n schema_name,\n table_name,\n policy_name)"; + "set local search_path = '';\n\n(\nwith foreign_keys as (\n select\n cl.relnamespace::regnamespace::text as schema_name,\n cl.relname as table_name,\n cl.oid as table_oid,\n ct.conname as fkey_name,\n ct.conkey as col_attnums\n from\n pg_catalog.pg_constraint ct\n join pg_catalog.pg_class cl -- fkey owning table\n on ct.conrelid = cl.oid\n left join pg_catalog.pg_depend d\n on d.objid = cl.oid\n and d.deptype = 'e'\n where\n ct.contype = 'f' -- foreign key constraints\n and d.objid is null -- exclude tables that are dependencies of extensions\n and cl.relnamespace::regnamespace::text not in (\n 'pg_catalog', 'information_schema', 'auth', 'storage', 'vault', 'extensions'\n )\n),\nindex_ as (\n select\n pi.indrelid as table_oid,\n indexrelid::regclass as index_,\n string_to_array(indkey::text, ' ')::smallint[] as col_attnums\n from\n pg_catalog.pg_index pi\n where\n indisvalid\n)\nselect\n 'unindexed_foreign_keys' as name,\n 'Unindexed foreign keys' as title,\n 'INFO' as level,\n 'EXTERNAL' as facing,\n array['PERFORMANCE'] as categories,\n 'Identifies foreign key constraints without a covering index, which can impact database performance.' as description,\n format(\n 'Table `%s.%s` has a foreign key `%s` without a covering index. This can lead to suboptimal query performance.',\n fk.schema_name,\n fk.table_name,\n fk.fkey_name\n ) as detail,\n 'https://supabase.com/docs/guides/database/database-linter?lint=0001_unindexed_foreign_keys' as remediation,\n jsonb_build_object(\n 'schema', fk.schema_name,\n 'name', fk.table_name,\n 'type', 'table',\n 'fkey_name', fk.fkey_name,\n 'fkey_columns', fk.col_attnums\n ) as metadata,\n format('unindexed_foreign_keys_%s_%s_%s', fk.schema_name, fk.table_name, fk.fkey_name) as cache_key\nfrom\n foreign_keys fk\n left join index_ idx\n on fk.table_oid = idx.table_oid\n and fk.col_attnums = idx.col_attnums[1:array_length(fk.col_attnums, 1)]\n left join pg_catalog.pg_depend dep\n on idx.table_oid = dep.objid\n and dep.deptype = 'e'\nwhere\n idx.index_ is null\n and fk.schema_name not in (\n '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n )\n and dep.objid is null -- exclude tables owned by extensions\norder by\n fk.schema_name,\n fk.table_name,\n fk.fkey_name)\nunion all\n(\nselect\n 'auth_users_exposed' as name,\n 'Exposed Auth Users' as title,\n 'ERROR' as level,\n 'EXTERNAL' as facing,\n array['SECURITY'] as categories,\n 'Detects if auth.users is exposed to anon or authenticated roles via a view or materialized view in schemas exposed to PostgREST, potentially compromising user data security.' as description,\n format(\n 'View/Materialized View \"%s\" in the public schema may expose `auth.users` data to anon or authenticated roles.',\n c.relname\n ) as detail,\n 'https://supabase.com/docs/guides/database/database-linter?lint=0002_auth_users_exposed' as remediation,\n jsonb_build_object(\n 'schema', n.nspname,\n 'name', c.relname,\n 'type', 'view',\n 'exposed_to', array_remove(array_agg(DISTINCT case when pg_catalog.has_table_privilege('anon', c.oid, 'SELECT') then 'anon' when pg_catalog.has_table_privilege('authenticated', c.oid, 'SELECT') then 'authenticated' end), null)\n ) as metadata,\n format('auth_users_exposed_%s_%s', n.nspname, c.relname) as cache_key\nfrom\n -- Identify the oid for auth.users\n pg_catalog.pg_class auth_users_pg_class\n join pg_catalog.pg_namespace auth_users_pg_namespace\n on auth_users_pg_class.relnamespace = auth_users_pg_namespace.oid\n and auth_users_pg_class.relname = 'users'\n and auth_users_pg_namespace.nspname = 'auth'\n -- Depends on auth.users\n join pg_catalog.pg_depend d\n on d.refobjid = auth_users_pg_class.oid\n join pg_catalog.pg_rewrite r\n on r.oid = d.objid\n join pg_catalog.pg_class c\n on c.oid = r.ev_class\n join pg_catalog.pg_namespace n\n on n.oid = c.relnamespace\n join pg_catalog.pg_class pg_class_auth_users\n on d.refobjid = pg_class_auth_users.oid\nwhere\n d.deptype = 'n'\n and (\n pg_catalog.has_table_privilege('anon', c.oid, 'SELECT')\n or pg_catalog.has_table_privilege('authenticated', c.oid, 'SELECT')\n )\n and n.nspname = any(array(select trim(unnest(string_to_array(coalesce(current_setting('pgrst.db_schemas', 't'), 'public'), ',')))))\n -- Exclude self\n and c.relname <> '0002_auth_users_exposed'\n -- There are 3 insecure configurations\n and\n (\n -- Materialized views don't support RLS so this is insecure by default\n (c.relkind in ('m')) -- m for materialized view\n or\n -- Standard View, accessible to anon or authenticated that is security_definer\n (\n c.relkind = 'v' -- v for view\n -- Exclude security invoker views\n and not (\n lower(coalesce(c.reloptions::text,'{}'))::text[]\n && array[\n 'security_invoker=1',\n 'security_invoker=true',\n 'security_invoker=yes',\n 'security_invoker=on'\n ]\n )\n )\n or\n -- Standard View, security invoker, but no RLS enabled on auth.users\n (\n c.relkind in ('v') -- v for view\n -- is security invoker\n and (\n lower(coalesce(c.reloptions::text,'{}'))::text[]\n && array[\n 'security_invoker=1',\n 'security_invoker=true',\n 'security_invoker=yes',\n 'security_invoker=on'\n ]\n )\n and not pg_class_auth_users.relrowsecurity\n )\n )\ngroup by\n n.nspname,\n c.relname,\n c.oid)\nunion all\n(\nwith policies as (\n select\n nsp.nspname as schema_name,\n pb.tablename as table_name,\n pc.relrowsecurity as is_rls_active,\n polname as policy_name,\n polpermissive as is_permissive, -- if not, then restrictive\n (select array_agg(r::regrole) from unnest(polroles) as x(r)) as roles,\n case polcmd\n when 'r' then 'SELECT'\n when 'a' then 'INSERT'\n when 'w' then 'UPDATE'\n when 'd' then 'DELETE'\n when '*' then 'ALL'\n end as command,\n qual,\n with_check\n from\n pg_catalog.pg_policy pa\n join pg_catalog.pg_class pc\n on pa.polrelid = pc.oid\n join pg_catalog.pg_namespace nsp\n on pc.relnamespace = nsp.oid\n join pg_catalog.pg_policies pb\n on pc.relname = pb.tablename\n and nsp.nspname = pb.schemaname\n and pa.polname = pb.policyname\n)\nselect\n 'auth_rls_initplan' as name,\n 'Auth RLS Initialization Plan' as title,\n 'WARN' as level,\n 'EXTERNAL' as facing,\n array['PERFORMANCE'] as categories,\n 'Detects if calls to `current_setting()` and `auth.()` in RLS policies are being unnecessarily re-evaluated for each row' as description,\n format(\n 'Table `%s.%s` has a row level security policy `%s` that re-evaluates current_setting() or auth.() for each row. This produces suboptimal query performance at scale. Resolve the issue by replacing `auth.()` with `(select auth.())`. See [docs](https://supabase.com/docs/guides/database/postgres/row-level-security#call-functions-with-select) for more info.',\n schema_name,\n table_name,\n policy_name\n ) as detail,\n 'https://supabase.com/docs/guides/database/database-linter?lint=0003_auth_rls_initplan' as remediation,\n jsonb_build_object(\n 'schema', schema_name,\n 'name', table_name,\n 'type', 'table'\n ) as metadata,\n format('auth_rls_init_plan_%s_%s_%s', schema_name, table_name, policy_name) as cache_key\nfrom\n policies\nwhere\n is_rls_active\n -- NOTE: does not include realtime in support of monitoring policies on realtime.messages\n and schema_name not in (\n '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n )\n and (\n -- Example: auth.uid()\n (\n qual like '%auth.uid()%'\n and lower(qual) not like '%select auth.uid()%'\n )\n or (\n qual like '%auth.jwt()%'\n and lower(qual) not like '%select auth.jwt()%'\n )\n or (\n qual like '%auth.role()%'\n and lower(qual) not like '%select auth.role()%'\n )\n or (\n qual like '%auth.email()%'\n and lower(qual) not like '%select auth.email()%'\n )\n or (\n qual like '%current\\_setting(%)%'\n and lower(qual) not like '%select current\\_setting(%)%'\n )\n or (\n with_check like '%auth.uid()%'\n and lower(with_check) not like '%select auth.uid()%'\n )\n or (\n with_check like '%auth.jwt()%'\n and lower(with_check) not like '%select auth.jwt()%'\n )\n or (\n with_check like '%auth.role()%'\n and lower(with_check) not like '%select auth.role()%'\n )\n or (\n with_check like '%auth.email()%'\n and lower(with_check) not like '%select auth.email()%'\n )\n or (\n with_check like '%current\\_setting(%)%'\n and lower(with_check) not like '%select current\\_setting(%)%'\n )\n ))\nunion all\n(\nselect\n 'no_primary_key' as name,\n 'No Primary Key' as title,\n 'INFO' as level,\n 'EXTERNAL' as facing,\n array['PERFORMANCE'] as categories,\n 'Detects if a table does not have a primary key. Tables without a primary key can be inefficient to interact with at scale.' as description,\n format(\n 'Table `%s.%s` does not have a primary key',\n pgns.nspname,\n pgc.relname\n ) as detail,\n 'https://supabase.com/docs/guides/database/database-linter?lint=0004_no_primary_key' as remediation,\n jsonb_build_object(\n 'schema', pgns.nspname,\n 'name', pgc.relname,\n 'type', 'table'\n ) as metadata,\n format(\n 'no_primary_key_%s_%s',\n pgns.nspname,\n pgc.relname\n ) as cache_key\nfrom\n pg_catalog.pg_class pgc\n join pg_catalog.pg_namespace pgns\n on pgns.oid = pgc.relnamespace\n left join pg_catalog.pg_index pgi\n on pgi.indrelid = pgc.oid\n left join pg_catalog.pg_depend dep\n on pgc.oid = dep.objid\n and dep.deptype = 'e'\nwhere\n pgc.relkind = 'r' -- regular tables\n and pgns.nspname not in (\n '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n )\n and dep.objid is null -- exclude tables owned by extensions\ngroup by\n pgc.oid,\n pgns.nspname,\n pgc.relname\nhaving\n max(coalesce(pgi.indisprimary, false)::int) = 0)\nunion all\n(\nselect\n 'unused_index' as name,\n 'Unused Index' as title,\n 'INFO' as level,\n 'EXTERNAL' as facing,\n array['PERFORMANCE'] as categories,\n 'Detects if an index has never been used and may be a candidate for removal.' as description,\n format(\n 'Index `%s` on table `%s.%s` has not been used',\n psui.indexrelname,\n psui.schemaname,\n psui.relname\n ) as detail,\n 'https://supabase.com/docs/guides/database/database-linter?lint=0005_unused_index' as remediation,\n jsonb_build_object(\n 'schema', psui.schemaname,\n 'name', psui.relname,\n 'type', 'table'\n ) as metadata,\n format(\n 'unused_index_%s_%s_%s',\n psui.schemaname,\n psui.relname,\n psui.indexrelname\n ) as cache_key\n\nfrom\n pg_catalog.pg_stat_user_indexes psui\n join pg_catalog.pg_index pi\n on psui.indexrelid = pi.indexrelid\n left join pg_catalog.pg_depend dep\n on psui.relid = dep.objid\n and dep.deptype = 'e'\nwhere\n psui.idx_scan = 0\n and not pi.indisunique\n and not pi.indisprimary\n and dep.objid is null -- exclude tables owned by extensions\n and psui.schemaname not in (\n '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n ))\nunion all\n(\nselect\n 'multiple_permissive_policies' as name,\n 'Multiple Permissive Policies' as title,\n 'WARN' as level,\n 'EXTERNAL' as facing,\n array['PERFORMANCE'] as categories,\n 'Detects if multiple permissive row level security policies are present on a table for the same `role` and `action` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query.' as description,\n format(\n 'Table `%s.%s` has multiple permissive policies for role `%s` for action `%s`. Policies include `%s`',\n n.nspname,\n c.relname,\n r.rolname,\n act.cmd,\n array_agg(p.polname order by p.polname)\n ) as detail,\n 'https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies' as remediation,\n jsonb_build_object(\n 'schema', n.nspname,\n 'name', c.relname,\n 'type', 'table'\n ) as metadata,\n format(\n 'multiple_permissive_policies_%s_%s_%s_%s',\n n.nspname,\n c.relname,\n r.rolname,\n act.cmd\n ) as cache_key\nfrom\n pg_catalog.pg_policy p\n join pg_catalog.pg_class c\n on p.polrelid = c.oid\n join pg_catalog.pg_namespace n\n on c.relnamespace = n.oid\n join pg_catalog.pg_roles r\n on p.polroles @> array[r.oid]\n or p.polroles = array[0::oid]\n left join pg_catalog.pg_depend dep\n on c.oid = dep.objid\n and dep.deptype = 'e',\n lateral (\n select x.cmd\n from unnest((\n select\n case p.polcmd\n when 'r' then array['SELECT']\n when 'a' then array['INSERT']\n when 'w' then array['UPDATE']\n when 'd' then array['DELETE']\n when '*' then array['SELECT', 'INSERT', 'UPDATE', 'DELETE']\n else array['ERROR']\n end as actions\n )) x(cmd)\n ) act(cmd)\nwhere\n c.relkind = 'r' -- regular tables\n and p.polpermissive -- policy is permissive\n and n.nspname not in (\n '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n )\n and r.rolname not like 'pg_%'\n and r.rolname not like 'supabase%admin'\n and not r.rolbypassrls\n and dep.objid is null -- exclude tables owned by extensions\ngroup by\n n.nspname,\n c.relname,\n r.rolname,\n act.cmd\nhaving\n count(1) > 1)\nunion all\n(\nselect\n 'policy_exists_rls_disabled' as name,\n 'Policy Exists RLS Disabled' as title,\n 'ERROR' as level,\n 'EXTERNAL' as facing,\n array['SECURITY'] as categories,\n 'Detects cases where row level security (RLS) policies have been created, but RLS has not been enabled for the underlying table.' as description,\n format(\n 'Table `%s.%s` has RLS policies but RLS is not enabled on the table. Policies include %s.',\n n.nspname,\n c.relname,\n array_agg(p.polname order by p.polname)\n ) as detail,\n 'https://supabase.com/docs/guides/database/database-linter?lint=0007_policy_exists_rls_disabled' as remediation,\n jsonb_build_object(\n 'schema', n.nspname,\n 'name', c.relname,\n 'type', 'table'\n ) as metadata,\n format(\n 'policy_exists_rls_disabled_%s_%s',\n n.nspname,\n c.relname\n ) as cache_key\nfrom\n pg_catalog.pg_policy p\n join pg_catalog.pg_class c\n on p.polrelid = c.oid\n join pg_catalog.pg_namespace n\n on c.relnamespace = n.oid\n left join pg_catalog.pg_depend dep\n on c.oid = dep.objid\n and dep.deptype = 'e'\nwhere\n c.relkind = 'r' -- regular tables\n and n.nspname not in (\n '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n )\n -- RLS is disabled\n and not c.relrowsecurity\n and dep.objid is null -- exclude tables owned by extensions\ngroup by\n n.nspname,\n c.relname)\nunion all\n(\nselect\n 'rls_enabled_no_policy' as name,\n 'RLS Enabled No Policy' as title,\n 'INFO' as level,\n 'EXTERNAL' as facing,\n array['SECURITY'] as categories,\n 'Detects cases where row level security (RLS) has been enabled on a table but no RLS policies have been created.' as description,\n format(\n 'Table `%s.%s` has RLS enabled, but no policies exist',\n n.nspname,\n c.relname\n ) as detail,\n 'https://supabase.com/docs/guides/database/database-linter?lint=0008_rls_enabled_no_policy' as remediation,\n jsonb_build_object(\n 'schema', n.nspname,\n 'name', c.relname,\n 'type', 'table'\n ) as metadata,\n format(\n 'rls_enabled_no_policy_%s_%s',\n n.nspname,\n c.relname\n ) as cache_key\nfrom\n pg_catalog.pg_class c\n left join pg_catalog.pg_policy p\n on p.polrelid = c.oid\n join pg_catalog.pg_namespace n\n on c.relnamespace = n.oid\n left join pg_catalog.pg_depend dep\n on c.oid = dep.objid\n and dep.deptype = 'e'\nwhere\n c.relkind = 'r' -- regular tables\n and n.nspname not in (\n '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n )\n -- RLS is enabled\n and c.relrowsecurity\n and p.polname is null\n and dep.objid is null -- exclude tables owned by extensions\ngroup by\n n.nspname,\n c.relname)\nunion all\n(\nselect\n 'duplicate_index' as name,\n 'Duplicate Index' as title,\n 'WARN' as level,\n 'EXTERNAL' as facing,\n array['PERFORMANCE'] as categories,\n 'Detects cases where two ore more identical indexes exist.' as description,\n format(\n 'Table `%s.%s` has identical indexes %s. Drop all except one of them',\n n.nspname,\n c.relname,\n array_agg(pi.indexname order by pi.indexname)\n ) as detail,\n 'https://supabase.com/docs/guides/database/database-linter?lint=0009_duplicate_index' as remediation,\n jsonb_build_object(\n 'schema', n.nspname,\n 'name', c.relname,\n 'type', case\n when c.relkind = 'r' then 'table'\n when c.relkind = 'm' then 'materialized view'\n else 'ERROR'\n end,\n 'indexes', array_agg(pi.indexname order by pi.indexname)\n ) as metadata,\n format(\n 'duplicate_index_%s_%s_%s',\n n.nspname,\n c.relname,\n array_agg(pi.indexname order by pi.indexname)\n ) as cache_key\nfrom\n pg_catalog.pg_indexes pi\n join pg_catalog.pg_namespace n\n on n.nspname = pi.schemaname\n join pg_catalog.pg_class c\n on pi.tablename = c.relname\n and n.oid = c.relnamespace\n left join pg_catalog.pg_depend dep\n on c.oid = dep.objid\n and dep.deptype = 'e'\nwhere\n c.relkind in ('r', 'm') -- tables and materialized views\n and n.nspname not in (\n '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n )\n and dep.objid is null -- exclude tables owned by extensions\ngroup by\n n.nspname,\n c.relkind,\n c.relname,\n replace(pi.indexdef, pi.indexname, '')\nhaving\n count(*) > 1)\nunion all\n(\nselect\n 'security_definer_view' as name,\n 'Security Definer View' as title,\n 'ERROR' as level,\n 'EXTERNAL' as facing,\n array['SECURITY'] as categories,\n 'Detects views defined with the SECURITY DEFINER property. These views enforce Postgres permissions and row level security policies (RLS) of the view creator, rather than that of the querying user' as description,\n format(\n 'View `%s.%s` is defined with the SECURITY DEFINER property',\n n.nspname,\n c.relname\n ) as detail,\n 'https://supabase.com/docs/guides/database/database-linter?lint=0010_security_definer_view' as remediation,\n jsonb_build_object(\n 'schema', n.nspname,\n 'name', c.relname,\n 'type', 'view'\n ) as metadata,\n format(\n 'security_definer_view_%s_%s',\n n.nspname,\n c.relname\n ) as cache_key\nfrom\n pg_catalog.pg_class c\n join pg_catalog.pg_namespace n\n on n.oid = c.relnamespace\n left join pg_catalog.pg_depend dep\n on c.oid = dep.objid\n and dep.deptype = 'e'\nwhere\n c.relkind = 'v'\n and (\n pg_catalog.has_table_privilege('anon', c.oid, 'SELECT')\n or pg_catalog.has_table_privilege('authenticated', c.oid, 'SELECT')\n )\n and substring(pg_catalog.version() from 'PostgreSQL ([0-9]+)') >= '15' -- security invoker was added in pg15\n and n.nspname = any(array(select trim(unnest(string_to_array(coalesce(current_setting('pgrst.db_schemas', 't'), 'public'), ',')))))\n and n.nspname not in (\n '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n )\n and dep.objid is null -- exclude views owned by extensions\n and not (\n lower(coalesce(c.reloptions::text,'{}'))::text[]\n && array[\n 'security_invoker=1',\n 'security_invoker=true',\n 'security_invoker=yes',\n 'security_invoker=on'\n ]\n ))\nunion all\n(\nselect\n 'function_search_path_mutable' as name,\n 'Function Search Path Mutable' as title,\n 'WARN' as level,\n 'EXTERNAL' as facing,\n array['SECURITY'] as categories,\n 'Detects functions where the search_path parameter is not set.' as description,\n format(\n 'Function `%s.%s` has a role mutable search_path',\n n.nspname,\n p.proname\n ) as detail,\n 'https://supabase.com/docs/guides/database/database-linter?lint=0011_function_search_path_mutable' as remediation,\n jsonb_build_object(\n 'schema', n.nspname,\n 'name', p.proname,\n 'type', 'function'\n ) as metadata,\n format(\n 'function_search_path_mutable_%s_%s_%s',\n n.nspname,\n p.proname,\n md5(p.prosrc) -- required when function is polymorphic\n ) as cache_key\nfrom\n pg_catalog.pg_proc p\n join pg_catalog.pg_namespace n\n on p.pronamespace = n.oid\n left join pg_catalog.pg_depend dep\n on p.oid = dep.objid\n and dep.deptype = 'e'\nwhere\n n.nspname not in (\n '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n )\n and dep.objid is null -- exclude functions owned by extensions\n -- Search path not set\n and not exists (\n select 1\n from unnest(coalesce(p.proconfig, '{}')) as config\n where config like 'search_path=%'\n ))\nunion all\n(\nselect\n 'rls_disabled_in_public' as name,\n 'RLS Disabled in Public' as title,\n 'ERROR' as level,\n 'EXTERNAL' as facing,\n array['SECURITY'] as categories,\n 'Detects cases where row level security (RLS) has not been enabled on tables in schemas exposed to PostgREST' as description,\n format(\n 'Table `%s.%s` is public, but RLS has not been enabled.',\n n.nspname,\n c.relname\n ) as detail,\n 'https://supabase.com/docs/guides/database/database-linter?lint=0013_rls_disabled_in_public' as remediation,\n jsonb_build_object(\n 'schema', n.nspname,\n 'name', c.relname,\n 'type', 'table'\n ) as metadata,\n format(\n 'rls_disabled_in_public_%s_%s',\n n.nspname,\n c.relname\n ) as cache_key\nfrom\n pg_catalog.pg_class c\n join pg_catalog.pg_namespace n\n on c.relnamespace = n.oid\nwhere\n c.relkind = 'r' -- regular tables\n -- RLS is disabled\n and not c.relrowsecurity\n and (\n pg_catalog.has_table_privilege('anon', c.oid, 'SELECT')\n or pg_catalog.has_table_privilege('authenticated', c.oid, 'SELECT')\n )\n and n.nspname = any(array(select trim(unnest(string_to_array(coalesce(current_setting('pgrst.db_schemas', 't'), 'public'), ',')))))\n and n.nspname not in (\n '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n ))\nunion all\n(\nselect\n 'extension_in_public' as name,\n 'Extension in Public' as title,\n 'WARN' as level,\n 'EXTERNAL' as facing,\n array['SECURITY'] as categories,\n 'Detects extensions installed in the `public` schema.' as description,\n format(\n 'Extension `%s` is installed in the public schema. Move it to another schema.',\n pe.extname\n ) as detail,\n 'https://supabase.com/docs/guides/database/database-linter?lint=0014_extension_in_public' as remediation,\n jsonb_build_object(\n 'schema', pe.extnamespace::regnamespace,\n 'name', pe.extname,\n 'type', 'extension'\n ) as metadata,\n format(\n 'extension_in_public_%s',\n pe.extname\n ) as cache_key\nfrom\n pg_catalog.pg_extension pe\nwhere\n -- plpgsql is installed by default in public and outside user control\n -- confirmed safe\n pe.extname not in ('plpgsql')\n -- Scoping this to public is not optimal. Ideally we would use the postgres\n -- search path. That currently isn't available via SQL. In other lints\n -- we have used has_schema_privilege('anon', 'extensions', 'USAGE') but that\n -- is not appropriate here as it would evaluate true for the extensions schema\n and pe.extnamespace::regnamespace::text = 'public')\nunion all\n(\nwith policies as (\n select\n nsp.nspname as schema_name,\n pb.tablename as table_name,\n polname as policy_name,\n qual,\n with_check\n from\n pg_catalog.pg_policy pa\n join pg_catalog.pg_class pc\n on pa.polrelid = pc.oid\n join pg_catalog.pg_namespace nsp\n on pc.relnamespace = nsp.oid\n join pg_catalog.pg_policies pb\n on pc.relname = pb.tablename\n and nsp.nspname = pb.schemaname\n and pa.polname = pb.policyname\n)\nselect\n 'rls_references_user_metadata' as name,\n 'RLS references user metadata' as title,\n 'ERROR' as level,\n 'EXTERNAL' as facing,\n array['SECURITY'] as categories,\n 'Detects when Supabase Auth user_metadata is referenced insecurely in a row level security (RLS) policy.' as description,\n format(\n 'Table `%s.%s` has a row level security policy `%s` that references Supabase Auth `user_metadata`. `user_metadata` is editable by end users and should never be used in a security context.',\n schema_name,\n table_name,\n policy_name\n ) as detail,\n 'https://supabase.com/docs/guides/database/database-linter?lint=0015_rls_references_user_metadata' as remediation,\n jsonb_build_object(\n 'schema', schema_name,\n 'name', table_name,\n 'type', 'table'\n ) as metadata,\n format('rls_references_user_metadata_%s_%s_%s', schema_name, table_name, policy_name) as cache_key\nfrom\n policies\nwhere\n schema_name not in (\n '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n )\n and (\n -- Example: auth.jwt() -> 'user_metadata'\n -- False positives are possible, but it isn't practical to string match\n -- If false positive rate is too high, this expression can iterate\n qual like '%auth.jwt()%user_metadata%'\n or qual like '%current_setting(%request.jwt.claims%)%user_metadata%'\n or with_check like '%auth.jwt()%user_metadata%'\n or with_check like '%current_setting(%request.jwt.claims%)%user_metadata%'\n ))\nunion all\n(\nselect\n 'materialized_view_in_api' as name,\n 'Materialized View in API' as title,\n 'WARN' as level,\n 'EXTERNAL' as facing,\n array['SECURITY'] as categories,\n 'Detects materialized views that are accessible over the Data APIs.' as description,\n format(\n 'Materialized view `%s.%s` is selectable by anon or authenticated roles',\n n.nspname,\n c.relname\n ) as detail,\n 'https://supabase.com/docs/guides/database/database-linter?lint=0016_materialized_view_in_api' as remediation,\n jsonb_build_object(\n 'schema', n.nspname,\n 'name', c.relname,\n 'type', 'materialized view'\n ) as metadata,\n format(\n 'materialized_view_in_api_%s_%s',\n n.nspname,\n c.relname\n ) as cache_key\nfrom\n pg_catalog.pg_class c\n join pg_catalog.pg_namespace n\n on n.oid = c.relnamespace\n left join pg_catalog.pg_depend dep\n on c.oid = dep.objid\n and dep.deptype = 'e'\nwhere\n c.relkind = 'm'\n and (\n pg_catalog.has_table_privilege('anon', c.oid, 'SELECT')\n or pg_catalog.has_table_privilege('authenticated', c.oid, 'SELECT')\n )\n and n.nspname = any(array(select trim(unnest(string_to_array(coalesce(current_setting('pgrst.db_schemas', 't'), 'public'), ',')))))\n and n.nspname not in (\n '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n )\n and dep.objid is null)\nunion all\n(\nselect\n 'foreign_table_in_api' as name,\n 'Foreign Table in API' as title,\n 'WARN' as level,\n 'EXTERNAL' as facing,\n array['SECURITY'] as categories,\n 'Detects foreign tables that are accessible over APIs. Foreign tables do not respect row level security policies.' as description,\n format(\n 'Foreign table `%s.%s` is accessible over APIs',\n n.nspname,\n c.relname\n ) as detail,\n 'https://supabase.com/docs/guides/database/database-linter?lint=0017_foreign_table_in_api' as remediation,\n jsonb_build_object(\n 'schema', n.nspname,\n 'name', c.relname,\n 'type', 'foreign table'\n ) as metadata,\n format(\n 'foreign_table_in_api_%s_%s',\n n.nspname,\n c.relname\n ) as cache_key\nfrom\n pg_catalog.pg_class c\n join pg_catalog.pg_namespace n\n on n.oid = c.relnamespace\n left join pg_catalog.pg_depend dep\n on c.oid = dep.objid\n and dep.deptype = 'e'\nwhere\n c.relkind = 'f'\n and (\n pg_catalog.has_table_privilege('anon', c.oid, 'SELECT')\n or pg_catalog.has_table_privilege('authenticated', c.oid, 'SELECT')\n )\n and n.nspname = any(array(select trim(unnest(string_to_array(coalesce(current_setting('pgrst.db_schemas', 't'), 'public'), ',')))))\n and n.nspname not in (\n '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n )\n and dep.objid is null)\nunion all\n(\nselect\n 'unsupported_reg_types' as name,\n 'Unsupported reg types' as title,\n 'WARN' as level,\n 'EXTERNAL' as facing,\n array['SECURITY'] as categories,\n 'Identifies columns using unsupported reg* types outside pg_catalog schema, which prevents database upgrades using pg_upgrade.' as description,\n format(\n 'Table `%s.%s` has a column `%s` with unsupported reg* type `%s`.',\n n.nspname,\n c.relname,\n a.attname,\n t.typname\n ) as detail,\n 'https://supabase.com/docs/guides/database/database-linter?lint=unsupported_reg_types' as remediation,\n jsonb_build_object(\n 'schema', n.nspname,\n 'name', c.relname,\n 'column', a.attname,\n 'type', 'table'\n ) as metadata,\n format(\n 'unsupported_reg_types_%s_%s_%s',\n n.nspname,\n c.relname,\n a.attname\n ) AS cache_key\nfrom\n pg_catalog.pg_attribute a\n join pg_catalog.pg_class c\n on a.attrelid = c.oid\n join pg_catalog.pg_namespace n\n on c.relnamespace = n.oid\n join pg_catalog.pg_type t\n on a.atttypid = t.oid\n join pg_catalog.pg_namespace tn\n on t.typnamespace = tn.oid\nwhere\n tn.nspname = 'pg_catalog'\n and t.typname in ('regcollation', 'regconfig', 'regdictionary', 'regnamespace', 'regoper', 'regoperator', 'regproc', 'regprocedure')\n and n.nspname not in ('pg_catalog', 'information_schema', 'pgsodium'))\nunion all\n(\nselect\n 'insecure_queue_exposed_in_api' as name,\n 'Insecure Queue Exposed in API' as title,\n 'ERROR' as level,\n 'EXTERNAL' as facing,\n array['SECURITY'] as categories,\n 'Detects cases where an insecure Queue is exposed over Data APIs' as description,\n format(\n 'Table `%s.%s` is public, but RLS has not been enabled.',\n n.nspname,\n c.relname\n ) as detail,\n 'https://supabase.com/docs/guides/database/database-linter?lint=0019_insecure_queue_exposed_in_api' as remediation,\n jsonb_build_object(\n 'schema', n.nspname,\n 'name', c.relname,\n 'type', 'table'\n ) as metadata,\n format(\n 'rls_disabled_in_public_%s_%s',\n n.nspname,\n c.relname\n ) as cache_key\nfrom\n pg_catalog.pg_class c\n join pg_catalog.pg_namespace n\n on c.relnamespace = n.oid\nwhere\n c.relkind in ('r', 'I') -- regular or partitioned tables\n and not c.relrowsecurity -- RLS is disabled\n and (\n pg_catalog.has_table_privilege('anon', c.oid, 'SELECT')\n or pg_catalog.has_table_privilege('authenticated', c.oid, 'SELECT')\n )\n and n.nspname = 'pgmq' -- tables in the pgmq schema\n and c.relname like 'q_%' -- only queue tables\n -- Constant requirements\n and 'pgmq_public' = any(array(select trim(unnest(string_to_array(coalesce(current_setting('pgrst.db_schemas', 't'), 'public'), ','))))))\nunion all\n(\nwith constants as (\n select current_setting('block_size')::numeric as bs, 23 as hdr, 4 as ma\n),\n\nbloat_info as (\n select\n ma,\n bs,\n schemaname,\n tablename,\n (datawidth + (hdr + ma - (case when hdr % ma = 0 then ma else hdr % ma end)))::numeric as datahdr,\n (maxfracsum * (nullhdr + ma - (case when nullhdr % ma = 0 then ma else nullhdr % ma end))) as nullhdr2\n from (\n select\n schemaname,\n tablename,\n hdr,\n ma,\n bs,\n sum((1 - null_frac) * avg_width) as datawidth,\n max(null_frac) as maxfracsum,\n hdr + (\n select 1 + count(*) / 8\n from pg_stats s2\n where\n null_frac <> 0\n and s2.schemaname = s.schemaname\n and s2.tablename = s.tablename\n ) as nullhdr\n from pg_stats s, constants\n group by 1, 2, 3, 4, 5\n ) as foo\n),\n\ntable_bloat as (\n select\n schemaname,\n tablename,\n cc.relpages,\n bs,\n ceil((cc.reltuples * ((datahdr + ma -\n (case when datahdr % ma = 0 then ma else datahdr % ma end)) + nullhdr2 + 4)) / (bs - 20::float)) as otta\n from\n bloat_info\n join pg_class cc\n on cc.relname = bloat_info.tablename\n join pg_namespace nn\n on cc.relnamespace = nn.oid\n and nn.nspname = bloat_info.schemaname\n and nn.nspname <> 'information_schema'\n where\n cc.relkind = 'r'\n and cc.relam = (select oid from pg_am where amname = 'heap')\n),\n\nbloat_data as (\n select\n 'table' as type,\n schemaname,\n tablename as object_name,\n round(case when otta = 0 then 0.0 else table_bloat.relpages / otta::numeric end, 1) as bloat,\n case when relpages < otta then 0 else (bs * (table_bloat.relpages - otta)::bigint)::bigint end as raw_waste\n from\n table_bloat\n)\n\nselect\n 'table_bloat' as name,\n 'Table Bloat' as title,\n 'INFO' as level,\n 'EXTERNAL' as facing,\n array['PERFORMANCE'] as categories,\n 'Detects if a table has excess bloat and may benefit from maintenance operations like vacuum full or cluster.' as description,\n format(\n 'Table `%s`.`%s` has excessive bloat',\n bloat_data.schemaname,\n bloat_data.object_name\n ) as detail,\n 'Consider running vacuum full (WARNING: incurs downtime) and tweaking autovacuum settings to reduce bloat.' as remediation,\n jsonb_build_object(\n 'schema', bloat_data.schemaname,\n 'name', bloat_data.object_name,\n 'type', bloat_data.type\n ) as metadata,\n format(\n 'table_bloat_%s_%s',\n bloat_data.schemaname,\n bloat_data.object_name\n ) as cache_key\nfrom\n bloat_data\nwhere\n bloat > 70.0\n and raw_waste > (20 * 1024 * 1024) -- filter for waste > 200 MB\norder by\n schemaname,\n object_name)\nunion all\n(\nselect\n 'fkey_to_auth_unique' as name,\n 'Foreign Key to Auth Unique Constraint' as title,\n 'ERROR' as level,\n 'EXTERNAL' as facing,\n array['SECURITY'] as categories,\n 'Detects user defined foreign keys to unique constraints in the auth schema.' as description,\n format(\n 'Table `%s`.`%s` has a foreign key `%s` referencing an auth unique constraint',\n n.nspname, -- referencing schema\n c_rel.relname, -- referencing table\n c.conname -- fkey name\n ) as detail,\n 'Drop the foreign key constraint that references the auth schema.' as remediation,\n jsonb_build_object(\n 'schema', n.nspname,\n 'name', c_rel.relname,\n 'foreign_key', c.conname\n ) as metadata,\n format(\n 'fkey_to_auth_unique_%s_%s_%s',\n n.nspname, -- referencing schema\n c_rel.relname, -- referencing table\n c.conname\n ) as cache_key\nfrom\n pg_catalog.pg_constraint c\n join pg_catalog.pg_class c_rel\n on c.conrelid = c_rel.oid\n join pg_catalog.pg_namespace n\n on c_rel.relnamespace = n.oid\n join pg_catalog.pg_class ref_rel\n on c.confrelid = ref_rel.oid\n join pg_catalog.pg_namespace cn\n on ref_rel.relnamespace = cn.oid\n join pg_catalog.pg_index i\n on c.conindid = i.indexrelid\nwhere c.contype = 'f'\n and cn.nspname = 'auth'\n and i.indisunique\n and not i.indisprimary)\nunion all\n(\nselect\n 'extension_versions_outdated' as name,\n 'Extension Versions Outdated' as title,\n 'WARN' as level,\n 'EXTERNAL' as facing,\n array['SECURITY'] as categories,\n 'Detects extensions that are not using the default (recommended) version.' as description,\n format(\n 'Extension `%s` is using version `%s` but version `%s` is available. Using outdated extension versions may expose the database to security vulnerabilities.',\n ext.name,\n ext.installed_version,\n ext.default_version\n ) as detail,\n 'https://supabase.com/docs/guides/database/database-linter?lint=0022_extension_versions_outdated' as remediation,\n jsonb_build_object(\n 'extension_name', ext.name,\n 'installed_version', ext.installed_version,\n 'default_version', ext.default_version\n ) as metadata,\n format(\n 'extension_versions_outdated_%s_%s',\n ext.name,\n ext.installed_version\n ) as cache_key\nfrom\n pg_catalog.pg_available_extensions ext\njoin\n -- ignore versions not in pg_available_extension_versions\n -- e.g. residue of pg_upgrade\n pg_catalog.pg_available_extension_versions extv\n on extv.name = ext.name and extv.installed\nwhere\n ext.installed_version is not null\n and ext.default_version is not null\n and ext.installed_version != ext.default_version\norder by\n ext.name)\nunion all\n(\n-- Detects tables exposed via API that contain columns with sensitive names\n-- Inspired by patterns from security scanners that detect PII/credential exposure\nwith sensitive_patterns as (\n select unnest(array[\n -- Authentication & Credentials\n 'password', 'passwd', 'pwd', 'passphrase',\n 'secret', 'secret_key', 'private_key', 'api_key', 'apikey',\n 'auth_key', 'token', 'jwt', 'access_token', 'refresh_token',\n 'oauth_token', 'session_token', 'bearer_token', 'auth_code',\n 'session_id', 'session_key', 'session_secret',\n 'recovery_code', 'backup_code', 'verification_code',\n 'otp', 'two_factor', '2fa_secret', '2fa_code',\n -- Personal Identifiers\n 'ssn', 'social_security', 'social_security_number',\n 'driver_license', 'drivers_license', 'license_number',\n 'passport_number', 'passport_id', 'national_id', 'tax_id',\n -- Financial Information\n 'credit_card', 'card_number', 'cvv', 'cvc', 'cvn',\n 'bank_account', 'account_number', 'routing_number',\n 'iban', 'swift_code', 'bic',\n -- Health & Medical\n 'health_record', 'medical_record', 'patient_id',\n 'insurance_number', 'health_insurance', 'medical_insurance',\n 'treatment',\n -- Device Identifiers\n 'mac_address', 'macaddr', 'imei', 'device_uuid',\n -- Digital Keys & Certificates\n 'pgp_key', 'gpg_key', 'ssh_key', 'certificate',\n 'license_key', 'activation_key',\n -- Biometric Data\n 'facial_recognition'\n ]) as pattern\n),\nexposed_tables as (\n select\n n.nspname as schema_name,\n c.relname as table_name,\n c.oid as table_oid\n from\n pg_catalog.pg_class c\n join pg_catalog.pg_namespace n\n on c.relnamespace = n.oid\n where\n c.relkind = 'r' -- regular tables\n and (\n pg_catalog.has_table_privilege('anon', c.oid, 'SELECT')\n or pg_catalog.has_table_privilege('authenticated', c.oid, 'SELECT')\n )\n and n.nspname = any(array(select trim(unnest(string_to_array(coalesce(current_setting('pgrst.db_schemas', 't'), 'public'), ',')))))\n and n.nspname not in (\n '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n )\n -- Only flag tables without RLS enabled\n and not c.relrowsecurity\n),\nsensitive_columns as (\n select\n et.schema_name,\n et.table_name,\n a.attname as column_name,\n sp.pattern as matched_pattern\n from\n exposed_tables et\n join pg_catalog.pg_attribute a\n on a.attrelid = et.table_oid\n and a.attnum > 0\n and not a.attisdropped\n cross join sensitive_patterns sp\n where\n -- Match column name against sensitive patterns (case insensitive), allowing '-'/'_' variants\n replace(lower(a.attname), '-', '_') = sp.pattern\n)\nselect\n 'sensitive_columns_exposed' as name,\n 'Sensitive Columns Exposed' as title,\n 'ERROR' as level,\n 'EXTERNAL' as facing,\n array['SECURITY'] as categories,\n 'Detects tables exposed via API that contain columns with potentially sensitive data (PII, credentials, financial info) without RLS protection.' as description,\n format(\n 'Table `%s.%s` is exposed via API without RLS and contains potentially sensitive column(s): %s. This may lead to data exposure.',\n schema_name,\n table_name,\n string_agg(distinct column_name, ', ' order by column_name)\n ) as detail,\n 'https://supabase.com/docs/guides/database/database-linter?lint=0023_sensitive_columns_exposed' as remediation,\n jsonb_build_object(\n 'schema', schema_name,\n 'name', table_name,\n 'type', 'table',\n 'sensitive_columns', array_agg(distinct column_name order by column_name),\n 'matched_patterns', array_agg(distinct matched_pattern order by matched_pattern)\n ) as metadata,\n format(\n 'sensitive_columns_exposed_%s_%s',\n schema_name,\n table_name\n ) as cache_key\nfrom\n sensitive_columns\ngroup by\n schema_name,\n table_name\norder by\n schema_name,\n table_name)\nunion all\n(\n-- Detects RLS policies that are overly permissive (e.g., USING (true), USING (1=1))\n-- These policies effectively disable row-level security while giving a false sense of security\nwith policies as (\n select\n nsp.nspname as schema_name,\n pb.tablename as table_name,\n pc.relrowsecurity as is_rls_active,\n pa.polname as policy_name,\n pa.polpermissive as is_permissive,\n pa.polroles as role_oids,\n (select array_agg(r::regrole::text) from unnest(pa.polroles) as x(r)) as roles,\n case pa.polcmd\n when 'r' then 'SELECT'\n when 'a' then 'INSERT'\n when 'w' then 'UPDATE'\n when 'd' then 'DELETE'\n when '*' then 'ALL'\n end as command,\n pb.qual,\n pb.with_check,\n -- Normalize expressions by removing whitespace and lowercasing\n replace(replace(replace(lower(coalesce(pb.qual, '')), ' ', ''), E'\\n', ''), E'\\t', '') as normalized_qual,\n replace(replace(replace(lower(coalesce(pb.with_check, '')), ' ', ''), E'\\n', ''), E'\\t', '') as normalized_with_check\n from\n pg_catalog.pg_policy pa\n join pg_catalog.pg_class pc\n on pa.polrelid = pc.oid\n join pg_catalog.pg_namespace nsp\n on pc.relnamespace = nsp.oid\n join pg_catalog.pg_policies pb\n on pc.relname = pb.tablename\n and nsp.nspname = pb.schemaname\n and pa.polname = pb.policyname\n where\n pc.relkind = 'r' -- regular tables\n and nsp.nspname not in (\n '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n )\n),\npermissive_patterns as (\n select\n p.*,\n -- Check for always-true USING clause patterns\n -- Note: SELECT with (true) is often intentional and documented, so we only flag UPDATE/DELETE\n case when (\n command in ('UPDATE', 'DELETE', 'ALL')\n and (\n normalized_qual in ('true', '(true)', '1=1', '(1=1)')\n -- Empty or null qual on permissive policy means allow all\n or (qual is null and is_permissive)\n )\n ) then true else false end as has_permissive_using,\n -- Check for always-true WITH CHECK clause patterns\n case when (\n normalized_with_check in ('true', '(true)', '1=1', '(1=1)')\n -- Empty with_check on INSERT means allow all (INSERT has no USING to fall back on)\n or (with_check is null and is_permissive and command = 'INSERT')\n -- Empty with_check on UPDATE/ALL with permissive USING means allow all writes\n or (with_check is null and is_permissive and command in ('UPDATE', 'ALL')\n and normalized_qual in ('true', '(true)', '1=1', '(1=1)'))\n ) then true else false end as has_permissive_with_check\n from\n policies p\n where\n -- Only check tables with RLS enabled (otherwise it's a different lint)\n is_rls_active\n -- Only check permissive policies (restrictive policies with true are less dangerous)\n and is_permissive\n -- Only flag policies that apply to anon or authenticated roles (or public/all roles)\n and (\n role_oids = array[0::oid] -- public (all roles)\n or exists (\n select 1\n from unnest(role_oids) as r\n where r::regrole::text in ('anon', 'authenticated')\n )\n )\n)\nselect\n 'rls_policy_always_true' as name,\n 'RLS Policy Always True' as title,\n 'WARN' as level,\n 'EXTERNAL' as facing,\n array['SECURITY'] as categories,\n 'Detects RLS policies that use overly permissive expressions like `USING (true)` or `WITH CHECK (true)` for UPDATE, DELETE, or INSERT operations. SELECT policies with `USING (true)` are intentionally excluded as this pattern is often used deliberately for public read access.' as description,\n format(\n 'Table `%s.%s` has an RLS policy `%s` for `%s` that allows unrestricted access%s. This effectively bypasses row-level security for %s.',\n schema_name,\n table_name,\n policy_name,\n command,\n case\n when has_permissive_using and has_permissive_with_check then ' (both USING and WITH CHECK are always true)'\n when has_permissive_using then ' (USING clause is always true)'\n when has_permissive_with_check then ' (WITH CHECK clause is always true)'\n else ''\n end,\n array_to_string(roles, ', ')\n ) as detail,\n 'https://supabase.com/docs/guides/database/database-linter?lint=0024_permissive_rls_policy' as remediation,\n jsonb_build_object(\n 'schema', schema_name,\n 'name', table_name,\n 'type', 'table',\n 'policy_name', policy_name,\n 'command', command,\n 'roles', roles,\n 'qual', qual,\n 'with_check', with_check,\n 'permissive_using', has_permissive_using,\n 'permissive_with_check', has_permissive_with_check\n ) as metadata,\n format(\n 'rls_policy_always_true_%s_%s_%s',\n schema_name,\n table_name,\n policy_name\n ) as cache_key\nfrom\n permissive_patterns\nwhere\n has_permissive_using or has_permissive_with_check\norder by\n schema_name,\n table_name,\n policy_name)"; /** * Splits the embedded SQL into [setup, query] on the first `";\n\n"`, From 2cb2d6e83a3f7d65ebdab905c35b8f8257e685dc Mon Sep 17 00:00:00 2001 From: "supabase-cli-releaser[bot]" <246109035+supabase-cli-releaser[bot]@users.noreply.github.com> Date: Thu, 16 Jul 2026 18:38:05 +0200 Subject: [PATCH 56/79] chore(api): sync Management API OpenAPI spec (#5882) This PR was automatically created to sync the generated `@supabase/api` package with the latest Management API OpenAPI document. Changes were detected in the upstream OpenAPI document exposed by `https://api.supabase.com/api/v1-json`. --------- Co-authored-by: jgoux <1443499+jgoux@users.noreply.github.com> Co-authored-by: avallete Co-authored-by: Claude Opus 4.8 --- .../commands/secrets/set/set.handler.ts | 28 ++++++- .../secrets/set/set.integration.test.ts | 60 +++++++++++++++ packages/api/src/generated/contracts.ts | 35 ++++++++- packages/api/src/generated/effect-client.ts | 18 +++++ packages/api/src/generated/openapi.json | 75 ++++++++++++++++++- 5 files changed, 211 insertions(+), 5 deletions(-) diff --git a/apps/cli/src/legacy/commands/secrets/set/set.handler.ts b/apps/cli/src/legacy/commands/secrets/set/set.handler.ts index 53a46ee15f..fed1646688 100644 --- a/apps/cli/src/legacy/commands/secrets/set/set.handler.ts +++ b/apps/cli/src/legacy/commands/secrets/set/set.handler.ts @@ -6,6 +6,7 @@ import { type ProjectConfig, type ProjectConfigParseError, } from "@supabase/config"; +import { V1BulkCreateSecretsInput } from "@supabase/api/effect"; import { parse as parseDotenv } from "dotenv"; import { Effect, FileSystem, Option, Path, Redacted, Schema } from "effect"; @@ -375,8 +376,33 @@ export const legacySecretsSet = Effect.fn("legacy.secrets.set")(function* ( ); } + // The Management API caps a single bulk-create request at 100 secrets + // (`V1BulkCreateSecretsInput`'s `isMaxLength(100)` check in `@supabase/api`). + // Go issues one unbatched request (`internal/secrets/set/set.go`), so against + // the capped API a >100-entry env file would be rejected wholesale; split into + // batches of at most 100 so large env files still upload. + const SECRETS_PER_REQUEST = 100; + const batches: Array = []; + for (let i = 0; i < body.length; i += SECRETS_PER_REQUEST) { + batches.push(body.slice(i, i + SECRETS_PER_REQUEST)); + } + + // Validate every batch (per-entry name/value constraints and the 100-item + // cap) before sending any request. Without this, a schema-invalid entry in a + // later batch would only surface after earlier batches had already been + // uploaded, leaving the project partially updated. Decoding fails with the + // same `SchemaError` `bulkCreateSecrets` raises, so `mapSetError` keeps the + // error surface identical to the previous single-call path. + yield* Effect.forEach( + batches, + (batch) => Schema.decodeUnknownEffect(V1BulkCreateSecretsInput)({ ref, body: batch }), + { discard: true }, + ).pipe(Effect.catch(mapSetError)); + const setting = output.format === "text" ? yield* output.task("Setting secrets...") : undefined; - yield* api.v1.bulkCreateSecrets({ ref, body }).pipe( + yield* Effect.forEach(batches, (batch) => api.v1.bulkCreateSecrets({ ref, body: batch }), { + discard: true, + }).pipe( Effect.tapError(() => setting?.fail() ?? Effect.void), Effect.catch(mapSetError), ); diff --git a/apps/cli/src/legacy/commands/secrets/set/set.integration.test.ts b/apps/cli/src/legacy/commands/secrets/set/set.integration.test.ts index 4e6a3e4dfd..29bbaf3fd0 100644 --- a/apps/cli/src/legacy/commands/secrets/set/set.integration.test.ts +++ b/apps/cli/src/legacy/commands/secrets/set/set.integration.test.ts @@ -123,6 +123,66 @@ describe("legacy secrets set integration", () => { }).pipe(Effect.provide(layer)); }); + it.live("batches large secret sets into requests of at most 100", () => { + const { layer, out, api } = setup(); + return Effect.gen(function* () { + yield* legacySecretsSet({ + projectRef: Option.none(), + envFile: Option.none(), + secrets: Array.from({ length: 150 }, (_, i) => `KEY${i}=value${i}`), + }); + expect(api.requests).toHaveLength(2); + const first = parsePostBody(api.requests[0]!.body); + const second = parsePostBody(api.requests[1]!.body); + expect(first).toHaveLength(100); + expect(second).toHaveLength(50); + const names = new Set([...first, ...second].map((entry) => entry.name)); + for (let i = 0; i < 150; i++) { + expect(names.has(`KEY${i}`)).toBe(true); + } + expect(out.stdoutText).toContain("Finished supabase secrets set."); + }).pipe(Effect.provide(layer)); + }); + + it.live("batches 250 secrets into three requests (100/100/50)", () => { + const { layer, api } = setup(); + return Effect.gen(function* () { + yield* legacySecretsSet({ + projectRef: Option.none(), + envFile: Option.none(), + secrets: Array.from({ length: 250 }, (_, i) => `KEY${i}=value${i}`), + }); + expect(api.requests).toHaveLength(3); + expect(parsePostBody(api.requests[0]!.body)).toHaveLength(100); + expect(parsePostBody(api.requests[1]!.body)).toHaveLength(100); + expect(parsePostBody(api.requests[2]!.body)).toHaveLength(50); + }).pipe(Effect.provide(layer)); + }); + + it.live( + "rejects the whole upload when a later batch has an invalid entry (no partial update)", + () => { + const { layer, api } = setup(); + // Index 120 lands in the SECOND batch (batch 0 covers indices 0-99): a + // value exceeding the 24576-byte cap there must fail up-front validation + // before batch 0 (which is otherwise entirely valid) is ever sent. + const secrets = Array.from({ length: 150 }, (_, i) => + i === 120 ? `KEY${i}=${"x".repeat(24577)}` : `KEY${i}=value${i}`, + ); + return Effect.gen(function* () { + const exit = yield* Effect.exit( + legacySecretsSet({ + projectRef: Option.none(), + envFile: Option.none(), + secrets, + }), + ); + expect(Exit.isFailure(exit)).toBe(true); + expect(api.requests).toHaveLength(0); + }).pipe(Effect.provide(layer)); + }, + ); + it.live("sets secrets from --env-file with a relative path (joined to CWD)", () => { writeFileSync(join(tempRoot.current, "myfile.env"), "FROM_FILE=fromvalue\n"); const { layer, api } = setup(); diff --git a/packages/api/src/generated/contracts.ts b/packages/api/src/generated/contracts.ts index 7fb3cbb824..a7904a4b78 100644 --- a/packages/api/src/generated/contracts.ts +++ b/packages/api/src/generated/contracts.ts @@ -419,7 +419,7 @@ export const V1BulkCreateSecretsInput = Schema.Struct({ .check(Schema.isPattern(new RegExp("^(?!SUPABASE_).*"))), value: Schema.String.check(Schema.isMaxLength(24576)), }), - ), + ).check(Schema.isMaxLength(100)), }); export const V1BulkDeleteSecretsInput = Schema.Struct({ ref: Schema.String.check(Schema.isMinLength(20)) @@ -2421,7 +2421,11 @@ export const V1GetJitAccessConfigOutput = Schema.Union( }), Schema.Struct({ state: Schema.Literal("unavailable"), - unavailableReason: Schema.Literals(["postgres_upgrade_required", "temporarily_unavailable"]), + unavailableReason: Schema.Literals([ + "postgres_upgrade_required", + "ssl_enforcement_required", + "temporarily_unavailable", + ]), }), ], { mode: "oneOf" }, @@ -4354,6 +4358,12 @@ export const V1RunAQueryInput = Schema.Struct({ parameters: Schema.optionalKey(Schema.Array(Schema.Json)), read_only: Schema.optionalKey(Schema.Boolean), }); +export const V1ScrapeProjectMetricsInput = Schema.Struct({ + ref: Schema.String.check(Schema.isMinLength(20)) + .check(Schema.isMaxLength(20)) + .check(Schema.isPattern(new RegExp("^[a-z]+$"))), +}); +export const V1ScrapeProjectMetricsOutput = Schema.String; export const V1SetupAReadReplicaInput = Schema.Struct({ ref: Schema.String.check(Schema.isMinLength(20)) .check(Schema.isMaxLength(20)) @@ -5559,7 +5569,11 @@ export const V1UpdateJitAccessConfigOutput = Schema.Union( }), Schema.Struct({ state: Schema.Literal("unavailable"), - unavailableReason: Schema.Literals(["postgres_upgrade_required", "temporarily_unavailable"]), + unavailableReason: Schema.Literals([ + "postgres_upgrade_required", + "ssl_enforcement_required", + "temporarily_unavailable", + ]), }), ], { mode: "oneOf" }, @@ -6282,6 +6296,7 @@ export const openApiOperationIdMap = { "v1-revoke-token": "v1RevokeToken", "v1-rollback-migrations": "v1RollbackMigrations", "v1-run-a-query": "v1RunAQuery", + "v1-scrape-project-metrics": "v1ScrapeProjectMetrics", "v1-setup-a-read-replica": "v1SetupAReadReplica", "v1-shutdown-realtime": "v1ShutdownRealtime", "v1-undo": "v1Undo", @@ -8276,6 +8291,20 @@ export const operationDefinitions = { inputSchema: V1RunAQueryInput, outputSchema: V1RunAQueryOutput, }, + v1ScrapeProjectMetrics: { + id: "v1ScrapeProjectMetrics", + description: + "Prometheus scrape endpoint. Returns metrics of a customer project in the Prometheus open exposition format.", + method: "GET", + path: "/v1/projects/{ref}/analytics/endpoints/metrics", + pathParams: ["ref"], + queryParams: [], + headerParams: [], + requestBody: { kind: "none" }, + response: { kind: "text" }, + inputSchema: V1ScrapeProjectMetricsInput, + outputSchema: V1ScrapeProjectMetricsOutput, + }, v1SetupAReadReplica: { id: "v1SetupAReadReplica", description: "[Beta] Set up a read replica", diff --git a/packages/api/src/generated/effect-client.ts b/packages/api/src/generated/effect-client.ts index 43648171dd..f8651d8acf 100644 --- a/packages/api/src/generated/effect-client.ts +++ b/packages/api/src/generated/effect-client.ts @@ -1936,6 +1936,20 @@ export const versionedEffectOperations = { const client = yield* SupabaseApiClient; return yield* client.execute<"v1RunAQuery">(operationDefinitions.v1RunAQuery, input); }), + scrapeProjectMetrics: ( + input: typeof operationDefinitions.v1ScrapeProjectMetrics.inputSchema.Type, + ): Effect.Effect< + typeof operationDefinitions.v1ScrapeProjectMetrics.outputSchema.Type, + SupabaseApiError, + SupabaseApiClient + > => + Effect.gen(function* () { + const client = yield* SupabaseApiClient; + return yield* client.execute<"v1ScrapeProjectMetrics">( + operationDefinitions.v1ScrapeProjectMetrics, + input, + ); + }), setupAReadReplica: ( input: typeof operationDefinitions.v1SetupAReadReplica.inputSchema.Type, ): Effect.Effect< @@ -2901,6 +2915,10 @@ export function executeApiClientOperation( return Schema.decodeUnknownEffect(operationDefinitions.v1RunAQuery.inputSchema)(input).pipe( Effect.flatMap((decoded) => api.v1.runAQuery(decoded)), ); + case "v1ScrapeProjectMetrics": + return Schema.decodeUnknownEffect(operationDefinitions.v1ScrapeProjectMetrics.inputSchema)( + input, + ).pipe(Effect.flatMap((decoded) => api.v1.scrapeProjectMetrics(decoded))); case "v1SetupAReadReplica": return Schema.decodeUnknownEffect(operationDefinitions.v1SetupAReadReplica.inputSchema)( input, diff --git a/packages/api/src/generated/openapi.json b/packages/api/src/generated/openapi.json index af9e2e6e6a..1def222168 100644 --- a/packages/api/src/generated/openapi.json +++ b/packages/api/src/generated/openapi.json @@ -7049,6 +7049,74 @@ "x-endpoint-owners": ["analytics"] } }, + "/v1/projects/{ref}/analytics/endpoints/metrics": { + "get": { + "description": "Prometheus scrape endpoint. Returns metrics of a customer project in the Prometheus open exposition format.", + "operationId": "v1-scrape-project-metrics", + "parameters": [ + { + "name": "ref", + "required": true, + "in": "path", + "description": "Project ref", + "schema": { + "minLength": 20, + "maxLength": 20, + "pattern": "^[a-z]+$", + "example": "abcdefghijklmnopqrst", + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Prometheus / OpenMetrics text exposition", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + }, + "application/openmetrics-text": { + "schema": { + "type": "string" + } + } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden action" + }, + "429": { + "description": "Rate limit exceeded" + }, + "500": { + "description": "Failed to fetch project's metrics" + } + }, + "security": [ + { + "bearer": [] + }, + { + "fga_permissions": ["analytics_logs_read"] + } + ], + "summary": "Scrape a project's metrics", + "tags": ["Analytics"], + "x-badges": [ + { + "name": "OAuth scope: analytics:read", + "position": "after" + } + ], + "x-endpoint-owners": ["analytics"], + "x-oauth-scope": "analytics:read" + } + }, "/v1/projects/{ref}/cli/login-role": { "post": { "operationId": "v1-create-login-role", @@ -13018,7 +13086,11 @@ }, "unavailableReason": { "type": "string", - "enum": ["postgres_upgrade_required", "temporarily_unavailable"] + "enum": [ + "postgres_upgrade_required", + "ssl_enforcement_required", + "temporarily_unavailable" + ] } }, "required": ["state", "unavailableReason"] @@ -13474,6 +13546,7 @@ }, "required": ["name", "value"] }, + "maxItems": 100, "example": [ { "name": "OPENAI_API_KEY", From 770d7cc1a7c7d676961156a1af99a13d4420011e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2026 00:13:26 +0000 Subject: [PATCH 57/79] chore(deps): bump the go-minor group across 2 directories with 3 updates (#5885) Bumps the go-minor group with 2 updates in the /apps/cli-go directory: [golang.org/x/mod](https://github.com/golang/mod) and [golang.org/x/net](https://github.com/golang/net). Bumps the go-minor group with 1 update in the /apps/cli-go/pkg directory: [golang.org/x/mod](https://github.com/golang/mod). Updates `golang.org/x/mod` from 0.37.0 to 0.38.0
Commits
  • 792ac16 go.mod: update golang.org/x dependencies
  • fe2ec04 all: fix some comments to improve readability
  • See full diff in compare view

Updates `golang.org/x/net` from 0.56.0 to 0.57.0
Commits
  • b8f09f6 go.mod: update golang.org/x dependencies
  • f05f21b idna: reject all-ASCII xn-- labels on all Go versions
  • 0f748cf internal/http3: clean up stream I/O methods usages in tests
  • 0bb961e internal/http3: add net/http.ResponseController support
  • 0ca694d webdav: document Dir's lack of defense against filesystem modification
  • bd5f1dc http2: initialize Transport on NewClientConn
  • 488ff63 bpf: add security considerations to package docs
  • 93d1f25 xsrftoken: avoid token collisions
  • 5a3baee internal/http3: prevent panic in QPACK decoder due to overflow
  • See full diff in compare view

Updates `golang.org/x/term` from 0.44.0 to 0.45.0
Commits

Updates `golang.org/x/mod` from 0.37.0 to 0.38.0
Commits
  • 792ac16 go.mod: update golang.org/x dependencies
  • fe2ec04 all: fix some comments to improve readability
  • See full diff in compare view

Updates `golang.org/x/mod` from 0.37.0 to 0.38.0
Commits
  • 792ac16 go.mod: update golang.org/x dependencies
  • fe2ec04 all: fix some comments to improve readability
  • See full diff in compare view

Updates `golang.org/x/mod` from 0.37.0 to 0.38.0
Commits
  • 792ac16 go.mod: update golang.org/x dependencies
  • fe2ec04 all: fix some comments to improve readability
  • See full diff in compare view

Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/cli-go/go.mod | 16 ++++++++-------- apps/cli-go/go.sum | 32 ++++++++++++++++---------------- apps/cli-go/pkg/go.mod | 2 +- apps/cli-go/pkg/go.sum | 4 ++-- 4 files changed, 27 insertions(+), 27 deletions(-) diff --git a/apps/cli-go/go.mod b/apps/cli-go/go.mod index ede19cb805..81765a2e1a 100644 --- a/apps/cli-go/go.mod +++ b/apps/cli-go/go.mod @@ -54,10 +54,10 @@ require ( github.com/withfig/autocomplete-tools/packages/cobra v1.2.0 github.com/zalando/go-keyring v0.2.8 go.opentelemetry.io/otel v1.44.0 - golang.org/x/mod v0.37.0 - golang.org/x/net v0.56.0 + golang.org/x/mod v0.38.0 + golang.org/x/net v0.57.0 golang.org/x/oauth2 v0.36.0 - golang.org/x/term v0.44.0 + golang.org/x/term v0.45.0 google.golang.org/grpc v1.82.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -458,13 +458,13 @@ require ( go.uber.org/zap v1.27.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.53.0 // indirect + golang.org/x/crypto v0.54.0 // indirect golang.org/x/exp/typeparams v0.0.0-20260209203927-2842357ff358 // indirect - golang.org/x/sync v0.21.0 // indirect - golang.org/x/sys v0.46.0 // indirect - golang.org/x/text v0.38.0 // indirect + golang.org/x/sync v0.22.0 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/text v0.40.0 // indirect golang.org/x/time v0.14.0 // indirect - golang.org/x/tools v0.45.0 // indirect + golang.org/x/tools v0.47.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect google.golang.org/protobuf v1.36.11 // indirect diff --git a/apps/cli-go/go.sum b/apps/cli-go/go.sum index a9cb686621..320bc87f37 100644 --- a/apps/cli-go/go.sum +++ b/apps/cli-go/go.sum @@ -1286,8 +1286,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= -golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= -golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= +golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= +golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= @@ -1303,8 +1303,8 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= -golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= +golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk= +golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -1325,8 +1325,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= -golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= +golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= +golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1339,8 +1339,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= -golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1380,16 +1380,16 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= -golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= -golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= +golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= +golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -1399,8 +1399,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= -golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= +golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= +golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -1422,8 +1422,8 @@ golang.org/x/tools v0.1.1-0.20210302220138-2ac05c832e1a/go.mod h1:9bzcO0MWcOuT0t golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= -golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= +golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= +golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM= golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM= diff --git a/apps/cli-go/pkg/go.mod b/apps/cli-go/pkg/go.mod index 8afe2e0de5..c2ffb1bc38 100644 --- a/apps/cli-go/pkg/go.mod +++ b/apps/cli-go/pkg/go.mod @@ -25,7 +25,7 @@ require ( github.com/spf13/viper v1.21.0 github.com/stretchr/testify v1.11.1 github.com/tidwall/jsonc v0.3.3 - golang.org/x/mod v0.37.0 + golang.org/x/mod v0.38.0 google.golang.org/grpc v1.82.0 ) diff --git a/apps/cli-go/pkg/go.sum b/apps/cli-go/pkg/go.sum index 08ff799d66..56f14ba857 100644 --- a/apps/cli-go/pkg/go.sum +++ b/apps/cli-go/pkg/go.sum @@ -224,8 +224,8 @@ golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKG golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= -golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= +golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk= +golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= From a3925ee0eaeb11685c24087a898d3ef18b8c6502 Mon Sep 17 00:00:00 2001 From: "supabase-cli-releaser[bot]" <246109035+supabase-cli-releaser[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2026 06:13:49 +0000 Subject: [PATCH 58/79] chore: sync API types from infrastructure (#5893) This PR was automatically created to sync API types from the infrastructure repository. Changes were detected in the generated API code after syncing with the latest spec from infrastructure. Co-authored-by: supabase-cli-releaser[bot] <246109035+supabase-cli-releaser[bot]@users.noreply.github.com> --- apps/cli-go/pkg/api/client.gen.go | 51 +++++++++++++++++++++++++++++++ apps/cli-go/pkg/api/types.gen.go | 26 ++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/apps/cli-go/pkg/api/client.gen.go b/apps/cli-go/pkg/api/client.gen.go index 1fb6625479..17a4a14ad1 100644 --- a/apps/cli-go/pkg/api/client.gen.go +++ b/apps/cli-go/pkg/api/client.gen.go @@ -14424,6 +14424,7 @@ type V1GetBackupScheduleResponse struct { Body []byte HTTPResponse *http.Response JSON200 *V1BackupScheduleResponse + JSON402 *PlanGateErrorBody } // Status returns HTTPResponse.Status @@ -14446,6 +14447,7 @@ type V1UpdateBackupScheduleResponse struct { Body []byte HTTPResponse *http.Response JSON200 *V1BackupScheduleResponse + JSON402 *PlanGateErrorBody } // Status returns HTTPResponse.Status @@ -15421,6 +15423,7 @@ func (r V1RemoveAReadReplicaResponse) StatusCode() int { type V1SetupAReadReplicaResponse struct { Body []byte HTTPResponse *http.Response + JSON402 *PlanGateErrorBody } // Status returns HTTPResponse.Status @@ -15810,6 +15813,7 @@ type V1GetVanitySubdomainConfigResponse struct { Body []byte HTTPResponse *http.Response JSON200 *VanitySubdomainConfigResponse + JSON400 *PlanGateErrorBody } // Status returns HTTPResponse.Status @@ -15832,6 +15836,7 @@ type V1ActivateVanitySubdomainConfigResponse struct { Body []byte HTTPResponse *http.Response JSON201 *ActivateVanitySubdomainResponse + JSON400 *PlanGateErrorBody } // Status returns HTTPResponse.Status @@ -15854,6 +15859,7 @@ type V1CheckVanitySubdomainAvailabilityResponse struct { Body []byte HTTPResponse *http.Response JSON201 *SubdomainAvailabilityResponse + JSON400 *PlanGateErrorBody } // Status returns HTTPResponse.Status @@ -20377,6 +20383,13 @@ func ParseV1GetBackupScheduleResponse(rsp *http.Response) (*V1GetBackupScheduleR } response.JSON200 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 402: + var dest PlanGateErrorBody + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON402 = &dest + } return response, nil @@ -20403,6 +20416,13 @@ func ParseV1UpdateBackupScheduleResponse(rsp *http.Response) (*V1UpdateBackupSch } response.JSON200 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 402: + var dest PlanGateErrorBody + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON402 = &dest + } return response, nil @@ -21425,6 +21445,16 @@ func ParseV1SetupAReadReplicaResponse(rsp *http.Response) (*V1SetupAReadReplicaR HTTPResponse: rsp, } + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 402: + var dest PlanGateErrorBody + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON402 = &dest + + } + return response, nil } @@ -21821,6 +21851,13 @@ func ParseV1GetVanitySubdomainConfigResponse(rsp *http.Response) (*V1GetVanitySu } response.JSON200 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest PlanGateErrorBody + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + } return response, nil @@ -21847,6 +21884,13 @@ func ParseV1ActivateVanitySubdomainConfigResponse(rsp *http.Response) (*V1Activa } response.JSON201 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest PlanGateErrorBody + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + } return response, nil @@ -21873,6 +21917,13 @@ func ParseV1CheckVanitySubdomainAvailabilityResponse(rsp *http.Response) (*V1Che } response.JSON201 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest PlanGateErrorBody + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + } return response, nil diff --git a/apps/cli-go/pkg/api/types.gen.go b/apps/cli-go/pkg/api/types.gen.go index 308eafac63..bd0961de95 100644 --- a/apps/cli-go/pkg/api/types.gen.go +++ b/apps/cli-go/pkg/api/types.gen.go @@ -840,6 +840,11 @@ const ( OrganizationProjectsResponseProjectsStatusUPGRADING OrganizationProjectsResponseProjectsStatus = "UPGRADING" ) +// Defines values for PlanGateErrorBodyErrorCode. +const ( + EntitlementRequired PlanGateErrorBodyErrorCode = "entitlement_required" +) + // Defines values for PostgresConfigResponseSessionReplicationRole. const ( PostgresConfigResponseSessionReplicationRoleLocal PostgresConfigResponseSessionReplicationRole = "local" @@ -3436,6 +3441,27 @@ type PgsodiumConfigResponse struct { RootKey string `json:"root_key"` } +// PlanGateErrorBody defines model for PlanGateErrorBody. +type PlanGateErrorBody struct { + // Error Present on entitlement denials. Other errors with this status code (validation, billing state) carry only message. + Error *struct { + // Code Machine-readable marker for plan-gated denials + Code PlanGateErrorBodyErrorCode `json:"code"` + + // Feature Entitlement feature key that failed the check + Feature string `json:"feature"` + + // UpgradeUrl Billing page URL for the organization, present when the org is resolvable + UpgradeUrl *string `json:"upgrade_url,omitempty"` + } `json:"error,omitempty"` + + // Message Human-readable explanation of the plan gate + Message string `json:"message"` +} + +// PlanGateErrorBodyErrorCode Machine-readable marker for plan-gated denials +type PlanGateErrorBodyErrorCode string + // PostgresConfigResponse defines model for PostgresConfigResponse. type PostgresConfigResponse struct { // CheckpointTimeout Default unit: s From 2da1ee0c46e79966d39f0fbf580a4b70927a51cf Mon Sep 17 00:00:00 2001 From: Vaibhav <117663341+7ttp@users.noreply.github.com> Date: Fri, 17 Jul 2026 13:37:39 +0530 Subject: [PATCH 59/79] fix(cli): redact API keys (#5891) ## TL;DR fixes project keys being written to CLI traces, matching the Go cli's behavior... ## Problem The Go-cli use to send keys through the `apikey` header: https://github.com/supabase/cli/blob/2cb2d6e83a3f7d65ebdab905c35b8f8257e685dc/apps/cli-go/pkg/fetcher/gateway.go#L22-L33 Its debug transport only logs the request method and URL: https://github.com/supabase/cli/blob/2cb2d6e83a3f7d65ebdab905c35b8f8257e685dc/apps/cli-go/internal/debug/http.go#L14-L16 Effect tracing captures request headers, but `apikey` is not included in its default redaction list... this allowed values to be written to local traces... ## sol exclude `apikey` header attributes at the tracing boundary.... requests and all other telemetry remain unchanged, while NDJSON and debug output no longer contain project keys.... ## ref: - reported in: #5890 --- .../cli/src/shared/telemetry/tracing.layer.ts | 1 + .../telemetry/tracing.layer.unit.test.ts | 27 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/apps/cli/src/shared/telemetry/tracing.layer.ts b/apps/cli/src/shared/telemetry/tracing.layer.ts index f9638cad11..770a2218ee 100644 --- a/apps/cli/src/shared/telemetry/tracing.layer.ts +++ b/apps/cli/src/shared/telemetry/tracing.layer.ts @@ -76,6 +76,7 @@ class ExportableSpan implements Tracer.Span { } attribute(key: string, value: unknown): void { + if (key.endsWith(".header.apikey")) return; this.attributes.set(key, value); } diff --git a/apps/cli/src/shared/telemetry/tracing.layer.unit.test.ts b/apps/cli/src/shared/telemetry/tracing.layer.unit.test.ts index 25b306e041..d6642f8492 100644 --- a/apps/cli/src/shared/telemetry/tracing.layer.unit.test.ts +++ b/apps/cli/src/shared/telemetry/tracing.layer.unit.test.ts @@ -242,6 +242,33 @@ describe("tracingLayer – span behaviour", () => { ); }); + it.live("does not write API keys to trace files", () => { + const home = makeTempDir(); + const tracesDir = path.join(home, ".supabase", "traces"); + const secretKey = `sb_secret_${"a".repeat(40)}`; + return Effect.gen(function* () { + const tracer = yield* Tracer.Tracer; + const span = tracer.span(makeSpanOptions()); + span.attribute("http.request.header.apikey", secretKey); + span.end(BigInt(Date.now() + 100) * 1_000_000n, Exit.void); + }).pipe( + Effect.provide(buildTracingLayer({ home })), + Effect.ensuring( + Effect.sync(() => { + try { + const traceFile = readdirSync(tracesDir).find((file) => file.endsWith(".ndjson")); + expect(traceFile).toBeDefined(); + const trace = readFileSync(path.join(tracesDir, traceFile!), "utf8"); + expect(trace).not.toContain(secretKey); + expect(trace).not.toContain("http.request.header.apikey"); + } finally { + rmSync(home, { recursive: true, force: true }); + } + }), + ), + ); + }); + it.live("span end does NOT export to NDJSON when SUPABASE_TELEMETRY_DISABLED=1", () => { const home = makeTempDir(); const configDir = path.join(home, ".supabase"); From 4e99f37dd9217f1f51aeaed3a3ff554759d4ffb1 Mon Sep 17 00:00:00 2001 From: "supabase-cli-releaser[bot]" <246109035+supabase-cli-releaser[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2026 10:22:57 +0200 Subject: [PATCH 60/79] chore(api): sync Management API OpenAPI spec (#5889) This PR was automatically created to sync the generated `@supabase/api` package with the latest Management API OpenAPI document. Changes were detected in the upstream OpenAPI document exposed by `https://api.supabase.com/api/v1-json`. Co-authored-by: jgoux <1443499+jgoux@users.noreply.github.com> --- packages/api/src/generated/contracts.ts | 16 +++- packages/api/src/generated/openapi.json | 97 ++++++++++++++++++++++--- 2 files changed, 100 insertions(+), 13 deletions(-) diff --git a/packages/api/src/generated/contracts.ts b/packages/api/src/generated/contracts.ts index a7904a4b78..d694679387 100644 --- a/packages/api/src/generated/contracts.ts +++ b/packages/api/src/generated/contracts.ts @@ -2661,7 +2661,11 @@ export const V1GetPgsodiumConfigInput = Schema.Struct({ .check(Schema.isMaxLength(20)) .check(Schema.isPattern(new RegExp("^[a-z]+$"))), }); -export const V1GetPgsodiumConfigOutput = Schema.Struct({ root_key: Schema.String }); +export const V1GetPgsodiumConfigOutput = Schema.Struct({ + root_key: Schema.String.annotate({ + description: "The pgsodium root key: 32 bytes, hex-encoded (64 characters).", + }), +}); export const V1GetPoolerConfigInput = Schema.Struct({ ref: Schema.String.check(Schema.isMinLength(20)) .check(Schema.isMaxLength(20)) @@ -5611,9 +5615,15 @@ export const V1UpdatePgsodiumConfigInput = Schema.Struct({ ref: Schema.String.check(Schema.isMinLength(20)) .check(Schema.isMaxLength(20)) .check(Schema.isPattern(new RegExp("^[a-z]+$"))), - root_key: Schema.String, + root_key: Schema.String.annotate({ + description: "The pgsodium root key: 32 bytes, hex-encoded (64 characters).", + }), +}); +export const V1UpdatePgsodiumConfigOutput = Schema.Struct({ + root_key: Schema.String.annotate({ + description: "The pgsodium root key: 32 bytes, hex-encoded (64 characters).", + }), }); -export const V1UpdatePgsodiumConfigOutput = Schema.Struct({ root_key: Schema.String }); export const V1UpdatePoolerConfigInput = Schema.Struct({ ref: Schema.String.check(Schema.isMinLength(20)) .check(Schema.isMaxLength(20)) diff --git a/packages/api/src/generated/openapi.json b/packages/api/src/generated/openapi.json index 1def222168..3ae234fd03 100644 --- a/packages/api/src/generated/openapi.json +++ b/packages/api/src/generated/openapi.json @@ -4273,7 +4273,14 @@ } }, "400": { - "description": "This feature requires the Pro, Team, or Enterprise organization plan." + "description": "This feature requires the Pro, Team, or Enterprise organization plan.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlanGateErrorBody" + } + } + } }, "401": { "description": "Unauthorized" @@ -4406,7 +4413,14 @@ } }, "400": { - "description": "This feature requires the Pro, Team, or Enterprise organization plan." + "description": "This feature requires the Pro, Team, or Enterprise organization plan.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlanGateErrorBody" + } + } + } }, "401": { "description": "Unauthorized" @@ -4486,7 +4500,14 @@ } }, "400": { - "description": "This feature requires the Pro, Team, or Enterprise organization plan." + "description": "This feature requires the Pro, Team, or Enterprise organization plan.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlanGateErrorBody" + } + } + } }, "401": { "description": "Unauthorized" @@ -4884,7 +4905,14 @@ "description": "Unauthorized" }, "402": { - "description": "This feature requires the Pro, Team, or Enterprise organization plan." + "description": "This feature requires the Pro, Team, or Enterprise organization plan.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlanGateErrorBody" + } + } + } }, "403": { "description": "Forbidden action" @@ -10897,7 +10925,14 @@ "description": "Unauthorized" }, "402": { - "description": "This feature requires the Enterprise organization plan." + "description": "This feature requires the Enterprise organization plan.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlanGateErrorBody" + } + } + } }, "403": { "description": "Forbidden action" @@ -10982,7 +11017,14 @@ "description": "Unauthorized" }, "402": { - "description": "This feature requires the Enterprise organization plan." + "description": "This feature requires the Enterprise organization plan.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlanGateErrorBody" + } + } + } }, "403": { "description": "Forbidden action" @@ -13371,21 +13413,26 @@ "type": "object", "properties": { "root_key": { - "type": "string" + "type": "string", + "description": "The pgsodium root key: 32 bytes, hex-encoded (64 characters)." } }, - "required": ["root_key"] + "required": ["root_key"], + "example": { + "root_key": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + } }, "UpdatePgsodiumConfigBody": { "type": "object", "properties": { "root_key": { - "type": "string" + "type": "string", + "description": "The pgsodium root key: 32 bytes, hex-encoded (64 characters)." } }, "required": ["root_key"], "example": { - "root_key": "MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=" + "root_key": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" } }, "PostgrestConfigWithJWTSecretResponse": { @@ -13626,6 +13673,36 @@ }, "required": ["status"] }, + "PlanGateErrorBody": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Human-readable explanation of the plan gate" + }, + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "enum": ["entitlement_required"], + "description": "Machine-readable marker for plan-gated denials" + }, + "feature": { + "type": "string", + "description": "Entitlement feature key that failed the check" + }, + "upgrade_url": { + "type": "string", + "description": "Billing page URL for the organization, present when the org is resolvable" + } + }, + "required": ["code", "feature"], + "description": "Present on entitlement denials. Other errors with this status code (validation, billing state) carry only message." + } + }, + "required": ["message"] + }, "VanitySubdomainBody": { "type": "object", "properties": { From 377d47576e5e018cf7378aae820a5179f577597b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2026 11:01:41 +0000 Subject: [PATCH 61/79] fix(docker): bump the docker-minor group across 1 directory with 6 updates (#5876) Bumps the docker-minor group with 6 updates in the /apps/cli-go/pkg/config/templates directory: | Package | From | To | | --- | --- | --- | | postgrest/postgrest | `v14.14` | `v14.15` | | supabase/studio | `2026.07.07-sha-a6a04f2` | `2026.07.13-sha-b5ada96` | | supabase/gotrue | `v2.192.0` | `v2.193.0` | | supabase/realtime | `v2.112.10` | `v2.113.4` | | supabase/storage-api | `v1.66.2` | `v1.66.4` | | supabase/logflare | `1.47.0` | `1.47.1` | Updates `postgrest/postgrest` from v14.14 to v14.15 Updates `supabase/studio` from 2026.07.07-sha-a6a04f2 to 2026.07.13-sha-b5ada96 Updates `supabase/gotrue` from v2.192.0 to v2.193.0 Updates `supabase/realtime` from v2.112.10 to v2.113.4 Updates `supabase/storage-api` from v1.66.2 to v1.66.4 Updates `supabase/logflare` from 1.47.0 to 1.47.1 Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
--------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Julien Goux --- apps/cli-go/pkg/config/templates/Dockerfile | 12 ++++++------ packages/stack/src/versions.ts | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/apps/cli-go/pkg/config/templates/Dockerfile b/apps/cli-go/pkg/config/templates/Dockerfile index 6b9c671d1d..19f01f35b7 100644 --- a/apps/cli-go/pkg/config/templates/Dockerfile +++ b/apps/cli-go/pkg/config/templates/Dockerfile @@ -3,17 +3,17 @@ FROM supabase/postgres:17.6.1.143 AS pg # Append to ServiceImages when adding new dependencies below FROM library/kong:2.8.1 AS kong FROM axllent/mailpit:v1.30.2 AS mailpit -FROM postgrest/postgrest:v14.14 AS postgrest +FROM postgrest/postgrest:v14.15 AS postgrest FROM supabase/postgres-meta:v0.96.6 AS pgmeta -FROM supabase/studio:2026.07.07-sha-a6a04f2 AS studio +FROM supabase/studio:2026.07.13-sha-b5ada96 AS studio FROM darthsim/imgproxy:v3.8.0 AS imgproxy FROM supabase/edge-runtime:v1.74.2 AS edgeruntime FROM timberio/vector:0.53.0-alpine AS vector FROM supabase/supavisor:2.9.7 AS supavisor -FROM supabase/gotrue:v2.192.0 AS gotrue -FROM supabase/realtime:v2.112.10 AS realtime -FROM supabase/storage-api:v1.66.2 AS storage -FROM supabase/logflare:1.47.0 AS logflare +FROM supabase/gotrue:v2.193.0 AS gotrue +FROM supabase/realtime:v2.113.4 AS realtime +FROM supabase/storage-api:v1.66.4 AS storage +FROM supabase/logflare:1.47.1 AS logflare # Append to JobImages when adding new dependencies below FROM supabase/pgadmin-schema-diff:cli-0.0.5 AS differ FROM supabase/migra:3.0.1663481299 AS migra diff --git a/packages/stack/src/versions.ts b/packages/stack/src/versions.ts index be3d7f5687..989d9357b0 100644 --- a/packages/stack/src/versions.ts +++ b/packages/stack/src/versions.ts @@ -47,16 +47,16 @@ export interface VersionManifest { export const DEFAULT_VERSIONS: VersionManifest = { postgres: "17.6.1.143", - postgrest: "14.14", - auth: "2.192.0", + postgrest: "14.15", + auth: "2.193.0", "edge-runtime": "1.74.2", - realtime: "2.112.10", - storage: "1.66.2", + realtime: "2.113.4", + storage: "1.66.4", imgproxy: "v3.8.0", mailpit: "v1.30.2", pgmeta: "0.96.6", - studio: "2026.07.07-sha-a6a04f2", - analytics: "1.47.0", + studio: "2026.07.13-sha-b5ada96", + analytics: "1.47.1", vector: "0.53.0-alpine", pooler: "2.9.7", } as const; From 850156f7e3d87c0ab9ebee6d289a87b2033a4805 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2026 15:21:18 +0200 Subject: [PATCH 62/79] chore(deps): bump the npm-major group across 1 directory with 19 updates (#5886) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the npm-major group with 19 updates in the / directory: | Package | From | To | | --- | --- | --- | | [undici](https://github.com/nodejs/undici) | `8.6.0` | `8.7.0` | | [@supabase/supabase-js](https://github.com/supabase/supabase-js/tree/HEAD/packages/core/supabase-js) | `2.110.0` | `2.110.2` | | [@anthropic-ai/claude-agent-sdk](https://github.com/anthropics/claude-agent-sdk-typescript) | `0.3.201` | `0.3.206` | | [posthog-node](https://github.com/PostHog/posthog-js/tree/HEAD/packages/node) | `5.39.4` | `5.40.0` | | [fumadocs-core](https://github.com/fuma-nama/fumadocs) | `16.10.7` | `16.11.1` | | [fumadocs-mdx](https://github.com/fuma-nama/fumadocs) | `15.0.13` | `15.1.0` | | [fumadocs-ui](https://github.com/fuma-nama/fumadocs) | `16.10.7` | `16.11.1` | | [typescript](https://github.com/microsoft/TypeScript) | `6.0.3` | `7.0.2` | | [@effect/atom-react](https://github.com/Effect-TS/effect-smol/tree/HEAD/packages/atom/react) | `4.0.0-beta.93` | `4.0.0-beta.94` | | [@effect/platform-bun](https://github.com/Effect-TS/effect/tree/HEAD/packages/platform-bun) | `4.0.0-beta.93` | `4.0.0-beta.94` | | [@effect/platform-node](https://github.com/Effect-TS/effect/tree/HEAD/packages/platform-node) | `4.0.0-beta.93` | `4.0.0-beta.94` | | [@effect/sql-pg](https://github.com/Effect-TS/effect/tree/HEAD/packages/sql-pg) | `4.0.0-beta.93` | `4.0.0-beta.94` | | [@effect/vitest](https://github.com/Effect-TS/effect/tree/HEAD/packages/vitest) | `4.0.0-beta.93` | `4.0.0-beta.97` | | [@typescript/native-preview](https://github.com/microsoft/typescript-go) | `7.0.0-dev.20260703.1` | `7.0.0-dev.20260707.2` | | [@vitest/coverage-istanbul](https://github.com/vitest-dev/vitest/tree/HEAD/packages/coverage-istanbul) | `4.1.9` | `4.1.10` | | [effect](https://github.com/Effect-TS/effect/tree/HEAD/packages/effect) | `4.0.0-beta.93` | `4.0.0-beta.97` | | [oxfmt](https://github.com/oxc-project/oxc/tree/HEAD/npm/oxfmt) | `0.57.0` | `0.58.0` | | [tldts](https://github.com/remusao/tldts) | `6.1.86` | `7.4.8` | | [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) | `4.1.9` | `4.1.10` | Updates `undici` from 8.6.0 to 8.7.0
Release notes

Sourced from undici's releases.

v8.7.0

What's Changed

New Contributors

Full Changelog: https://github.com/nodejs/undici/compare/v8.6.0...v8.7.0

Commits
  • cb4c2f1 Bumped v8.7.0 (#5501)
  • a8d1a95 fix: auto-detect HTTP proxy tunneling (#5116)
  • cb30e58 fix: add static buildDispatch method to RedirectHandler type definition (#5442)
  • 0c08579 fix(h2): requeue request on GOAWAY'd session instead of crashing (#5453)
  • e5b3364 fix(h2): guard onResponse against a 'response' event delivered after completi...
  • c0007f4 docs: add reproduction guide and update bug report template (#5451)
  • e529cab fix: ignore an unparseable Set-Cookie Expires attribute (#5488)
  • 754742c fix(h2): destroy the stream on abort instead of relying on close() (#5462)
  • db34f5f fix(readable): ignore late consume chunks (#5375)
  • 4ea05a8 fix: reject non-ascii octets in validateCookiePath (#5452)
  • Additional commits viewable in compare view

Updates `@supabase/supabase-js` from 2.110.0 to 2.110.2
Release notes

Sourced from @​supabase/supabase-js's releases.

v2.110.2

2.110.2 (2026-07-09)

🩹 Fixes

  • auth: clear local session on signout failures (#2504)

❤️ Thank You

  • Luc Peng

v2.110.2-canary.0

2.110.2-canary.0 (2026-07-07)

🩹 Fixes

  • auth: clear local session on signout failures (#2504)

❤️ Thank You

  • Luc Peng

v2.110.1

2.110.1 (2026-07-07)

🩹 Fixes

  • auth: defer init-time notifications until initializePromise resolves (#2498)
  • realtime: suppress disconnected status from onHeartbeat consumers (#2496)

❤️ Thank You

v2.110.1-canary.0

2.110.1-canary.0 (2026-07-07)

🩹 Fixes

  • auth: defer init-time notifications until initializePromise resolves (#2498)
  • realtime: suppress disconnected status from onHeartbeat consumers (#2496)

❤️ Thank You

Changelog

Sourced from @​supabase/supabase-js's changelog.

2.110.2 (2026-07-09)

This was a version bump only for @​supabase/supabase-js to align it with other projects, there were no code changes.

2.110.1 (2026-07-07)

This was a version bump only for @​supabase/supabase-js to align it with other projects, there were no code changes.

Commits

Updates `@anthropic-ai/claude-agent-sdk` from 0.3.201 to 0.3.206
Release notes

Sourced from @​anthropic-ai/claude-agent-sdk's releases.

v0.3.206

What's changed

  • Added command_lifecycle frames to stream-json and SDK sessions, reporting each uuid-stamped message's terminal state (queued/started/completed/cancelled/discarded); zero-API results no longer report stale duration_api_ms

Update

npm install @anthropic-ai/claude-agent-sdk@0.3.206
# or
yarn add @anthropic-ai/claude-agent-sdk@0.3.206
# or
pnpm add @anthropic-ai/claude-agent-sdk@0.3.206
# or
bun add @anthropic-ai/claude-agent-sdk@0.3.206

v0.3.205

What's changed

  • Interrupt control responses now include still_queued (UUIDs of queued async messages that will still run), Query.interrupt() returns the typed receipt, and system/init advertises an interrupt_receipt_v1 capability for feature detection
  • Added structured name and body fields to peer-message session events, exposing the sender display name and decoded message body

Update

npm install @anthropic-ai/claude-agent-sdk@0.3.205
# or
yarn add @anthropic-ai/claude-agent-sdk@0.3.205
# or
pnpm add @anthropic-ai/claude-agent-sdk@0.3.205
# or
bun add @anthropic-ai/claude-agent-sdk@0.3.205

v0.3.204

What's changed

  • Updated to parity with Claude Code v2.1.204

Update

npm install @anthropic-ai/claude-agent-sdk@0.3.204
# or
yarn add @anthropic-ai/claude-agent-sdk@0.3.204
# or
pnpm add @anthropic-ai/claude-agent-sdk@0.3.204
# or
bun add @anthropic-ai/claude-agent-sdk@0.3.204
</tr></table>

... (truncated)

Changelog

Sourced from @​anthropic-ai/claude-agent-sdk's changelog.

0.3.206

  • Added command_lifecycle frames to stream-json and SDK sessions, reporting each uuid-stamped message's terminal state (queued/started/completed/cancelled/discarded); zero-API results no longer report stale duration_api_ms

0.3.205

  • Interrupt control responses now include still_queued (UUIDs of queued async messages that will still run), Query.interrupt() returns the typed receipt, and system/init advertises an interrupt_receipt_v1 capability for feature detection
  • Added structured name and body fields to peer-message session events, exposing the sender display name and decoded message body

0.3.204

  • Added terminal_reason values tool_deferred_unavailable (deferred tool resume found the tool gone — previously an is_error result with no reason, read as a clean completion by lifecycle sweeps) and turn_setup_failed (the turn-input builder threw before the turn started). Both classify as dead turns, so commands consumed by them report command_lifecycle state cancelled
  • Fixed the post-merge cancel backstop cancelling every member of a coalesced prompt batch when a cancel named only one: uncancelled siblings now re-merge and run (previously they were reported cancelled — on remote transports that acknowledged them as processed, silently dropping messages nobody cancelled)
  • Added terminal_reason values api_error, malformed_tool_use_exhausted, budget_exhausted, and structured_output_retry_exhausted. Turns that die on an exhausted-API-retry or malformed-tool-use give-up previously reported completed; budget and structured-output exhaustion results previously omitted terminal_reason. Commands consumed by such turns now report command_lifecycle state cancelled instead of completed (dup-over-loss)
  • Updated to parity with Claude Code v2.1.204

0.3.203

  • Added a background_tasks_changed system message with the full set of live background tasks on every membership change, so consumers can track background activity as a level instead of pairing task_started/task_notification edges
  • Fixed stable releases shipping a sdk.d.ts with unresolved type references that broke consumer typechecking with skipLibCheck disabled

0.3.202

  • Added parent_agent_id field to subagent session messages for building depth-2+ agent trees from disk-persisted metadata
  • Fixed apply_flag_settings with a non-object settings value crashing the session instead of returning a control error
Commits

Updates `posthog-node` from 5.39.4 to 5.40.0
Release notes

Sourced from posthog-node's releases.

posthog-node@5.40.0

5.40.0

Minor Changes

  • #4060 0b49a4c Thanks @​turnipdabeets! - Add secretKey config as the canonical alias for the deprecated personalApiKey (accepts a Personal API Key or Project Secret API Key). (2026-07-07)
Changelog

Sourced from posthog-node's changelog.

5.40.0

Minor Changes

  • #4060 0b49a4c Thanks @​turnipdabeets! - Add secretKey config as the canonical alias for the deprecated personalApiKey (accepts a Personal API Key or Project Secret API Key). (2026-07-07)
Commits
  • 954919d chore: update versions and lockfile [version bump]
  • 0b49a4c feat(node): add secretKey config, deprecate personalApiKey (#4060)
  • 859fd44 chore(deps): weekly safe npm updates · 10 packages (#4087)
  • See full diff in compare view

Updates `fumadocs-core` from 16.10.7 to 16.11.1
Commits
  • ab09f50 Version Packages (#3405)
  • 889a296 docs: update stale content
  • a5c081d fix: UI inconsistencies
  • 9a26903 Version Packages
  • 3e33f43 perf(satteri): reduce clones
  • 3597c9d perf(satteri): persist results
  • 0f389cf feat(satteri): decouple imports/exports from compile()
  • 4611f97 feat(satteri): full rehype-toc functionality
  • d095300 fix(satteri): workaround common issues
  • 0297e25 configure pretrust
  • Additional commits viewable in compare view

Updates `fumadocs-mdx` from 15.0.13 to 15.1.0
Release notes

Sourced from fumadocs-mdx's releases.

fumadocs-mdx@15.1.0

Default to Base UI

Internal packages & templates now use Base UI rather than Radix UI.

Commits
  • 9a26903 Version Packages
  • 3e33f43 perf(satteri): reduce clones
  • 3597c9d perf(satteri): persist results
  • 0f389cf feat(satteri): decouple imports/exports from compile()
  • 4611f97 feat(satteri): full rehype-toc functionality
  • d095300 fix(satteri): workaround common issues
  • 0297e25 configure pretrust
  • 02c242b chore(satteri): clean code
  • 3d80b8b fix(mdx): ensure satteri integration is optional
  • 0ec19af feat(satteri): more tests & move remark-include
  • Additional commits viewable in compare view

Updates `fumadocs-ui` from 16.10.7 to 16.11.1
Commits
  • ab09f50 Version Packages (#3405)
  • 889a296 docs: update stale content
  • a5c081d fix: UI inconsistencies
  • 9a26903 Version Packages
  • 3e33f43 perf(satteri): reduce clones
  • 3597c9d perf(satteri): persist results
  • 0f389cf feat(satteri): decouple imports/exports from compile()
  • 4611f97 feat(satteri): full rehype-toc functionality
  • d095300 fix(satteri): workaround common issues
  • 0297e25 configure pretrust
  • Additional commits viewable in compare view

Updates `typescript` from 6.0.3 to 7.0.2
Commits
Maintainer changes

This version was pushed to npm by microsoft1es, a new releaser for typescript since your current version.


Updates `@effect/atom-react` from 4.0.0-beta.93 to 4.0.0-beta.94
Release notes

Sourced from @​effect/atom-react's releases.

@​effect/atom-react@​4.0.0-beta.94

Patch Changes

Changelog

Sourced from @​effect/atom-react's changelog.

4.0.0-beta.94

Patch Changes

Commits

Updates `@effect/platform-bun` from 4.0.0-beta.93 to 4.0.0-beta.94
Changelog

Sourced from @​effect/platform-bun's changelog.

4.0.0-beta.94

Patch Changes

Commits

Updates `@effect/platform-node` from 4.0.0-beta.93 to 4.0.0-beta.94
Changelog

Sourced from @​effect/platform-node's changelog.

4.0.0-beta.94

Patch Changes

Commits

Updates `@effect/sql-pg` from 4.0.0-beta.93 to 4.0.0-beta.94
Commits

Updates `@effect/vitest` from 4.0.0-beta.93 to 4.0.0-beta.97
Changelog

Sourced from @​effect/vitest's changelog.

4.0.0-beta.97

Patch Changes

  • Updated dependencies []:
    • effect@4.0.0-beta.97

4.0.0-beta.96

Patch Changes

4.0.0-beta.95

Patch Changes

4.0.0-beta.94

Patch Changes

Commits

Updates `@typescript/native-preview` from 7.0.0-dev.20260703.1 to 7.0.0-dev.20260707.2
Commits

Updates `@vitest/coverage-istanbul` from 4.1.9 to 4.1.10
Release notes

Sourced from @​vitest/coverage-istanbul's releases.

v4.1.10

   🐞 Bug Fixes

    View changes on GitHub
Commits

Updates `effect` from 4.0.0-beta.93 to 4.0.0-beta.97
Changelog

Sourced from effect's changelog.

4.0.0-beta.97

4.0.0-beta.96

Patch Changes

4.0.0-beta.95

Patch Changes

  • #2542 a482442 Thanks @​IGassmann! - Add Schema.DateFromMillis and SchemaTransformation.dateFromMillis for decoding millisecond timestamps into Date values.

  • #2559 fbefa85 Thanks @​tim-smart! - fix activity retry policy

  • #2547 0b4a32f Thanks @​fubhy! - Allow cron fields like 5/15 to expand from the starting value through the field maximum.

  • #2557 18a49e1 Thanks @​fubhy! - Fix Schedule.cron when the test clock is adjusted to infinity.

  • #2560 266cb90 Thanks @​gcanti! - Treat empty strings as missing values in built-in ConfigProviders by default.

    ConfigProvider.fromEnv, ConfigProvider.fromDotEnvContents, ConfigProvider.fromDotEnv, ConfigProvider.fromUnknown, and ConfigProvider.fromDir now treat literal empty strings as absent values when loaded as values, allowing Config.withDefault and Config.option to recover. Container discovery still reflects the source structure. Pass preserveEmptyStrings: true to restore the previous behavior.

    ConfigProvider.fromDotEnv({ expandVariables: true }) now expands variables consistently with ConfigProvider.fromDotEnvContents.

  • #2554 912f095 Thanks @​tim-smart! - Add Schedule.upTo options for limiting schedules by duration and/or recurrence count.

  • #2556 a6718f9 Thanks @​fubhy! - Fix cron parsing and scheduling edge cases for whitespace, Sunday 7, strict numeric tokens, explicit full day ranges, and month-constrained day-of-month / weekday matching.

  • #2551 bef5154 Thanks @​tim-smart! - Remove the Schedule.both APIs and add Schedule.max for combining schedules by their slowest delay.

  • #2553 18e0564 Thanks @​tim-smart! - Remove some Schedule APIs: collectInputs, collectOutputs, collectWhile, delays, reduce, satisfiesErrorType, satisfiesInputType, satisfiesOutputType, satisfiesServicesType, and unfold.

  • #2558 fb50f14 Thanks @​tim-smart! - Remove the Schedule.either APIs and add Schedule.min for fastest-duration schedule composition.

4.0.0-beta.94

Patch Changes

... (truncated)

Commits

Updates `oxfmt` from 0.57.0 to 0.58.0
Commits

Updates `tldts` from 6.1.86 to 7.4.8
Release notes

Sourced from tldts's releases.

v7.4.8

:scroll: Update Public Suffix List

  • tldts-experimental, tldts

Authors: 1

v7.4.7

:scroll: Update Public Suffix List

  • tldts-experimental, tldts

Authors: 1

v7.4.6

:scroll: Update Public Suffix List

  • tldts-experimental, tldts

:nut_and_bolt: Dependencies

Authors: 2

v7.4.5

:scroll: Update Public Suffix List

  • tldts-experimental, tldts

... (truncated)

Changelog

Sourced from tldts's changelog.

v7.4.8 (Thu Jul 09 2026)

:scroll: Update Public Suffix List

  • tldts-experimental, tldts

Authors: 1


v7.4.7 (Tue Jul 07 2026)

:scroll: Update Public Suffix List

  • tldts-experimental, tldts

Authors: 1


v7.4.6 (Thu Jul 02 2026)

:scroll: Update Public Suffix List

  • tldts-experimental, tldts

:nut_and_bolt: Dependencies

Authors: 2

... (truncated)

Commits
Maintainer changes

This version was pushed to npm by GitHub Actions, a new releaser for tldts since your current version.


Updates `vitest` from 4.1.9 to 4.1.10
Release notes

Sourced from = - Schedule.exponential("3 seconds", 1.5).pipe( - Schedule.modifyDelay((_, delay) => Effect.succeed(Duration.min(delay, MAX_INTERVAL))), - Schedule.modifyDelay((_, delay) => - Random.next.pipe( - Effect.map((random) => Duration.millis(Duration.toMillis(delay) * (0.5 + random))), - ), +export const legacyBootstrapBackoff = Schedule.exponential("3 seconds", 1.5).pipe( + Schedule.modifyDelay(({ duration }) => Effect.succeed(Duration.min(duration, MAX_INTERVAL))), + Schedule.modifyDelay(({ duration }) => + Random.next.pipe( + Effect.map((random) => Duration.millis(Duration.toMillis(duration) * (0.5 + random))), ), - Schedule.both(Schedule.during("15 minutes")), - ); + ), + Schedule.upTo({ duration: "15 minutes" }), +); /** * Reproduces Go's `utils.NewErrorCallback` (`internal/utils/retry.go:19-35`): after diff --git a/apps/cli/src/legacy/shared/legacy-storage-url.ts b/apps/cli/src/legacy/shared/legacy-storage-url.ts index 0810c61872..9a4cce090f 100644 --- a/apps/cli/src/legacy/shared/legacy-storage-url.ts +++ b/apps/cli/src/legacy/shared/legacy-storage-url.ts @@ -129,7 +129,7 @@ function unescapePath(s: string): string { pending = []; } }; - for (let i = 0; i < s.length; ) { + for (let i = 0; i < s.length;) { if (s.charCodeAt(i) === 0x25 /* % */) { const h1 = i + 1 < s.length ? s.charCodeAt(i + 1) : -1; const h2 = i + 2 < s.length ? s.charCodeAt(i + 2) : -1; diff --git a/apps/cli/src/next/docs/markdown-formatter.ts b/apps/cli/src/next/docs/markdown-formatter.ts index 0897915e77..62e6eac47d 100644 --- a/apps/cli/src/next/docs/markdown-formatter.ts +++ b/apps/cli/src/next/docs/markdown-formatter.ts @@ -2,7 +2,11 @@ import { Option } from "effect"; import type { HelpDoc } from "effect/unstable/cli"; function escapeMdxText(value: string): string { - return value.replace(//g, ">"); + return value + .replace(//g, ">") + .replace(/\{/g, "{") + .replace(/\}/g, "}"); } function formatTable(headers: string[], rows: string[][]): string { diff --git a/apps/cli/src/next/docs/markdown-formatter.unit.test.ts b/apps/cli/src/next/docs/markdown-formatter.unit.test.ts index 7f1a166d93..f6e50610d5 100644 --- a/apps/cli/src/next/docs/markdown-formatter.unit.test.ts +++ b/apps/cli/src/next/docs/markdown-formatter.unit.test.ts @@ -69,6 +69,12 @@ describe("formatHelpDocAsMarkdown", () => { const usageIndex = result.indexOf("## Usage"); expect(descIndex).toBeLessThan(usageIndex); }); + + it("escapes MDX expression braces in prose", () => { + const doc = makeDoc({ description: "Request /v1/projects/{ref}." }); + const result = formatHelpDocAsMarkdown(doc); + expect(result).toContain("Request /v1/projects/{ref}."); + }); }); describe("flags section", () => { diff --git a/apps/docs/next-env.d.ts b/apps/docs/next-env.d.ts index 9edff1c7ca..ce4e94a6b1 100644 --- a/apps/docs/next-env.d.ts +++ b/apps/docs/next-env.d.ts @@ -1,6 +1,7 @@ /// /// import "./.next/types/routes.d.ts"; +import "./.next/types/root-params.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/docs/next.config.ts b/apps/docs/next.config.ts index 8b4e5917ac..f4068ca072 100644 --- a/apps/docs/next.config.ts +++ b/apps/docs/next.config.ts @@ -2,6 +2,9 @@ import { createMDX } from "fumadocs-mdx/next"; import type { NextConfig } from "next"; const config: NextConfig = { + experimental: { + useTypeScriptCli: true, + }, reactStrictMode: true, }; diff --git a/apps/docs/package.json b/apps/docs/package.json index ff5e585113..6d71e288bb 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -8,10 +8,10 @@ "build": "bun run generate && next build" }, "dependencies": { - "fumadocs-core": "^16.10.7", - "fumadocs-mdx": "^15.0.13", - "fumadocs-ui": "^16.10.7", - "next": "^16.2.10", + "fumadocs-core": "^16.11.1", + "fumadocs-mdx": "^15.1.0", + "fumadocs-ui": "^16.11.1", + "next": "16.3.0-preview.6", "react": "^19.2.7", "react-dom": "^19.2.7" }, @@ -20,7 +20,7 @@ "@types/node": "^26.1.0", "@types/react": "^19.2.17", "@types/react-dom": "^19.1.6", - "typescript": "^6.0.3" + "typescript": "^7.0.2" }, "nx": { "implicitDependencies": [ diff --git a/docs/nx-inference-plugins.md b/docs/nx-inference-plugins.md index d6fafd719d..c408031d9a 100644 --- a/docs/nx-inference-plugins.md +++ b/docs/nx-inference-plugins.md @@ -164,4 +164,6 @@ export const createNodesV2: CreateNodesV2 = [ ## How TypeScript plugins are loaded -Nx 22 loads `.ts` plugin files by registering `@swc-node/register` as a CommonJS transpiler before calling `require()` on the plugin path. This workspace has `@swc-node/register` and `@swc/core` installed at the root, along with a minimal `tsconfig.json` at the workspace root — both are required for Nx to find and activate the transpiler. Without either, Nx falls back to Node.js's native TypeScript type-stripping, which returns a non-extensible ES module namespace that Nx cannot annotate. +Nx loads `.ts` plugin files by registering `@swc-node/register` as a CommonJS transpiler before calling `require()` on the plugin path. This workspace has `@swc-node/register` and `@swc/core` installed at the root, along with a minimal `tsconfig.json` at the workspace root — both are required for Nx to find and activate the transpiler. Without either, Nx falls back to Node.js's native TypeScript type-stripping, which returns a non-extensible ES module namespace that Nx cannot annotate. + +TypeScript 7 does not yet expose the programmatic compiler API that the Nx transpiler uses. Following the [Nx TypeScript 7 guide](https://nx.dev/docs/technologies/typescript/guides/typescript-7), the root package aliases `typescript` to `@typescript/typescript6` for API consumers and installs TypeScript 7 as `@typescript/native`, which provides the `tsc` executable. diff --git a/package.json b/package.json index d835f5b2c4..4ac9174815 100644 --- a/package.json +++ b/package.json @@ -22,8 +22,10 @@ "devDependencies": { "@swc-node/register": "catalog:", "@swc/core": "catalog:", + "@typescript/native": "npm:typescript@^7.0.2", "nx": "catalog:", "pkg-pr-new": "0.0.75", + "typescript": "npm:@typescript/typescript6@^6.0.2", "verdaccio": "^6.7.4" } } diff --git a/packages/api/package.json b/packages/api/package.json index 32bf5d93ce..8e60e45359 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -23,7 +23,7 @@ "@effect/platform-bun": "catalog:", "@effect/platform-node": "catalog:", "effect": "catalog:", - "undici": "^8.6.0" + "undici": "^8.7.0" }, "devDependencies": { "@tsconfig/bun": "catalog:", diff --git a/packages/stack/package.json b/packages/stack/package.json index 3b6b7c3e9e..632976a7e2 100644 --- a/packages/stack/package.json +++ b/packages/stack/package.json @@ -28,7 +28,7 @@ }, "devDependencies": { "@effect/vitest": "catalog:", - "@supabase/supabase-js": "^2.110.0", + "@supabase/supabase-js": "^2.110.2", "@tsconfig/bun": "catalog:", "@types/bun": "catalog:", "@typescript/native-preview": "catalog:", diff --git a/packages/stack/src/ApiProxy.ts b/packages/stack/src/ApiProxy.ts index 12f258235e..116de838f2 100644 --- a/packages/stack/src/ApiProxy.ts +++ b/packages/stack/src/ApiProxy.ts @@ -110,7 +110,7 @@ function addCorsHeaders( // does so. Its `/_internal/health` probe answers immediately, so "Healthy" // status does not mean a function is servable yet. Briefly retry transport // failures on that route so a user's first call doesn't surface as a 502. -const COLD_START_RETRY_SCHEDULE = Schedule.spaced("250 millis").pipe(Schedule.take(8)); +const COLD_START_RETRY_SCHEDULE = Schedule.spaced("250 millis").pipe(Schedule.upTo({ times: 8 })); interface ProxyHandlerOptions { readonly backendPort: number; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fb2200064f..500f87ad3b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,26 +7,26 @@ settings: catalogs: default: '@effect/atom-react': - specifier: 4.0.0-beta.93 - version: 4.0.0-beta.93 + specifier: 4.0.0-beta.94 + version: 4.0.0-beta.94 '@effect/platform-bun': - specifier: 4.0.0-beta.93 - version: 4.0.0-beta.93 + specifier: 4.0.0-beta.97 + version: 4.0.0-beta.97 '@effect/platform-node': - specifier: 4.0.0-beta.93 - version: 4.0.0-beta.93 + specifier: 4.0.0-beta.97 + version: 4.0.0-beta.97 '@effect/sql-pg': - specifier: 4.0.0-beta.93 - version: 4.0.0-beta.93 + specifier: 4.0.0-beta.97 + version: 4.0.0-beta.97 '@effect/vitest': - specifier: ^4.0.0-beta.93 - version: 4.0.0-beta.93 + specifier: 4.0.0-beta.97 + version: 4.0.0-beta.97 '@nx/devkit': specifier: ^23.0.0 version: 23.0.1 '@swc-node/register': specifier: ^1.10.9 - version: 1.11.1 + version: 1.12.0 '@swc/core': specifier: ^1.15.43 version: 1.15.43 @@ -37,14 +37,14 @@ catalogs: specifier: ^1.3.14 version: 1.3.14 '@typescript/native-preview': - specifier: 7.0.0-dev.20260703.1 - version: 7.0.0-dev.20260703.1 + specifier: 7.0.0-dev.20260707.2 + version: 7.0.0-dev.20260707.2 '@vitest/coverage-istanbul': - specifier: ^4.1.9 - version: 4.1.9 + specifier: ^4.1.10 + version: 4.1.10 effect: - specifier: 4.0.0-beta.93 - version: 4.0.0-beta.93 + specifier: 4.0.0-beta.97 + version: 4.0.0-beta.97 knip: specifier: ^6.24.0 version: 6.25.0 @@ -52,8 +52,8 @@ catalogs: specifier: ^23.0.0 version: 23.0.1 oxfmt: - specifier: ^0.57.0 - version: 0.57.0 + specifier: ^0.58.0 + version: 0.58.0 oxlint: specifier: ^1.72.0 version: 1.73.0 @@ -61,11 +61,14 @@ catalogs: specifier: ^0.24.0 version: 0.24.0 tldts: - specifier: ^7.4.6 + specifier: ^7.4.8 version: 7.4.8 vitest: - specifier: ^4.1.9 - version: 4.1.9 + specifier: ^4.1.10 + version: 4.1.10 + +overrides: + '@effect/platform-node-shared': 4.0.0-beta.97 importers: @@ -73,16 +76,22 @@ importers: devDependencies: '@swc-node/register': specifier: 'catalog:' - version: 1.11.1(@swc/core@1.15.43)(@swc/types@0.1.27)(typescript@6.0.3) + version: 1.12.0(@swc/core@1.15.43)(@swc/types@0.1.27)(@typescript/typescript6@6.0.2) '@swc/core': specifier: 'catalog:' version: 1.15.43 + '@typescript/native': + specifier: npm:typescript@^7.0.2 + version: typescript@7.0.2 nx: specifier: 'catalog:' - version: 23.0.1(@swc-node/register@1.11.1(@swc/core@1.15.43)(@swc/types@0.1.27)(typescript@6.0.3))(@swc/core@1.15.43) + version: 23.0.1(@swc-node/register@1.12.0(@swc/core@1.15.43)(@swc/types@0.1.27)(@typescript/typescript6@6.0.2))(@swc/core@1.15.43) pkg-pr-new: specifier: 0.0.75 version: 0.0.75 + typescript: + specifier: npm:@typescript/typescript6@^6.0.2 + version: '@typescript/typescript6@6.0.2' verdaccio: specifier: ^6.7.4 version: 6.7.4(typanion@3.14.0) @@ -97,8 +106,8 @@ importers: version: 6.2.3 devDependencies: '@anthropic-ai/claude-agent-sdk': - specifier: ^0.3.201 - version: 0.3.201(@anthropic-ai/sdk@0.110.0(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(zod@4.4.3) + specifier: ^0.3.206 + version: 0.3.206(@anthropic-ai/sdk@0.110.0(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(zod@4.4.3) '@anthropic-ai/sdk': specifier: ^0.110.0 version: 0.110.0(zod@4.4.3) @@ -107,16 +116,16 @@ importers: version: 1.7.0 '@effect/atom-react': specifier: 'catalog:' - version: 4.0.0-beta.93(effect@4.0.0-beta.93)(react@19.2.7)(scheduler@0.27.0) + version: 4.0.0-beta.94(effect@4.0.0-beta.97)(react@19.2.7)(scheduler@0.27.0) '@effect/platform-bun': specifier: 'catalog:' - version: 4.0.0-beta.93(effect@4.0.0-beta.93) + version: 4.0.0-beta.97(effect@4.0.0-beta.97) '@effect/sql-pg': specifier: 'catalog:' - version: 4.0.0-beta.93(effect@4.0.0-beta.93) + version: 4.0.0-beta.97(effect@4.0.0-beta.97) '@effect/vitest': specifier: 'catalog:' - version: 4.0.0-beta.93(effect@4.0.0-beta.93)(vitest@4.1.9) + version: 4.0.0-beta.97(effect@4.0.0-beta.97)(vitest@4.1.10) '@modelcontextprotocol/sdk': specifier: ^1.29.0 version: 1.29.0(zod@4.4.3) @@ -155,19 +164,19 @@ importers: version: 19.2.17 '@typescript/native-preview': specifier: 'catalog:' - version: 7.0.0-dev.20260703.1 + version: 7.0.0-dev.20260707.2 '@vercel/detect-agent': specifier: ^1.2.3 version: 1.2.3 '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.9(vitest@4.1.9) + version: 4.1.10(vitest@4.1.10) dotenv: specifier: ^17.4.2 version: 17.4.2 effect: specifier: 'catalog:' - version: 4.0.0-beta.93 + version: 4.0.0-beta.97 esbuild: specifier: ^0.28.1 version: 0.28.1 @@ -182,7 +191,7 @@ importers: version: 6.25.0 oxfmt: specifier: 'catalog:' - version: 0.57.0 + version: 0.58.0 oxlint: specifier: 'catalog:' version: 1.73.0(oxlint-tsgolint@0.24.0) @@ -196,8 +205,8 @@ importers: specifier: ^7.0.0 version: 7.0.0 posthog-node: - specifier: ^5.39.4 - version: 5.39.4 + specifier: ^5.40.0 + version: 5.40.0 react: specifier: ^19.2.7 version: 19.2.7 @@ -206,7 +215,7 @@ importers: version: 7.0.1 semantic-release: specifier: ^25.0.5 - version: 25.0.5(typescript@6.0.3) + version: 25.0.5(typescript@7.0.2) smol-toml: specifier: ^1.7.0 version: 1.7.0 @@ -215,7 +224,7 @@ importers: version: 7.4.8 vitest: specifier: 'catalog:' - version: 4.1.9(@types/node@26.1.1)(@vitest/coverage-istanbul@4.1.9)(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) + version: 4.1.10(@types/node@26.1.1)(@vitest/coverage-istanbul@4.1.10)(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) yaml: specifier: ^2.9.0 version: 2.9.0 @@ -259,16 +268,16 @@ importers: version: 1.3.14 '@typescript/native-preview': specifier: 'catalog:' - version: 7.0.0-dev.20260703.1 + version: 7.0.0-dev.20260707.2 '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.9(vitest@4.1.9) + version: 4.1.10(vitest@4.1.10) knip: specifier: 'catalog:' version: 6.25.0 oxfmt: specifier: 'catalog:' - version: 0.57.0 + version: 0.58.0 oxlint: specifier: 'catalog:' version: 1.73.0(oxlint-tsgolint@0.24.0) @@ -277,22 +286,22 @@ importers: version: 0.24.0 vitest: specifier: 'catalog:' - version: 4.1.9(@types/node@26.1.1)(@vitest/coverage-istanbul@4.1.9)(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) + version: 4.1.10(@types/node@26.1.1)(@vitest/coverage-istanbul@4.1.10)(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) apps/docs: dependencies: fumadocs-core: - specifier: ^16.10.7 - version: 16.10.7(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.23.0(react@19.2.7))(next@16.2.10(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3) + specifier: ^16.11.1 + version: 16.11.1(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.24.0(react@19.2.7))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3) fumadocs-mdx: - specifier: ^15.0.13 - version: 15.0.13(@types/mdast@4.0.4)(@types/mdx@2.0.14)(@types/react@19.2.17)(fumadocs-core@16.10.7(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.23.0(react@19.2.7))(next@16.2.10(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3))(next@16.2.10(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(rolldown@1.0.2)(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) + specifier: ^15.1.0 + version: 15.1.0(@types/mdast@4.0.4)(@types/mdx@2.0.14)(@types/react@19.2.17)(fumadocs-core@16.11.1(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.24.0(react@19.2.7))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(rolldown@1.0.2)(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) fumadocs-ui: - specifier: ^16.10.7 - version: 16.10.7(@types/mdx@2.0.14)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(fumadocs-core@16.10.7(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.23.0(react@19.2.7))(next@16.2.10(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3))(next@16.2.10(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + specifier: ^16.11.1 + version: 16.11.1(@types/mdx@2.0.14)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(fumadocs-core@16.11.1(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.24.0(react@19.2.7))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) next: - specifier: ^16.2.10 - version: 16.2.10(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + specifier: 16.3.0-preview.6 + version: 16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react: specifier: ^19.2.7 version: 19.2.7 @@ -313,23 +322,23 @@ importers: specifier: ^19.1.6 version: 19.2.3(@types/react@19.2.17) typescript: - specifier: ^6.0.3 - version: 6.0.3 + specifier: ^7.0.2 + version: 7.0.2 packages/api: dependencies: '@effect/platform-bun': specifier: 'catalog:' - version: 4.0.0-beta.93(effect@4.0.0-beta.93) + version: 4.0.0-beta.97(effect@4.0.0-beta.97) '@effect/platform-node': specifier: 'catalog:' - version: 4.0.0-beta.93(effect@4.0.0-beta.93)(ioredis@5.11.0) + version: 4.0.0-beta.97(effect@4.0.0-beta.97)(ioredis@5.11.0) effect: specifier: 'catalog:' - version: 4.0.0-beta.93 + version: 4.0.0-beta.97 undici: - specifier: ^8.6.0 - version: 8.6.0 + specifier: ^8.7.0 + version: 8.7.0 devDependencies: '@tsconfig/bun': specifier: 'catalog:' @@ -339,16 +348,16 @@ importers: version: 1.3.14 '@typescript/native-preview': specifier: 'catalog:' - version: 7.0.0-dev.20260703.1 + version: 7.0.0-dev.20260707.2 '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.9(vitest@4.1.9) + version: 4.1.10(vitest@4.1.10) knip: specifier: 'catalog:' version: 6.25.0 oxfmt: specifier: 'catalog:' - version: 0.57.0 + version: 0.58.0 oxlint: specifier: 'catalog:' version: 1.73.0(oxlint-tsgolint@0.24.0) @@ -357,7 +366,7 @@ importers: version: 0.24.0 vitest: specifier: 'catalog:' - version: 4.1.9(@types/node@26.1.1)(@vitest/coverage-istanbul@4.1.9)(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) + version: 4.1.10(@types/node@26.1.1)(@vitest/coverage-istanbul@4.1.10)(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) packages/cli-darwin-arm64: {} @@ -381,16 +390,16 @@ importers: version: 1.3.14 '@typescript/native-preview': specifier: 'catalog:' - version: 7.0.0-dev.20260703.1 + version: 7.0.0-dev.20260707.2 '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.9(vitest@4.1.9) + version: 4.1.10(vitest@4.1.10) knip: specifier: 'catalog:' version: 6.25.0 oxfmt: specifier: 'catalog:' - version: 0.57.0 + version: 0.58.0 oxlint: specifier: 'catalog:' version: 1.73.0(oxlint-tsgolint@0.24.0) @@ -399,7 +408,7 @@ importers: version: 0.24.0 vitest: specifier: 'catalog:' - version: 4.1.9(@types/node@26.1.1)(@vitest/coverage-istanbul@4.1.9)(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) + version: 4.1.10(@types/node@26.1.1)(@vitest/coverage-istanbul@4.1.10)(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) packages/cli-windows-arm64: {} @@ -409,16 +418,16 @@ importers: dependencies: '@effect/platform-bun': specifier: 'catalog:' - version: 4.0.0-beta.93(effect@4.0.0-beta.93) + version: 4.0.0-beta.97(effect@4.0.0-beta.97) '@effect/platform-node': specifier: 'catalog:' - version: 4.0.0-beta.93(effect@4.0.0-beta.93)(ioredis@5.11.0) + version: 4.0.0-beta.97(effect@4.0.0-beta.97)(ioredis@5.11.0) dedent: specifier: ^1.7.2 version: 1.7.2 effect: specifier: 'catalog:' - version: 4.0.0-beta.93 + version: 4.0.0-beta.97 smol-toml: specifier: ^1.7.0 version: 1.7.0 @@ -431,16 +440,16 @@ importers: version: 1.3.14 '@typescript/native-preview': specifier: 'catalog:' - version: 7.0.0-dev.20260703.1 + version: 7.0.0-dev.20260707.2 '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.9(vitest@4.1.9) + version: 4.1.10(vitest@4.1.10) knip: specifier: 'catalog:' version: 6.25.0 oxfmt: specifier: 'catalog:' - version: 0.57.0 + version: 0.58.0 oxlint: specifier: 'catalog:' version: 1.73.0(oxlint-tsgolint@0.24.0) @@ -449,20 +458,20 @@ importers: version: 0.24.0 vitest: specifier: 'catalog:' - version: 4.1.9(@types/node@26.1.1)(@vitest/coverage-istanbul@4.1.9)(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) + version: 4.1.10(@types/node@26.1.1)(@vitest/coverage-istanbul@4.1.10)(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) packages/process-compose: dependencies: '@effect/platform-bun': specifier: 'catalog:' - version: 4.0.0-beta.93(effect@4.0.0-beta.93) + version: 4.0.0-beta.97(effect@4.0.0-beta.97) effect: specifier: 'catalog:' - version: 4.0.0-beta.93 + version: 4.0.0-beta.97 devDependencies: '@effect/vitest': specifier: 'catalog:' - version: 4.0.0-beta.93(effect@4.0.0-beta.93)(vitest@4.1.9) + version: 4.0.0-beta.97(effect@4.0.0-beta.97)(vitest@4.1.10) '@tsconfig/bun': specifier: 'catalog:' version: 1.0.10 @@ -471,16 +480,16 @@ importers: version: 1.3.14 '@typescript/native-preview': specifier: 'catalog:' - version: 7.0.0-dev.20260703.1 + version: 7.0.0-dev.20260707.2 '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.9(vitest@4.1.9) + version: 4.1.10(vitest@4.1.10) knip: specifier: 'catalog:' version: 6.25.0 oxfmt: specifier: 'catalog:' - version: 0.57.0 + version: 0.58.0 oxlint: specifier: 'catalog:' version: 1.73.0(oxlint-tsgolint@0.24.0) @@ -489,16 +498,16 @@ importers: version: 0.24.0 vitest: specifier: 'catalog:' - version: 4.1.9(@types/node@26.1.1)(@vitest/coverage-istanbul@4.1.9)(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) + version: 4.1.10(@types/node@26.1.1)(@vitest/coverage-istanbul@4.1.10)(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) packages/stack: dependencies: '@effect/platform-bun': specifier: 'catalog:' - version: 4.0.0-beta.93(effect@4.0.0-beta.93) + version: 4.0.0-beta.97(effect@4.0.0-beta.97) '@effect/platform-node': specifier: 'catalog:' - version: 4.0.0-beta.93(effect@4.0.0-beta.93)(ioredis@5.11.0) + version: 4.0.0-beta.97(effect@4.0.0-beta.97)(ioredis@5.11.0) '@supabase/config': specifier: workspace:* version: link:../config @@ -507,14 +516,14 @@ importers: version: link:../process-compose effect: specifier: 'catalog:' - version: 4.0.0-beta.93 + version: 4.0.0-beta.97 devDependencies: '@effect/vitest': specifier: 'catalog:' - version: 4.0.0-beta.93(effect@4.0.0-beta.93)(vitest@4.1.9) + version: 4.0.0-beta.97(effect@4.0.0-beta.97)(vitest@4.1.10) '@supabase/supabase-js': - specifier: ^2.110.0 - version: 2.110.0 + specifier: ^2.110.2 + version: 2.110.2 '@tsconfig/bun': specifier: 'catalog:' version: 1.0.10 @@ -523,16 +532,16 @@ importers: version: 1.3.14 '@typescript/native-preview': specifier: 'catalog:' - version: 7.0.0-dev.20260703.1 + version: 7.0.0-dev.20260707.2 '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.9(vitest@4.1.9) + version: 4.1.10(vitest@4.1.10) knip: specifier: 'catalog:' version: 6.25.0 oxfmt: specifier: 'catalog:' - version: 0.57.0 + version: 0.58.0 oxlint: specifier: 'catalog:' version: 1.73.0(oxlint-tsgolint@0.24.0) @@ -541,16 +550,19 @@ importers: version: 0.24.0 vitest: specifier: 'catalog:' - version: 4.1.9(@types/node@26.1.1)(@vitest/coverage-istanbul@4.1.9)(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) + version: 4.1.10(@types/node@26.1.1)(@vitest/coverage-istanbul@4.1.10)(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) tools/nx-plugins: dependencies: '@nx/devkit': specifier: 'catalog:' - version: 23.0.1(nx@23.0.2(@swc-node/register@1.11.1(@swc/core@1.15.43)(@swc/types@0.1.27)(typescript@6.0.3))(@swc/core@1.15.43)) + version: 23.0.1(nx@23.1.0(@swc-node/register@1.12.0(@swc/core@1.15.43)(@swc/types@0.1.27)(@typescript/typescript6@6.0.2))(@swc/core@1.15.43)) + typescript: + specifier: npm:@typescript/typescript6@^6.0.2 + version: '@typescript/typescript6@6.0.2' vitest: specifier: 'catalog:' - version: 4.1.9(@types/node@26.1.1)(@vitest/coverage-istanbul@4.1.9)(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) + version: 4.1.10(@types/node@26.1.1)(@vitest/coverage-istanbul@4.1.10)(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) packages: @@ -570,52 +582,52 @@ packages: resolution: {integrity: sha512-p+CMKJ93HFmLkjXKlXiVGlMQEuRb6H0MokBSwUsX+S6BRX8eV5naFZpQJFfJHjRZY0Hmnqy1/r6UWl3x+19zYA==} engines: {node: '>=18'} - '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.201': - resolution: {integrity: sha512-8Mcb3BDyKUGfJWFFTWwt+at37lbDH3ZwVtUNPWGG1toZ75RDCJry5U4kXRvQ2xokvJQlA0E+eNp6keWe5ZH22Q==} + '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.206': + resolution: {integrity: sha512-FL2+NKcMMN47vcnCW2Fkt3AOgeRRlQxrisbPNaxrxqPJFzhUKs17x5j0XzLefd0xRbDAr74hd0PK/tnp6PHM6w==} cpu: [arm64] os: [darwin] - '@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.201': - resolution: {integrity: sha512-TFR2bu0+ml3RHoMrtsgD0qDK5Oknw8kYGBV7qpQHn+IWmE96gnHhogG1LpJwpHtni08XkJIjfWk1DdlsUYtRkQ==} + '@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.206': + resolution: {integrity: sha512-mRW8PPMfQN15EunLwpdmcVzk3XuM4DXQUM8DOzaeA1Hr1yxYPaVVBLr9hkdBKOqr8XTl3ueoTR6RZAy6a/n7OA==} cpu: [x64] os: [darwin] - '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.201': - resolution: {integrity: sha512-EiqbpfJIpChfkn+8Uj061Qjyw0eaRcOXtdrvVuHANyj8ZErVOr8HlH6op9PSeIUa9TX0m2+tNgKPQvOGseQckA==} + '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.206': + resolution: {integrity: sha512-aMZe1Kl+kYv5QlA15W9Ae25MAzBsA9FA40f5TOtJebA+M/xliF0r2LLb2NdyBviiZCDllcE31l0zgYE0rQqFQw==} cpu: [arm64] os: [linux] libc: [musl] - '@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.201': - resolution: {integrity: sha512-mShTo3MwF0gkN4dDw78wWJiB6aBDVRkl81cnApvoBofpdyUBYgm9Gw16CCjDTgelMKeBFqN6ErJpwjI3wbP00A==} + '@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.206': + resolution: {integrity: sha512-Id6H8l6EsGb7849EAZDOB4Ic+FQpbzt4D5HGOwp59CTW3o/1c4etjQA/Kl1K+DSEWn3FGo6D5UMVgc/rwhBj8g==} cpu: [arm64] os: [linux] libc: [glibc] - '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.201': - resolution: {integrity: sha512-IbxnzO5UCbqbm2TnzCHkSyJorAFw2isdKdIsFCTxJJjSs3ZC+v3LC1QSUiVCx0qi+CV6w3MKx6mLI11mrvhbbQ==} + '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.206': + resolution: {integrity: sha512-xQBOBhlcmTNc7YeYT5qLZOikpSq3WHlsJ/t6i7kJqUWrcXlOPaAoSMdKp5xO/V0CjAdzdJoWB88I55UAh8mclQ==} cpu: [x64] os: [linux] libc: [musl] - '@anthropic-ai/claude-agent-sdk-linux-x64@0.3.201': - resolution: {integrity: sha512-jrJBrRWrSuoFKIgjyqxHqmfd6Pb3Bs5Bvakg0knXCTC4fbUXGnC9Q6u7gdDwgXohUNP6/DD+s8U7bivvvVv0dg==} + '@anthropic-ai/claude-agent-sdk-linux-x64@0.3.206': + resolution: {integrity: sha512-egZhOC1RlEVhZyq6Oa1b04AF7hh4hO+8oCsJCZ3gOifJQuHih1oW3lZ8fOUxU85jlT2ytAnt5kRp4uQr6PJgbg==} cpu: [x64] os: [linux] libc: [glibc] - '@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.201': - resolution: {integrity: sha512-UsoytRJ/037uHpb3ATrIoe+AgwTf+PwKuFLGjddHAV/11wERJs0hlrnSmcnp43kf0PFxoSNinngme96YYASmQg==} + '@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.206': + resolution: {integrity: sha512-oRQk23bFXSz4QRhOxqnnvlLWq/KiF2PtSBYXrMg1AQrCOHJd2k66aOAS7AJ4ZSzejiyV4N6sS6UThfmF6ipHBQ==} cpu: [arm64] os: [win32] - '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.201': - resolution: {integrity: sha512-PhalN/0cWcqDfbx7iwoLNR2gurjTiqhBk1G6K+NRScxEcQjWuu5xKXCcdbX8ePVpT+nbEMmFEFpn2y+8V8hIdA==} + '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.206': + resolution: {integrity: sha512-BdjKmDojZjc5RjN+8Q6K7Yqf1WYhel6I3DkMXc9zdxS/xIuN42sx7zgQpdMGY73pm7ROPLqo3LieOeMhVH161w==} cpu: [x64] os: [win32] - '@anthropic-ai/claude-agent-sdk@0.3.201': - resolution: {integrity: sha512-InT1XLmf2QpldWdtznKDWEoGJT4p+sXh24yxbeBQ++lMJCzMrI0W27MEmmmDWx0otpa+ubdHCF5YQ6oiNt7cmg==} + '@anthropic-ai/claude-agent-sdk@0.3.206': + resolution: {integrity: sha512-KljDh9Pg4YCYpoXS8dnWoVSsOHtU4yLCW268K2iOruSxFXE8/Tay6DPvmJzYuqjP5YLNYfj05ZGykwZSUn6GXA==} engines: {node: '>=18.0.0'} peerDependencies: '@anthropic-ai/sdk': '>=0.93.0' @@ -724,40 +736,40 @@ packages: peerDependencies: '@noble/ciphers': ^1.0.0 - '@effect/atom-react@4.0.0-beta.93': - resolution: {integrity: sha512-43FOYozhZT+VIQBH5LVAbabhdjGCuxGKdVTeUnkMp5NY+/g3nDLcAyAkNYn99G3+XDbqFvJaqmNJP7NLF5+WFg==} + '@effect/atom-react@4.0.0-beta.94': + resolution: {integrity: sha512-6A3Dhw8dC5f5ThuUH+JENgUZjrXdsiTTWWfgOr7gB4Rjj5JS4Ws5jzqbu+fpXlyBhwl3bEd+VmiyXNOOgtcslQ==} peerDependencies: - effect: ^4.0.0-beta.93 + effect: ^4.0.0-beta.94 react: ^19.2.4 scheduler: '*' - '@effect/platform-bun@4.0.0-beta.93': - resolution: {integrity: sha512-pny29d1NhJ7XLv19as7A0pIZr+QFi5KejnwB/lLW1c8P63t+PDMPEn21toy2vx2yzl9xRTo8heyiXPXRsqPN5w==} + '@effect/platform-bun@4.0.0-beta.97': + resolution: {integrity: sha512-WYjC7nKiWfNywIz1zeBEXnrpuHJM86DOi3lSZSSBeHCPz8HYw7IT2FL7u+aaJHHsJCyFiZVAgg2KFS8aMFSJBQ==} peerDependencies: - effect: ^4.0.0-beta.93 + effect: ^4.0.0-beta.97 - '@effect/platform-node-shared@4.0.0-beta.93': - resolution: {integrity: sha512-XUqZ2u5GglBqY8q2jj4Q7GjN5K/enedk8auZM9rY/l5a/myaQTrQp3QnvpIK4/Yg0WFjLGuctGPMKWRk3OLIrA==} + '@effect/platform-node-shared@4.0.0-beta.97': + resolution: {integrity: sha512-sAlygOlDLLERk7fC24f7Aa4G3wkEbGyyMEileHTHU2fThiPYSGMqNRK1LLnNr17m2+JR7BHbycwXkJkM18BAQw==} engines: {node: '>=18.0.0'} peerDependencies: - effect: ^4.0.0-beta.93 + effect: ^4.0.0-beta.97 - '@effect/platform-node@4.0.0-beta.93': - resolution: {integrity: sha512-QagsCGR0ZOXaCQqS5qGR2mcDng4LiP2bYhiiX1D6UC8cT9vsusVVOHiJWn8CupeDx+yVnPcu81QmA/SDt6GM1w==} + '@effect/platform-node@4.0.0-beta.97': + resolution: {integrity: sha512-Vd40VLh+Y08Bs37KWtbe9AgO2+t3RKZKGX9YC6ZKAcswu4tXR3CwSI7V2MN+GgS+ez/GLmqH1WrzDDlaIAxKPA==} engines: {node: '>=18.0.0'} peerDependencies: - effect: ^4.0.0-beta.93 + effect: ^4.0.0-beta.97 ioredis: ^5.7.0 - '@effect/sql-pg@4.0.0-beta.93': - resolution: {integrity: sha512-wpoGGezcmMVGG9p9vumFTR42+LPE2PaaUZ+vTcpo0hyc4TvChSRZiv4uZafrXEnDdYgdUFw7UQzrEO8+pa5crg==} + '@effect/sql-pg@4.0.0-beta.97': + resolution: {integrity: sha512-q0SZRN63Spm4lM72b1Aji0HZHEZgQxqp3mdYSVaskqXQxdZGyFxni+c19mzFNTYp+op8N+2PGoFzwqUdxFrxZA==} peerDependencies: - effect: ^4.0.0-beta.93 + effect: ^4.0.0-beta.97 - '@effect/vitest@4.0.0-beta.93': - resolution: {integrity: sha512-gMAnZ9PiMeJMDED9s0jWgCOhc2JccrTCxowhur/KriImsHnHIRj4VG/vK0xLw0Axe4AkTWzXNdRsFrYOjBTl3A==} + '@effect/vitest@4.0.0-beta.97': + resolution: {integrity: sha512-1dH6LBWSZyqnTV7ZO+yIpPGPf/xd7RtFfvQ4ZpTy9elzFN+wr1YBFpHSCr8+BfXOml6b8g9Mtj5eDy1qjbizUA==} peerDependencies: - effect: ^4.0.0-beta.93 + effect: ^4.0.0-beta.97 vitest: ^3.0.0 || ^4.0.0 '@emnapi/core@1.10.0': @@ -952,20 +964,20 @@ packages: cpu: [x64] os: [win32] - '@floating-ui/core@1.7.5': - resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + '@floating-ui/core@1.8.0': + resolution: {integrity: sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==} - '@floating-ui/dom@1.7.6': - resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + '@floating-ui/dom@1.8.0': + resolution: {integrity: sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==} - '@floating-ui/react-dom@2.1.8': - resolution: {integrity: sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==} + '@floating-ui/react-dom@2.1.9': + resolution: {integrity: sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==} peerDependencies: react: '>=16.8.0' react-dom: '>=16.8.0' - '@floating-ui/utils@0.2.11': - resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + '@floating-ui/utils@0.2.12': + resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==} '@fuma-translate/react@1.0.2': resolution: {integrity: sha512-uOiOtBx3nRXR8Nu1GzBf1tApgF1FErDBTHxRIAQeyQdyOoZbrNRN6H4kDCWObY4qyGeGbHydG0DHzgeUgFDMIw==} @@ -977,14 +989,11 @@ packages: '@types/react': optional: true - '@fumadocs/tailwind@0.0.5': - resolution: {integrity: sha512-ENKPWUDRmriccsrUDE4bDBq3FNr/ms3BP2rWlsAEMV1yP23pcCaan+ceGfeBUsAQjw7sj9Q3R4Kl3g/TCStPzQ==} + '@fumadocs/tailwind@0.1.0': + resolution: {integrity: sha512-nF/DCAwOR21HZ4AkjIOv3Iqwyqywzb6pdyeMcoa+aZzirXj5ntvNZbe3jJ0v3ehhtrRfYYeXBezvjn8ZmV+fuQ==} peerDependencies: - '@tailwindcss/oxide': ^4.0.0 tailwindcss: ^4.0.0 peerDependenciesMeta: - '@tailwindcss/oxide': - optional: true tailwindcss: optional: true @@ -1298,6 +1307,9 @@ packages: resolution: {integrity: sha512-WrOw/bcXm0f9qHkumlT1QlArXSTWqaY9sunsDpOk+yCCorCKMxvWT/a3xko4EYHVdeZoh00yI2TydXn6eyICDA==} engines: {node: '>= 10'} + '@napi-rs/wasm-runtime@0.2.12': + resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} + '@napi-rs/wasm-runtime@0.2.4': resolution: {integrity: sha512-9zESzOO5aDByvhIAsOy9TbpZ0Ur2AJbUI7UT73kcUTS2mxAMHOBaa1st/jAymNoCtvrit99kkzT1FZuXVcgfIQ==} @@ -1307,57 +1319,57 @@ packages: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 - '@next/env@16.2.10': - resolution: {integrity: sha512-zLPxg9M0MEHmygpj5OuxjQ+vHMiy/K7cSp74G8ecYolmgUWw0RwN02tF56npup/+qaI8JB97hQgS/r2Hb6QwVA==} + '@next/env@16.3.0-preview.6': + resolution: {integrity: sha512-tdsih48yzumsL060VIsZ9BwDYlZk0AAMT8HccspSbPUzp6ntCZ9xOMFU4W0SmqaOCEpPmTyP+5e6WT36mqN2fw==} - '@next/swc-darwin-arm64@16.2.10': - resolution: {integrity: sha512-v9IdJCa0H0mbo+8z5zwUpOk1Vj7RjkcI5uNYf5Ws1y6szf/p3Mzl9hLaST8SCt6L9h8NGnruZcd2+o0NTNwDhA==} + '@next/swc-darwin-arm64@16.3.0-preview.6': + resolution: {integrity: sha512-cQB2whnW1PD/Q+AxoI2X4LU3af1B+aFxWx+kmAiCCKDyjoO5Nh+9GyNu+nDAbP/WoloohbOWgO8m1U9j1EIZuw==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@16.2.10': - resolution: {integrity: sha512-17IS0jJRViROGmA9uGdNR8VPJpfbnaVG7E9qhso5jDLkmyd0lSDORWxbcKINzcFqzZqGwGtMSnrFRxBpuUYjLQ==} + '@next/swc-darwin-x64@16.3.0-preview.6': + resolution: {integrity: sha512-Ctrzjav21HlHwMV6afeC9/PePUdyJ+AXDQNfrrqdkIEdem9f3vsBVKtuB6gcCNT4umtql7ZLB1qaQObVqKVp5g==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@16.2.10': - resolution: {integrity: sha512-GRQRsRtuciNJvB54AvvuQTiq0oZtFwa1owQqtZD8wwnGpM2L39MV22kpI72YSXLKIyY40LC66EiLFv4PiicXxg==} + '@next/swc-linux-arm64-gnu@16.3.0-preview.6': + resolution: {integrity: sha512-V2o4V3ghCDwZ0K3BI2ts/+zDaLI1Y28Q/Bs53K3Zqk4/3M8yBTImXX7XL6BVOihncvSl8QUG3c32/W6AlgEsJA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [glibc] - '@next/swc-linux-arm64-musl@16.2.10': - resolution: {integrity: sha512-zkN9MQYS7UQBro+FnISUq1itaQjXI9xqISzuQ+2bc921NcJ1x4yPCqrn77tVN6/dOOXaaWVX3k6/bR07pPwK+A==} + '@next/swc-linux-arm64-musl@16.3.0-preview.6': + resolution: {integrity: sha512-btfR0TMj4RvscjYW7QvRQHstXas2969M4TBaQW98BFBLyuD8R0RFwmxYxep/aDu+xkKzfDvLsBFYcYX0ucsIHQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [musl] - '@next/swc-linux-x64-gnu@16.2.10': - resolution: {integrity: sha512-iCVJnwvrPYECvA6WM/7+oo+OiTvedIKLxtCLAZP4xZR3nXa1zmzZyLPbYCmWvpd4CvMYF1EMTafd0ii3DygLvA==} + '@next/swc-linux-x64-gnu@16.3.0-preview.6': + resolution: {integrity: sha512-arKlC6NTJ43NjhH+yVwEChLSWLWRtuUQPHYkuoKBK62hyL9rnMhNOXUWcQdvvhQ75UBqIDo7dpkfpOlizLClGw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [glibc] - '@next/swc-linux-x64-musl@16.2.10': - resolution: {integrity: sha512-ov2g4H0dHY9bPoOU83m91hWT7Iq5qy13bUnyyshLU3HGR1Ownn0X9QpmDPc5iIUaahTp7f7LeGAhV4DSFtackw==} + '@next/swc-linux-x64-musl@16.3.0-preview.6': + resolution: {integrity: sha512-ETrNCY3R1FJpOmh6I3pM+EbNA5kTTxQsCxVJTvKE9QN6+AxC6H4Ybu9F26WBseZT7zOFawlujWl8P6xo1hbgDQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [musl] - '@next/swc-win32-arm64-msvc@16.2.10': - resolution: {integrity: sha512-DwAnhLX76HQiFFQNgWlcK+JzlnD1rZ+UK/WY0ZMI/deXpvgnesjNYrqcfo1JzBuz4Kf7o3brIBL0glI1junatA==} + '@next/swc-win32-arm64-msvc@16.3.0-preview.6': + resolution: {integrity: sha512-6N92Tg9ImGX3anKiUu1EQYcFRXvrteL2+U/cQ863SMX9Huh8+/o3jIyCni15+NJD02Va7KQIAovURMd+EkaSOA==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@16.2.10': - resolution: {integrity: sha512-0JXq3b85Jk9Jg4ntLUbXSPvoDw3gpZou7twuKdoFG2jOw635v7+IiXfTaa0TxVMyx78pUjnrVYwLgjKfX4e6/A==} + '@next/swc-win32-x64-msvc@16.3.0-preview.6': + resolution: {integrity: sha512-DR6uqZI+ytCOg+F3MouQKjWwEI1I7+cAuunKtI0V3mEHdBGUmhh0t60+rrKhh+h24bvEfVr5UrH/aljAV9L0rw==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -1374,6 +1386,97 @@ packages: resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} engines: {node: ^14.21.3 || >=16} + '@node-rs/xxhash-android-arm-eabi@1.7.6': + resolution: {integrity: sha512-ptmfpFZ8SgTef58Us+0HsZ9BKhyX/gZYbhLkuzPt7qUoMqMSJK85NC7LEgzDgjUiG+S5GahEEQ9/tfh9BVvKhw==} + engines: {node: '>= 12'} + cpu: [arm] + os: [android] + + '@node-rs/xxhash-android-arm64@1.7.6': + resolution: {integrity: sha512-n4MyZvqifuoARfBvrZ2IBqmsGzwlVI3kb2mB0gVvoHtMsPbl/q94zoDBZ7WgeP3t4Wtli+QS3zgeTCOWUbqqUQ==} + engines: {node: '>= 12'} + cpu: [arm64] + os: [android] + + '@node-rs/xxhash-darwin-arm64@1.7.6': + resolution: {integrity: sha512-6xGuE07CiCIry/KT3IiwQd/kykTOmjKzO/ZnHlE5ibGMx64NFE0qDuwJbxQ4rGyUzgJ0KuN9ZdOhUDJmepnpcw==} + engines: {node: '>= 12'} + cpu: [arm64] + os: [darwin] + + '@node-rs/xxhash-darwin-x64@1.7.6': + resolution: {integrity: sha512-Z4oNnhyznDvHhxv+s0ka+5KG8mdfLVucZMZMejj9BL+CPmamClygPiHIRiifRcPAoX9uPZykaCsULngIfLeF3Q==} + engines: {node: '>= 12'} + cpu: [x64] + os: [darwin] + + '@node-rs/xxhash-freebsd-x64@1.7.6': + resolution: {integrity: sha512-arCDOf3xZ5NfBL5fk5J52sNPjXL2cVWN6nXNB3nrtRFFdPBLsr6YXtshAc6wMVxnIW4VGaEv/5K6IpTA8AFyWw==} + engines: {node: '>= 12'} + cpu: [x64] + os: [freebsd] + + '@node-rs/xxhash-linux-arm-gnueabihf@1.7.6': + resolution: {integrity: sha512-ndLLEW+MwLH3lFS0ahlHCcmkf2ykOv/pbP8OBBeAOlz/Xc3jKztg5IJ9HpkjKOkHk470yYxgHVaw1QMoMzU00A==} + engines: {node: '>= 12'} + cpu: [arm] + os: [linux] + + '@node-rs/xxhash-linux-arm64-gnu@1.7.6': + resolution: {integrity: sha512-VX7VkTG87mAdrF2vw4aroiRpFIIN8Lj6NgtGHF+IUVbzQxPudl4kG+FPEjsNH8y04yQxRbPE7naQNgHcTKMrNw==} + engines: {node: '>= 12'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@node-rs/xxhash-linux-arm64-musl@1.7.6': + resolution: {integrity: sha512-AB5m6crGYSllM9F/xZNOQSPImotR5lOa9e4arW99Bv82S+gcpphI8fGMDOVTTCXY/RLRhvvhwzLDxmLB2O8VDg==} + engines: {node: '>= 12'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@node-rs/xxhash-linux-x64-gnu@1.7.6': + resolution: {integrity: sha512-a2A6M+5tc0PVlJlE/nl0XsLEzMpKkwg7Y1lR5urFUbW9uVQnKjJYQDrUojhlXk0Uv3VnYQPa6ThmwlacZA5mvQ==} + engines: {node: '>= 12'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@node-rs/xxhash-linux-x64-musl@1.7.6': + resolution: {integrity: sha512-WioGJSC1GoxQpmdQrG5l/uddSBAS4XCWczHNwXe895J5xadGQzyvmr0r17BNfihvbBUDH1H9jwouNYzDDeA6+A==} + engines: {node: '>= 12'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@node-rs/xxhash-wasm32-wasi@1.7.6': + resolution: {integrity: sha512-WDXXKMMFMrez+esm2DzMPHFNPFYf+wQUtaXrXwtxXeQMFEzleOLwEaqV0+bbXGJTwhPouL3zY1Qo2xmIH4kkTg==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@node-rs/xxhash-win32-arm64-msvc@1.7.6': + resolution: {integrity: sha512-qjDFUZJT/Zq0yFS+0TApkD86p0NBdPXlOoHur9yNeO9YX2/9/b1sC2P7N27PgOu13h61TUOvTUC00e/82jAZRQ==} + engines: {node: '>= 12'} + cpu: [arm64] + os: [win32] + + '@node-rs/xxhash-win32-ia32-msvc@1.7.6': + resolution: {integrity: sha512-s7a+mQWOTnU4NiiypRq/vbNGot/il0HheXuy9oxJ0SW2q/e4BJ8j0pnP6UBlAjsk+005A76vOwsEj01qbQw8+A==} + engines: {node: '>= 12'} + cpu: [ia32] + os: [win32] + + '@node-rs/xxhash-win32-x64-msvc@1.7.6': + resolution: {integrity: sha512-zHOHm2UaIahRhgRPJll+4Xy4Z18aAT/7KNeQW+QJupGvFz+GzOFXMGs3R/3B1Ktob/F5ui3i1MrW9GEob3CWTg==} + engines: {node: '>= 12'} + cpu: [x64] + os: [win32] + + '@node-rs/xxhash@1.7.6': + resolution: {integrity: sha512-XMisO+aQHsVpxRp/85EszTtOQTOlhPbd149P/Xa9F55wafA6UM3h2UhOgOs7aAzItnHU/Aw1WQ1FVTEg7WB43Q==} + engines: {node: '>= 12'} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1396,8 +1499,8 @@ packages: cpu: [arm64] os: [darwin] - '@nx/nx-darwin-arm64@23.0.2': - resolution: {integrity: sha512-9sqhZMVFpF+qM7hq6y2xA4gVK+6RdxRioAwHxorhOZRSXdW7Y7NESs5fm8vOmdddlG07QB7sMefOKLrqCV3zGg==} + '@nx/nx-darwin-arm64@23.1.0': + resolution: {integrity: sha512-wIFH5p0AXLxbYUHgYEH/zpWpPTQ9LpqESo/GcOPXkSBtUkSAZcqXZ6jmFzTPqGAeP4PBcVZIetO0TXmSSyEO7Q==} cpu: [arm64] os: [darwin] @@ -1406,8 +1509,8 @@ packages: cpu: [x64] os: [darwin] - '@nx/nx-darwin-x64@23.0.2': - resolution: {integrity: sha512-p6L3AvRhRRaR8Bl3jr76/9H04RdWUQbSgB7agK7GB7vqaLI8RifP2lqeaXcAngzjDAjw2EAf0TjOBP+T67hhcg==} + '@nx/nx-darwin-x64@23.1.0': + resolution: {integrity: sha512-D7fEW3+agyb0cWGi8qgN/0irHUwcO8nsuYOrRJjphtWE6J76EORW5sMY93/YFfnLeFVhBMBNVrE6aj2Y4wYCbA==} cpu: [x64] os: [darwin] @@ -1416,8 +1519,8 @@ packages: cpu: [x64] os: [freebsd] - '@nx/nx-freebsd-x64@23.0.2': - resolution: {integrity: sha512-/py4I8Rp2UURses9H/+SQmgPVnHVSJgPimJLhXIfsRavKGu4RS7Ddu1OyNqSkCT3Otic6ImMTtkufURW22KiEQ==} + '@nx/nx-freebsd-x64@23.1.0': + resolution: {integrity: sha512-yZoZyc9t3ViaA9WWDe5F7IlJ+58Hqdn6gqoO9UG2FH3Io1mj+KpF3jhUoj3S1m18D28MN2oItS762QIt6otO3g==} cpu: [x64] os: [freebsd] @@ -1426,8 +1529,8 @@ packages: cpu: [arm] os: [linux] - '@nx/nx-linux-arm-gnueabihf@23.0.2': - resolution: {integrity: sha512-xv2IzeiWJFWi4WjK0ocMkP+ze1lDeoPVCg0xOTqVs40gM66V1wVw3EK077gTqU4m0Bq1wUxe6/I8WaIGlkLgug==} + '@nx/nx-linux-arm-gnueabihf@23.1.0': + resolution: {integrity: sha512-D6QhBsLRUmwCc1jSjmY5MkyKOSWuQXQmu5K3BH6d1/uoX2HEZ68XgNyMkSwMA0g6jBKHIAguGkujiaCiXBEPeA==} cpu: [arm] os: [linux] @@ -1437,8 +1540,8 @@ packages: os: [linux] libc: [glibc] - '@nx/nx-linux-arm64-gnu@23.0.2': - resolution: {integrity: sha512-ckA6hTXST+agxt/HzPGqMss9qFCZhO9b07o8usygb7QFBYQRXFgcYzhTblq4yiTL5ibJmXAGGh98011fLA2MVw==} + '@nx/nx-linux-arm64-gnu@23.1.0': + resolution: {integrity: sha512-Q61stbUFVBw9TprlVLkRQmk+fuMXPlyTlvGExJ1y8KYymDTFsjs8LFk7zTnRvFpI9LVMRuhyD3osaLy2TxPU7Q==} cpu: [arm64] os: [linux] libc: [glibc] @@ -1449,8 +1552,8 @@ packages: os: [linux] libc: [musl] - '@nx/nx-linux-arm64-musl@23.0.2': - resolution: {integrity: sha512-PAxBxy7m//cKxUeIb6Sk2X5MJ/wjcJcqCx7/L0p8omTt/y/+q1TGpVy6qmJMPUWzNgAULXtsVRSOK4rmiBcrQQ==} + '@nx/nx-linux-arm64-musl@23.1.0': + resolution: {integrity: sha512-Lm5PC4yuBHw4gr37vcAX3kvS75prqL1VpbQkPHs4Z2Us1ri29ALxgdrEFgF6fbCXxl4w3sWmcbKGvrcMdpuvIQ==} cpu: [arm64] os: [linux] libc: [musl] @@ -1461,8 +1564,8 @@ packages: os: [linux] libc: [glibc] - '@nx/nx-linux-x64-gnu@23.0.2': - resolution: {integrity: sha512-gR146Mo+BjhrIU2fNOJeVjX8HEAHrtmc7IyrD0qD9yqyq0l9Mdx92JnMW3yVQaYIMpPJIqsOvHTDIUbTLFrmTA==} + '@nx/nx-linux-x64-gnu@23.1.0': + resolution: {integrity: sha512-v1ELSkVskidK6A+mm/Ynm8TWIb5UdzGWMcD7CTgmvjTzKfXIO0QY1ToDTqvoBwnetcehWR5gnhQlhQgOSH8XFA==} cpu: [x64] os: [linux] libc: [glibc] @@ -1473,8 +1576,8 @@ packages: os: [linux] libc: [musl] - '@nx/nx-linux-x64-musl@23.0.2': - resolution: {integrity: sha512-L7JkaoAI0p+DpAi2CNCqPq8nHWkApQw2td0Cyt+stOx/OJlmEteyc/MXyTHBoMPoopNj1wWANJJQm/G1anOLhQ==} + '@nx/nx-linux-x64-musl@23.1.0': + resolution: {integrity: sha512-pD/Axn0usK2/RV2bSrLX7h59YpOXGiaYHmlZynVVS+CqkKbyFzxnrvvLmRdOdMxsfM6oDXom4lCcJMg+wUYcpw==} cpu: [x64] os: [linux] libc: [musl] @@ -1484,8 +1587,8 @@ packages: cpu: [arm64] os: [win32] - '@nx/nx-win32-arm64-msvc@23.0.2': - resolution: {integrity: sha512-B3ePaYeu31ivP3i72Vou5RBYFVrDKCMZgz7Jsax+RvEJa8dNfI+Ynh/PSoXzHudN0YsrekkKbjlxNvp/D6fWFA==} + '@nx/nx-win32-arm64-msvc@23.1.0': + resolution: {integrity: sha512-lhnRaXPGUwqw/dqBnWl/6d6r8mRQXuZS7qnnTd+U5O1R7YPb2494BQyE7aSVEFLHcX9nXywMGxo5+OUCH84xQA==} cpu: [arm64] os: [win32] @@ -1494,8 +1597,8 @@ packages: cpu: [x64] os: [win32] - '@nx/nx-win32-x64-msvc@23.0.2': - resolution: {integrity: sha512-/NiB9w8nYrw7LUkcmwAJ9wis5O+kh3ahSZXMDHVYgFnD8yN7kLql39MNJD+/DGstjNSDvWh45ftqr7mZMi6wpQ==} + '@nx/nx-win32-x64-msvc@23.1.0': + resolution: {integrity: sha512-fJaNYwNk4RiUnyEDTc0bMu69dmUzgM7iGinoqFenwYEtwaXPWREBRWL6lgRDyR3CFsJw3Wi0uc+33cfreTbhVw==} cpu: [x64] os: [win32] @@ -1787,124 +1890,124 @@ packages: cpu: [x64] os: [win32] - '@oxfmt/binding-android-arm-eabi@0.57.0': - resolution: {integrity: sha512-qVBsEO+KugOsCmUHcO8iqNnqc65p7PCKpCs8M66mPZ+Ri+CWbcpoQOEJBg2OTu03+0qu++NK1jj6IzvQVs0Sig==} + '@oxfmt/binding-android-arm-eabi@0.58.0': + resolution: {integrity: sha512-Uz62sHduGGPftXtILGyxdSW4PX82rUg+rfdNqhsgxe881g4rIoXlIqmZQ6HVKcF4f+F8qMhdD03Bx5u7gmeTdg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@oxfmt/binding-android-arm64@0.57.0': - resolution: {integrity: sha512-mp6PibWbao3aizijcheOeHQaYEhcUAt8pwLniYbtLfHxL/psFF0BykAwCj+s3c6qIpa8yN8keZICWrqtZ70w8g==} + '@oxfmt/binding-android-arm64@0.58.0': + resolution: {integrity: sha512-rD0lRaJp1b+9vw6X4A2dJWKukd6X8yxiicN4JxXcXayolmUypRZxk+lKR+fVOu5q/iYc0fh5fR4bgmfOfVlbaA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxfmt/binding-darwin-arm64@0.57.0': - resolution: {integrity: sha512-T+0stuCBqmUVY+aMIvrgXhzGhHO3sD5tNiiEcYqgSdPsnukskQqn2u5qOVD0sv1l7RLdFS5Z/f5Wi9Ktyjr3Eg==} + '@oxfmt/binding-darwin-arm64@0.58.0': + resolution: {integrity: sha512-uzbPPk7O6M+w2K65vcQ1woga3wgP8zghjL1KOG5b6qJ8dvYHZJ1VShaslg2KOK6yQIwCQtcMCXqLBM6sqXUNTg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxfmt/binding-darwin-x64@0.57.0': - resolution: {integrity: sha512-O+3JbqWs/mCI2oi4xfhRO2IVPFJNDDEBV8Odo+ZpmsUOeKJfjXoNH7nDmBEQcDgK7NfjDIyE7kRgYSZcTLDO0A==} + '@oxfmt/binding-darwin-x64@0.58.0': + resolution: {integrity: sha512-L0nKYDxU32oxeQqJj21W9SlIMnf81VZEhyah6iDvFhf5q0oynq498Fopth7blErUJVBpVtxQ98RMCfMPqpJX6w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxfmt/binding-freebsd-x64@0.57.0': - resolution: {integrity: sha512-pxwhxVC+JkLX9twOQ/8C/vbuOQcMZyKIDmiRDZfO7yITuVcIdZCiLRqqf4QOxb2+8FWrRXzQpm+1DBKcMpHSSQ==} + '@oxfmt/binding-freebsd-x64@0.58.0': + resolution: {integrity: sha512-woNwfD58dC5PGS9LSLSD5JYfo/EFK5iG9vhDWkcCg3q78ag7KC8bpDqgvPHrMoXpx83OLXxoSOhu6z8FsVTHlg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxfmt/binding-linux-arm-gnueabihf@0.57.0': - resolution: {integrity: sha512-pxBU4zH2imB/MDBfth2rOMeVxXUMjRQLCazagwLARIFH3hVlxZJBlM4nSnHXaIHJK4/qezoFCIORN6AY8Mra4A==} + '@oxfmt/binding-linux-arm-gnueabihf@0.58.0': + resolution: {integrity: sha512-Sqs8nMLxuQpY21NKJ1u4stPDmO5hskBCNNh2E3AdCfI1QqWtf4m+Qn4mGEIUO4KGmuq3SWc/SZ80uy5IiwTCDw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxfmt/binding-linux-arm-musleabihf@0.57.0': - resolution: {integrity: sha512-JAprOzt8tycYou36ZgEw14DlRHTiN8qdtKANdV3VZIRIvTI/lh/cX13c9pJ/EnDk2GT3FASH7KvCgQ2AufAifQ==} + '@oxfmt/binding-linux-arm-musleabihf@0.58.0': + resolution: {integrity: sha512-Vd4exzBI5B5hB9m22JiTQzIL23WvHo/Pe+sNXPNeBLXSP9swCBPKCEBRwKpmpQzYhlgYaCgfPcGXPKAJBRIiZQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxfmt/binding-linux-arm64-gnu@0.57.0': - resolution: {integrity: sha512-ajtjaxSaj9xl4BW7REt+Cef/ttzbAq00Bq4z7JUDZEfgFXdwSjH8K9bF+IcIJzZB9lKqMfQ4eHuSFOvvlvtqOg==} + '@oxfmt/binding-linux-arm64-gnu@0.58.0': + resolution: {integrity: sha512-bUWi5mHV+4Vi56RLHE1h6q/HHfwAIT3XoB9vJAVeRzfu5NriXM8y6eeJu0vlKa0C9kq2rq1sOWRClhdLHPocrg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-arm64-musl@0.57.0': - resolution: {integrity: sha512-p4Y/+RYk9Bk5WO+zHSUXAClRmZ2fbJCejMuCAsU2HhyME4jqf6Ftt/mJYEwIah1wGCBDYOB7wEGV1x5bCEZ6hA==} + '@oxfmt/binding-linux-arm64-musl@0.58.0': + resolution: {integrity: sha512-2ZHxemzgHcjtktAuVUwSoyXmGo/t+aF5tS1ciPpPei4rhSyrz3JOqDosXXrmhN/yLUSzJjtuW7ToTWqfQpCj2w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@oxfmt/binding-linux-ppc64-gnu@0.57.0': - resolution: {integrity: sha512-By6tRALAZsno0F4zedmtG+wdMvJiJmJoXM4d3+A9zHE4HRXLqXITwRH8mgrlcXc5yJM2g2W3riRPwTYdgemZLQ==} + '@oxfmt/binding-linux-ppc64-gnu@0.58.0': + resolution: {integrity: sha512-AwKkVwjVmFQ3bcO7j0McGYAqCKH2a326fswfofng/E8VewCT/raeeGQr4huVhY704deK8AWASSTlxzMj0eZc6Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-riscv64-gnu@0.57.0': - resolution: {integrity: sha512-skYeG+RgvyzspqVEBsEprL90OYYZfoVNqB3HcCNR6QDJyXKOzfDRT3zncnHmUaFluIlBHuY23mU1b5WGgR98hA==} + '@oxfmt/binding-linux-riscv64-gnu@0.58.0': + resolution: {integrity: sha512-xsRpTxfUnJF8D3AUKko/qyWdjw4GZVHlCVFuGlzSCTeewLmykKINW8em1+wx+axsDVtJJcMtvsiaXggXxrlHgw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-riscv64-musl@0.57.0': - resolution: {integrity: sha512-FFgACrZOXAXUh5KQh2mt1CDOVOZmn+QzHP71wM9QobNwyQvoFfyAeefVUltW83g3sm7LTiH3yfFqLLVUpA5ZFQ==} + '@oxfmt/binding-linux-riscv64-musl@0.58.0': + resolution: {integrity: sha512-Z4AYOTcy7nYEIiXwD62PlerimyYRcfJOgUbQAEBjXz098kxKuERBlRntofGy69HHhe9E0TLVNMl1yspVNu+efw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [musl] - '@oxfmt/binding-linux-s390x-gnu@0.57.0': - resolution: {integrity: sha512-Nm/BAOfQeFiiKd502mZn/GAVKJwtd0RdCg17G3Wz/WSOIQmDi3+7/SZH4BHn1Ye5KvTVH3ua8WvfwLLycNIuvA==} + '@oxfmt/binding-linux-s390x-gnu@0.58.0': + resolution: {integrity: sha512-A3nhhtZPC/TKVWOPj9q/H3p2znJDCcHWYlJBhWL8hGq/bFmBaNBHC8Np6E581yVq1w9Mi3rMDNzDalWvtUfJtQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-x64-gnu@0.57.0': - resolution: {integrity: sha512-BiSy5Ku3mQqyxS6YIqAJgd403wEUWvI7kerfzPxc2l/txZVmZM0pSj7oDM+4bGBExowxOi7o73jEam1W0EDTZg==} + '@oxfmt/binding-linux-x64-gnu@0.58.0': + resolution: {integrity: sha512-2g+tVkgwqphw8R4hgo+kF4oz8+P5RwVOtr9+irsC7uwEp0e9j7Crw8kDGKL20uYlLPD7g02DqA61mC/UNYx98A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-x64-musl@0.57.0': - resolution: {integrity: sha512-BCRkJiotz5s9afLYD2LuMvzAoDYx9H17E/YbDyu4xK7l4zHDPeny9ErSXL//i/nJyaOwRk08x4b8cgJC00+JDg==} + '@oxfmt/binding-linux-x64-musl@0.58.0': + resolution: {integrity: sha512-rc15P6AbyyB7426aN8AakLd02Trb3a6ML/mmfAQeVHJEfVofWLcWIrBdy6zDEY+DIaL/s8E4GGPboVw+oP3+EA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@oxfmt/binding-openharmony-arm64@0.57.0': - resolution: {integrity: sha512-4Oaxe1qrGgXfpCJ1C/ERJ2iCtV2rN1R79ga9fsfyVHfSQRu/hVW780u2KDqZWFZ/iGTHODJji0JemxqFZ63eIQ==} + '@oxfmt/binding-openharmony-arm64@0.58.0': + resolution: {integrity: sha512-ZWoTM27/HYPOh9iq86DAbhPu9nXb8qKvvGU/h8OfliyVUFAMMNTLDkGsWDKKnDqIkqvZ9+dXlgUOsH1LYO3O7g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@oxfmt/binding-win32-arm64-msvc@0.57.0': - resolution: {integrity: sha512-MYLAsDnhdNsSGheLYhWgbk0vfIrlS84iQYun/y21fX6u0jj8iBtYtbpZMdiqYeuf8U12eVPUjVY2xE2NrCfJ0g==} + '@oxfmt/binding-win32-arm64-msvc@0.58.0': + resolution: {integrity: sha512-LHZnqFXe2dEfkRI4XdZS/57nEOT/I4UCRX5IyM9v4GYW9XwQCjGe1IUK59SuKw3POwvcgWQ4pme2cYXmNqTNPg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxfmt/binding-win32-ia32-msvc@0.57.0': - resolution: {integrity: sha512-PBwdzZALJY/jcCx2E6is0yu+cuVXeySTDmwuseD+9j0mHqlRNxwlKgsyRTBed/woPeqfVfuXfWjoq4Cx2Zt3Eg==} + '@oxfmt/binding-win32-ia32-msvc@0.58.0': + resolution: {integrity: sha512-mZKpg20TpheCJym1rarcZCUJeW1sSruw8zAAaCYWvuVfwIUDN1CXdrPU/JgCWReXTCTrEfCB8Wyo3hh9jSZ2EA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@oxfmt/binding-win32-x64-msvc@0.57.0': - resolution: {integrity: sha512-bQJdH9i4RRfw55jm7+8/xS7GzHLLTbHx4huhrrDxQJaJtbSDbsyOnODvP1ftT7EG0KFKAYO2S+q6AcioXODx8w==} + '@oxfmt/binding-win32-x64-msvc@0.58.0': + resolution: {integrity: sha512-N/wUU4N5PZ2orBtI+Ko7MnMfYLfE7K91UrGMY/c/pYyHR3lA9kwst1XugkZx+92YcRh/Eo+iv2eTESSWXfiZPA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -2164,11 +2267,11 @@ packages: resolution: {integrity: sha512-//0sR/cow/s4ICQaYoAobOl4aU8cjU6x/V24V7XkKotb9+O+3zySIYp146vpaobYHnxa4pZX8NkV54Z5AwbDKA==} engines: {node: '>=12'} - '@posthog/core@1.40.1': - resolution: {integrity: sha512-jXuMtZCwA7AMpYlo1wjm6GlC58YBlz/ZxpDIsF9hroUfqYoPPDmQbsQb4iiZAS43+o/kwloxdKJIlE0IiwQ5HQ==} + '@posthog/core@1.43.1': + resolution: {integrity: sha512-hGM8f5sp3we6Em/RQHXbmyYm554hUx9+9jhf92ZQgDS4/xW72KHyZiO9wcFye+qAx2cJAOhOCDIznmO1FV8IbA==} - '@posthog/types@1.393.0': - resolution: {integrity: sha512-vzWeEJZ7ERQhFRoQYaP5jzN1JvIu46UJyHXsuv+dTGW2r3sMgREOhNxXLZjmFHwZ8/FOHQoyqqQmXTCXZSfMSg==} + '@posthog/types@1.397.0': + resolution: {integrity: sha512-Pa7FtsBo3V0XrhY4y8Xquwp8U07syuZ2IGQdlsRyxhz0yejQLceFjI58UssCXE5zDCDj3km5l7H3ufD6rHChCg==} '@radix-ui/number@1.1.2': resolution: {integrity: sha512-ceTwaxc4I5IOi97DgCotl3pqiyRGvffcc0oOsE2dQYaJOFIDsDt4VWG6xEbg1QePv9QWausCEIppud/tJ1wNig==} @@ -2704,45 +2807,45 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@supabase/auth-js@2.110.0': - resolution: {integrity: sha512-Mi288WCTp6wxMFCOu/UgzgHEXODjdl2uVTLqK11eanzGZaldU3RyP8Am+ZbNuVzFP+5+iOvppxzv7N5Ym84xTg==} + '@supabase/auth-js@2.110.2': + resolution: {integrity: sha512-Qj7a6EDP+AMMQFWqGv+qFa8r6re//dk+qQI5bA0KK+PZmnI3JPu97TDeNt6SMiQ2FkklP79hP2yDFYSnA989OA==} engines: {node: '>=22.0.0'} - '@supabase/functions-js@2.110.0': - resolution: {integrity: sha512-Fde5wlY8ZZy+9yqrWlQHo8MacSyUBArBEtN2boB4thJQigPnQD/cc61qZN0n3I1L0gwhWtHYwIMnOBKxSvF6Hw==} + '@supabase/functions-js@2.110.2': + resolution: {integrity: sha512-ZjjqrXpxM9/rE+eAtZxiK45EWy9EBoJQ322Q5Y75LccYQNh212neHTgXP/o4MIzmH0LNXT8UzvTZtQOfOzyoeQ==} engines: {node: '>=22.0.0'} '@supabase/phoenix@0.4.4': resolution: {integrity: sha512-Gt0pqoXuIqX/8dvG0OKp/wMCobXNH3klNbUPBNyOfN0YA1IswrM3HyWFMOPk1Jy+BRaIyDPcFx4jLBwHNmlyfQ==} - '@supabase/postgrest-js@2.110.0': - resolution: {integrity: sha512-ZbC1QZL3jcvBUfVKjJbgRM27G4Mg3Zzqdm44m5pJafe1e52Cli793EOnwQucomBAGEUDd03Nzaf7XV3ji/XexQ==} + '@supabase/postgrest-js@2.110.2': + resolution: {integrity: sha512-++LBmcIMwCtgO4tISQUmo9+2xkRwHQqS8ZKMCnhXLe9P8k8YQRXuMoh/RiSzQSoev8gqet0W7yOboW0cUxnt0Q==} engines: {node: '>=22.0.0'} - '@supabase/realtime-js@2.110.0': - resolution: {integrity: sha512-Wn2AWpneZuDFTkp/65tqctvoh+3JvyTjMam8sTMqVWy5BgkU8zAvFwilPYPPPhkINeKF8NAJKP7FclJ2iGCUMw==} + '@supabase/realtime-js@2.110.2': + resolution: {integrity: sha512-z3jTOTPgyn6E3r6dVOOQ10He4yAMB2czjFw7xVdX3s16MHElna5rY1gVaePs0NIo6xvtMYbtmOXlFaFt/ePLpg==} engines: {node: '>=22.0.0'} - '@supabase/storage-js@2.110.0': - resolution: {integrity: sha512-71+gU3HrhiylAhftY6FmO5PPdcsScnVcS766CVD+vTYK9qTDLbrx8FhgBYbqGm3iV/wkTfzrNJfjGsMeFRkJRQ==} + '@supabase/storage-js@2.110.2': + resolution: {integrity: sha512-EhsRSwSnmQefKJsAxoRUZ0hvHr92ECM8DDGAKR5z0HdoJx4heI60PjHUTruVNZxKX6XeobLGDyLud020Bw1iwg==} engines: {node: '>=22.0.0'} - '@supabase/supabase-js@2.110.0': - resolution: {integrity: sha512-8yI84VJiEVW4zxZpLUmxXmjzQ7O2St9X/ymzlBETDHTURPWG3LmvbSiibq+7dqAJmyoUfxZnSfXeM4HCM8s4XQ==} + '@supabase/supabase-js@2.110.2': + resolution: {integrity: sha512-r9q9w4ZQ6mOjh36aqUNFSisBF611vzpO8JphBESr2Q1SWvmGFQeI7Jq7Y+PaNMZ6Zszz+S2yTlJStCpnaMSnQg==} engines: {node: '>=22.0.0'} - '@swc-node/core@1.14.1': - resolution: {integrity: sha512-jrt5GUaZUU6cmMS+WTJEvGvaB6j1YNKPHPzC2PUi2BjaFbtxURHj6641Az6xN7b665hNniAIdvjxWcRml5yCnw==} + '@swc-node/core@1.15.0': + resolution: {integrity: sha512-WAerrrl087WgenB92XG4Th2t0NQFfMNLYSe0sW2cEMMqM/LQmP4rozsDJl0vuzTrbewjpKQryxFXI+aYig/dBg==} engines: {node: '>= 10'} peerDependencies: '@swc/core': '>= 1.13.3' '@swc/types': '>= 0.1' - '@swc-node/register@1.11.1': - resolution: {integrity: sha512-VQ0hJ5jX31TVv/fhZx4xJRzd8pwn6VvzYd2tGOHHr2TfXGCBixZoqdPDXTiEoJLCTS2MmvBf6zyQZZ0M8aGQCQ==} + '@swc-node/register@1.12.0': + resolution: {integrity: sha512-RnkF4NOHqv3JAKJS9YguLeFH81NPv8VQKTXt5lMJfOujB7qvZXJRei5LJeulihF0ZMD1UIruIA0gm9it/bH37g==} peerDependencies: '@swc/core': '>= 1.4.13' - typescript: '>= 4.3' + typescript: '>= 4.3 < 7' '@swc-node/sourcemap-support@0.6.1': resolution: {integrity: sha512-ovltDVH5QpdHXZkW138vG4+dgcNsxfwxHVoV6BtmTbz2KKl1A8ZSlbdtxzzfNjCjbpayda8Us9eMtcHobm38dA==} @@ -2877,6 +2980,9 @@ packages: '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/hast@3.0.5': + resolution: {integrity: sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==} + '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} @@ -2921,55 +3027,179 @@ packages: '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} - '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260703.1': - resolution: {integrity: sha512-EwJEd6hfaHrrbSLat6+xX8fZiAvG+/Ae1gqvJEayTNdDbkrZxmeLS23YdjUmDMIOKI2wa7zXQDid8WtrvDfMTQ==} + '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260707.2': + resolution: {integrity: sha512-wny2pgKjGbiZtnOIHVa3tXC1UfDqxNEFzyPGmiqybedG8hipG2Nfp0l5UxbaKCjkLacUpH/W5bP2hBOMVhCOzg==} engines: {node: '>=16.20.0'} cpu: [arm64] os: [darwin] - '@typescript/native-preview-darwin-x64@7.0.0-dev.20260703.1': - resolution: {integrity: sha512-tcS3gpivMq+BAiaupFqM5vuERykogJlfD4CjoxkSCksmmsKgWV96S2U/LjrKgll8R6/OEkg2VL2ycRG9+tIuHw==} + '@typescript/native-preview-darwin-x64@7.0.0-dev.20260707.2': + resolution: {integrity: sha512-Afc7M5zOwo+GpfcYwz5Z8HMB2tPVsui7nNIqEuuFB73MPdVqNn/Wmpe4tP4MRri0AtJnJknoHBaTJ/VDAp/Jhw==} engines: {node: '>=16.20.0'} cpu: [x64] os: [darwin] - '@typescript/native-preview-linux-arm64@7.0.0-dev.20260703.1': - resolution: {integrity: sha512-m8IJTOneLXRtq6prLz8uuhp463kEt+AHV5Ceqp7G0o3eAvbKP059OwzK6WXCS5J0B3ZxX+BwBf9wDXSNidWJCw==} + '@typescript/native-preview-linux-arm64@7.0.0-dev.20260707.2': + resolution: {integrity: sha512-iITBa2WjjTI5N9t5l7Z4KoOSI+2zBlhbvFzsD/f8qX8QoKjz/Y4DPyBDgezYi8nkqjjksbgSOJ3/ykzhwrB9cg==} engines: {node: '>=16.20.0'} cpu: [arm64] os: [linux] - '@typescript/native-preview-linux-arm@7.0.0-dev.20260703.1': - resolution: {integrity: sha512-nluwnKcdGo6laWWYtWF3zFUDfi7nNrcD5S/T5382fVmtKmNqIdzFm9ANgSSGAavuzfdu9YJLaVWpx72Oe40AEQ==} + '@typescript/native-preview-linux-arm@7.0.0-dev.20260707.2': + resolution: {integrity: sha512-hJm/UOqZTr9FHmR7uNm8VGX4oKtfWk0Jem0zPeJFNC8ckGUfSBueyiEYMZB+XmRc1aG4x1E46y3CplP4CLHvGQ==} engines: {node: '>=16.20.0'} cpu: [arm] os: [linux] - '@typescript/native-preview-linux-x64@7.0.0-dev.20260703.1': - resolution: {integrity: sha512-5ga94rso68kaxJUzaufDVhka1zPaSbPgNXaemQO8faPW0crvRIkbv0g8bpG4pdWMV0tTylmluDles4ZcAzOprg==} + '@typescript/native-preview-linux-x64@7.0.0-dev.20260707.2': + resolution: {integrity: sha512-du0dzi6y97Po5vDNdPJTyyijHCpaS22JLRnKZEJXBDaO9gCIymOv/5QQokFRuOlQm0bWl3i9PF4OVdGP6uAOQA==} engines: {node: '>=16.20.0'} cpu: [x64] os: [linux] - '@typescript/native-preview-win32-arm64@7.0.0-dev.20260703.1': - resolution: {integrity: sha512-YeRqOUuSyhbtryBSUE073OLpReIQXWJVyjP45gPLJ+0KAGvkd1VFz2FY1JEo++LyHoqXsGD2+qvdPpnTfSAIJg==} + '@typescript/native-preview-win32-arm64@7.0.0-dev.20260707.2': + resolution: {integrity: sha512-SsAwfhyHJ1akgBc+99z4+hwdbHsdWaKB8EwCNIMA6JfSLMeUjffrYvxu+vfMyxVtOVOz7RrRXRoiDiu4a2sCtg==} engines: {node: '>=16.20.0'} cpu: [arm64] os: [win32] - '@typescript/native-preview-win32-x64@7.0.0-dev.20260703.1': - resolution: {integrity: sha512-WYNcbTaxCPgiDLrL2aPJ3BJA05nJU8DK0fUFGkGAER0oM+KcJcQUBeUkXg/7A4HBTNYcj7zIxqNgkJU1TRa/Kw==} + '@typescript/native-preview-win32-x64@7.0.0-dev.20260707.2': + resolution: {integrity: sha512-DL4u27stv0fo71sVhOzHSwE+YMZsbBijVI+kg5dLDLilSH79WFTJ8RSQ46vJrCMt+Gjlv/JOZP1PuLJDfioYeQ==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [win32] + + '@typescript/native-preview@7.0.0-dev.20260707.2': + resolution: {integrity: sha512-oUGp+Rep/hqMhPunyinsALUwSlzHINSxitifPiSaeqoKOKD2OlR9NE3TaPqwsl4NlGslsOSUXI1JotWQzpYCPg==} + engines: {node: '>=16.20.0'} + hasBin: true + + '@typescript/typescript-aix-ppc64@7.0.2': + resolution: {integrity: sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==} + engines: {node: '>=16.20.0'} + cpu: [ppc64] + os: [aix] + + '@typescript/typescript-darwin-arm64@7.0.2': + resolution: {integrity: sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [darwin] + + '@typescript/typescript-darwin-x64@7.0.2': + resolution: {integrity: sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [darwin] + + '@typescript/typescript-freebsd-arm64@7.0.2': + resolution: {integrity: sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [freebsd] + + '@typescript/typescript-freebsd-x64@7.0.2': + resolution: {integrity: sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [freebsd] + + '@typescript/typescript-linux-arm64@7.0.2': + resolution: {integrity: sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [linux] + + '@typescript/typescript-linux-arm@7.0.2': + resolution: {integrity: sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==} + engines: {node: '>=16.20.0'} + cpu: [arm] + os: [linux] + + '@typescript/typescript-linux-loong64@7.0.2': + resolution: {integrity: sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==} + engines: {node: '>=16.20.0'} + cpu: [loong64] + os: [linux] + + '@typescript/typescript-linux-mips64el@7.0.2': + resolution: {integrity: sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==} + engines: {node: '>=16.20.0'} + cpu: [mips64el] + os: [linux] + + '@typescript/typescript-linux-ppc64@7.0.2': + resolution: {integrity: sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==} + engines: {node: '>=16.20.0'} + cpu: [ppc64] + os: [linux] + + '@typescript/typescript-linux-riscv64@7.0.2': + resolution: {integrity: sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==} + engines: {node: '>=16.20.0'} + cpu: [riscv64] + os: [linux] + + '@typescript/typescript-linux-s390x@7.0.2': + resolution: {integrity: sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==} + engines: {node: '>=16.20.0'} + cpu: [s390x] + os: [linux] + + '@typescript/typescript-linux-x64@7.0.2': + resolution: {integrity: sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [linux] + + '@typescript/typescript-netbsd-arm64@7.0.2': + resolution: {integrity: sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [netbsd] + + '@typescript/typescript-netbsd-x64@7.0.2': + resolution: {integrity: sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [netbsd] + + '@typescript/typescript-openbsd-arm64@7.0.2': + resolution: {integrity: sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [openbsd] + + '@typescript/typescript-openbsd-x64@7.0.2': + resolution: {integrity: sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==} engines: {node: '>=16.20.0'} cpu: [x64] + os: [openbsd] + + '@typescript/typescript-sunos-x64@7.0.2': + resolution: {integrity: sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [sunos] + + '@typescript/typescript-win32-arm64@7.0.2': + resolution: {integrity: sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] os: [win32] - '@typescript/native-preview@7.0.0-dev.20260703.1': - resolution: {integrity: sha512-qyEHkEeRWCSGLa6a8oArnMPdn3Vcl1AZj8YHLO7lK0VjEEF/YJfz1FvxmpEeuhy0ZDfvJ7GtgXbvbjcU3zpPjQ==} + '@typescript/typescript-win32-x64@7.0.2': + resolution: {integrity: sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==} engines: {node: '>=16.20.0'} + cpu: [x64] + os: [win32] + + '@typescript/typescript6@6.0.2': + resolution: {integrity: sha512-mbCddXd+jm7hfx7w2YU64/Av4/NqqeG3GoRZgxPcgoTxYjhrcfJRw9ULch71SS4G+Q3bOXFhRvPqjguN0Hyp5w==} hasBin: true - '@ungap/structured-clone@1.3.2': - resolution: {integrity: sha512-5jsZFwgR5rTdKwidH9Qmat75RKwqfpKlWWB1frDkljN127mwqBu8K0PYo7/hFpF03IEJpfVPpCQDY/eDx3iHvA==} + '@ungap/structured-clone@1.3.3': + resolution: {integrity: sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==} '@vercel/detect-agent@1.2.3': resolution: {integrity: sha512-VYNCgUc0nOmC4WJmWw9GkrKdfr8Zl4/rxhC5SvgacBgxiW9W/9NRttUoHHXV8xdII3MaRgkZZVX8Ikzc/Jmjag==} @@ -3050,16 +3280,16 @@ packages: resolution: {integrity: sha512-tJ3XO0MaFe8gx9oiLBu3Jim8JVyJRC08c2Y4dfx3wd1j9HlVkiWnW5qYOoTHA4NfdMzugsBsI8GlX3v/y/EtPg==} engines: {node: '>=18'} - '@vitest/coverage-istanbul@4.1.9': - resolution: {integrity: sha512-4a7DsIwycTf4eYwEDtnMfMV8H80KSKH9PuMHhqL5SwPZzDyUKq2X/TPCVZ7NqIuSz7UbZckmEmkip6iZBI/gEA==} + '@vitest/coverage-istanbul@4.1.10': + resolution: {integrity: sha512-AyNJ5pQRFqCX7pwB9PSTmoVKPaZ4H5IEVJfJsT+q1DYkXvZMEFYgJlyk5sfStmt9rVYRyYYRRsuBeImCOc39ww==} peerDependencies: - vitest: 4.1.9 + vitest: 4.1.10 - '@vitest/expect@4.1.9': - resolution: {integrity: sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==} + '@vitest/expect@4.1.10': + resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==} - '@vitest/mocker@4.1.9': - resolution: {integrity: sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==} + '@vitest/mocker@4.1.10': + resolution: {integrity: sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==} peerDependencies: msw: ^2.4.9 vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -3069,20 +3299,20 @@ packages: vite: optional: true - '@vitest/pretty-format@4.1.9': - resolution: {integrity: sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==} + '@vitest/pretty-format@4.1.10': + resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==} - '@vitest/runner@4.1.9': - resolution: {integrity: sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==} + '@vitest/runner@4.1.10': + resolution: {integrity: sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==} - '@vitest/snapshot@4.1.9': - resolution: {integrity: sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==} + '@vitest/snapshot@4.1.10': + resolution: {integrity: sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==} - '@vitest/spy@4.1.9': - resolution: {integrity: sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==} + '@vitest/spy@4.1.10': + resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==} - '@vitest/utils@4.1.9': - resolution: {integrity: sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==} + '@vitest/utils@4.1.10': + resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==} '@yarnpkg/lockfile@1.1.0': resolution: {integrity: sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==} @@ -3343,6 +3573,10 @@ packages: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} + call-bind@1.0.9: + resolution: {integrity: sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==} + engines: {node: '>= 0.4'} + call-bound@1.0.4: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} @@ -3657,6 +3891,10 @@ packages: resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} engines: {node: '>=10'} + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + define-lazy-prop@2.0.0: resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} engines: {node: '>=8'} @@ -3734,8 +3972,8 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - effect@4.0.0-beta.93: - resolution: {integrity: sha512-wNS5MKFa3C42uBfIDik2oJ78lhpoYz2hN4oBR0229BeeDCIrkg/FiOvoiPGdCVlWa7MEKxEL5I0f8AILVHSD9A==} + effect@4.0.0-beta.97: + resolution: {integrity: sha512-pK03HpQVxGZOWdwDAy/iwvV8u3KYcUf2mOWyWqaut2zau8V2u6ejWP7b4BELjyUIiZWW1fl/s/VJpgZUcTjThg==} ejs@5.0.1: resolution: {integrity: sha512-COqBPFMxuPTPspXl2DkVYaDS3HtrD1GpzOGkNTJ1IYkifq/r9h8SVEFrjA3D9/VJGOEoMQcrlhpntcSUrM8k6A==} @@ -3928,8 +4166,8 @@ packages: resolution: {integrity: sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==} engines: {'0': node >=0.6.0} - fast-check@4.8.0: - resolution: {integrity: sha512-GOJ158CUMnN6cSahsv4+ExARvIDuzzinFjkp0E9WtiBa5zcVeLozVkWaE4IzFcc+Y48Wp1EDlUZsXRyAztQcSg==} + fast-check@4.9.0: + resolution: {integrity: sha512-7ms6T7SybUev/PQITciI0yLM2pOSFy5zpG8Ty7tQofcVaQUvrMXp6CBwqF6fThLCLOrfBtuHAtwq6Yu4XPCllg==} engines: {node: '>=12.17.0'} fast-deep-equal@3.1.3: @@ -4077,8 +4315,8 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] - fumadocs-core@16.10.7: - resolution: {integrity: sha512-lR1hDOtJ8ubsLKYH2VMkp+iVZTDdOiMh4StFaWtuTvy8Wfnngz0YSHeFijmm2K+4lg2DLXLMuCEHQ6my54g2Eg==} + fumadocs-core@16.11.1: + resolution: {integrity: sha512-tKuh1AKoVTb+f7IoAOM2cfz5djd3YhePeqA95q6mf422gEvDTeJms23OJ+icYRWZ6ryNQ5W/ZsgKEe87M5HVYg==} peerDependencies: '@mdx-js/mdx': '*' '@mixedbread/sdk': 0.x.x @@ -4136,10 +4374,11 @@ packages: zod: optional: true - fumadocs-mdx@15.0.13: - resolution: {integrity: sha512-VsGhCiLriXXMzm3WbgrVP7t6LvOthwh1BC+IGSI1ZW63UcSo1jE4aAiuUrTIF0jv1EGQkJG8cPsy0cOnf4sejA==} + fumadocs-mdx@15.1.0: + resolution: {integrity: sha512-2nDusSlYFuNVcyB51jgY3tA3r01ALTwoURrMDNoc7cbJKZ2sac/PW+CDq6SHTArkgRMmFiKYQGfspJdjgTtPTg==} hasBin: true peerDependencies: + '@fumadocs/satteri': 0.x.x '@types/mdast': '*' '@types/mdx': '*' '@types/react': '*' @@ -4148,8 +4387,11 @@ packages: next: ^15.3.0 || ^16.0.0 react: ^19.2.0 rolldown: '*' + satteri: ^0.9.4 vite: 7.x.x || 8.x.x peerDependenciesMeta: + '@fumadocs/satteri': + optional: true '@types/mdast': optional: true '@types/mdx': @@ -4164,16 +4406,18 @@ packages: optional: true rolldown: optional: true + satteri: + optional: true vite: optional: true - fumadocs-ui@16.10.7: - resolution: {integrity: sha512-zE93/DKW5bhedXRKHYg3rhE/juYi+kXx1xl3ey1dArWbCiPx6lq6/4RLswYXS0lQp1W1f8NsbY1TeA8wSQuEvw==} + fumadocs-ui@16.11.1: + resolution: {integrity: sha512-Dq819PFV4RGhAI9Wd4erSCiRlEDLVOZae+kgE5LeOKFH8mbKX49U8N17ldFOhdkC9EZpxMZdEKul77RDgFHQww==} peerDependencies: '@takumi-rs/image-response': '*' '@types/mdx': '*' '@types/react': '*' - fumadocs-core: 16.10.7 + fumadocs-core: 16.11.1 next: 16.x.x react: ^19.2.0 react-dom: ^19.2.0 @@ -4289,6 +4533,9 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + has-symbols@1.1.0: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} @@ -4574,6 +4821,9 @@ packages: isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -4652,6 +4902,10 @@ packages: json-schema@0.4.0: resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + json-stable-stringify@1.3.0: + resolution: {integrity: sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==} + engines: {node: '>= 0.4'} + json-stringify-safe@5.0.1: resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} @@ -4669,6 +4923,9 @@ packages: jsonfile@6.2.1: resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==} + jsonify@0.0.1: + resolution: {integrity: sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==} + jsonparse@1.3.1: resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} engines: {'0': node >= 0.2.0} @@ -4855,8 +5112,8 @@ packages: resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} engines: {node: '>=12'} - lucide-react@1.23.0: - resolution: {integrity: sha512-38BpJcD0JhFosxHApP/BYsBetLpQFRoTRzEzstM/XCc3jsAG7wqaY1lgVwxiUe3xqYE+lNxo2PkCmYwXWrwwIw==} + lucide-react@1.24.0: + resolution: {integrity: sha512-YT6mBD8lGKkg4nM39enlm94/sfJIiW0YKUT60fBy4YK8tai31ylg1VhGNWxkpSKHo9UagfnZqwIff3HTDQwXeA==} peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -5176,17 +5433,17 @@ packages: resolution: {integrity: sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw==} hasBin: true - msgpackr@2.0.2: - resolution: {integrity: sha512-c5hYOXFbP79Slh6Dzd2wzk+jnV7mX1UxfMYtilnY1NmalXPqG8DGb5cYCMBrW4AsH3zekBBZd4QrKz9NhtvYLQ==} + msgpackr@2.0.4: + resolution: {integrity: sha512-o1C5KRmuRt+apqMr1HuGSqWStZoRBUpEsCsl15uM9VdAF1qHLtvMOU2En747EnTyEl6c4pzPewRMFF31s1CNbA==} - multipasta@0.2.7: - resolution: {integrity: sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA==} + multipasta@0.2.8: + resolution: {integrity: sha512-ZPWuMKyv0cSO29f7hozp+k6+crZbQijV8ipMvxNxRf2SwtYGTX1ZX89Kd20VV4H9Znonx+EQn+iy1wGQsJ+b+Q==} mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} - nanoid@3.3.15: - resolution: {integrity: sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==} + nanoid@3.3.16: + resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -5214,8 +5471,8 @@ packages: react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc - next@16.2.10: - resolution: {integrity: sha512-2som5AVXb3kE6Yjine3/mNbBayYF58eguBWIVVUdr1y/L426xyVEgYxgBG+1QC34P2x5E+tcDup6XkuOAX3dCA==} + next@16.3.0-preview.6: + resolution: {integrity: sha512-mNUH6FJz/oB0htRV2wBPeXXU+bT3+ZCrTtGkZe1YTRdV0qmsx3P8ajMAKfVQI/dTQ/Me9IOAVjfEHfeoFGmPtQ==} engines: {node: '>=20.9.0'} hasBin: true peerDependencies: @@ -5370,8 +5627,8 @@ packages: '@swc/core': optional: true - nx@23.0.2: - resolution: {integrity: sha512-e5H6ceqj0Z8ovAmtsiHXswwpiNPrr1DRhvJMTpc2AW8G1za9PKxk3bP5josShsIrmGEsOlBNZZsxszXA2+Q2dw==} + nx@23.1.0: + resolution: {integrity: sha512-Z4hzIdRF6naDDpOTOazrJODmbU5D7slq94FR5q/dySW+evGxiOSFLO981VqDVvtNEvHPnEDh/C1wvFseWyvSrQ==} hasBin: true peerDependencies: '@swc-node/register': ^1.11.1 @@ -5390,6 +5647,10 @@ packages: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + obuf@1.1.2: resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} @@ -5440,8 +5701,8 @@ packages: oxc-resolver@11.21.3: resolution: {integrity: sha512-2Mx3fKQz7+xgrBONjsxOgCGtMHOn38/HxMzW1I5efwXB5a4lRN0Vp40gYUJFBWJslcrvwoofTrqoTnLbwTd3pA==} - oxfmt@0.57.0: - resolution: {integrity: sha512-ZB7Bi+rGDSqmVIo9jwcLyFgjxXvQhDdU+jx+ZrVy6VRiVXK2+CHc4hO3J4dUQjHe7V0ymHB+MDuv5z+NhK07HA==} + oxfmt@0.58.0: + resolution: {integrity: sha512-8feG/7NVEHDVwc1OUpP6Pks+TnaDFUw2jLLFIMi5bcmmwxAX2wBQvjSzj62RRTYBf2Op1Wt8xbkmagmPTR5ETg==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -5590,17 +5851,14 @@ packages: pg-cloudflare@1.4.0: resolution: {integrity: sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==} - pg-connection-string@2.12.0: - resolution: {integrity: sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==} - pg-connection-string@2.14.0: resolution: {integrity: sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==} pg-copy-streams@7.0.0: resolution: {integrity: sha512-zBvnY6wtaBRE2ae2xXWOOGMaNVPkXh1vhypAkNSKgMdciJeTyIQAHZaEeRAxUjs/p1El5jgzYmwG5u871Zj3dQ==} - pg-cursor@2.20.0: - resolution: {integrity: sha512-HP/EbUafheaUOs7DxlG6tda/rhmsX2hCTJJJ+gCnhljGyNEs6pBHddbNuomlW3DqEhP3zYD+GqBWkYnJPIZ4tA==} + pg-cursor@2.21.0: + resolution: {integrity: sha512-IYvk/j+Suhtbo/C3uOf4JLsLK/gWxOTUOmYbDsbKnLaVJDq+KwhwK6ngpRfiCk8eDMS3AmGQABZCv0cREEzHQw==} peerDependencies: pg: ^8 @@ -5691,12 +5949,12 @@ packages: resolution: {integrity: sha512-u9mdErTewKSMsr+ceCt8VcNuNP0ro5AXiPXhUVApuEyqr2Zlvt+DdCFBcm+yGWN8mhOdZJ27meIDbnoZgfzpOw==} hasBin: true - postcss@8.4.31: - resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} + postcss@8.5.10: + resolution: {integrity: sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==} engines: {node: ^10 || ^12 || >=14} - postcss@8.5.16: - resolution: {integrity: sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==} + postcss@8.5.19: + resolution: {integrity: sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==} engines: {node: ^10 || ^12 || >=14} postgres-array@2.0.0: @@ -5734,8 +5992,8 @@ packages: postgres-range@1.1.4: resolution: {integrity: sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==} - posthog-node@5.39.4: - resolution: {integrity: sha512-+fCQ7htBFRQQFbIzl1T0TA7bDwYyaB9XP308ZFMCUoB5LzTzOFxBa6TYVrxdH/VQl43WXTp6sf0QsG2Z4XlNBg==} + posthog-node@5.40.0: + resolution: {integrity: sha512-DrLfHuauO0W6qruF80iqr5JdmLysef74XzOB4eh36oRLRhxCySLraTqsi2Pj161LZnp9/JNdRDxwT8ei8VK2YA==} engines: {node: ^20.20.0 || >=22.22.0} peerDependencies: rxjs: ^7.0.0 @@ -6095,6 +6353,10 @@ packages: resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} engines: {node: '>= 18'} + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} @@ -6429,8 +6691,8 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} - toml@4.1.1: - resolution: {integrity: sha512-EBJnVBr3dTXdA89WVFoAIPUqkBjxPMwRqsfuo1r240tKFHXv3zgca4+NJib/h6TyvGF7vOawz0jGuryJCdNHrw==} + toml@4.3.0: + resolution: {integrity: sha512-lVb8X9BsPVuH0M4BKeS91tXAmJvCjQ5UIyAbQFaxkKGyUFK2RPkhwaFSQH8vbpl1d23eu/IBH+dwVMHWaq9A5A==} engines: {node: '>=20'} tough-cookie@5.1.2: @@ -6505,6 +6767,11 @@ packages: engines: {node: '>=14.17'} hasBin: true + typescript@7.0.2: + resolution: {integrity: sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==} + engines: {node: '>=16.20.0'} + hasBin: true + uglify-js@3.19.3: resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} engines: {node: '>=0.8.0'} @@ -6528,8 +6795,8 @@ packages: resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==} engines: {node: '>=20.18.1'} - undici@8.6.0: - resolution: {integrity: sha512-l2FlC6I510GawyEd1qgcE/okihKrzy+BRTEBlu6T0fdbM9m5yxtIH5Oa3ysRsH0zC4EhmWUEaSDsy2QngBeRlw==} + undici@8.7.0: + resolution: {integrity: sha512-N7iQtfyLhIMOFgQubvmLV26svHpO0bqKnAiWotTQCVKCmWrcGbBotPuW1x+xwYZ2VHdSTVUfPQQnlEt1/LouTQ==} engines: {node: '>=22.19.0'} unicode-emoji-modifier-base@1.0.0: @@ -6630,8 +6897,8 @@ packages: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} - uuid@14.0.0: - resolution: {integrity: sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==} + uuid@14.0.1: + resolution: {integrity: sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==} hasBin: true uuid@8.3.2: @@ -6719,20 +6986,20 @@ packages: yaml: optional: true - vitest@4.1.9: - resolution: {integrity: sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==} + vitest@4.1.10: + resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.1.9 - '@vitest/browser-preview': 4.1.9 - '@vitest/browser-webdriverio': 4.1.9 - '@vitest/coverage-istanbul': 4.1.9 - '@vitest/coverage-v8': 4.1.9 - '@vitest/ui': 4.1.9 + '@vitest/browser-playwright': 4.1.10 + '@vitest/browser-preview': 4.1.10 + '@vitest/browser-webdriverio': 4.1.10 + '@vitest/coverage-istanbul': 4.1.10 + '@vitest/coverage-v8': 4.1.10 + '@vitest/ui': 4.1.10 happy-dom: '*' jsdom: '*' vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -6921,44 +7188,44 @@ snapshots: ansi-styles: 6.2.3 is-fullwidth-code-point: 5.1.0 - '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.201': + '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.206': optional: true - '@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.201': + '@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.206': optional: true - '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.201': + '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.206': optional: true - '@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.201': + '@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.206': optional: true - '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.201': + '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.206': optional: true - '@anthropic-ai/claude-agent-sdk-linux-x64@0.3.201': + '@anthropic-ai/claude-agent-sdk-linux-x64@0.3.206': optional: true - '@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.201': + '@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.206': optional: true - '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.201': + '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.206': optional: true - '@anthropic-ai/claude-agent-sdk@0.3.201(@anthropic-ai/sdk@0.110.0(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(zod@4.4.3)': + '@anthropic-ai/claude-agent-sdk@0.3.206(@anthropic-ai/sdk@0.110.0(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(zod@4.4.3)': dependencies: '@anthropic-ai/sdk': 0.110.0(zod@4.4.3) '@modelcontextprotocol/sdk': 1.29.0(zod@4.4.3) zod: 4.4.3 optionalDependencies: - '@anthropic-ai/claude-agent-sdk-darwin-arm64': 0.3.201 - '@anthropic-ai/claude-agent-sdk-darwin-x64': 0.3.201 - '@anthropic-ai/claude-agent-sdk-linux-arm64': 0.3.201 - '@anthropic-ai/claude-agent-sdk-linux-arm64-musl': 0.3.201 - '@anthropic-ai/claude-agent-sdk-linux-x64': 0.3.201 - '@anthropic-ai/claude-agent-sdk-linux-x64-musl': 0.3.201 - '@anthropic-ai/claude-agent-sdk-win32-arm64': 0.3.201 - '@anthropic-ai/claude-agent-sdk-win32-x64': 0.3.201 + '@anthropic-ai/claude-agent-sdk-darwin-arm64': 0.3.206 + '@anthropic-ai/claude-agent-sdk-darwin-x64': 0.3.206 + '@anthropic-ai/claude-agent-sdk-linux-arm64': 0.3.206 + '@anthropic-ai/claude-agent-sdk-linux-arm64-musl': 0.3.206 + '@anthropic-ai/claude-agent-sdk-linux-x64': 0.3.206 + '@anthropic-ai/claude-agent-sdk-linux-x64-musl': 0.3.206 + '@anthropic-ai/claude-agent-sdk-win32-arm64': 0.3.206 + '@anthropic-ai/claude-agent-sdk-win32-x64': 0.3.206 '@anthropic-ai/sdk@0.110.0(zod@4.4.3)': dependencies: @@ -7109,55 +7376,55 @@ snapshots: dependencies: '@noble/ciphers': 1.3.0 - '@effect/atom-react@4.0.0-beta.93(effect@4.0.0-beta.93)(react@19.2.7)(scheduler@0.27.0)': + '@effect/atom-react@4.0.0-beta.94(effect@4.0.0-beta.97)(react@19.2.7)(scheduler@0.27.0)': dependencies: - effect: 4.0.0-beta.93 + effect: 4.0.0-beta.97 react: 19.2.7 scheduler: 0.27.0 - '@effect/platform-bun@4.0.0-beta.93(effect@4.0.0-beta.93)': + '@effect/platform-bun@4.0.0-beta.97(effect@4.0.0-beta.97)': dependencies: - '@effect/platform-node-shared': 4.0.0-beta.93(effect@4.0.0-beta.93) - effect: 4.0.0-beta.93 + '@effect/platform-node-shared': 4.0.0-beta.97(effect@4.0.0-beta.97) + effect: 4.0.0-beta.97 transitivePeerDependencies: - bufferutil - utf-8-validate - '@effect/platform-node-shared@4.0.0-beta.93(effect@4.0.0-beta.93)': + '@effect/platform-node-shared@4.0.0-beta.97(effect@4.0.0-beta.97)': dependencies: '@types/ws': 8.18.1 - effect: 4.0.0-beta.93 + effect: 4.0.0-beta.97 ws: 8.21.0 transitivePeerDependencies: - bufferutil - utf-8-validate - '@effect/platform-node@4.0.0-beta.93(effect@4.0.0-beta.93)(ioredis@5.11.0)': + '@effect/platform-node@4.0.0-beta.97(effect@4.0.0-beta.97)(ioredis@5.11.0)': dependencies: - '@effect/platform-node-shared': 4.0.0-beta.93(effect@4.0.0-beta.93) - effect: 4.0.0-beta.93 + '@effect/platform-node-shared': 4.0.0-beta.97(effect@4.0.0-beta.97) + effect: 4.0.0-beta.97 ioredis: 5.11.0 mime: 4.1.0 - undici: 8.6.0 + undici: 8.7.0 transitivePeerDependencies: - bufferutil - utf-8-validate - '@effect/sql-pg@4.0.0-beta.93(effect@4.0.0-beta.93)': + '@effect/sql-pg@4.0.0-beta.97(effect@4.0.0-beta.97)': dependencies: - effect: 4.0.0-beta.93 + effect: 4.0.0-beta.97 pg: 8.22.0 - pg-connection-string: 2.12.0 - pg-cursor: 2.20.0(pg@8.22.0) + pg-connection-string: 2.14.0 + pg-cursor: 2.21.0(pg@8.22.0) pg-pool: 3.14.0(pg@8.22.0) pg-types: 4.1.0 transitivePeerDependencies: - pg-native - '@effect/vitest@4.0.0-beta.93(effect@4.0.0-beta.93)(vitest@4.1.9)': + '@effect/vitest@4.0.0-beta.97(effect@4.0.0-beta.97)(vitest@4.1.10)': dependencies: - effect: 4.0.0-beta.93 - vitest: 4.1.9(@types/node@26.1.1)(@vitest/coverage-istanbul@4.1.9)(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) + effect: 4.0.0-beta.97 + vitest: 4.1.10(@types/node@26.1.1)(@vitest/coverage-istanbul@4.1.10)(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) '@emnapi/core@1.10.0': dependencies: @@ -7295,22 +7562,22 @@ snapshots: '@esbuild/win32-x64@0.28.1': optional: true - '@floating-ui/core@1.7.5': + '@floating-ui/core@1.8.0': dependencies: - '@floating-ui/utils': 0.2.11 + '@floating-ui/utils': 0.2.12 - '@floating-ui/dom@1.7.6': + '@floating-ui/dom@1.8.0': dependencies: - '@floating-ui/core': 1.7.5 - '@floating-ui/utils': 0.2.11 + '@floating-ui/core': 1.8.0 + '@floating-ui/utils': 0.2.12 - '@floating-ui/react-dom@2.1.8(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@floating-ui/react-dom@2.1.9(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@floating-ui/dom': 1.7.6 + '@floating-ui/dom': 1.8.0 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) - '@floating-ui/utils@0.2.11': {} + '@floating-ui/utils@0.2.12': {} '@fuma-translate/react@1.0.2(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: @@ -7319,7 +7586,7 @@ snapshots: optionalDependencies: '@types/react': 19.2.17 - '@fumadocs/tailwind@0.0.5': {} + '@fumadocs/tailwind@0.1.0': {} '@hono/node-server@1.19.14(hono@4.12.21)': dependencies: @@ -7451,7 +7718,7 @@ snapshots: dependencies: '@types/estree': 1.0.9 '@types/estree-jsx': 1.0.5 - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/mdx': 2.0.14 acorn: 8.17.0 collapse-white-space: 2.1.0 @@ -7568,6 +7835,13 @@ snapshots: '@napi-rs/keyring-win32-ia32-msvc': 1.3.0 '@napi-rs/keyring-win32-x64-msvc': 1.3.0 + '@napi-rs/wasm-runtime@0.2.12': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.2 + '@tybys/wasm-util': 0.10.3 + optional: true + '@napi-rs/wasm-runtime@0.2.4': dependencies: '@emnapi/core': 1.11.1 @@ -7595,30 +7869,30 @@ snapshots: '@tybys/wasm-util': 0.10.3 optional: true - '@next/env@16.2.10': {} + '@next/env@16.3.0-preview.6': {} - '@next/swc-darwin-arm64@16.2.10': + '@next/swc-darwin-arm64@16.3.0-preview.6': optional: true - '@next/swc-darwin-x64@16.2.10': + '@next/swc-darwin-x64@16.3.0-preview.6': optional: true - '@next/swc-linux-arm64-gnu@16.2.10': + '@next/swc-linux-arm64-gnu@16.3.0-preview.6': optional: true - '@next/swc-linux-arm64-musl@16.2.10': + '@next/swc-linux-arm64-musl@16.3.0-preview.6': optional: true - '@next/swc-linux-x64-gnu@16.2.10': + '@next/swc-linux-x64-gnu@16.3.0-preview.6': optional: true - '@next/swc-linux-x64-musl@16.2.10': + '@next/swc-linux-x64-musl@16.3.0-preview.6': optional: true - '@next/swc-win32-arm64-msvc@16.2.10': + '@next/swc-win32-arm64-msvc@16.3.0-preview.6': optional: true - '@next/swc-win32-x64-msvc@16.2.10': + '@next/swc-win32-x64-msvc@16.3.0-preview.6': optional: true '@noble/ciphers@1.3.0': {} @@ -7629,6 +7903,67 @@ snapshots: '@noble/hashes@1.8.0': {} + '@node-rs/xxhash-android-arm-eabi@1.7.6': + optional: true + + '@node-rs/xxhash-android-arm64@1.7.6': + optional: true + + '@node-rs/xxhash-darwin-arm64@1.7.6': + optional: true + + '@node-rs/xxhash-darwin-x64@1.7.6': + optional: true + + '@node-rs/xxhash-freebsd-x64@1.7.6': + optional: true + + '@node-rs/xxhash-linux-arm-gnueabihf@1.7.6': + optional: true + + '@node-rs/xxhash-linux-arm64-gnu@1.7.6': + optional: true + + '@node-rs/xxhash-linux-arm64-musl@1.7.6': + optional: true + + '@node-rs/xxhash-linux-x64-gnu@1.7.6': + optional: true + + '@node-rs/xxhash-linux-x64-musl@1.7.6': + optional: true + + '@node-rs/xxhash-wasm32-wasi@1.7.6': + dependencies: + '@napi-rs/wasm-runtime': 0.2.12 + optional: true + + '@node-rs/xxhash-win32-arm64-msvc@1.7.6': + optional: true + + '@node-rs/xxhash-win32-ia32-msvc@1.7.6': + optional: true + + '@node-rs/xxhash-win32-x64-msvc@1.7.6': + optional: true + + '@node-rs/xxhash@1.7.6': + optionalDependencies: + '@node-rs/xxhash-android-arm-eabi': 1.7.6 + '@node-rs/xxhash-android-arm64': 1.7.6 + '@node-rs/xxhash-darwin-arm64': 1.7.6 + '@node-rs/xxhash-darwin-x64': 1.7.6 + '@node-rs/xxhash-freebsd-x64': 1.7.6 + '@node-rs/xxhash-linux-arm-gnueabihf': 1.7.6 + '@node-rs/xxhash-linux-arm64-gnu': 1.7.6 + '@node-rs/xxhash-linux-arm64-musl': 1.7.6 + '@node-rs/xxhash-linux-x64-gnu': 1.7.6 + '@node-rs/xxhash-linux-x64-musl': 1.7.6 + '@node-rs/xxhash-wasm32-wasi': 1.7.6 + '@node-rs/xxhash-win32-arm64-msvc': 1.7.6 + '@node-rs/xxhash-win32-ia32-msvc': 1.7.6 + '@node-rs/xxhash-win32-x64-msvc': 1.7.6 + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -7641,13 +7976,13 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 - '@nx/devkit@23.0.1(nx@23.0.2(@swc-node/register@1.11.1(@swc/core@1.15.43)(@swc/types@0.1.27)(typescript@6.0.3))(@swc/core@1.15.43))': + '@nx/devkit@23.0.1(nx@23.1.0(@swc-node/register@1.12.0(@swc/core@1.15.43)(@swc/types@0.1.27)(@typescript/typescript6@6.0.2))(@swc/core@1.15.43))': dependencies: '@zkochan/js-yaml': 0.0.7 ejs: 5.0.1 enquirer: 2.3.6 minimatch: 10.2.5 - nx: 23.0.2(@swc-node/register@1.11.1(@swc/core@1.15.43)(@swc/types@0.1.27)(typescript@6.0.3))(@swc/core@1.15.43) + nx: 23.1.0(@swc-node/register@1.12.0(@swc/core@1.15.43)(@swc/types@0.1.27)(@typescript/typescript6@6.0.2))(@swc/core@1.15.43) semver: 7.8.5 tslib: 2.8.1 yargs-parser: 21.1.1 @@ -7655,61 +7990,61 @@ snapshots: '@nx/nx-darwin-arm64@23.0.1': optional: true - '@nx/nx-darwin-arm64@23.0.2': + '@nx/nx-darwin-arm64@23.1.0': optional: true '@nx/nx-darwin-x64@23.0.1': optional: true - '@nx/nx-darwin-x64@23.0.2': + '@nx/nx-darwin-x64@23.1.0': optional: true '@nx/nx-freebsd-x64@23.0.1': optional: true - '@nx/nx-freebsd-x64@23.0.2': + '@nx/nx-freebsd-x64@23.1.0': optional: true '@nx/nx-linux-arm-gnueabihf@23.0.1': optional: true - '@nx/nx-linux-arm-gnueabihf@23.0.2': + '@nx/nx-linux-arm-gnueabihf@23.1.0': optional: true '@nx/nx-linux-arm64-gnu@23.0.1': optional: true - '@nx/nx-linux-arm64-gnu@23.0.2': + '@nx/nx-linux-arm64-gnu@23.1.0': optional: true '@nx/nx-linux-arm64-musl@23.0.1': optional: true - '@nx/nx-linux-arm64-musl@23.0.2': + '@nx/nx-linux-arm64-musl@23.1.0': optional: true '@nx/nx-linux-x64-gnu@23.0.1': optional: true - '@nx/nx-linux-x64-gnu@23.0.2': + '@nx/nx-linux-x64-gnu@23.1.0': optional: true '@nx/nx-linux-x64-musl@23.0.1': optional: true - '@nx/nx-linux-x64-musl@23.0.2': + '@nx/nx-linux-x64-musl@23.1.0': optional: true '@nx/nx-win32-arm64-msvc@23.0.1': optional: true - '@nx/nx-win32-arm64-msvc@23.0.2': + '@nx/nx-win32-arm64-msvc@23.1.0': optional: true '@nx/nx-win32-x64-msvc@23.0.1': optional: true - '@nx/nx-win32-x64-msvc@23.0.2': + '@nx/nx-win32-x64-msvc@23.1.0': optional: true '@octokit/auth-token@6.0.0': {} @@ -7903,61 +8238,61 @@ snapshots: '@oxc-resolver/binding-win32-x64-msvc@11.21.3': optional: true - '@oxfmt/binding-android-arm-eabi@0.57.0': + '@oxfmt/binding-android-arm-eabi@0.58.0': optional: true - '@oxfmt/binding-android-arm64@0.57.0': + '@oxfmt/binding-android-arm64@0.58.0': optional: true - '@oxfmt/binding-darwin-arm64@0.57.0': + '@oxfmt/binding-darwin-arm64@0.58.0': optional: true - '@oxfmt/binding-darwin-x64@0.57.0': + '@oxfmt/binding-darwin-x64@0.58.0': optional: true - '@oxfmt/binding-freebsd-x64@0.57.0': + '@oxfmt/binding-freebsd-x64@0.58.0': optional: true - '@oxfmt/binding-linux-arm-gnueabihf@0.57.0': + '@oxfmt/binding-linux-arm-gnueabihf@0.58.0': optional: true - '@oxfmt/binding-linux-arm-musleabihf@0.57.0': + '@oxfmt/binding-linux-arm-musleabihf@0.58.0': optional: true - '@oxfmt/binding-linux-arm64-gnu@0.57.0': + '@oxfmt/binding-linux-arm64-gnu@0.58.0': optional: true - '@oxfmt/binding-linux-arm64-musl@0.57.0': + '@oxfmt/binding-linux-arm64-musl@0.58.0': optional: true - '@oxfmt/binding-linux-ppc64-gnu@0.57.0': + '@oxfmt/binding-linux-ppc64-gnu@0.58.0': optional: true - '@oxfmt/binding-linux-riscv64-gnu@0.57.0': + '@oxfmt/binding-linux-riscv64-gnu@0.58.0': optional: true - '@oxfmt/binding-linux-riscv64-musl@0.57.0': + '@oxfmt/binding-linux-riscv64-musl@0.58.0': optional: true - '@oxfmt/binding-linux-s390x-gnu@0.57.0': + '@oxfmt/binding-linux-s390x-gnu@0.58.0': optional: true - '@oxfmt/binding-linux-x64-gnu@0.57.0': + '@oxfmt/binding-linux-x64-gnu@0.58.0': optional: true - '@oxfmt/binding-linux-x64-musl@0.57.0': + '@oxfmt/binding-linux-x64-musl@0.58.0': optional: true - '@oxfmt/binding-openharmony-arm64@0.57.0': + '@oxfmt/binding-openharmony-arm64@0.58.0': optional: true - '@oxfmt/binding-win32-arm64-msvc@0.57.0': + '@oxfmt/binding-win32-arm64-msvc@0.58.0': optional: true - '@oxfmt/binding-win32-ia32-msvc@0.57.0': + '@oxfmt/binding-win32-ia32-msvc@0.58.0': optional: true - '@oxfmt/binding-win32-x64-msvc@0.57.0': + '@oxfmt/binding-win32-x64-msvc@0.58.0': optional: true '@oxlint-tsgolint/darwin-arm64@0.24.0': @@ -8109,11 +8444,11 @@ snapshots: '@pnpm/network.ca-file': 1.0.2 config-chain: 1.1.13 - '@posthog/core@1.40.1': + '@posthog/core@1.43.1': dependencies: - '@posthog/types': 1.393.0 + '@posthog/types': 1.397.0 - '@posthog/types@1.393.0': {} + '@posthog/types@1.397.0': {} '@radix-ui/number@1.1.2': {} @@ -8297,7 +8632,7 @@ snapshots: '@radix-ui/react-popper@1.3.3(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@floating-ui/react-dom': 2.1.8(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@floating-ui/react-dom': 2.1.9(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@radix-ui/react-arrow': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) '@radix-ui/react-context': 1.2.0(@types/react@19.2.17)(react@19.2.7) @@ -8517,7 +8852,7 @@ snapshots: '@sec-ant/readable-stream@0.4.1': {} - '@semantic-release/commit-analyzer@13.0.1(semantic-release@25.0.5(typescript@6.0.3))': + '@semantic-release/commit-analyzer@13.0.1(semantic-release@25.0.5(typescript@7.0.2))': dependencies: conventional-changelog-angular: 8.3.1 conventional-changelog-writer: 8.4.0 @@ -8527,13 +8862,13 @@ snapshots: import-from-esm: 2.0.0 lodash-es: 4.18.1 micromatch: 4.0.8 - semantic-release: 25.0.5(typescript@6.0.3) + semantic-release: 25.0.5(typescript@7.0.2) transitivePeerDependencies: - supports-color '@semantic-release/error@4.0.0': {} - '@semantic-release/github@12.0.8(semantic-release@25.0.5(typescript@6.0.3))': + '@semantic-release/github@12.0.8(semantic-release@25.0.5(typescript@7.0.2))': dependencies: '@octokit/core': 7.0.6 '@octokit/plugin-paginate-rest': 14.0.0(@octokit/core@7.0.6) @@ -8549,7 +8884,7 @@ snapshots: lodash-es: 4.18.1 mime: 4.1.0 p-filter: 4.1.0 - semantic-release: 25.0.5(typescript@6.0.3) + semantic-release: 25.0.5(typescript@7.0.2) tinyglobby: 0.2.17 undici: 7.28.0 url-join: 5.0.0 @@ -8557,7 +8892,7 @@ snapshots: - kerberos - supports-color - '@semantic-release/npm@13.1.5(semantic-release@25.0.5(typescript@6.0.3))': + '@semantic-release/npm@13.1.5(semantic-release@25.0.5(typescript@7.0.2))': dependencies: '@actions/core': 3.0.1 '@semantic-release/error': 4.0.0 @@ -8572,11 +8907,11 @@ snapshots: rc: 1.2.8 read-pkg: 10.1.0 registry-auth-token: 5.1.1 - semantic-release: 25.0.5(typescript@6.0.3) + semantic-release: 25.0.5(typescript@7.0.2) semver: 7.8.5 tempy: 3.2.0 - '@semantic-release/release-notes-generator@14.1.1(semantic-release@25.0.5(typescript@6.0.3))': + '@semantic-release/release-notes-generator@14.1.1(semantic-release@25.0.5(typescript@7.0.2))': dependencies: conventional-changelog-angular: 8.3.1 conventional-changelog-writer: 8.4.0 @@ -8586,7 +8921,7 @@ snapshots: import-from-esm: 2.0.0 lodash-es: 4.18.1 read-package-up: 11.0.0 - semantic-release: 25.0.5(typescript@6.0.3) + semantic-release: 25.0.5(typescript@7.0.2) transitivePeerDependencies: - supports-color @@ -8595,7 +8930,7 @@ snapshots: '@shikijs/primitive': 4.3.1 '@shikijs/types': 4.3.1 '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 hast-util-to-html: 9.0.5 '@shikijs/engine-javascript@4.3.1': @@ -8617,7 +8952,7 @@ snapshots: dependencies: '@shikijs/types': 4.3.1 '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@shikijs/themes@4.3.1': dependencies: @@ -8626,7 +8961,7 @@ snapshots: '@shikijs/types@4.3.1': dependencies: '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@shikijs/vscode-textmate@10.0.2': {} @@ -8640,54 +8975,56 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@supabase/auth-js@2.110.0': + '@supabase/auth-js@2.110.2': dependencies: tslib: 2.8.1 - '@supabase/functions-js@2.110.0': + '@supabase/functions-js@2.110.2': dependencies: tslib: 2.8.1 '@supabase/phoenix@0.4.4': {} - '@supabase/postgrest-js@2.110.0': + '@supabase/postgrest-js@2.110.2': dependencies: tslib: 2.8.1 - '@supabase/realtime-js@2.110.0': + '@supabase/realtime-js@2.110.2': dependencies: '@supabase/phoenix': 0.4.4 tslib: 2.8.1 - '@supabase/storage-js@2.110.0': + '@supabase/storage-js@2.110.2': dependencies: iceberg-js: 0.8.1 tslib: 2.8.1 - '@supabase/supabase-js@2.110.0': + '@supabase/supabase-js@2.110.2': dependencies: - '@supabase/auth-js': 2.110.0 - '@supabase/functions-js': 2.110.0 - '@supabase/postgrest-js': 2.110.0 - '@supabase/realtime-js': 2.110.0 - '@supabase/storage-js': 2.110.0 + '@supabase/auth-js': 2.110.2 + '@supabase/functions-js': 2.110.2 + '@supabase/postgrest-js': 2.110.2 + '@supabase/realtime-js': 2.110.2 + '@supabase/storage-js': 2.110.2 - '@swc-node/core@1.14.1(@swc/core@1.15.43)(@swc/types@0.1.27)': + '@swc-node/core@1.15.0(@swc/core@1.15.43)(@swc/types@0.1.27)': dependencies: '@swc/core': 1.15.43 '@swc/types': 0.1.27 - '@swc-node/register@1.11.1(@swc/core@1.15.43)(@swc/types@0.1.27)(typescript@6.0.3)': + '@swc-node/register@1.12.0(@swc/core@1.15.43)(@swc/types@0.1.27)(@typescript/typescript6@6.0.2)': dependencies: - '@swc-node/core': 1.14.1(@swc/core@1.15.43)(@swc/types@0.1.27) + '@node-rs/xxhash': 1.7.6 + '@swc-node/core': 1.15.0(@swc/core@1.15.43)(@swc/types@0.1.27) '@swc-node/sourcemap-support': 0.6.1 '@swc/core': 1.15.43 colorette: 2.0.20 debug: 4.4.3(supports-color@7.2.0) + json-stable-stringify: 1.3.0 oxc-resolver: 11.21.3 pirates: 4.0.7 tslib: 2.8.1 - typescript: 6.0.3 + typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - '@swc/types' - supports-color @@ -8801,6 +9138,10 @@ snapshots: dependencies: '@types/unist': 3.0.3 + '@types/hast@3.0.5': + dependencies: + '@types/unist': 3.0.3 + '@types/mdast@4.0.4': dependencies: '@types/unist': 3.0.3 @@ -8850,38 +9191,102 @@ snapshots: dependencies: '@types/node': 26.1.1 - '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260703.1': + '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260707.2': optional: true - '@typescript/native-preview-darwin-x64@7.0.0-dev.20260703.1': + '@typescript/native-preview-darwin-x64@7.0.0-dev.20260707.2': optional: true - '@typescript/native-preview-linux-arm64@7.0.0-dev.20260703.1': + '@typescript/native-preview-linux-arm64@7.0.0-dev.20260707.2': optional: true - '@typescript/native-preview-linux-arm@7.0.0-dev.20260703.1': + '@typescript/native-preview-linux-arm@7.0.0-dev.20260707.2': optional: true - '@typescript/native-preview-linux-x64@7.0.0-dev.20260703.1': + '@typescript/native-preview-linux-x64@7.0.0-dev.20260707.2': optional: true - '@typescript/native-preview-win32-arm64@7.0.0-dev.20260703.1': + '@typescript/native-preview-win32-arm64@7.0.0-dev.20260707.2': optional: true - '@typescript/native-preview-win32-x64@7.0.0-dev.20260703.1': + '@typescript/native-preview-win32-x64@7.0.0-dev.20260707.2': optional: true - '@typescript/native-preview@7.0.0-dev.20260703.1': + '@typescript/native-preview@7.0.0-dev.20260707.2': optionalDependencies: - '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260703.1 - '@typescript/native-preview-darwin-x64': 7.0.0-dev.20260703.1 - '@typescript/native-preview-linux-arm': 7.0.0-dev.20260703.1 - '@typescript/native-preview-linux-arm64': 7.0.0-dev.20260703.1 - '@typescript/native-preview-linux-x64': 7.0.0-dev.20260703.1 - '@typescript/native-preview-win32-arm64': 7.0.0-dev.20260703.1 - '@typescript/native-preview-win32-x64': 7.0.0-dev.20260703.1 + '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260707.2 + '@typescript/native-preview-darwin-x64': 7.0.0-dev.20260707.2 + '@typescript/native-preview-linux-arm': 7.0.0-dev.20260707.2 + '@typescript/native-preview-linux-arm64': 7.0.0-dev.20260707.2 + '@typescript/native-preview-linux-x64': 7.0.0-dev.20260707.2 + '@typescript/native-preview-win32-arm64': 7.0.0-dev.20260707.2 + '@typescript/native-preview-win32-x64': 7.0.0-dev.20260707.2 + + '@typescript/typescript-aix-ppc64@7.0.2': + optional: true + + '@typescript/typescript-darwin-arm64@7.0.2': + optional: true + + '@typescript/typescript-darwin-x64@7.0.2': + optional: true - '@ungap/structured-clone@1.3.2': {} + '@typescript/typescript-freebsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-freebsd-x64@7.0.2': + optional: true + + '@typescript/typescript-linux-arm64@7.0.2': + optional: true + + '@typescript/typescript-linux-arm@7.0.2': + optional: true + + '@typescript/typescript-linux-loong64@7.0.2': + optional: true + + '@typescript/typescript-linux-mips64el@7.0.2': + optional: true + + '@typescript/typescript-linux-ppc64@7.0.2': + optional: true + + '@typescript/typescript-linux-riscv64@7.0.2': + optional: true + + '@typescript/typescript-linux-s390x@7.0.2': + optional: true + + '@typescript/typescript-linux-x64@7.0.2': + optional: true + + '@typescript/typescript-netbsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-netbsd-x64@7.0.2': + optional: true + + '@typescript/typescript-openbsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-openbsd-x64@7.0.2': + optional: true + + '@typescript/typescript-sunos-x64@7.0.2': + optional: true + + '@typescript/typescript-win32-arm64@7.0.2': + optional: true + + '@typescript/typescript-win32-x64@7.0.2': + optional: true + + '@typescript/typescript6@6.0.2': + dependencies: + '@typescript/old': typescript@6.0.3 + + '@ungap/structured-clone@1.3.3': {} '@vercel/detect-agent@1.2.3': {} @@ -9047,7 +9452,7 @@ snapshots: lodash: 4.18.1 minimatch: 7.4.9 - '@vitest/coverage-istanbul@4.1.9(vitest@4.1.9)': + '@vitest/coverage-istanbul@4.1.10(vitest@4.1.10)': dependencies: '@babel/core': 7.29.7 '@istanbuljs/schema': 0.1.6 @@ -9059,48 +9464,48 @@ snapshots: magicast: 0.5.3 obug: 2.1.1 tinyrainbow: 3.1.0 - vitest: 4.1.9(@types/node@26.1.1)(@vitest/coverage-istanbul@4.1.9)(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) + vitest: 4.1.10(@types/node@26.1.1)(@vitest/coverage-istanbul@4.1.10)(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) transitivePeerDependencies: - supports-color - '@vitest/expect@4.1.9': + '@vitest/expect@4.1.10': dependencies: '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.3 - '@vitest/spy': 4.1.9 - '@vitest/utils': 4.1.9 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.9(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0))': + '@vitest/mocker@4.1.10(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0))': dependencies: - '@vitest/spy': 4.1.9 + '@vitest/spy': 4.1.10 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: vite: 8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0) - '@vitest/pretty-format@4.1.9': + '@vitest/pretty-format@4.1.10': dependencies: tinyrainbow: 3.1.0 - '@vitest/runner@4.1.9': + '@vitest/runner@4.1.10': dependencies: - '@vitest/utils': 4.1.9 + '@vitest/utils': 4.1.10 pathe: 2.0.3 - '@vitest/snapshot@4.1.9': + '@vitest/snapshot@4.1.10': dependencies: - '@vitest/pretty-format': 4.1.9 - '@vitest/utils': 4.1.9 + '@vitest/pretty-format': 4.1.10 + '@vitest/utils': 4.1.10 magic-string: 0.30.21 pathe: 2.0.3 - '@vitest/spy@4.1.9': {} + '@vitest/spy@4.1.10': {} - '@vitest/utils@4.1.9': + '@vitest/utils@4.1.10': dependencies: - '@vitest/pretty-format': 4.1.9 + '@vitest/pretty-format': 4.1.10 convert-source-map: 2.0.0 tinyrainbow: 3.1.0 @@ -9359,6 +9764,13 @@ snapshots: es-errors: 1.3.0 function-bind: 1.1.2 + call-bind@1.0.9: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + call-bound@1.0.4: dependencies: call-bind-apply-helpers: 1.0.2 @@ -9583,14 +9995,14 @@ snapshots: object-assign: 4.1.1 vary: 1.1.2 - cosmiconfig@9.0.2(typescript@6.0.3): + cosmiconfig@9.0.2(typescript@7.0.2): dependencies: env-paths: 2.2.1 import-fresh: 3.3.1 js-yaml: 4.3.0 parse-json: 5.2.0 optionalDependencies: - typescript: 6.0.3 + typescript: 7.0.2 cross-spawn@7.0.6: dependencies: @@ -9638,6 +10050,12 @@ snapshots: defer-to-connect@2.0.1: {} + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + define-lazy-prop@2.0.0: {} delayed-stream@1.0.0: {} @@ -9709,17 +10127,17 @@ snapshots: ee-first@1.1.1: {} - effect@4.0.0-beta.93: + effect@4.0.0-beta.97: dependencies: '@standard-schema/spec': 1.1.0 - fast-check: 4.8.0 + fast-check: 4.9.0 find-my-way-ts: 0.1.6 ini: 7.0.0 kubernetes-types: 1.30.0 - msgpackr: 2.0.2 - multipasta: 0.2.7 - toml: 4.1.1 - uuid: 14.0.0 + msgpackr: 2.0.4 + multipasta: 0.2.8 + toml: 4.3.0 + uuid: 14.0.1 yaml: 2.9.0 ejs@5.0.1: {} @@ -10031,7 +10449,7 @@ snapshots: extsprintf@1.3.0: {} - fast-check@4.8.0: + fast-check@4.9.0: dependencies: pure-rand: 8.4.0 @@ -10173,7 +10591,7 @@ snapshots: fsevents@2.3.3: optional: true - fumadocs-core@16.10.7(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.23.0(react@19.2.7))(next@16.2.10(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3): + fumadocs-core@16.11.1(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.24.0(react@19.2.7))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3): dependencies: '@orama/orama': 3.1.18 estree-util-value-to-estree: 3.5.0 @@ -10195,25 +10613,26 @@ snapshots: optionalDependencies: '@mdx-js/mdx': 3.1.1 '@types/estree-jsx': 1.0.5 - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/mdast': 4.0.4 '@types/react': 19.2.17 - lucide-react: 1.23.0(react@19.2.7) - next: 16.2.10(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + lucide-react: 1.24.0(react@19.2.7) + next: 16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) zod: 4.4.3 transitivePeerDependencies: - supports-color - fumadocs-mdx@15.0.13(@types/mdast@4.0.4)(@types/mdx@2.0.14)(@types/react@19.2.17)(fumadocs-core@16.10.7(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.23.0(react@19.2.7))(next@16.2.10(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3))(next@16.2.10(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(rolldown@1.0.2)(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)): + fumadocs-mdx@15.1.0(@types/mdast@4.0.4)(@types/mdx@2.0.14)(@types/react@19.2.17)(fumadocs-core@16.11.1(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.24.0(react@19.2.7))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(rolldown@1.0.2)(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)): dependencies: '@mdx-js/mdx': 3.1.1 '@standard-schema/spec': 1.1.0 chokidar: 5.0.0 esbuild: 0.28.1 estree-util-value-to-estree: 3.5.0 - fumadocs-core: 16.10.7(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.23.0(react@19.2.7))(next@16.2.10(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3) + fumadocs-core: 16.11.1(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.24.0(react@19.2.7))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3) + github-slugger: 2.0.0 js-yaml: 5.2.1 mdast-util-mdx: 3.0.0 picocolors: 1.1.1 @@ -10229,17 +10648,17 @@ snapshots: '@types/mdast': 4.0.4 '@types/mdx': 2.0.14 '@types/react': 19.2.17 - next: 16.2.10(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + next: 16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react: 19.2.7 rolldown: 1.0.2 vite: 8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0) transitivePeerDependencies: - supports-color - fumadocs-ui@16.10.7(@types/mdx@2.0.14)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(fumadocs-core@16.10.7(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.23.0(react@19.2.7))(next@16.2.10(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3))(next@16.2.10(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + fumadocs-ui@16.11.1(@types/mdx@2.0.14)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(fumadocs-core@16.11.1(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.24.0(react@19.2.7))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7): dependencies: '@fuma-translate/react': 1.0.2(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@fumadocs/tailwind': 0.0.5 + '@fumadocs/tailwind': 0.1.0 '@radix-ui/react-accordion': 1.2.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@radix-ui/react-collapsible': 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@radix-ui/react-dialog': 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -10252,8 +10671,8 @@ snapshots: '@radix-ui/react-tabs': 1.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) class-variance-authority: 0.7.1 cnfast: 0.0.8 - fumadocs-core: 16.10.7(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.23.0(react@19.2.7))(next@16.2.10(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3) - lucide-react: 1.23.0(react@19.2.7) + fumadocs-core: 16.11.1(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.24.0(react@19.2.7))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3) + lucide-react: 1.24.0(react@19.2.7) motion: 12.42.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) next-themes: 0.4.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react: 19.2.7 @@ -10266,10 +10685,9 @@ snapshots: optionalDependencies: '@types/mdx': 2.0.14 '@types/react': 19.2.17 - next: 16.2.10(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + next: 16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7) transitivePeerDependencies: - '@emotion/is-prop-valid' - - '@tailwindcss/oxide' - '@types/react-dom' - tailwindcss @@ -10393,6 +10811,10 @@ snapshots: has-flag@4.0.0: {} + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + has-symbols@1.1.0: {} has-tostringtag@1.0.2: @@ -10405,7 +10827,7 @@ snapshots: hast-util-from-parse5@8.0.3: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/unist': 3.0.3 devlop: 1.1.0 hastscript: 9.0.1 @@ -10416,13 +10838,13 @@ snapshots: hast-util-parse-selector@4.0.0: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 hast-util-raw@9.1.0: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/unist': 3.0.3 - '@ungap/structured-clone': 1.3.2 + '@ungap/structured-clone': 1.3.3 hast-util-from-parse5: 8.0.3 hast-util-to-parse5: 8.0.1 html-void-elements: 3.0.0 @@ -10438,7 +10860,7 @@ snapshots: dependencies: '@types/estree': 1.0.9 '@types/estree-jsx': 1.0.5 - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 comma-separated-tokens: 2.0.3 devlop: 1.1.0 estree-util-attach-comments: 3.0.0 @@ -10457,7 +10879,7 @@ snapshots: hast-util-to-html@9.0.5: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/unist': 3.0.3 ccount: 2.0.1 comma-separated-tokens: 2.0.3 @@ -10472,7 +10894,7 @@ snapshots: hast-util-to-jsx-runtime@2.3.6: dependencies: '@types/estree': 1.0.9 - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/unist': 3.0.3 comma-separated-tokens: 2.0.3 devlop: 1.1.0 @@ -10491,7 +10913,7 @@ snapshots: hast-util-to-parse5@8.0.1: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 comma-separated-tokens: 2.0.3 devlop: 1.1.0 property-information: 7.2.0 @@ -10501,11 +10923,11 @@ snapshots: hast-util-whitespace@3.0.0: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 hastscript@9.0.1: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 comma-separated-tokens: 2.0.3 hast-util-parse-selector: 4.0.0 property-information: 7.2.0 @@ -10741,6 +11163,8 @@ snapshots: isarray@1.0.0: {} + isarray@2.0.5: {} + isexe@2.0.0: {} isstream@0.1.2: {} @@ -10807,6 +11231,14 @@ snapshots: json-schema@0.4.0: {} + json-stable-stringify@1.3.0: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + isarray: 2.0.5 + jsonify: 0.0.1 + object-keys: 1.1.1 + json-stringify-safe@5.0.1: {} json-with-bigint@3.5.8: {} @@ -10821,6 +11253,8 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 + jsonify@0.0.1: {} + jsonparse@1.3.1: {} jsonwebtoken@9.0.3: @@ -10996,7 +11430,7 @@ snapshots: lru-cache@7.18.3: {} - lucide-react@1.23.0(react@19.2.7): + lucide-react@1.24.0(react@19.2.7): dependencies: react: 19.2.7 @@ -11176,9 +11610,9 @@ snapshots: mdast-util-to-hast@13.2.1: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/mdast': 4.0.4 - '@ungap/structured-clone': 1.3.2 + '@ungap/structured-clone': 1.3.3 devlop: 1.1.0 micromark-util-sanitize-uri: 2.0.1 trim-lines: 3.0.1 @@ -11555,11 +11989,11 @@ snapshots: '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.4 optional: true - msgpackr@2.0.2: + msgpackr@2.0.4: optionalDependencies: msgpackr-extract: 3.0.4 - multipasta@0.2.7: {} + multipasta@0.2.8: {} mz@2.7.0: dependencies: @@ -11567,7 +12001,7 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 - nanoid@3.3.15: {} + nanoid@3.3.16: {} negotiator@0.6.3: {} @@ -11584,25 +12018,25 @@ snapshots: react: 19.2.7 react-dom: 19.2.7(react@19.2.7) - next@16.2.10(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7): dependencies: - '@next/env': 16.2.10 + '@next/env': 16.3.0-preview.6 '@swc/helpers': 0.5.15 baseline-browser-mapping: 2.10.42 caniuse-lite: 1.0.30001803 - postcss: 8.4.31 + postcss: 8.5.10 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) styled-jsx: 5.1.6(react@19.2.7) optionalDependencies: - '@next/swc-darwin-arm64': 16.2.10 - '@next/swc-darwin-x64': 16.2.10 - '@next/swc-linux-arm64-gnu': 16.2.10 - '@next/swc-linux-arm64-musl': 16.2.10 - '@next/swc-linux-x64-gnu': 16.2.10 - '@next/swc-linux-x64-musl': 16.2.10 - '@next/swc-win32-arm64-msvc': 16.2.10 - '@next/swc-win32-x64-msvc': 16.2.10 + '@next/swc-darwin-arm64': 16.3.0-preview.6 + '@next/swc-darwin-x64': 16.3.0-preview.6 + '@next/swc-linux-arm64-gnu': 16.3.0-preview.6 + '@next/swc-linux-arm64-musl': 16.3.0-preview.6 + '@next/swc-linux-x64-gnu': 16.3.0-preview.6 + '@next/swc-linux-x64-musl': 16.3.0-preview.6 + '@next/swc-win32-arm64-msvc': 16.3.0-preview.6 + '@next/swc-win32-x64-msvc': 16.3.0-preview.6 sharp: 0.34.5 transitivePeerDependencies: - '@babel/core' @@ -11659,7 +12093,7 @@ snapshots: npm@11.17.0: {} - nx@23.0.1(@swc-node/register@1.11.1(@swc/core@1.15.43)(@swc/types@0.1.27)(typescript@6.0.3))(@swc/core@1.15.43): + nx@23.0.1(@swc-node/register@1.12.0(@swc/core@1.15.43)(@swc/types@0.1.27)(@typescript/typescript6@6.0.2))(@swc/core@1.15.43): dependencies: '@emnapi/core': 1.4.5 '@emnapi/runtime': 1.4.5 @@ -11783,12 +12217,12 @@ snapshots: '@nx/nx-linux-x64-musl': 23.0.1 '@nx/nx-win32-arm64-msvc': 23.0.1 '@nx/nx-win32-x64-msvc': 23.0.1 - '@swc-node/register': 1.11.1(@swc/core@1.15.43)(@swc/types@0.1.27)(typescript@6.0.3) + '@swc-node/register': 1.12.0(@swc/core@1.15.43)(@swc/types@0.1.27)(@typescript/typescript6@6.0.2) '@swc/core': 1.15.43 transitivePeerDependencies: - debug - nx@23.0.2(@swc-node/register@1.11.1(@swc/core@1.15.43)(@swc/types@0.1.27)(typescript@6.0.3))(@swc/core@1.15.43): + nx@23.1.0(@swc-node/register@1.12.0(@swc/core@1.15.43)(@swc/types@0.1.27)(@typescript/typescript6@6.0.2))(@swc/core@1.15.43): dependencies: '@emnapi/core': 1.4.5 '@emnapi/runtime': 1.4.5 @@ -11884,7 +12318,7 @@ snapshots: resolve.exports: 2.0.3 restore-cursor: 3.1.0 safe-buffer: 5.2.1 - semver: 7.7.4 + semver: 7.8.4 signal-exit: 3.0.7 smol-toml: 1.6.1 string-width: 4.2.3 @@ -11906,23 +12340,25 @@ snapshots: yargs: 17.7.2 yargs-parser: 21.1.1 optionalDependencies: - '@nx/nx-darwin-arm64': 23.0.2 - '@nx/nx-darwin-x64': 23.0.2 - '@nx/nx-freebsd-x64': 23.0.2 - '@nx/nx-linux-arm-gnueabihf': 23.0.2 - '@nx/nx-linux-arm64-gnu': 23.0.2 - '@nx/nx-linux-arm64-musl': 23.0.2 - '@nx/nx-linux-x64-gnu': 23.0.2 - '@nx/nx-linux-x64-musl': 23.0.2 - '@nx/nx-win32-arm64-msvc': 23.0.2 - '@nx/nx-win32-x64-msvc': 23.0.2 - '@swc-node/register': 1.11.1(@swc/core@1.15.43)(@swc/types@0.1.27)(typescript@6.0.3) + '@nx/nx-darwin-arm64': 23.1.0 + '@nx/nx-darwin-x64': 23.1.0 + '@nx/nx-freebsd-x64': 23.1.0 + '@nx/nx-linux-arm-gnueabihf': 23.1.0 + '@nx/nx-linux-arm64-gnu': 23.1.0 + '@nx/nx-linux-arm64-musl': 23.1.0 + '@nx/nx-linux-x64-gnu': 23.1.0 + '@nx/nx-linux-x64-musl': 23.1.0 + '@nx/nx-win32-arm64-msvc': 23.1.0 + '@nx/nx-win32-x64-msvc': 23.1.0 + '@swc-node/register': 1.12.0(@swc/core@1.15.43)(@swc/types@0.1.27)(@typescript/typescript6@6.0.2) '@swc/core': 1.15.43 object-assign@4.1.1: {} object-inspect@1.13.4: {} + object-keys@1.1.1: {} + obuf@1.1.2: {} obug@2.1.1: {} @@ -12020,29 +12456,29 @@ snapshots: '@oxc-resolver/binding-win32-arm64-msvc': 11.21.3 '@oxc-resolver/binding-win32-x64-msvc': 11.21.3 - oxfmt@0.57.0: + oxfmt@0.58.0: dependencies: tinypool: 2.1.0 optionalDependencies: - '@oxfmt/binding-android-arm-eabi': 0.57.0 - '@oxfmt/binding-android-arm64': 0.57.0 - '@oxfmt/binding-darwin-arm64': 0.57.0 - '@oxfmt/binding-darwin-x64': 0.57.0 - '@oxfmt/binding-freebsd-x64': 0.57.0 - '@oxfmt/binding-linux-arm-gnueabihf': 0.57.0 - '@oxfmt/binding-linux-arm-musleabihf': 0.57.0 - '@oxfmt/binding-linux-arm64-gnu': 0.57.0 - '@oxfmt/binding-linux-arm64-musl': 0.57.0 - '@oxfmt/binding-linux-ppc64-gnu': 0.57.0 - '@oxfmt/binding-linux-riscv64-gnu': 0.57.0 - '@oxfmt/binding-linux-riscv64-musl': 0.57.0 - '@oxfmt/binding-linux-s390x-gnu': 0.57.0 - '@oxfmt/binding-linux-x64-gnu': 0.57.0 - '@oxfmt/binding-linux-x64-musl': 0.57.0 - '@oxfmt/binding-openharmony-arm64': 0.57.0 - '@oxfmt/binding-win32-arm64-msvc': 0.57.0 - '@oxfmt/binding-win32-ia32-msvc': 0.57.0 - '@oxfmt/binding-win32-x64-msvc': 0.57.0 + '@oxfmt/binding-android-arm-eabi': 0.58.0 + '@oxfmt/binding-android-arm64': 0.58.0 + '@oxfmt/binding-darwin-arm64': 0.58.0 + '@oxfmt/binding-darwin-x64': 0.58.0 + '@oxfmt/binding-freebsd-x64': 0.58.0 + '@oxfmt/binding-linux-arm-gnueabihf': 0.58.0 + '@oxfmt/binding-linux-arm-musleabihf': 0.58.0 + '@oxfmt/binding-linux-arm64-gnu': 0.58.0 + '@oxfmt/binding-linux-arm64-musl': 0.58.0 + '@oxfmt/binding-linux-ppc64-gnu': 0.58.0 + '@oxfmt/binding-linux-riscv64-gnu': 0.58.0 + '@oxfmt/binding-linux-riscv64-musl': 0.58.0 + '@oxfmt/binding-linux-s390x-gnu': 0.58.0 + '@oxfmt/binding-linux-x64-gnu': 0.58.0 + '@oxfmt/binding-linux-x64-musl': 0.58.0 + '@oxfmt/binding-openharmony-arm64': 0.58.0 + '@oxfmt/binding-win32-arm64-msvc': 0.58.0 + '@oxfmt/binding-win32-ia32-msvc': 0.58.0 + '@oxfmt/binding-win32-x64-msvc': 0.58.0 oxlint-tsgolint@0.24.0: optionalDependencies: @@ -12181,13 +12617,11 @@ snapshots: pg-cloudflare@1.4.0: optional: true - pg-connection-string@2.12.0: {} - pg-connection-string@2.14.0: {} pg-copy-streams@7.0.0: {} - pg-cursor@2.20.0(pg@8.22.0): + pg-cursor@2.21.0(pg@8.22.0): dependencies: pg: 8.22.0 @@ -12281,15 +12715,15 @@ snapshots: pkg-pr-new@0.0.75: {} - postcss@8.4.31: + postcss@8.5.10: dependencies: - nanoid: 3.3.15 + nanoid: 3.3.16 picocolors: 1.1.1 source-map-js: 1.2.1 - postcss@8.5.16: + postcss@8.5.19: dependencies: - nanoid: 3.3.15 + nanoid: 3.3.16 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -12315,9 +12749,9 @@ snapshots: postgres-range@1.1.4: {} - posthog-node@5.39.4: + posthog-node@5.40.0: dependencies: - '@posthog/core': 1.40.1 + '@posthog/core': 1.43.1 pretty-ms@9.3.0: dependencies: @@ -12554,14 +12988,14 @@ snapshots: rehype-raw@7.0.0: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 hast-util-raw: 9.1.0 vfile: 6.0.3 rehype-recma@1.0.0: dependencies: '@types/estree': 1.0.9 - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 hast-util-to-estree: 3.1.3 transitivePeerDependencies: - supports-color @@ -12595,7 +13029,7 @@ snapshots: remark-rehype@11.1.2: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/mdast': 4.0.4 mdast-util-to-hast: 13.2.1 unified: 11.0.5 @@ -12699,15 +13133,15 @@ snapshots: dependencies: compute-scroll-into-view: 3.1.1 - semantic-release@25.0.5(typescript@6.0.3): + semantic-release@25.0.5(typescript@7.0.2): dependencies: - '@semantic-release/commit-analyzer': 13.0.1(semantic-release@25.0.5(typescript@6.0.3)) + '@semantic-release/commit-analyzer': 13.0.1(semantic-release@25.0.5(typescript@7.0.2)) '@semantic-release/error': 4.0.0 - '@semantic-release/github': 12.0.8(semantic-release@25.0.5(typescript@6.0.3)) - '@semantic-release/npm': 13.1.5(semantic-release@25.0.5(typescript@6.0.3)) - '@semantic-release/release-notes-generator': 14.1.1(semantic-release@25.0.5(typescript@6.0.3)) + '@semantic-release/github': 12.0.8(semantic-release@25.0.5(typescript@7.0.2)) + '@semantic-release/npm': 13.1.5(semantic-release@25.0.5(typescript@7.0.2)) + '@semantic-release/release-notes-generator': 14.1.1(semantic-release@25.0.5(typescript@7.0.2)) aggregate-error: 5.0.0 - cosmiconfig: 9.0.2(typescript@6.0.3) + cosmiconfig: 9.0.2(typescript@7.0.2) debug: 4.4.3(supports-color@7.2.0) env-ci: 11.2.0 execa: 9.6.1 @@ -12798,6 +13232,15 @@ snapshots: transitivePeerDependencies: - supports-color + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + setprototypeof@1.2.0: {} sharp@0.34.5: @@ -12849,7 +13292,7 @@ snapshots: '@shikijs/themes': 4.3.1 '@shikijs/types': 4.3.1 '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 side-channel-list@1.0.1: dependencies: @@ -13172,7 +13615,7 @@ snapshots: toidentifier@1.0.1: {} - toml@4.1.1: {} + toml@4.3.0: {} tough-cookie@5.1.2: dependencies: @@ -13233,6 +13676,29 @@ snapshots: typescript@6.0.3: {} + typescript@7.0.2: + optionalDependencies: + '@typescript/typescript-aix-ppc64': 7.0.2 + '@typescript/typescript-darwin-arm64': 7.0.2 + '@typescript/typescript-darwin-x64': 7.0.2 + '@typescript/typescript-freebsd-arm64': 7.0.2 + '@typescript/typescript-freebsd-x64': 7.0.2 + '@typescript/typescript-linux-arm': 7.0.2 + '@typescript/typescript-linux-arm64': 7.0.2 + '@typescript/typescript-linux-loong64': 7.0.2 + '@typescript/typescript-linux-mips64el': 7.0.2 + '@typescript/typescript-linux-ppc64': 7.0.2 + '@typescript/typescript-linux-riscv64': 7.0.2 + '@typescript/typescript-linux-s390x': 7.0.2 + '@typescript/typescript-linux-x64': 7.0.2 + '@typescript/typescript-netbsd-arm64': 7.0.2 + '@typescript/typescript-netbsd-x64': 7.0.2 + '@typescript/typescript-openbsd-arm64': 7.0.2 + '@typescript/typescript-openbsd-x64': 7.0.2 + '@typescript/typescript-sunos-x64': 7.0.2 + '@typescript/typescript-win32-arm64': 7.0.2 + '@typescript/typescript-win32-x64': 7.0.2 + uglify-js@3.19.3: optional: true @@ -13246,7 +13712,7 @@ snapshots: undici@7.28.0: {} - undici@8.6.0: {} + undici@8.7.0: {} unicode-emoji-modifier-base@1.0.0: {} @@ -13339,7 +13805,7 @@ snapshots: utils-merge@1.0.1: {} - uuid@14.0.0: {} + uuid@14.0.1: {} uuid@8.3.2: {} @@ -13440,7 +13906,7 @@ snapshots: dependencies: lightningcss: 1.32.0 picomatch: 4.0.5 - postcss: 8.5.16 + postcss: 8.5.19 rolldown: 1.0.2 tinyglobby: 0.2.17 optionalDependencies: @@ -13450,21 +13916,21 @@ snapshots: jiti: 2.7.0 yaml: 2.9.0 - vitest@4.1.9(@types/node@26.1.1)(@vitest/coverage-istanbul@4.1.9)(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)): + vitest@4.1.10(@types/node@26.1.1)(@vitest/coverage-istanbul@4.1.10)(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)): dependencies: - '@vitest/expect': 4.1.9 - '@vitest/mocker': 4.1.9(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) - '@vitest/pretty-format': 4.1.9 - '@vitest/runner': 4.1.9 - '@vitest/snapshot': 4.1.9 - '@vitest/spy': 4.1.9 - '@vitest/utils': 4.1.9 + '@vitest/expect': 4.1.10 + '@vitest/mocker': 4.1.10(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) + '@vitest/pretty-format': 4.1.10 + '@vitest/runner': 4.1.10 + '@vitest/snapshot': 4.1.10 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 es-module-lexer: 2.1.0 expect-type: 1.3.0 magic-string: 0.30.21 obug: 2.1.1 pathe: 2.0.3 - picomatch: 4.0.4 + picomatch: 4.0.5 std-env: 4.1.0 tinybench: 2.9.0 tinyexec: 1.2.4 @@ -13474,7 +13940,7 @@ snapshots: why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 26.1.1 - '@vitest/coverage-istanbul': 4.1.9(vitest@4.1.9) + '@vitest/coverage-istanbul': 4.1.10(vitest@4.1.10) transitivePeerDependencies: - msw diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 1c109e8892..8759edf174 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -12,29 +12,32 @@ allowBuilds: sharp: true catalog: - "@effect/atom-react": "4.0.0-beta.93" - "@effect/platform-bun": "4.0.0-beta.93" - "@effect/platform-node": "4.0.0-beta.93" - "@effect/sql-pg": "4.0.0-beta.93" - "@effect/vitest": "^4.0.0-beta.93" + "@effect/atom-react": "4.0.0-beta.94" + "@effect/platform-bun": "4.0.0-beta.97" + "@effect/platform-node": "4.0.0-beta.97" + "@effect/sql-pg": "4.0.0-beta.97" + "@effect/vitest": "4.0.0-beta.97" "@nx/devkit": "^23.0.0" "@swc-node/register": "^1.10.9" "@swc/core": "^1.15.43" "@tsconfig/bun": "^1.0.10" "@types/bun": "^1.3.14" - "@typescript/native-preview": "7.0.0-dev.20260703.1" - "@vitest/coverage-istanbul": "^4.1.9" - "effect": "4.0.0-beta.93" + "@typescript/native-preview": "7.0.0-dev.20260707.2" + "@vitest/coverage-istanbul": "^4.1.10" + "effect": "4.0.0-beta.97" "knip": "^6.24.0" "nx": "^23.0.0" - "oxfmt": "^0.57.0" + "oxfmt": "^0.58.0" "oxlint": "^1.72.0" "oxlint-tsgolint": "^0.24.0" - "tldts": "^7.4.6" - "vitest": "^4.1.9" + "tldts": "^7.4.8" + "vitest": "^4.1.10" blockExoticSubdeps: true +overrides: + "@effect/platform-node-shared": "4.0.0-beta.97" + minimumReleaseAge: 0 supportedArchitectures: diff --git a/tools/nx-plugins/package.json b/tools/nx-plugins/package.json index 875f079ca7..10615425b1 100644 --- a/tools/nx-plugins/package.json +++ b/tools/nx-plugins/package.json @@ -3,6 +3,7 @@ "private": true, "dependencies": { "@nx/devkit": "catalog:", + "typescript": "npm:@typescript/typescript6@^6.0.2", "vitest": "catalog:" } } From c6e1009a9cb37dc5dcfe0870afec38e851ad5e82 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2026 19:00:05 +0000 Subject: [PATCH 63/79] chore(deps): bump github.com/oapi-codegen/oapi-codegen/v2 from 2.4.1 to 2.7.1 in /apps/cli-go (#5896) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [github.com/oapi-codegen/oapi-codegen/v2](https://github.com/oapi-codegen/oapi-codegen) from 2.4.1 to 2.7.1.

Release notes

Sourced from github.com/oapi-codegen/oapi-codegen/v2's releases.

Security fix for Go code injection

This is a security fix for a code injection vulnerability in v2.7.0, please see:

https://github.com/oapi-codegen/oapi-codegen/security/advisories/GHSA-rjwr-m7qx-3fjr

[!NOTE] A vulnerability like this requires that it is missed in code review and that you then call the malicious method.

Using an init() function could be enough to not require a direct call to the code, and instead rely on you importing the package, but either way, code review should be performed before any oapi-codegen generated code is executed.

We strongly recommend all users to be reviewing changes to their generated code before they execute anything within it, to protect against supply chain attacks or malicious injected code.

This is also why we recommend oapi-codegen generated code is committed to source control.

We're more strict about escaping strings passed into the OpenAPI specification, so that people can't inject Go code into generated code.

The problem was that it was possible to craft a description for server URL's which would emit arbitrary Go code, so if an attacker controlled your specification, they could inject Go code into your generated code which could do something malicious.

v2.7.0: Squashing bugs, many bugs (and adding some features)

Many improvements and even more bug fixes

This v2.7.0 release of oapi-codegen contains quite a bit of internal refactoring, focused on our most historically fragile code paths, which relate to the aggregate types (allOf/anyOf/oneOf), $ref to external specs, enums, and the spec traversal logic missing quite a few leaf nodes where models should have been generated, but were skipped.

The biggest changes are explicitly described in the sections below, and the full list of commits is at the bottom.

Thank you to all contributors, we've been going through all past PR's and updating them and merging where we can, and thanks to all our users for reporting issues that you hit.

I've (@​mromaszewicz) used a lot of LLM help here to scrub through old issues and do some deep internal refactoring to address common problem areas. I intend to continue doing this, since the conditional generation logic is getting quite complicated. When I originally released oapi-codegen, the use case was much simpler, all the models were under #/components/schemas, and all the references to them were in the requests, responses, etc. I never imagine how many things would be external references or unions, and how many complex OpenAPI specifications people would be generating code for. The initial design was never flexible enough to handle that, so ongoing bug fixes are getting increasingly complex due to edge cases. This version has a lot of internal changes you won't see as a user, but the way we handle type generation internally is unifying lots of copy/paste re-implementations into reusable code for consistency. Most of these changes can be done transparently, but some can't, so, onto the changes:

Code generation changes which might require some changes on your end

This release contains three changes, all very narrow in scope, which will require some manual adjustment of your own code. We've decided that these are small enough and uncommon enough not to require opt-in, which causes internal complexity. It's always a judgment call with these. If we got it wrong, we're happy to revisit it in a maintenance release.

Strict-server external response refs require strict-server generation in both packages (#2357)

If your strict-server spec uses an external $ref to a components/responses/... defined in another spec, that other spec must also be generated with strict-server: true. Add it to the source spec's config and regenerate:

# config for the spec being $ref'd
generate:
  models: true
strict-server: true # now required when imported by a strict-server spec

This restores the v2.0.0 behavior that lets you cast response models across package boundaries — the standard pattern for sharing error models (e.g. a common 400) across services. PR #1387 had silently changed the embedded type from N400JSONResponse to the bare externalRef0.N400, so the local and external response structs no longer had matching types and casts stopped compiling.

Many more anonymous inner schemas are now hoisted into top level schemas

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/oapi-codegen/oapi-codegen/v2&package-manager=go_modules&previous-version=2.4.1&new-version=2.7.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/supabase/cli/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/cli-go/go.mod | 15 ++++++++------- apps/cli-go/go.sum | 29 ++++++++++++++++------------- 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/apps/cli-go/go.mod b/apps/cli-go/go.mod index 81765a2e1a..92920337f6 100644 --- a/apps/cli-go/go.mod +++ b/apps/cli-go/go.mod @@ -189,7 +189,7 @@ require ( github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/fzipp/gocyclo v0.6.0 // indirect github.com/gabriel-vasile/mimetype v1.4.13 // indirect - github.com/getkin/kin-openapi v0.131.0 // indirect + github.com/getkin/kin-openapi v0.135.0 // indirect github.com/ghostiam/protogetter v0.3.20 // indirect github.com/go-critic/go-critic v0.14.3 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect @@ -301,7 +301,7 @@ require ( github.com/lib/pq v1.12.3 // indirect github.com/lucasb-eyer/go-colorful v1.4.0 // indirect github.com/macabu/inamedparam v0.2.0 // indirect - github.com/mailru/easyjson v0.9.0 // indirect + github.com/mailru/easyjson v0.9.1 // indirect github.com/manuelarte/embeddedstructfieldcheck v0.4.0 // indirect github.com/manuelarte/funcorder v0.6.0 // indirect github.com/maratori/testableexamples v1.0.1 // indirect @@ -350,10 +350,10 @@ require ( github.com/nishanths/exhaustive v0.12.0 // indirect github.com/nishanths/predeclared v0.2.2 // indirect github.com/nunnatsa/ginkgolinter v0.23.0 // indirect - github.com/oapi-codegen/oapi-codegen/v2 v2.4.1 // indirect + github.com/oapi-codegen/oapi-codegen/v2 v2.7.1 // indirect github.com/oapi-codegen/runtime v1.4.2 // indirect - github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect - github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect + github.com/oasdiff/yaml v0.0.9 // indirect + github.com/oasdiff/yaml3 v0.0.9 // indirect github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect github.com/olekukonko/errors v1.2.0 // indirect github.com/olekukonko/ll v0.1.6 // indirect @@ -398,7 +398,8 @@ require ( github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect github.com/sonatard/noctx v0.5.1 // indirect github.com/sourcegraph/go-diff v0.8.0 // indirect - github.com/speakeasy-api/openapi-overlay v0.9.0 // indirect + github.com/speakeasy-api/jsonpath v0.6.3 // indirect + github.com/speakeasy-api/openapi v1.19.2 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect github.com/stbenjam/no-sprintf-host-port v0.3.1 // indirect @@ -421,6 +422,7 @@ require ( github.com/uudashr/gocognit v1.2.1 // indirect github.com/uudashr/iface v1.4.2 // indirect github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect + github.com/woodsbury/decimal128 v1.4.0 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect @@ -472,7 +474,6 @@ require ( gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect gotest.tools/gotestsum v1.12.2 // indirect honnef.co/go/tools v0.7.0 // indirect k8s.io/api v0.34.1 // indirect diff --git a/apps/cli-go/go.sum b/apps/cli-go/go.sum index 320bc87f37..df61dfd4ad 100644 --- a/apps/cli-go/go.sum +++ b/apps/cli-go/go.sum @@ -371,8 +371,8 @@ github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo= github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA= github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= -github.com/getkin/kin-openapi v0.131.0 h1:NO2UeHnFKRYhZ8wg6Nyh5Cq7dHk4suQQr72a4pMrDxE= -github.com/getkin/kin-openapi v0.131.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaEDsjiKUV8nL58= +github.com/getkin/kin-openapi v0.135.0 h1:751SjYfbiwqukYuVjwYEIKNfrSwS5YpA7DZnKSwQgtg= +github.com/getkin/kin-openapi v0.135.0/go.mod h1:6dd5FJl6RdX4usBtFBaQhk9q62Yb2J0Mk5IhUO/QqFI= github.com/getsentry/sentry-go v0.47.0 h1:AnSMSyrYA5qZCIN/2xpgAAwv63sVULV+vBq37ajouc8= github.com/getsentry/sentry-go v0.47.0/go.mod h1:h+b4VHpKnK7aUXB5wc+KDnPgp9ZtfliRD4eV85FbiSA= github.com/ghostiam/protogetter v0.3.20 h1:oW7OPFit2FxZOpmMRPP9FffU4uUpfeE/rEdE1f+MzD0= @@ -765,8 +765,8 @@ github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i github.com/macabu/inamedparam v0.2.0 h1:VyPYpOc10nkhI2qeNUdh3Zket4fcZjEWe35poddBCpE= github.com/macabu/inamedparam v0.2.0/go.mod h1:+Pee9/YfGe5LJ62pYXqB89lJ+0k5bsR8Wgz/C0Zlq3U= github.com/magiconair/properties v1.5.3/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= -github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= +github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/manuelarte/embeddedstructfieldcheck v0.4.0 h1:3mAIyaGRtjK6EO9E73JlXLtiy7ha80b2ZVGyacxgfww= github.com/manuelarte/embeddedstructfieldcheck v0.4.0/go.mod h1:z8dFSyXqp+fC6NLDSljRJeNQJJDWnY7RoWFzV3PC6UM= github.com/manuelarte/funcorder v0.6.0 h1:0hBngc4fa1IgNiI65A7sFGkMvoMCc878RjqB5V7rWP0= @@ -894,14 +894,14 @@ github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/oapi-codegen/nullable v1.2.0 h1:VflFkDW980KhBPiFF7nWSyjg+r4Obqj8lXipV0UkP5w= github.com/oapi-codegen/nullable v1.2.0/go.mod h1:KUZ3vUzkmEKY90ksAmit2+5juDIhIZhfDl+0PwOQlFY= -github.com/oapi-codegen/oapi-codegen/v2 v2.4.1 h1:ykgG34472DWey7TSjd8vIfNykXgjOgYJZoQbKfEeY/Q= -github.com/oapi-codegen/oapi-codegen/v2 v2.4.1/go.mod h1:N5+lY1tiTDV3V1BeHtOxeWXHoPVeApvsvjJqegfoaz8= +github.com/oapi-codegen/oapi-codegen/v2 v2.7.1 h1:a7Ab7YlpqkVG5HKrTaeFstm32Z5QOnyjnbsCO0jiMYM= +github.com/oapi-codegen/oapi-codegen/v2 v2.7.1/go.mod h1:qzFy6iuobJw/hD1aRILee4G87/ShmhR0xYCwcUtZMCw= github.com/oapi-codegen/runtime v1.4.2 h1:GMxFVYLzoYLua+/KvzgSphkyK1lLTReQI9Vf4hvATKE= github.com/oapi-codegen/runtime v1.4.2/go.mod h1:GwV7hC2hviaMzj+ITfHVRESK5J2W/GefVwIND/bMGvU= -github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY= -github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw= -github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c= -github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= +github.com/oasdiff/yaml v0.0.9 h1:zQOvd2UKoozsSsAknnWoDJlSK4lC0mpmjfDsfqNwX48= +github.com/oasdiff/yaml v0.0.9/go.mod h1:8lvhgJG4xiKPj3HN5lDow4jZHPlx1i7dIwzkdAo6oAM= +github.com/oasdiff/yaml3 v0.0.9 h1:rWPrKccrdUm8J0F3sGuU+fuh9+1K/RdJlWF7O/9yw2g= +github.com/oasdiff/yaml3 v0.0.9/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc= github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0= github.com/olekukonko/errors v1.2.0 h1:10Zcn4GeV59t/EGqJc8fUjtFT/FuUh5bTMzZ1XwmCRo= @@ -1070,8 +1070,10 @@ github.com/sourcegraph/go-diff v0.8.0 h1:ipIyu4cTsLbIrln4l0qtHA3r0a7gyK4ntKjtQyt github.com/sourcegraph/go-diff v0.8.0/go.mod h1:hWlcO7Al+UZStZAP8rBumHpCK5ZHQ5BXsMls8p4+F5E= github.com/spdx/tools-golang v0.5.5 h1:61c0KLfAcNqAjlg6UNMdkwpMernhw3zVRwDZ2x9XOmk= github.com/spdx/tools-golang v0.5.5/go.mod h1:MVIsXx8ZZzaRWNQpUDhC4Dud34edUYJYecciXgrw5vE= -github.com/speakeasy-api/openapi-overlay v0.9.0 h1:Wrz6NO02cNlLzx1fB093lBlYxSI54VRhy1aSutx0PQg= -github.com/speakeasy-api/openapi-overlay v0.9.0/go.mod h1:f5FloQrHA7MsxYg9djzMD5h6dxrHjVVByWKh7an8TRc= +github.com/speakeasy-api/jsonpath v0.6.3 h1:c+QPwzAOdrWvzycuc9HFsIZcxKIaWcNpC+xhOW9rJxU= +github.com/speakeasy-api/jsonpath v0.6.3/go.mod h1:2cXloNuQ+RSXi5HTRaeBh7JEmjRXTiaKpFTdZiL7URI= +github.com/speakeasy-api/openapi v1.19.2 h1:md90tE71/M8jS3cuRlsuWP5Aed4xoG5PSRvXeZgCv/M= +github.com/speakeasy-api/openapi v1.19.2/go.mod h1:UfKa7FqE4jgexJZuj51MmdHAFGmDv0Zaw3+yOd81YKU= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/cast v0.0.0-20150508191742-4d07383ffe94/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg= @@ -1161,6 +1163,8 @@ github.com/vmware-labs/yaml-jsonpath v0.3.2 h1:/5QKeCBGdsInyDCyVNLbXyilb61MXGi9N github.com/vmware-labs/yaml-jsonpath v0.3.2/go.mod h1:U6whw1z03QyqgWdgXxvVnQ90zN1BWz5V+51Ewf8k+rQ= github.com/withfig/autocomplete-tools/packages/cobra v1.2.0 h1:MzD3XeOOSO3mAjOPpF07jFteSKZxsRHvlIcAR9RQzKM= github.com/withfig/autocomplete-tools/packages/cobra v1.2.0/go.mod h1:RoXh7+7qknOXL65uTzdzE1mPxqcPwS7FLCE9K5GfmKo= +github.com/woodsbury/decimal128 v1.4.0 h1:xJATj7lLu4f2oObouMt2tgGiElE5gO6mSWUjQsBgUlc= +github.com/woodsbury/decimal128 v1.4.0/go.mod h1:BP46FUrVjVhdTbKT+XuQh2xfQaGki9LMIRJSFuh6THU= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= @@ -1484,7 +1488,6 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20191026110619-0b21df46bc1d/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From d90c85c95a0cb8c8b30b25f439d877c16db20904 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 18 Jul 2026 00:13:50 +0000 Subject: [PATCH 64/79] chore(deps): bump the npm-major group with 10 updates (#5900) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the npm-major group with 10 updates: | Package | From | To | | --- | --- | --- | | [@anthropic-ai/claude-agent-sdk](https://github.com/anthropics/claude-agent-sdk-typescript) | `0.3.206` | `0.3.207` | | [@anthropic-ai/sdk](https://github.com/anthropics/anthropic-sdk-typescript) | `0.110.0` | `0.111.0` | | [semantic-release](https://github.com/semantic-release/semantic-release) | `25.0.5` | `25.0.6` | | [fumadocs-core](https://github.com/fuma-nama/fumadocs) | `16.11.1` | `16.11.2` | | [fumadocs-ui](https://github.com/fuma-nama/fumadocs) | `16.11.1` | `16.11.2` | | [@effect/atom-react](https://github.com/Effect-TS/effect/tree/HEAD/packages/atom/react) | `4.0.0-beta.94` | `4.0.0-beta.97` | | [@nx/devkit](https://github.com/nrwl/nx/tree/HEAD/packages/devkit) | `23.0.1` | `23.0.2` | | [knip](https://github.com/webpro-nl/knip/tree/HEAD/packages/knip) | `6.25.0` | `6.26.0` | | [nx](https://github.com/nrwl/nx/tree/HEAD/packages/nx) | `23.0.1` | `23.0.2` | | [tldts](https://github.com/remusao/tldts) | `6.1.86` | `7.4.8` | Updates `@anthropic-ai/claude-agent-sdk` from 0.3.206 to 0.3.207
Release notes

Sourced from @​anthropic-ai/claude-agent-sdk's releases.

v0.3.207

What's changed

  • Fixed canUseTool returning {behavior: 'allow'} without updatedInput being rejected as a deny with a raw ZodError message; the tool now runs with the original input per the documented contract
  • The Agent tool's structured result now has a published SDK type (AgentToolCompletedOutput) that matches the emitted object exactly

Update

npm install @anthropic-ai/claude-agent-sdk@0.3.207
# or
yarn add @anthropic-ai/claude-agent-sdk@0.3.207
# or
pnpm add @anthropic-ai/claude-agent-sdk@0.3.207
# or
bun add @anthropic-ai/claude-agent-sdk@0.3.207
Changelog

Sourced from @​anthropic-ai/claude-agent-sdk's changelog.

0.3.207

  • Fixed canUseTool returning {behavior: 'allow'} without updatedInput being rejected as a deny with a raw ZodError message; the tool now runs with the original input per the documented contract
  • The Agent tool's structured result now has a published SDK type (AgentToolCompletedOutput) that matches the emitted object exactly
Commits

Updates `@anthropic-ai/sdk` from 0.110.0 to 0.111.0
Release notes

Sourced from @​anthropic-ai/sdk's releases.

sdk: v0.111.0

0.111.0 (2026-07-10)

Full Changelog: sdk-v0.110.0...sdk-v0.111.0

Features

  • api: add support for dreaming (77b28a6)
  • tools: gate session tool calls on evaluated_permission; bound idle by server stop_reason (68a6d7b)

Chores

  • docs: small updates to field descriptions (e25b885)
  • docs: update model example (a33f3f0)
  • docs: updates to descriptions and examples (eac4bac)
Changelog

Sourced from @​anthropic-ai/sdk's changelog.

0.111.0 (2026-07-10)

Full Changelog: sdk-v0.110.0...sdk-v0.111.0

Features

  • api: add support for dreaming (77b28a6)
  • tools: gate session tool calls on evaluated_permission; bound idle by server stop_reason (68a6d7b)

Chores

  • docs: small updates to field descriptions (e25b885)
  • docs: update model example (a33f3f0)
  • docs: updates to descriptions and examples (eac4bac)
Commits
  • 9e46760 chore: release main
  • 8d461ea feat(api): add support for dreaming
  • 9436e29 codegen metadata
  • 0aec1e7 feat(tools): gate session tool calls on evaluated_permission; bound idle by s...
  • ac2fc67 chore(docs): update model example
  • c8af65d chore(docs): updates to descriptions and examples
  • 2a3a904 codegen metadata
  • 57c56c9 chore(docs): small updates to field descriptions
  • See full diff in compare view

Updates `semantic-release` from 25.0.5 to 25.0.6
Release notes

Sourced from semantic-release's releases.

v25.0.6

25.0.6 (2026-07-10)

Bug Fixes

  • ensure encoded secrets get masked (8e28dd3)
Commits
  • 8e28dd3 fix: ensure encoded secrets get masked
  • f293647 Merge commit from fork
  • a973d20 ci(action): update github/codeql-action action to v4.37.0 (#4241)
  • c62136f chore(deps): update dependency mockserver-client to v7.4.0 (#4240)
  • 9be3e6c chore(deps): lock file maintenance (#4239)
  • 3259ebb chore(deps): update dependency got to v15.1.0 (#4238)
  • 1c17eb5 chore(deps): update dependency mockserver-client to v7.3.0 (#4237)
  • b3da275 chore(deps): update dependency testcontainers to v12.0.4 (#4233)
  • d0467bb chore(deps): update dependency fs-extra to v11.3.6 (#4236)
  • 96fb783 chore(deps): update npm to v11.18.0 (#4235)
  • Additional commits viewable in compare view

Updates `fumadocs-core` from 16.11.1 to 16.11.2
Commits

Updates `fumadocs-ui` from 16.11.1 to 16.11.2
Commits

Updates `@effect/atom-react` from 4.0.0-beta.94 to 4.0.0-beta.97
Changelog

Sourced from @​effect/atom-react's changelog.

4.0.0-beta.97

Patch Changes

  • Updated dependencies []:
    • effect@4.0.0-beta.97

4.0.0-beta.96

Patch Changes

4.0.0-beta.95

Patch Changes

Commits

Updates `@nx/devkit` from 23.0.1 to 23.0.2
Release notes

Sourced from @​nx/devkit's releases.

23.0.2 (2026-07-10)

🚀 Features

  • core: re-add isCacheableTask helper (#36177)

🩹 Fixes

  • bundling: prevent TS6059 when an app imports a workspace lib from source (#36217, #35017)
  • core: prefer module.registerHooks to avoid DEP0205 deprecation warning (#36081)
  • core: prevent nx migrate crash when include=optional filters out the target package (#36087)
  • core: make nx migrate honor preapproved packages and emit a valid temp workspace (#36086)
  • core: skip daemon project-graph recompute on no-op file rewrites (#36082)
  • core: prevent the TUI from auto-selecting a completed task when a batch finishes (#35833)
  • core: deregister pseudo-terminal exit handlers when tasks finish (#36115)
  • core: prevent path traversal / zip-slip in self-hosted remote cache (#36116)
  • core: respect explicit --nxCloud=skip for AI agents in create-nx-workspace (#36131)
  • core: warn when the self-hosted remote cache disables TLS verification (NXC-4593) (#36132, #36116)
  • core: throw actionable error when pnpm .modules.yaml is missing (#35666, #35635)
  • core: support ${configDir} in tsconfig path alias resolution (#36037, #35804)
  • core: prevent non-npm devEngines pin from breaking npm registry lookups (#36020, #35815)
  • core: clarify nx sync remediation messaging and surface spinner output in non-tty (#35747)
  • core: exclude direct-dependency overrides from generated package.json (#36040, #35675)
  • core: apply target defaults when project.json overrides an inferred run-commands target with different commands (#36142, #36067)
  • core: run the nx.bat wrapper for dot-nx setup on windows (#36048)
  • core: speed up bun lockfile parsing (#36198)
  • core: set NX_CLI_SET in batch worker processes (#36214, #36210)
  • core: preserve comments in catalog YAML updates (#35733)
  • core: box JsonFileSet payload to keep HashInstruction small (#36247, #35248, #36244, #36152)
  • core: share workspace fileset hash results instead of deep-cloning per task (#36244, #34971, #34942)
  • core: replace per-input visited clones with undo-log scoping in hash planner (#36248, #35071, #36152, #34971, #36244, #35248, #36247)
  • core: intern hash instructions in a pool and plan with id lists (#36249, #35071, #36152, #36248)
  • core: detect Codex sandbox on Linux to disable daemon and plugin isolation (#36273)
  • devkit: restore prettier v2 support in formatFiles (#36193)
  • graph: prevent project details web view top from being clipped (#36154)
  • js: prevent doubled output paths in buildable library path mappings (#36138, #36079)
  • js: scope incremental type-check .tsbuildinfo per project (#36137, #36113)
  • js: preserve npm allowScripts allowlist in pruned package.json (#36016, #35931)
  • js: resolve catalog references in pruned package.json output (#35805, #35419)
  • js: avoid import locator unicode position panic (#36133, #36128)
  • js: prevent Windows TS6059 rootDir errors in tsc builds (#36184, #35696)
  • js: wait for process tree exit when stopping node executor tasks (#36230)
  • linter: update terminal cli regex for local-dist build (#36201, #36199)
  • maven: de-flake maven e2e by dropping -X debug forks and widening timeout (#36091)
  • misc: bump axios to 1.16.1 (#36120)
  • misc: use default import for chalk in @​nx/workspace output.ts (#35523, #35521, #34111, #21201, #26667)
  • misc: remove duplicate nx init cloud-prompt telemetry event (#36145)
  • nx-cloud: use standard utm params on cloud prompt links (#36227, #36226)
  • nx-dev: remove empty SaaS and Mobile template filters (#36085)
  • react: reserve ports in rspack e2e test to avoid default port collisions (#36090)

... (truncated)

Commits

Updates `knip` from 6.25.0 to 6.26.0
Release notes

Sourced from knip's releases.

Release 6.26.0

  • ci: add path filters (#1871) (4249935adffe1b8eca9570fb325fa19cc8010584) - thanks @​trueberryless!
  • Add CodeRabbit as gold sponsor (1da09fdc8f4d851f3cad30659200018bfb47a5e0)
  • Fix up docs a bit more (39125a7f473e006f61629781bff1ac4a050ca460)
  • Don't report ambient declaration files as unused (aed361c00a82829c2fc80a65a2f7af174316782e)
  • Register oclif command files as entries (3b4d58c9da83d93b6821a2b24f57b919ba886756)
  • Add electron-vite plugin (d92107ea504833b3ee7ad2b695e22d40528b24c3)
  • Add esbuild plugin (ef3b601d957546554de2cb1fbff859f5bdafb741)
  • Ignore more globally available binaries (829298129fdfd1b41a1f8dff5022b9fbbe25f454)
  • Resolve #-imports to source when node condition is unbuilt (resolve #1873) (f2713ed1e499ab286b86615d4b8ec34f711d1bd2)
  • Ignore gh as a globally available binary (a6f0772e5db03a8412a3ac036756779f22d1a27d)
  • Resolve Vitest benchmark files as entries (57429139f7e93f2a4bee35a53d450c4f45dea9ff)
  • Extract shared Vue auto-import machinery into plugins/_vue (73010753d432ae58eb12c7eec9c5723036009749)
  • Scope compiler extensions per workspace (5e6f82b963a83e5801484efbc01a4b6a9ce39ea3)
  • Resolve auto-imported components in the Vue SFC compiler (009aad8f9dab589898e5dba4c85fa41ea57c1a49)
  • Add plugins for the Vue auto-import ecosystem (f638c8302cdd26a63e477344a7cf44ef60af7788)
  • Enable Vue SFC compiler on unplugin-vue and @​vitejs/plugin-vue (9396ab159919147ef81dad16e365e531b32b4b12)
  • Recognize vite-plugin-vue-meta-layouts (same layouts convention) (3ceee89228b0173eddc0466bf50e8181286ffebc)
  • Add vite-plugin-pages and unplugin-icons plugins (9bc17540bcf8eb142474ed84837c1e2cebb7fe73)
  • Add vite-plugin-pwa and @​intlify/unplugin-vue-i18n plugins (45dea0a397ed754c34617ed18deef4dc6f0ec0d4)
  • Dog, food. (e1249ade2558d3801d85e2dc63349d884f3443d4)
  • Update query snapshot (a45941d2db381c04f8a38416533217ee84f77ca7)
  • Don't misread Nitro route types as Vue component auto-imports (43aecd5414eb63bec3637f37ffac0c62a71e7e74)
  • Read oxlint jsPlugins from vite-plus vite.config and .oxlintrc.json (589ffda4e3a7d0c0d772191ad6f8d6e60dd298cf)
  • Add vite-plus plugin for run.tasks and staged scripts (f041c191e4e10dd181ad212b71079e3e10bcf341)
  • Support @​vite-pwa/nuxt PWA config in nuxt.config (8fa7b116690e37484798ad98eca002d26c0c80fe)
  • Add @​vite-pwa/assets-generator plugin (4254f7d53a284a4ea4612f650ae03495bcb0d582)
  • Add @​nuxtjs/i18n plugin (dfb9acbe5a08ebc85562f59a04e7f4bedb4925f1)
  • Read plugin entries from vite.config options and index.html (d533da8cc2b431d206eb7c55323ea51846eca0bb)
  • Fix false positives in VitePress, next-mdx and unplugin-vue-i18n plugins (b99702a76e26b1943a16517f1633de3a0713c196)
  • Respect optional peers in pnpm and Yarn packageExtensions (aab080bbd436494316df045011ab22cbebca06b4)
  • Detect babel plugins in the Storybook config (5dea975adf9b88971ad56434132282a099c7d58c)
  • Add shared AST helpers for imported calls and first property values (c84bb7a87a17f1921c12bb02bcfd43f905cae595)
  • Inline trivial resolveFromAST wrappers and normalize orval and sst (620079d1d07b69a69bbb67b6a6e68963b6c2fc21)
  • Extract inline resolveFromAST implementations to dedicated files (b5231c12f915428067d790187d2dd4c44e2559d4)
  • Normalize resolveFromAST files to a uniform contract (fc1ba0fb45aaf623de678a57bea3b5567ea04750)
  • chore: migrate to Astro 7 and Sätteri (#1875) (f256a5b276c3d69dd5081c591bbb183aacebf766) - thanks @​trueberryless!
  • Show contributor count on docs homepage (6b8454a2841905ce7f08e1a23b54bc17a0854608)
  • Exclude gitignored package entry points (606e5d051b4f0620c776aa23352153b8253bd75b)
  • Add Tauri plugin (199180da79c71acc80865dc65ce89653a26d9579)
  • Add Laravel plugin (197453378f2b60e5143a30ec78b438f02b37c53b)
  • Add Quasar plugin (a220729074bfbb9e7d03ba329e89ad553c3a7799)
Commits
  • 441faf0 Release knip@6.26.0
  • a220729 Add Quasar plugin
  • 1974533 Add Laravel plugin
  • 199180d Add Tauri plugin
  • 606e5d0 Exclude gitignored package entry points
  • fc1ba0f Normalize resolveFromAST files to a uniform contract
  • b5231c1 Extract inline resolveFromAST implementations to dedicated files
  • 620079d Inline trivial resolveFromAST wrappers and normalize orval and sst
  • c84bb7a Add shared AST helpers for imported calls and first property values
  • 5dea975 Detect babel plugins in the Storybook config
  • Additional commits viewable in compare view

Updates `nx` from 23.0.1 to 23.0.2
Release notes

Sourced from nx's releases.

23.0.2 (2026-07-10)

🚀 Features

  • core: re-add isCacheableTask helper (#36177)

🩹 Fixes

  • bundling: prevent TS6059 when an app imports a workspace lib from source (#36217, #35017)
  • core: prefer module.registerHooks to avoid DEP0205 deprecation warning (#36081)
  • core: prevent nx migrate crash when include=optional filters out the target package (#36087)
  • core: make nx migrate honor preapproved packages and emit a valid temp workspace (#36086)
  • core: skip daemon project-graph recompute on no-op file rewrites (#36082)
  • core: prevent the TUI from auto-selecting a completed task when a batch finishes (#35833)
  • core: deregister pseudo-terminal exit handlers when tasks finish (#36115)
  • core: prevent path traversal / zip-slip in self-hosted remote cache (#36116)
  • core: respect explicit --nxCloud=skip for AI agents in create-nx-workspace (#36131)
  • core: warn when the self-hosted remote cache disables TLS verification (NXC-4593) (#36132, #36116)
  • core: throw actionable error when pnpm .modules.yaml is missing (#35666, #35635)
  • core: support ${configDir} in tsconfig path alias resolution (#36037, #35804)
  • core: prevent non-npm devEngines pin from breaking npm registry lookups (#36020, #35815)
  • core: clarify nx sync remediation messaging and surface spinner output in non-tty (#35747)
  • core: exclude direct-dependency overrides from generated package.json (#36040, #35675)
  • core: apply target defaults when project.json overrides an inferred run-commands target with different commands (#36142, #36067)
  • core: run the nx.bat wrapper for dot-nx setup on windows (#36048)
  • core: speed up bun lockfile parsing (#36198)
  • core: set NX_CLI_SET in batch worker processes (#36214, #36210)
  • core: preserve comments in catalog YAML updates (#35733)
  • core: box JsonFileSet payload to keep HashInstruction small (#36247, #35248, #36244, #36152)
  • core: share workspace fileset hash results instead of deep-cloning per task (#36244, #34971, #34942)
  • core: replace per-input visited clones with undo-log scoping in hash planner (#36248, #35071, #36152, #34971, #36244, #35248, #36247)
  • core: intern hash instructions in a pool and plan with id lists (#36249, #35071, #36152, #36248)
  • core: detect Codex sandbox on Linux to disable daemon and plugin isolation (#36273)
  • devkit: restore prettier v2 support in formatFiles (#36193)
  • graph: prevent project details web view top from being clipped (#36154)
  • js: prevent doubled output paths in buildable library path mappings (#36138, #36079)
  • js: scope incremental type-check .tsbuildinfo per project (#36137, #36113)
  • js: preserve npm allowScripts allowlist in pruned package.json (#36016, #35931)
  • js: resolve catalog references in pruned package.json output (#35805, #35419)
  • js: avoid import locator unicode position panic (#36133, #36128)
  • js: prevent Windows TS6059 rootDir errors in tsc builds (#36184, #35696)
  • js: wait for process tree exit when stopping node executor tasks (#36230)
  • linter: update terminal cli regex for local-dist build (#36201, #36199)
  • maven: de-flake maven e2e by dropping -X debug forks and widening timeout (#36091)
  • misc: bump axios to 1.16.1 (#36120)
  • misc: use default import for chalk in @​nx/workspace output.ts (#35523, #35521, #34111, #21201, #26667)
  • misc: remove duplicate nx init cloud-prompt telemetry event (#36145)
  • nx-cloud: use standard utm params on cloud prompt links (#36227, #36226)
  • nx-dev: remove empty SaaS and Mobile template filters (#36085)
  • react: reserve ports in rspack e2e test to avoid default port collisions (#36090)

... (truncated)

Commits
  • e095537 chore(core): apply cargo fmt to tasks_list.rs
  • c02ed72 fix(core): detect Codex sandbox on Linux to disable daemon and plugin isolati...
  • a60514e docs(misc): refresh angular docs pages and fix broken angular urls (#36276)
  • 15e01d7 fix(core): intern hash instructions in a pool and plan with id lists (#36249)
  • 3524863 fix(core): replace per-input visited clones with undo-log scoping in hash pla...
  • 033a94a fix(core): share workspace fileset hash results instead of deep-cloning per t...
  • bf79d75 fix(core): box JsonFileSet payload to keep HashInstruction small (#36247)
  • fd4b65d fix(core): preserve comments in catalog YAML updates (#35733)
  • 2970003 fix(nx-cloud): use standard utm params on cloud prompt links (#36227)
  • f853ab3 cleanup(core): reduce redundant work in project graph construction (#36224)
  • Additional commits viewable in compare view

Updates `tldts` from 6.1.86 to 7.4.8
Release notes

Sourced from tldts's releases.

v7.4.8

:scroll: Update Public Suffix List

  • tldts-experimental, tldts

Authors: 1

v7.4.7

:scroll: Update Public Suffix List

  • tldts-experimental, tldts

Authors: 1

v7.4.6

:scroll: Update Public Suffix List

  • tldts-experimental, tldts

:nut_and_bolt: Dependencies

Authors: 2

v7.4.5

:scroll: Update Public Suffix List

  • tldts-experimental, tldts

... (truncated)

Changelog

Sourced from tldts's changelog.

v7.4.8 (Thu Jul 09 2026)

:scroll: Update Public Suffix List

  • tldts-experimental, tldts

Authors: 1


v7.4.7 (Tue Jul 07 2026)

:scroll: Update Public Suffix List

  • tldts-experimental, tldts

Authors: 1


v7.4.6 (Thu Jul 02 2026)

:scroll: Update Public Suffix List

  • tldts-experimental, tldts

:nut_and_bolt: Dependencies

Authors: 2

... (truncated)

Commits
Maintainer changes

This version was pushed to npm by GitHub Actions, a new releaser for tldts since your current version.


Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/cli/package.json | 6 +- apps/docs/package.json | 4 +- pnpm-lock.yaml | 570 +++++++++++++---------------------------- pnpm-workspace.yaml | 8 +- 4 files changed, 183 insertions(+), 405 deletions(-) diff --git a/apps/cli/package.json b/apps/cli/package.json index cee00a9052..22eff58160 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -43,8 +43,8 @@ "jose": "^6.2.3" }, "devDependencies": { - "@anthropic-ai/claude-agent-sdk": "^0.3.206", - "@anthropic-ai/sdk": "^0.110.0", + "@anthropic-ai/claude-agent-sdk": "^0.3.207", + "@anthropic-ai/sdk": "^0.111.0", "@clack/prompts": "^1.7.0", "@effect/atom-react": "catalog:", "@effect/platform-bun": "catalog:", @@ -79,7 +79,7 @@ "posthog-node": "^5.40.0", "react": "^19.2.7", "react-devtools-core": "^7.0.1", - "semantic-release": "^25.0.5", + "semantic-release": "^25.0.6", "smol-toml": "^1.7.0", "tldts": "catalog:", "vitest": "catalog:", diff --git a/apps/docs/package.json b/apps/docs/package.json index 6d71e288bb..f0da534a12 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -8,9 +8,9 @@ "build": "bun run generate && next build" }, "dependencies": { - "fumadocs-core": "^16.11.1", + "fumadocs-core": "^16.11.2", "fumadocs-mdx": "^15.1.0", - "fumadocs-ui": "^16.11.1", + "fumadocs-ui": "^16.11.2", "next": "16.3.0-preview.6", "react": "^19.2.7", "react-dom": "^19.2.7" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 500f87ad3b..86dcc23439 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,8 +7,8 @@ settings: catalogs: default: '@effect/atom-react': - specifier: 4.0.0-beta.94 - version: 4.0.0-beta.94 + specifier: 4.0.0-beta.97 + version: 4.0.0-beta.97 '@effect/platform-bun': specifier: 4.0.0-beta.97 version: 4.0.0-beta.97 @@ -22,8 +22,8 @@ catalogs: specifier: 4.0.0-beta.97 version: 4.0.0-beta.97 '@nx/devkit': - specifier: ^23.0.0 - version: 23.0.1 + specifier: ^23.0.2 + version: 23.1.0 '@swc-node/register': specifier: ^1.10.9 version: 1.12.0 @@ -46,11 +46,11 @@ catalogs: specifier: 4.0.0-beta.97 version: 4.0.0-beta.97 knip: - specifier: ^6.24.0 - version: 6.25.0 + specifier: ^6.26.0 + version: 6.27.0 nx: - specifier: ^23.0.0 - version: 23.0.1 + specifier: ^23.0.2 + version: 23.1.0 oxfmt: specifier: ^0.58.0 version: 0.58.0 @@ -85,7 +85,7 @@ importers: version: typescript@7.0.2 nx: specifier: 'catalog:' - version: 23.0.1(@swc-node/register@1.12.0(@swc/core@1.15.43)(@swc/types@0.1.27)(@typescript/typescript6@6.0.2))(@swc/core@1.15.43) + version: 23.1.0(@swc-node/register@1.12.0(@swc/core@1.15.43)(@swc/types@0.1.27)(@typescript/typescript6@6.0.2))(@swc/core@1.15.43) pkg-pr-new: specifier: 0.0.75 version: 0.0.75 @@ -106,17 +106,17 @@ importers: version: 6.2.3 devDependencies: '@anthropic-ai/claude-agent-sdk': - specifier: ^0.3.206 - version: 0.3.206(@anthropic-ai/sdk@0.110.0(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(zod@4.4.3) + specifier: ^0.3.207 + version: 0.3.207(@anthropic-ai/sdk@0.111.0(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(zod@4.4.3) '@anthropic-ai/sdk': - specifier: ^0.110.0 - version: 0.110.0(zod@4.4.3) + specifier: ^0.111.0 + version: 0.111.0(zod@4.4.3) '@clack/prompts': specifier: ^1.7.0 version: 1.7.0 '@effect/atom-react': specifier: 'catalog:' - version: 4.0.0-beta.94(effect@4.0.0-beta.97)(react@19.2.7)(scheduler@0.27.0) + version: 4.0.0-beta.97(effect@4.0.0-beta.97)(react@19.2.7)(scheduler@0.27.0) '@effect/platform-bun': specifier: 'catalog:' version: 4.0.0-beta.97(effect@4.0.0-beta.97) @@ -188,7 +188,7 @@ importers: version: 5.0.0(ink@7.1.0(@types/react@19.2.17)(react-devtools-core@7.0.1)(react@19.2.7))(react@19.2.7) knip: specifier: 'catalog:' - version: 6.25.0 + version: 6.27.0 oxfmt: specifier: 'catalog:' version: 0.58.0 @@ -214,8 +214,8 @@ importers: specifier: ^7.0.1 version: 7.0.1 semantic-release: - specifier: ^25.0.5 - version: 25.0.5(typescript@7.0.2) + specifier: ^25.0.6 + version: 25.0.6(typescript@7.0.2) smol-toml: specifier: ^1.7.0 version: 1.7.0 @@ -274,7 +274,7 @@ importers: version: 4.1.10(vitest@4.1.10) knip: specifier: 'catalog:' - version: 6.25.0 + version: 6.27.0 oxfmt: specifier: 'catalog:' version: 0.58.0 @@ -291,14 +291,14 @@ importers: apps/docs: dependencies: fumadocs-core: - specifier: ^16.11.1 - version: 16.11.1(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.24.0(react@19.2.7))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3) + specifier: ^16.11.2 + version: 16.11.2(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.25.0(react@19.2.7))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3) fumadocs-mdx: specifier: ^15.1.0 - version: 15.1.0(@types/mdast@4.0.4)(@types/mdx@2.0.14)(@types/react@19.2.17)(fumadocs-core@16.11.1(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.24.0(react@19.2.7))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(rolldown@1.0.2)(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) + version: 15.1.0(@types/mdast@4.0.4)(@types/mdx@2.0.14)(@types/react@19.2.17)(fumadocs-core@16.11.2(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.25.0(react@19.2.7))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(rolldown@1.0.2)(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) fumadocs-ui: - specifier: ^16.11.1 - version: 16.11.1(@types/mdx@2.0.14)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(fumadocs-core@16.11.1(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.24.0(react@19.2.7))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + specifier: ^16.11.2 + version: 16.11.2(@types/mdx@2.0.14)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(fumadocs-core@16.11.2(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.25.0(react@19.2.7))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) next: specifier: 16.3.0-preview.6 version: 16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -354,7 +354,7 @@ importers: version: 4.1.10(vitest@4.1.10) knip: specifier: 'catalog:' - version: 6.25.0 + version: 6.27.0 oxfmt: specifier: 'catalog:' version: 0.58.0 @@ -396,7 +396,7 @@ importers: version: 4.1.10(vitest@4.1.10) knip: specifier: 'catalog:' - version: 6.25.0 + version: 6.27.0 oxfmt: specifier: 'catalog:' version: 0.58.0 @@ -446,7 +446,7 @@ importers: version: 4.1.10(vitest@4.1.10) knip: specifier: 'catalog:' - version: 6.25.0 + version: 6.27.0 oxfmt: specifier: 'catalog:' version: 0.58.0 @@ -486,7 +486,7 @@ importers: version: 4.1.10(vitest@4.1.10) knip: specifier: 'catalog:' - version: 6.25.0 + version: 6.27.0 oxfmt: specifier: 'catalog:' version: 0.58.0 @@ -538,7 +538,7 @@ importers: version: 4.1.10(vitest@4.1.10) knip: specifier: 'catalog:' - version: 6.25.0 + version: 6.27.0 oxfmt: specifier: 'catalog:' version: 0.58.0 @@ -556,7 +556,7 @@ importers: dependencies: '@nx/devkit': specifier: 'catalog:' - version: 23.0.1(nx@23.1.0(@swc-node/register@1.12.0(@swc/core@1.15.43)(@swc/types@0.1.27)(@typescript/typescript6@6.0.2))(@swc/core@1.15.43)) + version: 23.1.0(nx@23.1.0(@swc-node/register@1.12.0(@swc/core@1.15.43)(@swc/types@0.1.27)(@typescript/typescript6@6.0.2))(@swc/core@1.15.43)) typescript: specifier: npm:@typescript/typescript6@^6.0.2 version: '@typescript/typescript6@6.0.2' @@ -582,60 +582,60 @@ packages: resolution: {integrity: sha512-p+CMKJ93HFmLkjXKlXiVGlMQEuRb6H0MokBSwUsX+S6BRX8eV5naFZpQJFfJHjRZY0Hmnqy1/r6UWl3x+19zYA==} engines: {node: '>=18'} - '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.206': - resolution: {integrity: sha512-FL2+NKcMMN47vcnCW2Fkt3AOgeRRlQxrisbPNaxrxqPJFzhUKs17x5j0XzLefd0xRbDAr74hd0PK/tnp6PHM6w==} + '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.207': + resolution: {integrity: sha512-08xSo1FDx8h0aLhL5tvcRxa2SMmcUV3aDWeZiEJVTclyiDAs61BgTjAxCg+SZcu1CndjJO8cfO0yM5dhamxz3g==} cpu: [arm64] os: [darwin] - '@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.206': - resolution: {integrity: sha512-mRW8PPMfQN15EunLwpdmcVzk3XuM4DXQUM8DOzaeA1Hr1yxYPaVVBLr9hkdBKOqr8XTl3ueoTR6RZAy6a/n7OA==} + '@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.207': + resolution: {integrity: sha512-1o7K4EYqyCixZ/oeOZSh7AzSy6TM86xoOuf4VuORjPSS31hBnoqY0NGZd27+2VDs9LGtsdksmsTqcNGx9xd1hA==} cpu: [x64] os: [darwin] - '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.206': - resolution: {integrity: sha512-aMZe1Kl+kYv5QlA15W9Ae25MAzBsA9FA40f5TOtJebA+M/xliF0r2LLb2NdyBviiZCDllcE31l0zgYE0rQqFQw==} + '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.207': + resolution: {integrity: sha512-oPj+g2DslhH4Y9nCTs7al4t9wZv78FZwLFQwOCg99BXuz1o0ZOpKmxyvR7J9eBR+GPszeMMS8gYplQTiZC9o2w==} cpu: [arm64] os: [linux] libc: [musl] - '@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.206': - resolution: {integrity: sha512-Id6H8l6EsGb7849EAZDOB4Ic+FQpbzt4D5HGOwp59CTW3o/1c4etjQA/Kl1K+DSEWn3FGo6D5UMVgc/rwhBj8g==} + '@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.207': + resolution: {integrity: sha512-X4uezYOifDiNTTmmugfRCdg3nNamrr1LFRY9hg30vWYTShL+bbN+nfC3KaFfSYCl4GTtsEEUbYdOTC2F3bBpcA==} cpu: [arm64] os: [linux] libc: [glibc] - '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.206': - resolution: {integrity: sha512-xQBOBhlcmTNc7YeYT5qLZOikpSq3WHlsJ/t6i7kJqUWrcXlOPaAoSMdKp5xO/V0CjAdzdJoWB88I55UAh8mclQ==} + '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.207': + resolution: {integrity: sha512-uRv+D5oG/7EYr41FAJ9IPo2pZYBe2ZMaA6nSHCeizsgPxCSMtl5bNppmU21+jZJvo4hivObEgkGFERAhdGqygg==} cpu: [x64] os: [linux] libc: [musl] - '@anthropic-ai/claude-agent-sdk-linux-x64@0.3.206': - resolution: {integrity: sha512-egZhOC1RlEVhZyq6Oa1b04AF7hh4hO+8oCsJCZ3gOifJQuHih1oW3lZ8fOUxU85jlT2ytAnt5kRp4uQr6PJgbg==} + '@anthropic-ai/claude-agent-sdk-linux-x64@0.3.207': + resolution: {integrity: sha512-Kg6BPH8Ee0ny/oEUWJmvT1jCRBne4jVpRSOMsJcYp1Fav1rMEgpU219oJJs+LWwx4ifuuLtNWedqJNnVw7mnKg==} cpu: [x64] os: [linux] libc: [glibc] - '@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.206': - resolution: {integrity: sha512-oRQk23bFXSz4QRhOxqnnvlLWq/KiF2PtSBYXrMg1AQrCOHJd2k66aOAS7AJ4ZSzejiyV4N6sS6UThfmF6ipHBQ==} + '@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.207': + resolution: {integrity: sha512-9fWpUzfkXlPAg2tf8JpQe7w9avFaomAUbfAwyAmykQgSIf66LwaJjvI5hNqhNqczRKyfsXPn3ei2S5HKlmFP+Q==} cpu: [arm64] os: [win32] - '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.206': - resolution: {integrity: sha512-BdjKmDojZjc5RjN+8Q6K7Yqf1WYhel6I3DkMXc9zdxS/xIuN42sx7zgQpdMGY73pm7ROPLqo3LieOeMhVH161w==} + '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.207': + resolution: {integrity: sha512-YPjVT0q6aXEM2MgN4CI6/9fqiTXwETji+4NoPOzCYuqAkhXZqp30Jsk7/NHqYGNNSfURKrsuAoliKB0rsbpbjg==} cpu: [x64] os: [win32] - '@anthropic-ai/claude-agent-sdk@0.3.206': - resolution: {integrity: sha512-KljDh9Pg4YCYpoXS8dnWoVSsOHtU4yLCW268K2iOruSxFXE8/Tay6DPvmJzYuqjP5YLNYfj05ZGykwZSUn6GXA==} + '@anthropic-ai/claude-agent-sdk@0.3.207': + resolution: {integrity: sha512-y0PkQRmQBi96MHiN5Xzfq+GaddxCZCqI/cXEQBLYBLXGa4i1nDSlulQqkMBj2RorrrSGQJ6Wdw+uhu6OfHNPzA==} engines: {node: '>=18.0.0'} peerDependencies: '@anthropic-ai/sdk': '>=0.93.0' '@modelcontextprotocol/sdk': ^1.29.0 zod: ^4.0.0 - '@anthropic-ai/sdk@0.110.0': - resolution: {integrity: sha512-hOP4bNYXDFHDxxiEgzlILXrxZIYCDnhe8sry0RDRKD/QnsEpvZcQpablCdm9X/WuD/YgOiSIkkqsL1mLLlTqJw==} + '@anthropic-ai/sdk@0.111.0': + resolution: {integrity: sha512-1hUqKi+uJQoS5X90+InwHbFAXMvgq0DnsC5hVLEeSRaODiU5WvmqDAcVCmGS2wC0pN9Z8jtWCbWw7JLzeDdm/Q==} hasBin: true peerDependencies: zod: ^3.25.0 || ^4.0.0 @@ -736,10 +736,10 @@ packages: peerDependencies: '@noble/ciphers': ^1.0.0 - '@effect/atom-react@4.0.0-beta.94': - resolution: {integrity: sha512-6A3Dhw8dC5f5ThuUH+JENgUZjrXdsiTTWWfgOr7gB4Rjj5JS4Ws5jzqbu+fpXlyBhwl3bEd+VmiyXNOOgtcslQ==} + '@effect/atom-react@4.0.0-beta.97': + resolution: {integrity: sha512-yKj5KqgDuAayxkVdXzt0kJaqob0QLozqepPVhGj/I0QPIvGSEfX3OaBGt09irGMyqbP4CKMGtiewyHwRwPZSsw==} peerDependencies: - effect: ^4.0.0-beta.94 + effect: ^4.0.0-beta.97 react: ^19.2.4 scheduler: '*' @@ -781,6 +781,9 @@ packages: '@emnapi/core@1.11.1': resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} + '@emnapi/core@1.11.2': + resolution: {integrity: sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA==} + '@emnapi/core@1.4.5': resolution: {integrity: sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==} @@ -1489,114 +1492,60 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@nx/devkit@23.0.1': - resolution: {integrity: sha512-A/chuNS1RZwdbRe/Nf+w0qtPEFHLcZNPzo8Abw5mBxyXmy9yvHZpuZuqDbt/lASFU+TEb74xExL1AnKWwqpOIg==} + '@nx/devkit@23.1.0': + resolution: {integrity: sha512-4LPBrjwtretBQksqph2aXqz2Atd5+oO7RTVlQoOweU8Sh2A0wM8CNig8n+LX2iwxniPYRooLN4kRtKxO4mi2Mw==} peerDependencies: nx: '>= 22 <= 24 || ^23.0.0-0' - '@nx/nx-darwin-arm64@23.0.1': - resolution: {integrity: sha512-gQJvgPnbI91DBe23Th2CqD9R/S54cPS3C1f0DhyQ8YEf9rR7EEc+sVGjhgVxlhfOk2W7I1Gy6EkXwpN4aDoW4w==} - cpu: [arm64] - os: [darwin] - '@nx/nx-darwin-arm64@23.1.0': resolution: {integrity: sha512-wIFH5p0AXLxbYUHgYEH/zpWpPTQ9LpqESo/GcOPXkSBtUkSAZcqXZ6jmFzTPqGAeP4PBcVZIetO0TXmSSyEO7Q==} cpu: [arm64] os: [darwin] - '@nx/nx-darwin-x64@23.0.1': - resolution: {integrity: sha512-e/lvzHKN6gpuD7MqEtfH1fOfnR75E55ytYNt8jaRxKI6EvpCq+Q3MunDuh9GQYAkqDrUqE7AhHrHc+eKATVEHw==} - cpu: [x64] - os: [darwin] - '@nx/nx-darwin-x64@23.1.0': resolution: {integrity: sha512-D7fEW3+agyb0cWGi8qgN/0irHUwcO8nsuYOrRJjphtWE6J76EORW5sMY93/YFfnLeFVhBMBNVrE6aj2Y4wYCbA==} cpu: [x64] os: [darwin] - '@nx/nx-freebsd-x64@23.0.1': - resolution: {integrity: sha512-f582OhSYN9qHpA9Ox9qnr3kZSZ7gQHs7crmBUutmbXmZQB2TDS/TlhvYSNnxudpwHR/tuWGi2IOQqa7zGOZj1Q==} - cpu: [x64] - os: [freebsd] - '@nx/nx-freebsd-x64@23.1.0': resolution: {integrity: sha512-yZoZyc9t3ViaA9WWDe5F7IlJ+58Hqdn6gqoO9UG2FH3Io1mj+KpF3jhUoj3S1m18D28MN2oItS762QIt6otO3g==} cpu: [x64] os: [freebsd] - '@nx/nx-linux-arm-gnueabihf@23.0.1': - resolution: {integrity: sha512-VjhqPc6E7aiI0e+lowrkVbdyulsmP9fgMdcX1mCzXCEu/XZDcUbZ5qveR964cMhvm5qKn0ILJtJOUqZgmOT3Xg==} - cpu: [arm] - os: [linux] - '@nx/nx-linux-arm-gnueabihf@23.1.0': resolution: {integrity: sha512-D6QhBsLRUmwCc1jSjmY5MkyKOSWuQXQmu5K3BH6d1/uoX2HEZ68XgNyMkSwMA0g6jBKHIAguGkujiaCiXBEPeA==} cpu: [arm] os: [linux] - '@nx/nx-linux-arm64-gnu@23.0.1': - resolution: {integrity: sha512-zX2JdHQejZWB3DRgNsh77qOVYaSSjSLuBP2qIqc7EWVlCUnR7Aj3e65PTIps4LxMMmUp4twZA2ezS0rtyK2A4w==} - cpu: [arm64] - os: [linux] - libc: [glibc] - '@nx/nx-linux-arm64-gnu@23.1.0': resolution: {integrity: sha512-Q61stbUFVBw9TprlVLkRQmk+fuMXPlyTlvGExJ1y8KYymDTFsjs8LFk7zTnRvFpI9LVMRuhyD3osaLy2TxPU7Q==} cpu: [arm64] os: [linux] libc: [glibc] - '@nx/nx-linux-arm64-musl@23.0.1': - resolution: {integrity: sha512-9lyhxRNBgNYwHt6paq0OLzoKNoEGF5LnNW2YYrgFY8Cjtsg/Q4pcfZ1vB5o9FX9OmUgUQs3t2d4tU8YDukRUWg==} - cpu: [arm64] - os: [linux] - libc: [musl] - '@nx/nx-linux-arm64-musl@23.1.0': resolution: {integrity: sha512-Lm5PC4yuBHw4gr37vcAX3kvS75prqL1VpbQkPHs4Z2Us1ri29ALxgdrEFgF6fbCXxl4w3sWmcbKGvrcMdpuvIQ==} cpu: [arm64] os: [linux] libc: [musl] - '@nx/nx-linux-x64-gnu@23.0.1': - resolution: {integrity: sha512-kVszY2xRyyrCXgdCdM1qG1WUhDjNPZxtdWq86a0TyIRJjfJTP9NHqpyhmvj9c2RdZxKVWHotx6fBJzY6Vn2ZrA==} - cpu: [x64] - os: [linux] - libc: [glibc] - '@nx/nx-linux-x64-gnu@23.1.0': resolution: {integrity: sha512-v1ELSkVskidK6A+mm/Ynm8TWIb5UdzGWMcD7CTgmvjTzKfXIO0QY1ToDTqvoBwnetcehWR5gnhQlhQgOSH8XFA==} cpu: [x64] os: [linux] libc: [glibc] - '@nx/nx-linux-x64-musl@23.0.1': - resolution: {integrity: sha512-co71K2n4zcS1SYR8EBRlCvIko7M1YycO2tZL0nrCrga87AF5dzCwx+wEclpyCR/4tNOY3FrACk4gIkVskh3CdA==} - cpu: [x64] - os: [linux] - libc: [musl] - '@nx/nx-linux-x64-musl@23.1.0': resolution: {integrity: sha512-pD/Axn0usK2/RV2bSrLX7h59YpOXGiaYHmlZynVVS+CqkKbyFzxnrvvLmRdOdMxsfM6oDXom4lCcJMg+wUYcpw==} cpu: [x64] os: [linux] libc: [musl] - '@nx/nx-win32-arm64-msvc@23.0.1': - resolution: {integrity: sha512-7oma7iy5fbnn+x5AP7SFGMuleAA2R5RZm26dn+faikyQ4PXjoRAikWJJNiOWAeCA0BaMAeVedI6fJeAsVeDUKg==} - cpu: [arm64] - os: [win32] - '@nx/nx-win32-arm64-msvc@23.1.0': resolution: {integrity: sha512-lhnRaXPGUwqw/dqBnWl/6d6r8mRQXuZS7qnnTd+U5O1R7YPb2494BQyE7aSVEFLHcX9nXywMGxo5+OUCH84xQA==} cpu: [arm64] os: [win32] - '@nx/nx-win32-x64-msvc@23.0.1': - resolution: {integrity: sha512-TE/wvBa2cpkVXmk/AXUQAneong4JReS2hyNpAUONKG1yXU7TDKe0wvn1xQXxAbyspudT9NuCnVtpVuEkRz8S+Q==} - cpu: [x64] - os: [win32] - '@nx/nx-win32-x64-msvc@23.1.0': resolution: {integrity: sha512-fJaNYwNk4RiUnyEDTc0bMu69dmUzgM7iGinoqFenwYEtwaXPWREBRWL6lgRDyR3CFsJw3Wi0uc+33cfreTbhVw==} cpu: [x64] @@ -1643,8 +1592,8 @@ packages: resolution: {integrity: sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw==} engines: {node: '>= 20'} - '@octokit/request@10.0.10': - resolution: {integrity: sha512-KxNC2pTqqhszMNrf12ZRd4PonRgyJdsM4F/jySiddQK+DsRcfBtUvqn8t7UsyZhnRJHvX46OohDt5N3VqIWC2w==} + '@octokit/request@10.0.11': + resolution: {integrity: sha512-+s7HUxjfFqOMS9VlIwDffq0MikjSAK0gSpG73W+meAvVAvX4MBrHYTK5Bj3Uot55qFT4gzUtfzE4mGWY4Br8/Q==} engines: {node: '>= 20'} '@octokit/types@16.0.0': @@ -2740,8 +2689,8 @@ packages: resolution: {integrity: sha512-mgdxrHTLOjOddRVYIYDo0fR3/v61GNN1YGkfbrjuIKg/uMgCd+Qzo3UAXJ+woLQQpos4pl5Esuw5A7AoNlzjUQ==} engines: {node: '>=18'} - '@semantic-release/github@12.0.8': - resolution: {integrity: sha512-tej5AAgK5X9wHRoDmYhecMXEHEkFeGOY1XsEblKxu8pIQwahzf1STYyr7iPU6Lpbg6C5I3N2w/ocXrBo+L7jhw==} + '@semantic-release/github@12.0.9': + resolution: {integrity: sha512-ODIqb0V3QqndipryEEiaBxUQCFjvv7Oese5Dt4omMGa60YRNEW0Sx3K+zri0uac2Y6S9nOlMehciWIzvvRCTGQ==} engines: {node: ^22.14.0 || >= 24.10.0} peerDependencies: semantic-release: '>=24.1.0' @@ -2977,9 +2926,6 @@ packages: '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} - '@types/hast@3.0.4': - resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} - '@types/hast@3.0.5': resolution: {integrity: sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==} @@ -3460,9 +3406,6 @@ packages: aws4@1.13.2: resolution: {integrity: sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==} - axios@1.16.0: - resolution: {integrity: sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==} - axios@1.16.1: resolution: {integrity: sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==} @@ -3523,8 +3466,8 @@ packages: bottleneck@2.19.5: resolution: {integrity: sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==} - brace-expansion@2.1.1: - resolution: {integrity: sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==} + brace-expansion@2.1.2: + resolution: {integrity: sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==} brace-expansion@5.0.6: resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} @@ -4306,8 +4249,8 @@ packages: fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} - fs-extra@11.3.5: - resolution: {integrity: sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==} + fs-extra@11.3.6: + resolution: {integrity: sha512-w8ZNZr2mKIc7qeNaQ9AVPT1+iFaI+Avd4xudVOvdDJ8VytREi1Ft5Ih7hd9jjehod8vAM5GMsfQ/TpPf4EyoEA==} engines: {node: '>=14.14'} fsevents@2.3.3: @@ -4315,8 +4258,8 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] - fumadocs-core@16.11.1: - resolution: {integrity: sha512-tKuh1AKoVTb+f7IoAOM2cfz5djd3YhePeqA95q6mf422gEvDTeJms23OJ+icYRWZ6ryNQ5W/ZsgKEe87M5HVYg==} + fumadocs-core@16.11.2: + resolution: {integrity: sha512-IKHcO1DPTnJX8Q0TeSlqhpSDl3GvH5RaV5ECqJ9/SLNeMxPndqu8M9XSv09jbB0K8WtQgafhsBQSLi1cmlJqBQ==} peerDependencies: '@mdx-js/mdx': '*' '@mixedbread/sdk': 0.x.x @@ -4411,25 +4354,25 @@ packages: vite: optional: true - fumadocs-ui@16.11.1: - resolution: {integrity: sha512-Dq819PFV4RGhAI9Wd4erSCiRlEDLVOZae+kgE5LeOKFH8mbKX49U8N17ldFOhdkC9EZpxMZdEKul77RDgFHQww==} + fumadocs-ui@16.11.2: + resolution: {integrity: sha512-o2gVletv8m3QZtM681KsHjxtBKeeYksLEr58aBag1WWK4i3aMUBmbA6hvVQIC6E/AIZgYGp2BPt/ynvMJTCeeQ==} peerDependencies: - '@takumi-rs/image-response': '*' '@types/mdx': '*' '@types/react': '*' - fumadocs-core: 16.11.1 + fumadocs-core: 16.11.2 next: 16.x.x react: ^19.2.0 react-dom: ^19.2.0 + takumi-js: '*' peerDependenciesMeta: - '@takumi-rs/image-response': - optional: true '@types/mdx': optional: true '@types/react': optional: true next: optional: true + takumi-js: + optional: true function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} @@ -4909,8 +4852,8 @@ packages: json-stringify-safe@5.0.1: resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} - json-with-bigint@3.5.8: - resolution: {integrity: sha512-eq/4KP6K34kwa7TcFdtvnftvHCD9KvHOGGICWwMFc4dOOKF5t4iYqnfLK8otCRCRv06FXOzGGyqE8h8ElMvvdw==} + json-with-bigint@3.5.10: + resolution: {integrity: sha512-Vcx+JVNEBts/xfcoCS69sKrOhOk/3TVlvlT+XzUOefVKnnrbYSCKpDCm10pohsJFtsJVYnwa/cXRZ4eElzaM6w==} json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} @@ -4947,8 +4890,8 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} - knip@6.25.0: - resolution: {integrity: sha512-Q3n41VjOOB/aqsbxb8kallAcFKrUz3b2S5fD5pTODljVpP01t+rvAgy2x3j0Cq8yEpRRHNdar1vHuqFfGuIakQ==} + knip@6.27.0: + resolution: {integrity: sha512-CngYEYrD0n20N06FXA8n3u/0Wnnugoa+B9k14OP+iKIgkCHuzvIdsP3nfwjhByoc1WfogpxfiriMboAXFETDUw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -5101,8 +5044,8 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - lru-cache@11.5.1: - resolution: {integrity: sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==} + lru-cache@11.5.2: + resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} engines: {node: 20 || >=22} lru-cache@5.1.1: @@ -5112,8 +5055,8 @@ packages: resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} engines: {node: '>=12'} - lucide-react@1.24.0: - resolution: {integrity: sha512-YT6mBD8lGKkg4nM39enlm94/sfJIiW0YKUT60fBy4YK8tai31ylg1VhGNWxkpSKHo9UagfnZqwIff3HTDQwXeA==} + lucide-react@1.25.0: + resolution: {integrity: sha512-/mdJTRbiwcLOQ1NZZK1amZF9rIZyvO18D6r9TngE6TG1NmqHgFuT4eE7Xrkm9UsXMbBJD1NlfwHVltCDWHrOTw==} peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -5544,8 +5487,8 @@ packages: resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} engines: {node: '>=18'} - npm@11.17.0: - resolution: {integrity: sha512-PurxiZexEHDTE4SSaLI3ZrnbAGiZfeyUcQcxcP5D+hfytNAze/D1IzDuInTn9XVLIbAQUnQuSPXJx02LHjLvQw==} + npm@11.18.0: + resolution: {integrity: sha512-T67M4L5wNm0cZ7EBLErcEkY1SmzEW/WJ+SADBzsFUY1UdAPfFHXFQtZ6SEXiK0+vzXysCvAsepbMaBTwnrAD+w==} engines: {node: ^20.17.0 || >=22.9.0} hasBin: true bundledDependencies: @@ -5615,18 +5558,6 @@ packages: - validate-npm-package-name - which - nx@23.0.1: - resolution: {integrity: sha512-HnK0Ke8FcPeVQffYm1oyzkLNn7khrI8SeDeC3iyLhw/UEMCB24hjI5JSs6Amlyeb0/GaeiuQuts8NkQKd/NpGA==} - hasBin: true - peerDependencies: - '@swc-node/register': ^1.11.1 - '@swc/core': ^1.15.8 - peerDependenciesMeta: - '@swc-node/register': - optional: true - '@swc/core': - optional: true - nx@23.1.0: resolution: {integrity: sha512-Z4hzIdRF6naDDpOTOazrJODmbU5D7slq94FR5q/dySW+evGxiOSFLO981VqDVvtNEvHPnEDh/C1wvFseWyvSrQ==} hasBin: true @@ -5755,8 +5686,8 @@ packages: resolution: {integrity: sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==} engines: {node: '>=4'} - p-map@7.0.4: - resolution: {integrity: sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==} + p-map@7.0.5: + resolution: {integrity: sha512-e8vJF4XdVkzqqSHguEMz41mQO1wKwxKm5ENrUJQUu9kLDCtn83cxbyHZcszr4QC5zEA7WffRRC4gsTecC7J9oA==} engines: {node: '>=18'} p-reduce@3.0.0: @@ -6304,8 +6235,8 @@ packages: scroll-into-view-if-needed@3.1.0: resolution: {integrity: sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==} - semantic-release@25.0.5: - resolution: {integrity: sha512-mn61SUJwtM8ThrWn2WmgLVpwVJeG/hPSupua1psdMoufmwRIPyvRLkRkL0JDXkP67OntlLWUYnBnfVc8EDO3/g==} + semantic-release@25.0.6: + resolution: {integrity: sha512-EGS5PWCDavYD4jAE4vKQ+VKcwXFpxLsKg05UcrUQwqxUCM1KtNRx9ZdMyYJL3z2aTyAH9zDgQSNFjRvbsIuKHQ==} engines: {node: ^22.14.0 || >= 24.10.0} hasBin: true @@ -6754,6 +6685,10 @@ packages: resolution: {integrity: sha512-1URUxUqfHFM1c+zfSPsa3gnkO7Aq21qyH75SIduNYz4SzY964rn1X2vCMQaHSHhktiw+0kPa2iyb6PUpXqB6Vg==} engines: {node: '>=20'} + type-fest@5.8.0: + resolution: {integrity: sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA==} + engines: {node: '>=20'} + type-is@1.6.18: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} @@ -7188,46 +7123,46 @@ snapshots: ansi-styles: 6.2.3 is-fullwidth-code-point: 5.1.0 - '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.206': + '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.207': optional: true - '@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.206': + '@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.207': optional: true - '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.206': + '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.207': optional: true - '@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.206': + '@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.207': optional: true - '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.206': + '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.207': optional: true - '@anthropic-ai/claude-agent-sdk-linux-x64@0.3.206': + '@anthropic-ai/claude-agent-sdk-linux-x64@0.3.207': optional: true - '@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.206': + '@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.207': optional: true - '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.206': + '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.207': optional: true - '@anthropic-ai/claude-agent-sdk@0.3.206(@anthropic-ai/sdk@0.110.0(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(zod@4.4.3)': + '@anthropic-ai/claude-agent-sdk@0.3.207(@anthropic-ai/sdk@0.111.0(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(zod@4.4.3)': dependencies: - '@anthropic-ai/sdk': 0.110.0(zod@4.4.3) + '@anthropic-ai/sdk': 0.111.0(zod@4.4.3) '@modelcontextprotocol/sdk': 1.29.0(zod@4.4.3) zod: 4.4.3 optionalDependencies: - '@anthropic-ai/claude-agent-sdk-darwin-arm64': 0.3.206 - '@anthropic-ai/claude-agent-sdk-darwin-x64': 0.3.206 - '@anthropic-ai/claude-agent-sdk-linux-arm64': 0.3.206 - '@anthropic-ai/claude-agent-sdk-linux-arm64-musl': 0.3.206 - '@anthropic-ai/claude-agent-sdk-linux-x64': 0.3.206 - '@anthropic-ai/claude-agent-sdk-linux-x64-musl': 0.3.206 - '@anthropic-ai/claude-agent-sdk-win32-arm64': 0.3.206 - '@anthropic-ai/claude-agent-sdk-win32-x64': 0.3.206 + '@anthropic-ai/claude-agent-sdk-darwin-arm64': 0.3.207 + '@anthropic-ai/claude-agent-sdk-darwin-x64': 0.3.207 + '@anthropic-ai/claude-agent-sdk-linux-arm64': 0.3.207 + '@anthropic-ai/claude-agent-sdk-linux-arm64-musl': 0.3.207 + '@anthropic-ai/claude-agent-sdk-linux-x64': 0.3.207 + '@anthropic-ai/claude-agent-sdk-linux-x64-musl': 0.3.207 + '@anthropic-ai/claude-agent-sdk-win32-arm64': 0.3.207 + '@anthropic-ai/claude-agent-sdk-win32-x64': 0.3.207 - '@anthropic-ai/sdk@0.110.0(zod@4.4.3)': + '@anthropic-ai/sdk@0.111.0(zod@4.4.3)': dependencies: json-schema-to-ts: 3.1.1 standardwebhooks: 1.0.0 @@ -7376,7 +7311,7 @@ snapshots: dependencies: '@noble/ciphers': 1.3.0 - '@effect/atom-react@4.0.0-beta.94(effect@4.0.0-beta.97)(react@19.2.7)(scheduler@0.27.0)': + '@effect/atom-react@4.0.0-beta.97(effect@4.0.0-beta.97)(react@19.2.7)(scheduler@0.27.0)': dependencies: effect: 4.0.0-beta.97 react: 19.2.7 @@ -7442,6 +7377,13 @@ snapshots: dependencies: '@emnapi/wasi-threads': 1.2.2 tslib: 2.8.1 + optional: true + + '@emnapi/core@1.11.2': + dependencies: + '@emnapi/wasi-threads': 1.2.2 + tslib: 2.8.1 + optional: true '@emnapi/core@1.4.5': dependencies: @@ -7461,6 +7403,7 @@ snapshots: '@emnapi/runtime@1.11.1': dependencies: tslib: 2.8.1 + optional: true '@emnapi/runtime@1.11.2': dependencies: @@ -7483,6 +7426,7 @@ snapshots: '@emnapi/wasi-threads@1.2.2': dependencies: tslib: 2.8.1 + optional: true '@esbuild/aix-ppc64@0.28.1': optional: true @@ -7837,15 +7781,15 @@ snapshots: '@napi-rs/wasm-runtime@0.2.12': dependencies: - '@emnapi/core': 1.11.1 + '@emnapi/core': 1.11.2 '@emnapi/runtime': 1.11.2 '@tybys/wasm-util': 0.10.3 optional: true '@napi-rs/wasm-runtime@0.2.4': dependencies: - '@emnapi/core': 1.11.1 - '@emnapi/runtime': 1.11.1 + '@emnapi/core': 1.4.5 + '@emnapi/runtime': 1.4.5 '@tybys/wasm-util': 0.9.0 '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': @@ -7976,7 +7920,7 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 - '@nx/devkit@23.0.1(nx@23.1.0(@swc-node/register@1.12.0(@swc/core@1.15.43)(@swc/types@0.1.27)(@typescript/typescript6@6.0.2))(@swc/core@1.15.43))': + '@nx/devkit@23.1.0(nx@23.1.0(@swc-node/register@1.12.0(@swc/core@1.15.43)(@swc/types@0.1.27)(@typescript/typescript6@6.0.2))(@swc/core@1.15.43))': dependencies: '@zkochan/js-yaml': 0.0.7 ejs: 5.0.1 @@ -7985,65 +7929,36 @@ snapshots: nx: 23.1.0(@swc-node/register@1.12.0(@swc/core@1.15.43)(@swc/types@0.1.27)(@typescript/typescript6@6.0.2))(@swc/core@1.15.43) semver: 7.8.5 tslib: 2.8.1 + yaml: 2.9.0 yargs-parser: 21.1.1 - '@nx/nx-darwin-arm64@23.0.1': - optional: true - '@nx/nx-darwin-arm64@23.1.0': optional: true - '@nx/nx-darwin-x64@23.0.1': - optional: true - '@nx/nx-darwin-x64@23.1.0': optional: true - '@nx/nx-freebsd-x64@23.0.1': - optional: true - '@nx/nx-freebsd-x64@23.1.0': optional: true - '@nx/nx-linux-arm-gnueabihf@23.0.1': - optional: true - '@nx/nx-linux-arm-gnueabihf@23.1.0': optional: true - '@nx/nx-linux-arm64-gnu@23.0.1': - optional: true - '@nx/nx-linux-arm64-gnu@23.1.0': optional: true - '@nx/nx-linux-arm64-musl@23.0.1': - optional: true - '@nx/nx-linux-arm64-musl@23.1.0': optional: true - '@nx/nx-linux-x64-gnu@23.0.1': - optional: true - '@nx/nx-linux-x64-gnu@23.1.0': optional: true - '@nx/nx-linux-x64-musl@23.0.1': - optional: true - '@nx/nx-linux-x64-musl@23.1.0': optional: true - '@nx/nx-win32-arm64-msvc@23.0.1': - optional: true - '@nx/nx-win32-arm64-msvc@23.1.0': optional: true - '@nx/nx-win32-x64-msvc@23.0.1': - optional: true - '@nx/nx-win32-x64-msvc@23.1.0': optional: true @@ -8053,7 +7968,7 @@ snapshots: dependencies: '@octokit/auth-token': 6.0.0 '@octokit/graphql': 9.0.3 - '@octokit/request': 10.0.10 + '@octokit/request': 10.0.11 '@octokit/request-error': 7.1.0 '@octokit/types': 16.0.0 before-after-hook: 4.0.0 @@ -8066,7 +7981,7 @@ snapshots: '@octokit/graphql@9.0.3': dependencies: - '@octokit/request': 10.0.10 + '@octokit/request': 10.0.11 '@octokit/types': 16.0.0 universal-user-agent: 7.0.3 @@ -8094,13 +8009,13 @@ snapshots: dependencies: '@octokit/types': 16.0.0 - '@octokit/request@10.0.10': + '@octokit/request@10.0.11': dependencies: '@octokit/endpoint': 11.0.3 '@octokit/request-error': 7.1.0 '@octokit/types': 16.0.0 content-type: 2.0.0 - json-with-bigint: 3.5.8 + json-with-bigint: 3.5.10 universal-user-agent: 7.0.3 '@octokit/types@16.0.0': @@ -8852,7 +8767,7 @@ snapshots: '@sec-ant/readable-stream@0.4.1': {} - '@semantic-release/commit-analyzer@13.0.1(semantic-release@25.0.5(typescript@7.0.2))': + '@semantic-release/commit-analyzer@13.0.1(semantic-release@25.0.6(typescript@7.0.2))': dependencies: conventional-changelog-angular: 8.3.1 conventional-changelog-writer: 8.4.0 @@ -8862,13 +8777,13 @@ snapshots: import-from-esm: 2.0.0 lodash-es: 4.18.1 micromatch: 4.0.8 - semantic-release: 25.0.5(typescript@7.0.2) + semantic-release: 25.0.6(typescript@7.0.2) transitivePeerDependencies: - supports-color '@semantic-release/error@4.0.0': {} - '@semantic-release/github@12.0.8(semantic-release@25.0.5(typescript@7.0.2))': + '@semantic-release/github@12.0.9(semantic-release@25.0.6(typescript@7.0.2))': dependencies: '@octokit/core': 7.0.6 '@octokit/plugin-paginate-rest': 14.0.0(@octokit/core@7.0.6) @@ -8884,7 +8799,7 @@ snapshots: lodash-es: 4.18.1 mime: 4.1.0 p-filter: 4.1.0 - semantic-release: 25.0.5(typescript@7.0.2) + semantic-release: 25.0.6(typescript@7.0.2) tinyglobby: 0.2.17 undici: 7.28.0 url-join: 5.0.0 @@ -8892,26 +8807,26 @@ snapshots: - kerberos - supports-color - '@semantic-release/npm@13.1.5(semantic-release@25.0.5(typescript@7.0.2))': + '@semantic-release/npm@13.1.5(semantic-release@25.0.6(typescript@7.0.2))': dependencies: '@actions/core': 3.0.1 '@semantic-release/error': 4.0.0 aggregate-error: 5.0.0 env-ci: 11.2.0 execa: 9.6.1 - fs-extra: 11.3.5 + fs-extra: 11.3.6 lodash-es: 4.18.1 nerf-dart: 1.0.0 normalize-url: 9.0.1 - npm: 11.17.0 + npm: 11.18.0 rc: 1.2.8 read-pkg: 10.1.0 registry-auth-token: 5.1.1 - semantic-release: 25.0.5(typescript@7.0.2) + semantic-release: 25.0.6(typescript@7.0.2) semver: 7.8.5 tempy: 3.2.0 - '@semantic-release/release-notes-generator@14.1.1(semantic-release@25.0.5(typescript@7.0.2))': + '@semantic-release/release-notes-generator@14.1.1(semantic-release@25.0.6(typescript@7.0.2))': dependencies: conventional-changelog-angular: 8.3.1 conventional-changelog-writer: 8.4.0 @@ -8921,7 +8836,7 @@ snapshots: import-from-esm: 2.0.0 lodash-es: 4.18.1 read-package-up: 11.0.0 - semantic-release: 25.0.5(typescript@7.0.2) + semantic-release: 25.0.6(typescript@7.0.2) transitivePeerDependencies: - supports-color @@ -9134,10 +9049,6 @@ snapshots: '@types/estree@1.0.9': {} - '@types/hast@3.0.4': - dependencies: - '@types/unist': 3.0.3 - '@types/hast@3.0.5': dependencies: '@types/unist': 3.0.3 @@ -9624,14 +9535,6 @@ snapshots: aws4@1.13.2: {} - axios@1.16.0: - dependencies: - follow-redirects: 1.16.0(debug@4.4.3(supports-color@7.2.0)) - form-data: 4.0.6 - proxy-from-env: 2.1.0 - transitivePeerDependencies: - - debug - axios@1.16.1(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0): dependencies: follow-redirects: 1.16.0(debug@4.4.3(supports-color@7.2.0)) @@ -9703,7 +9606,7 @@ snapshots: bottleneck@2.19.5: {} - brace-expansion@2.1.1: + brace-expansion@2.1.2: dependencies: balanced-match: 1.0.2 @@ -10582,7 +10485,7 @@ snapshots: fs-constants@1.0.0: {} - fs-extra@11.3.5: + fs-extra@11.3.6: dependencies: graceful-fs: 4.2.11 jsonfile: 6.2.1 @@ -10591,7 +10494,7 @@ snapshots: fsevents@2.3.3: optional: true - fumadocs-core@16.11.1(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.24.0(react@19.2.7))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3): + fumadocs-core@16.11.2(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.25.0(react@19.2.7))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3): dependencies: '@orama/orama': 3.1.18 estree-util-value-to-estree: 3.5.0 @@ -10616,7 +10519,7 @@ snapshots: '@types/hast': 3.0.5 '@types/mdast': 4.0.4 '@types/react': 19.2.17 - lucide-react: 1.24.0(react@19.2.7) + lucide-react: 1.25.0(react@19.2.7) next: 16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) @@ -10624,14 +10527,14 @@ snapshots: transitivePeerDependencies: - supports-color - fumadocs-mdx@15.1.0(@types/mdast@4.0.4)(@types/mdx@2.0.14)(@types/react@19.2.17)(fumadocs-core@16.11.1(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.24.0(react@19.2.7))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(rolldown@1.0.2)(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)): + fumadocs-mdx@15.1.0(@types/mdast@4.0.4)(@types/mdx@2.0.14)(@types/react@19.2.17)(fumadocs-core@16.11.2(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.25.0(react@19.2.7))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(rolldown@1.0.2)(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)): dependencies: '@mdx-js/mdx': 3.1.1 '@standard-schema/spec': 1.1.0 chokidar: 5.0.0 esbuild: 0.28.1 estree-util-value-to-estree: 3.5.0 - fumadocs-core: 16.11.1(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.24.0(react@19.2.7))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3) + fumadocs-core: 16.11.2(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.25.0(react@19.2.7))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3) github-slugger: 2.0.0 js-yaml: 5.2.1 mdast-util-mdx: 3.0.0 @@ -10655,7 +10558,7 @@ snapshots: transitivePeerDependencies: - supports-color - fumadocs-ui@16.11.1(@types/mdx@2.0.14)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(fumadocs-core@16.11.1(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.24.0(react@19.2.7))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + fumadocs-ui@16.11.2(@types/mdx@2.0.14)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(fumadocs-core@16.11.2(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.25.0(react@19.2.7))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7): dependencies: '@fuma-translate/react': 1.0.2(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@fumadocs/tailwind': 0.1.0 @@ -10671,8 +10574,8 @@ snapshots: '@radix-ui/react-tabs': 1.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) class-variance-authority: 0.7.1 cnfast: 0.0.8 - fumadocs-core: 16.11.1(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.24.0(react@19.2.7))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3) - lucide-react: 1.24.0(react@19.2.7) + fumadocs-core: 16.11.2(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.25.0(react@19.2.7))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3) + lucide-react: 1.25.0(react@19.2.7) motion: 12.42.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) next-themes: 0.4.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react: 19.2.7 @@ -10945,7 +10848,7 @@ snapshots: hosted-git-info@9.0.3: dependencies: - lru-cache: 11.5.1 + lru-cache: 11.5.2 html-escaper@2.0.2: {} @@ -11241,7 +11144,7 @@ snapshots: json-stringify-safe@5.0.1: {} - json-with-bigint@3.5.8: {} + json-with-bigint@3.5.10: {} json5@2.2.3: {} @@ -11268,7 +11171,7 @@ snapshots: lodash.isstring: 4.0.1 lodash.once: 4.1.1 ms: 2.1.3 - semver: 7.8.5 + semver: 7.8.2 jsprim@2.0.2: dependencies: @@ -11292,7 +11195,7 @@ snapshots: dependencies: json-buffer: 3.0.1 - knip@6.25.0: + knip@6.27.0: dependencies: fdir: 6.5.0(picomatch@4.0.5) formatly: 0.3.0 @@ -11422,7 +11325,7 @@ snapshots: lru-cache@10.4.3: {} - lru-cache@11.5.1: {} + lru-cache@11.5.2: {} lru-cache@5.1.1: dependencies: @@ -11430,7 +11333,7 @@ snapshots: lru-cache@7.18.3: {} - lucide-react@1.24.0(react@19.2.7): + lucide-react@1.25.0(react@19.2.7): dependencies: react: 19.2.7 @@ -11557,7 +11460,7 @@ snapshots: mdast-util-mdx-expression@2.0.1: dependencies: '@types/estree-jsx': 1.0.5 - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/mdast': 4.0.4 devlop: 1.1.0 mdast-util-from-markdown: 2.0.3 @@ -11568,7 +11471,7 @@ snapshots: mdast-util-mdx-jsx@3.2.0: dependencies: '@types/estree-jsx': 1.0.5 - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/mdast': 4.0.4 '@types/unist': 3.0.3 ccount: 2.0.1 @@ -11595,7 +11498,7 @@ snapshots: mdast-util-mdxjs-esm@2.0.1: dependencies: '@types/estree-jsx': 1.0.5 - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/mdast': 4.0.4 devlop: 1.1.0 mdast-util-from-markdown: 2.0.3 @@ -11953,7 +11856,7 @@ snapshots: minimatch@7.4.9: dependencies: - brace-expansion: 2.1.1 + brace-expansion: 2.1.2 minimist@1.2.8: {} @@ -12091,136 +11994,7 @@ snapshots: path-key: 4.0.0 unicorn-magic: 0.3.0 - npm@11.17.0: {} - - nx@23.0.1(@swc-node/register@1.12.0(@swc/core@1.15.43)(@swc/types@0.1.27)(@typescript/typescript6@6.0.2))(@swc/core@1.15.43): - dependencies: - '@emnapi/core': 1.4.5 - '@emnapi/runtime': 1.4.5 - '@emnapi/wasi-threads': 1.0.4 - '@jest/diff-sequences': 30.0.1 - '@napi-rs/wasm-runtime': 0.2.4 - '@tybys/wasm-util': 0.9.0 - '@yarnpkg/lockfile': 1.1.0 - '@zkochan/js-yaml': 0.0.7 - ansi-colors: 4.1.3 - ansi-regex: 5.0.1 - ansi-styles: 4.3.0 - argparse: 2.0.1 - asynckit: 0.4.0 - axios: 1.16.0 - balanced-match: 4.0.3 - base64-js: 1.5.1 - bl: 4.1.0 - brace-expansion: 5.0.6 - buffer: 5.7.1 - call-bind-apply-helpers: 1.0.2 - chalk: 4.1.2 - cli-cursor: 3.1.0 - cli-spinners: 2.6.1 - cliui: 8.0.1 - clone: 1.0.4 - color-convert: 2.0.1 - color-name: 1.1.4 - combined-stream: 1.0.8 - defaults: 1.0.4 - define-lazy-prop: 2.0.0 - delayed-stream: 1.0.0 - dotenv: 16.4.7 - dotenv-expand: 12.0.3 - dunder-proto: 1.0.1 - ejs: 5.0.1 - emoji-regex: 8.0.0 - end-of-stream: 1.4.5 - enquirer: 2.3.6 - es-define-property: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - es-set-tostringtag: 2.1.0 - escalade: 3.2.0 - escape-string-regexp: 1.0.5 - figures: 3.2.0 - flat: 5.0.2 - follow-redirects: 1.16.0(debug@4.4.3(supports-color@7.2.0)) - form-data: 4.0.6 - fs-constants: 1.0.0 - function-bind: 1.1.2 - get-caller-file: 2.0.5 - get-intrinsic: 1.3.0 - get-proto: 1.0.1 - gopd: 1.2.0 - has-flag: 4.0.0 - has-symbols: 1.1.0 - has-tostringtag: 1.0.2 - hasown: 2.0.4 - ieee754: 1.2.1 - ignore: 7.0.5 - inherits: 2.0.4 - is-docker: 2.2.1 - is-fullwidth-code-point: 3.0.0 - is-interactive: 1.0.0 - is-unicode-supported: 0.1.0 - is-wsl: 2.2.0 - isexe: 2.0.0 - json5: 2.2.3 - jsonc-parser: 3.3.1 - lines-and-columns: 2.0.3 - log-symbols: 4.1.0 - math-intrinsics: 1.1.0 - mime-db: 1.52.0 - mime-types: 2.1.35 - mimic-fn: 2.1.0 - minimatch: 10.2.5 - minimist: 1.2.8 - npm-run-path: 4.0.1 - once: 1.4.0 - onetime: 5.1.2 - open: 8.4.2 - ora: 5.4.1 - path-key: 3.1.1 - picocolors: 1.1.1 - proxy-from-env: 2.1.0 - readable-stream: 3.6.2 - require-directory: 2.1.1 - resolve.exports: 2.0.3 - restore-cursor: 3.1.0 - safe-buffer: 5.2.1 - semver: 7.7.4 - signal-exit: 3.0.7 - smol-toml: 1.6.1 - string-width: 4.2.3 - string_decoder: 1.3.0 - strip-ansi: 6.0.1 - strip-bom: 3.0.0 - supports-color: 7.2.0 - tar-stream: 2.2.0 - tmp: 0.2.7 - tsconfig-paths: 4.2.0 - tslib: 2.8.1 - util-deprecate: 1.0.2 - wcwidth: 1.0.1 - which: 3.0.1 - wrap-ansi: 7.0.0 - wrappy: 1.0.2 - y18n: 5.0.8 - yaml: 2.9.0 - yargs: 17.7.2 - yargs-parser: 21.1.1 - optionalDependencies: - '@nx/nx-darwin-arm64': 23.0.1 - '@nx/nx-darwin-x64': 23.0.1 - '@nx/nx-freebsd-x64': 23.0.1 - '@nx/nx-linux-arm-gnueabihf': 23.0.1 - '@nx/nx-linux-arm64-gnu': 23.0.1 - '@nx/nx-linux-arm64-musl': 23.0.1 - '@nx/nx-linux-x64-gnu': 23.0.1 - '@nx/nx-linux-x64-musl': 23.0.1 - '@nx/nx-win32-arm64-msvc': 23.0.1 - '@nx/nx-win32-x64-msvc': 23.0.1 - '@swc-node/register': 1.12.0(@swc/core@1.15.43)(@swc/types@0.1.27)(@typescript/typescript6@6.0.2) - '@swc/core': 1.15.43 - transitivePeerDependencies: - - debug + npm@11.18.0: {} nx@23.1.0(@swc-node/register@1.12.0(@swc/core@1.15.43)(@swc/types@0.1.27)(@typescript/typescript6@6.0.2))(@swc/core@1.15.43): dependencies: @@ -12402,7 +12176,7 @@ snapshots: bl: 4.1.0 chalk: 4.1.2 cli-cursor: 3.1.0 - cli-spinners: 2.9.2 + cli-spinners: 2.6.1 is-interactive: 1.0.0 is-unicode-supported: 0.1.0 log-symbols: 4.1.0 @@ -12522,7 +12296,7 @@ snapshots: p-filter@4.1.0: dependencies: - p-map: 7.0.4 + p-map: 7.0.5 p-limit@1.3.0: dependencies: @@ -12532,7 +12306,7 @@ snapshots: dependencies: p-limit: 1.3.0 - p-map@7.0.4: {} + p-map@7.0.5: {} p-reduce@3.0.0: {} @@ -12891,14 +12665,14 @@ snapshots: dependencies: find-up-simple: 1.0.1 read-pkg: 10.1.0 - type-fest: 5.7.0 + type-fest: 5.8.0 read-pkg@10.1.0: dependencies: '@types/normalize-package-data': 2.4.4 normalize-package-data: 8.0.0 parse-json: 8.3.0 - type-fest: 5.7.0 + type-fest: 5.8.0 unicorn-magic: 0.4.0 read-pkg@9.0.1: @@ -13133,13 +12907,13 @@ snapshots: dependencies: compute-scroll-into-view: 3.1.1 - semantic-release@25.0.5(typescript@7.0.2): + semantic-release@25.0.6(typescript@7.0.2): dependencies: - '@semantic-release/commit-analyzer': 13.0.1(semantic-release@25.0.5(typescript@7.0.2)) + '@semantic-release/commit-analyzer': 13.0.1(semantic-release@25.0.6(typescript@7.0.2)) '@semantic-release/error': 4.0.0 - '@semantic-release/github': 12.0.8(semantic-release@25.0.5(typescript@7.0.2)) - '@semantic-release/npm': 13.1.5(semantic-release@25.0.5(typescript@7.0.2)) - '@semantic-release/release-notes-generator': 14.1.1(semantic-release@25.0.5(typescript@7.0.2)) + '@semantic-release/github': 12.0.9(semantic-release@25.0.6(typescript@7.0.2)) + '@semantic-release/npm': 13.1.5(semantic-release@25.0.6(typescript@7.0.2)) + '@semantic-release/release-notes-generator': 14.1.1(semantic-release@25.0.6(typescript@7.0.2)) aggregate-error: 5.0.0 cosmiconfig: 9.0.2(typescript@7.0.2) debug: 4.4.3(supports-color@7.2.0) @@ -13160,7 +12934,7 @@ snapshots: p-reduce: 3.0.0 read-package-up: 12.0.0 resolve-from: 5.0.0 - semver: 7.8.4 + semver: 7.8.5 signale: 1.4.0 yargs: 18.0.0 transitivePeerDependencies: @@ -13663,6 +13437,10 @@ snapshots: dependencies: tagged-tag: 1.0.0 + type-fest@5.8.0: + dependencies: + tagged-tag: 1.0.0 + type-is@1.6.18: dependencies: media-typer: 0.3.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 8759edf174..12064aa39a 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -12,12 +12,12 @@ allowBuilds: sharp: true catalog: - "@effect/atom-react": "4.0.0-beta.94" + "@effect/atom-react": "4.0.0-beta.97" "@effect/platform-bun": "4.0.0-beta.97" "@effect/platform-node": "4.0.0-beta.97" "@effect/sql-pg": "4.0.0-beta.97" "@effect/vitest": "4.0.0-beta.97" - "@nx/devkit": "^23.0.0" + "@nx/devkit": "^23.0.2" "@swc-node/register": "^1.10.9" "@swc/core": "^1.15.43" "@tsconfig/bun": "^1.0.10" @@ -25,8 +25,8 @@ catalog: "@typescript/native-preview": "7.0.0-dev.20260707.2" "@vitest/coverage-istanbul": "^4.1.10" "effect": "4.0.0-beta.97" - "knip": "^6.24.0" - "nx": "^23.0.0" + "knip": "^6.26.0" + "nx": "^23.0.2" "oxfmt": "^0.58.0" "oxlint": "^1.72.0" "oxlint-tsgolint": "^0.24.0" From 37769699df2bf545e7e268dace30ce82aeb4abb0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 19 Jul 2026 00:12:08 +0000 Subject: [PATCH 65/79] chore(deps): bump the npm-major group with 3 updates (#5903) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the npm-major group with 3 updates: [posthog-node](https://github.com/PostHog/posthog-js/tree/HEAD/packages/node), [fumadocs-core](https://github.com/fuma-nama/fumadocs) and [fumadocs-ui](https://github.com/fuma-nama/fumadocs). Updates `posthog-node` from 5.40.0 to 5.41.0
Release notes

Sourced from posthog-node's releases.

posthog-node@5.41.0

5.41.0

Minor Changes

  • #4105 203284a Thanks @​eli-r-ph! - Add opt-in Capture V1 support. Set the POSTHOG_CAPTURE_MODE=v1 environment variable to submit analytics events to the Capture V1 endpoint (/i/v1/analytics/events) instead of the legacy /batch/ endpoint, on both the batched and immediate send paths. The default remains v0, so existing behavior is unchanged unless you opt in. Opt-in is env-var-only during the transition (no public option), so nothing on the API surface has to be removed when v1 later becomes the default.

    Capture V1 uses Bearer auth, lifts legacy $-sentinel properties into a typed options object, and does per-event partial retry with exponential backoff clamped against Retry-After. Dropped and undelivered events are surfaced on the client error channel as a CaptureV1Error. $ai_* events continue to use the legacy submitter for now, regardless of the capture mode.

    In v1 mode, $ai_* events are routed to an isolated in-memory queue and flushed independently of the Capture V1 queue, so the two transports never share a batch and a failure on one cannot re-send events already accepted on the other. Each queue keeps its own retry/durability semantics: the legacy queue re-queues on network failure (retrying on later flushes), while the V1 queue exhausts the sender's own attempt budget per cycle and then surfaces the failure rather than re-queuing. (2026-07-11)

Patch Changes

  • Updated dependencies [203284a]:
    • @​posthog/core@​1.40.2
Changelog

Sourced from posthog-node's changelog.

5.41.0

Minor Changes

  • #4105 203284a Thanks @​eli-r-ph! - Add opt-in Capture V1 support. Set the POSTHOG_CAPTURE_MODE=v1 environment variable to submit analytics events to the Capture V1 endpoint (/i/v1/analytics/events) instead of the legacy /batch/ endpoint, on both the batched and immediate send paths. The default remains v0, so existing behavior is unchanged unless you opt in. Opt-in is env-var-only during the transition (no public option), so nothing on the API surface has to be removed when v1 later becomes the default.

    Capture V1 uses Bearer auth, lifts legacy $-sentinel properties into a typed options object, and does per-event partial retry with exponential backoff clamped against Retry-After. Dropped and undelivered events are surfaced on the client error channel as a CaptureV1Error. $ai_* events continue to use the legacy submitter for now, regardless of the capture mode.

    In v1 mode, $ai_* events are routed to an isolated in-memory queue and flushed independently of the Capture V1 queue, so the two transports never share a batch and a failure on one cannot re-send events already accepted on the other. Each queue keeps its own retry/durability semantics: the legacy queue re-queues on network failure (retrying on later flushes), while the V1 queue exhausts the sender's own attempt budget per cycle and then surfaces the failure rather than re-queuing. (2026-07-11)

Patch Changes

  • Updated dependencies [203284a]:
    • @​posthog/core@​1.40.2
Commits
  • d7f7c8e chore: update versions and lockfile [version bump]
  • 203284a refactor(core): extract sendBatch seam for capture pluggability (#4105)
  • d0e531a fix(core): coalesce concurrent flushes (#4120)
  • See full diff in compare view

Updates `fumadocs-core` from 16.11.2 to 16.11.3
Commits

Updates `fumadocs-ui` from 16.11.2 to 16.11.3
Commits

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/cli/package.json | 2 +- apps/docs/package.json | 4 ++-- pnpm-lock.yaml | 48 +++++++++++++++++++++--------------------- 3 files changed, 27 insertions(+), 27 deletions(-) diff --git a/apps/cli/package.json b/apps/cli/package.json index 22eff58160..b04c7fab9d 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -76,7 +76,7 @@ "oxlint-tsgolint": "catalog:", "pg": "^8.22.0", "pg-copy-streams": "^7.0.0", - "posthog-node": "^5.40.0", + "posthog-node": "^5.41.0", "react": "^19.2.7", "react-devtools-core": "^7.0.1", "semantic-release": "^25.0.6", diff --git a/apps/docs/package.json b/apps/docs/package.json index f0da534a12..6783494b58 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -8,9 +8,9 @@ "build": "bun run generate && next build" }, "dependencies": { - "fumadocs-core": "^16.11.2", + "fumadocs-core": "^16.11.3", "fumadocs-mdx": "^15.1.0", - "fumadocs-ui": "^16.11.2", + "fumadocs-ui": "^16.11.3", "next": "16.3.0-preview.6", "react": "^19.2.7", "react-dom": "^19.2.7" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 86dcc23439..c92c7b3ed2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -205,8 +205,8 @@ importers: specifier: ^7.0.0 version: 7.0.0 posthog-node: - specifier: ^5.40.0 - version: 5.40.0 + specifier: ^5.41.0 + version: 5.41.0 react: specifier: ^19.2.7 version: 19.2.7 @@ -291,14 +291,14 @@ importers: apps/docs: dependencies: fumadocs-core: - specifier: ^16.11.2 - version: 16.11.2(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.25.0(react@19.2.7))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3) + specifier: ^16.11.3 + version: 16.11.3(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.25.0(react@19.2.7))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3) fumadocs-mdx: specifier: ^15.1.0 - version: 15.1.0(@types/mdast@4.0.4)(@types/mdx@2.0.14)(@types/react@19.2.17)(fumadocs-core@16.11.2(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.25.0(react@19.2.7))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(rolldown@1.0.2)(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) + version: 15.1.0(@types/mdast@4.0.4)(@types/mdx@2.0.14)(@types/react@19.2.17)(fumadocs-core@16.11.3(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.25.0(react@19.2.7))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(rolldown@1.0.2)(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) fumadocs-ui: - specifier: ^16.11.2 - version: 16.11.2(@types/mdx@2.0.14)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(fumadocs-core@16.11.2(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.25.0(react@19.2.7))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + specifier: ^16.11.3 + version: 16.11.3(@types/mdx@2.0.14)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(fumadocs-core@16.11.3(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.25.0(react@19.2.7))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) next: specifier: 16.3.0-preview.6 version: 16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -992,8 +992,8 @@ packages: '@types/react': optional: true - '@fumadocs/tailwind@0.1.0': - resolution: {integrity: sha512-nF/DCAwOR21HZ4AkjIOv3Iqwyqywzb6pdyeMcoa+aZzirXj5ntvNZbe3jJ0v3ehhtrRfYYeXBezvjn8ZmV+fuQ==} + '@fumadocs/tailwind@0.1.1': + resolution: {integrity: sha512-BnPe52UxSaG8yKlHMKBxXw8h6GpK5qO55ci6+Qd5JnquTvIw6SpfbC1P+qAi82PuPWv1KZAWY8bxRk4+x9ctXw==} peerDependencies: tailwindcss: ^4.0.0 peerDependenciesMeta: @@ -4258,8 +4258,8 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] - fumadocs-core@16.11.2: - resolution: {integrity: sha512-IKHcO1DPTnJX8Q0TeSlqhpSDl3GvH5RaV5ECqJ9/SLNeMxPndqu8M9XSv09jbB0K8WtQgafhsBQSLi1cmlJqBQ==} + fumadocs-core@16.11.3: + resolution: {integrity: sha512-AHhOYY2YA98vEbgzLBxqZ9yyfW6VmOTIIjK803xCOWUmqyiVU8ONckI6IuIEIWTwyvk4s/PAC49p01J0Cjio+w==} peerDependencies: '@mdx-js/mdx': '*' '@mixedbread/sdk': 0.x.x @@ -4354,12 +4354,12 @@ packages: vite: optional: true - fumadocs-ui@16.11.2: - resolution: {integrity: sha512-o2gVletv8m3QZtM681KsHjxtBKeeYksLEr58aBag1WWK4i3aMUBmbA6hvVQIC6E/AIZgYGp2BPt/ynvMJTCeeQ==} + fumadocs-ui@16.11.3: + resolution: {integrity: sha512-JM7wf10FxL2N0PL9RSX2ZOtPwRzH5FCkCkAxmqBhOxeTTL8FfPhhOt/MJCXArv0QrTBUIOAnb4M7oVQJN5DTmw==} peerDependencies: '@types/mdx': '*' '@types/react': '*' - fumadocs-core: 16.11.2 + fumadocs-core: 16.11.3 next: 16.x.x react: ^19.2.0 react-dom: ^19.2.0 @@ -5923,8 +5923,8 @@ packages: postgres-range@1.1.4: resolution: {integrity: sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==} - posthog-node@5.40.0: - resolution: {integrity: sha512-DrLfHuauO0W6qruF80iqr5JdmLysef74XzOB4eh36oRLRhxCySLraTqsi2Pj161LZnp9/JNdRDxwT8ei8VK2YA==} + posthog-node@5.41.0: + resolution: {integrity: sha512-jOkX6THOr5WD+FGUEaTxekas8c7NOC3TqJ2Byfe2KMimQdL/F/osz17uSbkNzR4V9WFZoe8YaGP3Xp0EUpPKGg==} engines: {node: ^20.20.0 || >=22.22.0} peerDependencies: rxjs: ^7.0.0 @@ -7530,7 +7530,7 @@ snapshots: optionalDependencies: '@types/react': 19.2.17 - '@fumadocs/tailwind@0.1.0': {} + '@fumadocs/tailwind@0.1.1': {} '@hono/node-server@1.19.14(hono@4.12.21)': dependencies: @@ -10494,7 +10494,7 @@ snapshots: fsevents@2.3.3: optional: true - fumadocs-core@16.11.2(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.25.0(react@19.2.7))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3): + fumadocs-core@16.11.3(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.25.0(react@19.2.7))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3): dependencies: '@orama/orama': 3.1.18 estree-util-value-to-estree: 3.5.0 @@ -10527,14 +10527,14 @@ snapshots: transitivePeerDependencies: - supports-color - fumadocs-mdx@15.1.0(@types/mdast@4.0.4)(@types/mdx@2.0.14)(@types/react@19.2.17)(fumadocs-core@16.11.2(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.25.0(react@19.2.7))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(rolldown@1.0.2)(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)): + fumadocs-mdx@15.1.0(@types/mdast@4.0.4)(@types/mdx@2.0.14)(@types/react@19.2.17)(fumadocs-core@16.11.3(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.25.0(react@19.2.7))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(rolldown@1.0.2)(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)): dependencies: '@mdx-js/mdx': 3.1.1 '@standard-schema/spec': 1.1.0 chokidar: 5.0.0 esbuild: 0.28.1 estree-util-value-to-estree: 3.5.0 - fumadocs-core: 16.11.2(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.25.0(react@19.2.7))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3) + fumadocs-core: 16.11.3(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.25.0(react@19.2.7))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3) github-slugger: 2.0.0 js-yaml: 5.2.1 mdast-util-mdx: 3.0.0 @@ -10558,10 +10558,10 @@ snapshots: transitivePeerDependencies: - supports-color - fumadocs-ui@16.11.2(@types/mdx@2.0.14)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(fumadocs-core@16.11.2(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.25.0(react@19.2.7))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + fumadocs-ui@16.11.3(@types/mdx@2.0.14)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(fumadocs-core@16.11.3(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.25.0(react@19.2.7))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7): dependencies: '@fuma-translate/react': 1.0.2(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@fumadocs/tailwind': 0.1.0 + '@fumadocs/tailwind': 0.1.1 '@radix-ui/react-accordion': 1.2.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@radix-ui/react-collapsible': 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@radix-ui/react-dialog': 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -10574,7 +10574,7 @@ snapshots: '@radix-ui/react-tabs': 1.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) class-variance-authority: 0.7.1 cnfast: 0.0.8 - fumadocs-core: 16.11.2(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.25.0(react@19.2.7))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3) + fumadocs-core: 16.11.3(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.25.0(react@19.2.7))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3) lucide-react: 1.25.0(react@19.2.7) motion: 12.42.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) next-themes: 0.4.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -12523,7 +12523,7 @@ snapshots: postgres-range@1.1.4: {} - posthog-node@5.40.0: + posthog-node@5.41.0: dependencies: '@posthog/core': 1.43.1 From b08e6e1fdab3492f5723fbf7ed10aa9187e20ec1 Mon Sep 17 00:00:00 2001 From: Andrew Valleteau Date: Tue, 21 Jul 2026 11:44:42 +0200 Subject: [PATCH 66/79] fix(cli): repair remote db pull with pg-delta and per-connection role step-down (#5895) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Remote `db pull --linked` with the pg-delta engine reported **"No schema changes found"** on every hosted project, and the migra fallback failed at the final migration-history write with `permission denied for database postgres` (Slack report, supabase/cli#5826). Three stacked causes, all addressed here: **1. pg-delta required superuser to extract (fixed upstream, version bump here).** Up to alpha.31, extraction read `pg_catalog.pg_user_mapping` (superuser-only), so extracting as the temp `cli_login_postgres` role failed with SQLSTATE 42501 on every hosted project. It only worked locally because local connections use `supabase_admin`. `@supabase/pg-delta@1.0.0-alpha.32` reads the world-readable `pg_user_mappings` view instead; this PR bumps the default pinned version (Go `DefaultPgDeltaNpmVersion` + TS `LEGACY_DEFAULT_PG_DELTA_NPM_VERSION`). **2. pg-delta script crashes were swallowed as an empty diff.** The Deno templates force the edge-runtime worker to exit by throwing on both the success and failure paths, and both runners (Go `RunEdgeRuntimeScript`, TS `legacy-edge-runtime-script.layer.ts`) suppress any non-zero exit whose stderr contains "main worker has been destroyed" — making a crash indistinguishable from a genuinely empty diff. The template catch blocks now print a `PGDELTA_SCRIPT_ERROR` sentinel to stderr, and both runners treat its presence as a hard failure that surfaces the collected stderr (so users see the real error instead of "No schema changes found"). **3. alpha.32 changed the plan API.** Plan statements moved into execution-aware `units` (+ `sessionStatements`); the diff template's `result?.plan.statements ?? []` silently produced an empty diff. The template now uses `flattenPlanStatements(result.plan)`. TS template embeds regenerated from the Go sources (byte-equality test unchanged). **4. The TS role step-down was lost mid-command (the migra-path failure).** The legacy shell ran `SET SESSION ROLE postgres` once per session, but `PgClient.make` leaves node-postgres' default 10s idle timeout — during the minutes-long shadow diff the pool silently reaped and redialed the stepped-down connection, so the final `CREATE SCHEMA IF NOT EXISTS supabase_migrations` executed as the bare login role (42501). The primary connection now uses a self-managed `pg.Pool` via `PgClient.fromPool` with idle reaping disabled (`idleTimeoutMillis: 0`, `max: 1`) and a pg-pool `verify` hook that re-runs the step-down on every new physical connection — matching Go's per-connection `AfterConnect` (`connect.go:337-362`). `verify` runs before the checkout resolves, so it cannot race the caller's first query (a `pool.on("connect")` `client.query()` hits node-postgres' concurrent-query deprecation, removed in pg@9). ## Verification against staging * Fresh project + table created via psql → `link` (login-role path, no `SUPABASE_DB_PASSWORD`) → `db pull --linked` (`"engine":"pg-delta"`): migration contains the table with PK and grants, and `Repaired migration history: [...] => applied` succeeds — exercising the step-down after a long-idle diff, exactly where 2.109.1 failed. * Incremental pull after a second remote change produces a clean delta migration. * FDW server + user mapping extract as the unprivileged role; emitted `CREATE USER MAPPING` carries no password option ([CLI-1467](https://linear.app/supabase/issue/CLI-1467/security-pg-delta-leaks-foreign-server-user-mapping-passwords-in) handling intact). ## Linked issue Closes supabase/cli#5826 - [X] The linked issue is **open** and carries the `open-for-contribution` label (or I'm a Supabase maintainer). ## Checklist - [X] The PR title follows [Conventional Commits]() (e.g. `fix(cli): …`). - [X] Tests added or updated for the change. - [X] `pnpm check:all` and `pnpm test` pass for the workspace(s) I touched. 🤖 Generated with [Claude Code]() --------- Co-authored-by: Claude Fable 5 --- apps/cli-go/docs/supabase/db/diff.md | 2 + apps/cli-go/docs/supabase/db/pull.md | 4 + .../internal/db/declarative/declarative.go | 2 +- apps/cli-go/internal/db/diff/diff.go | 35 ++- apps/cli-go/internal/db/diff/diff_test.go | 17 +- apps/cli-go/internal/db/diff/pgadmin.go | 24 +- apps/cli-go/internal/db/diff/pgadmin_test.go | 88 ++++++ apps/cli-go/internal/db/diff/pgdelta.go | 60 +++- apps/cli-go/internal/db/diff/pgdelta_debug.go | 10 +- .../internal/db/diff/pgdelta_migrations.go | 108 +++++++ .../db/diff/pgdelta_migrations_test.go | 133 ++++++++ apps/cli-go/internal/db/diff/pgdelta_test.go | 37 +++ .../internal/db/diff/templates/pgdelta.ts | 55 +++- .../diff/templates/pgdelta_catalog_export.ts | 8 + .../templates/pgdelta_declarative_export.ts | 4 + apps/cli-go/internal/db/pull/pull.go | 88 ++++-- apps/cli-go/internal/db/pull/pull_test.go | 17 +- apps/cli-go/internal/utils/edgeruntime.go | 20 ++ .../cli-go/internal/utils/edgeruntime_test.go | 83 +++++ apps/cli-go/internal/utils/misc.go | 9 +- apps/cli-go/pkg/config/pgdelta_version.go | 2 +- .../legacy/commands/db/diff/diff.handler.ts | 70 +++-- .../commands/db/diff/diff.integration.test.ts | 155 +++++++++- .../legacy/commands/db/pull/pull.handler.ts | 134 +++++--- .../commands/db/pull/pull.integration.test.ts | 182 ++++++++++- .../db/pull/pull.sync.integration.test.ts | 100 ++++++ .../src/legacy/commands/db/pull/pull.sync.ts | 101 ++++--- ...eclarative.orchestrate.integration.test.ts | 15 +- .../declarative/sync/sync.integration.test.ts | 36 ++- .../shared/legacy-pgdelta-migrations.write.ts | 135 +++++++++ .../shared/legacy-pgdelta.deno-templates.ts | 8 +- .../db/shared/legacy-pgdelta.errors.ts | 9 + .../shared/legacy-pgdelta.integration.test.ts | 41 ++- .../commands/db/shared/legacy-pgdelta.ts | 48 ++- .../legacy-db-connection.sql-pg.layer.ts | 286 +++++++++++++++--- .../legacy-db-connection.sql-pg.unit.test.ts | 186 ++++++++++++ ...e-runtime-script.layer.integration.test.ts | 123 ++++++++ .../legacy-edge-runtime-script.layer.ts | 16 + .../legacy-edge-runtime-script.service.ts | 13 + apps/cli/tests/helpers/legacy-mocks.ts | 44 ++- 40 files changed, 2250 insertions(+), 258 deletions(-) create mode 100644 apps/cli-go/internal/db/diff/pgadmin_test.go create mode 100644 apps/cli-go/internal/db/diff/pgdelta_migrations.go create mode 100644 apps/cli-go/internal/db/diff/pgdelta_migrations_test.go create mode 100644 apps/cli/src/legacy/commands/db/pull/pull.sync.integration.test.ts create mode 100644 apps/cli/src/legacy/commands/db/shared/legacy-pgdelta-migrations.write.ts create mode 100644 apps/cli/src/legacy/shared/legacy-edge-runtime-script.layer.integration.test.ts diff --git a/apps/cli-go/docs/supabase/db/diff.md b/apps/cli-go/docs/supabase/db/diff.md index 822c752485..0c0cf05a4d 100644 --- a/apps/cli-go/docs/supabase/db/diff.md +++ b/apps/cli-go/docs/supabase/db/diff.md @@ -10,6 +10,8 @@ By default, all schemas in the target database are diffed. Use the `--schema pub Projects created by a recent `supabase init` default to the pg-delta diff engine (`[experimental.pgdelta] enabled = true` in `config.toml`). Existing projects are unaffected and keep using migra unless they opt in. To fall back to the legacy migra engine, set `enabled = false` under `[experimental.pgdelta]`, or pass `--use-migra` for a single run. +With the pg-delta engine the diff SQL is formatted by default with the same settings the declarative export uses (uppercase keywords, wrapped at a max width of 180, indented and column-aligned); execution-aware transaction boundaries are preserved as per-unit header comments in the output. Configure overrides with `[experimental.pgdelta] format_options`, or set `format_options = "null"` to emit raw, unformatted statements. + While the diff command is able to capture most schema changes, there are cases where it is known to fail. Currently, this could happen if you schema contains: - Changes to publication diff --git a/apps/cli-go/docs/supabase/db/pull.md b/apps/cli-go/docs/supabase/db/pull.md index ff35019715..e10c0679f7 100644 --- a/apps/cli-go/docs/supabase/db/pull.md +++ b/apps/cli-go/docs/supabase/db/pull.md @@ -12,6 +12,10 @@ If no entries exist in the migration history table, the default diff engine uses Pass `--diff-engine pg-delta` to keep the migration-file `db pull` workflow while using pg-delta for the shadow diff step. On initial pull, pg-delta replaces `pg_dump` and produces the full migration from the shadow diff alone. Pass `--declarative` to switch to the declarative pg-delta export workflow instead. +pg-delta plans are execution-aware: when a plan crosses a transaction boundary — for example `ALTER TYPE ... ADD VALUE` followed by a statement that uses the new enum value, which cannot run in the same transaction — `db pull` writes one ordered migration file per plan unit instead of a single file (for example `_remote_schema_schema_changes.sql` and `_remote_schema_after_enum_values.sql`), each recorded in the migration history. The common case (a single unit) still produces exactly one `_remote_schema.sql` file. + +By default the emitted SQL is formatted with the same settings the declarative export uses (uppercase keywords, wrapped at a max width of 180, indented and column-aligned). Configure overrides with `[experimental.pgdelta] format_options` in `config.toml`, or set `format_options = "null"` to opt out and emit raw, unformatted statements. + When `[experimental.pgdelta] enabled = true` (the default for projects created by a recent `supabase init`), the migration-file `db pull` workflow uses pg-delta for the shadow diff step by default; it does not switch to declarative output. Existing projects without the section are unaffected and keep using migra. To fall back to the legacy migra engine, set `enabled = false` under `[experimental.pgdelta]`, or pass `--diff-engine migra` for a single run. When pulling from a remote database with `--db-url`, prefer a direct connection (`db..supabase.co:5432`) over the connection pooler so pg-delta can introspect the full catalog reliably. diff --git a/apps/cli-go/internal/db/declarative/declarative.go b/apps/cli-go/internal/db/declarative/declarative.go index c79df6289f..b84087bf9f 100644 --- a/apps/cli-go/internal/db/declarative/declarative.go +++ b/apps/cli-go/internal/db/declarative/declarative.go @@ -204,7 +204,7 @@ func SyncToMigrations(ctx context.Context, schema []string, file string, noCache if len(strings.TrimSpace(file)) == 0 { file = "declarative_sync" } - if err := diff.SaveDiff(result.DiffSQL, file, fsys); err != nil { + if err := diff.SaveDiff(diff.DatabaseDiff{SQL: result.DiffSQL}, file, fsys); err != nil { return err } if len(result.DropWarnings) > 0 { diff --git a/apps/cli-go/internal/db/diff/diff.go b/apps/cli-go/internal/db/diff/diff.go index 85c9c09c9c..32fabb2c21 100644 --- a/apps/cli-go/internal/db/diff/diff.go +++ b/apps/cli-go/internal/db/diff/diff.go @@ -38,7 +38,7 @@ func Run(ctx context.Context, schema []string, file string, config pgconn.Config out := result.SQL branch := utils.GetGitBranch(fsys) fmt.Fprintln(os.Stderr, "Finished "+utils.Aqua("supabase db diff")+" on branch "+utils.Aqua(branch)+".\n") - if err := SaveDiff(out, file, fsys); err != nil { + if err := SaveDiff(result, file, fsys); err != nil { return err } drops := findDropStatements(out) @@ -225,22 +225,31 @@ func DiffDatabase(ctx context.Context, schema []string, config pgconn.Config, w } else { fmt.Fprintln(w, "Diffing schemas...") } - if IsPgDeltaDebugEnabled() && usePgDelta { - // Capture the shadow baseline catalog and edge-runtime stderr so an - // empty diff can be inspected later. DiffPgDeltaRefDetailed mirrors the - // pg-delta differ but additionally surfaces stderr, which differ() drops. - debugCapture := &PgDeltaDebugCapture{} - if snapshot, exportErr := exportCatalogPgDelta(ctx, utils.ToPostgresURL(shadowConfig), "postgres", options...); exportErr == nil { - debugCapture.SourceCatalog = snapshot - } else { - fmt.Fprintf(w, "Warning: failed to export shadow pg-delta catalog: %v\n", exportErr) + if usePgDelta { + // pg-delta always goes through the diffPgDeltaRefDetailed seam so callers get + // the execution-aware per-unit files (db pull writes one migration file each); + // db diff/declarative flatten them back via SQL. This mirrors the config-based + // differ (DiffPgDelta) exactly, so it is safe to bypass the injected differ() + // here — differ() remains the migra engine path below. + var debugCapture *PgDeltaDebugCapture + if IsPgDeltaDebugEnabled() { + // Capture the shadow baseline catalog and edge-runtime stderr so an + // empty diff can be inspected later. + debugCapture = &PgDeltaDebugCapture{} + if snapshot, exportErr := exportCatalogPgDelta(ctx, utils.ToPostgresURL(shadowConfig), "postgres", options...); exportErr == nil { + debugCapture.SourceCatalog = snapshot + } else { + fmt.Fprintf(w, "Warning: failed to export shadow pg-delta catalog: %v\n", exportErr) + } } - result, err := DiffPgDeltaRefDetailed(ctx, utils.ToPostgresURL(shadowConfig), utils.ToPostgresURL(config), schema, pgDeltaFormatOptions(), options...) + result, err := diffPgDeltaRefDetailed(ctx, utils.ToPostgresURL(shadowConfig), utils.ToPostgresURL(config), schema, pgDeltaFormatOptions(), options...) if err != nil { return DatabaseDiff{}, err } - debugCapture.Stderr = result.Stderr - return DatabaseDiff{SQL: result.SQL, Debug: debugCapture}, nil + if debugCapture != nil { + debugCapture.Stderr = result.Stderr + } + return DatabaseDiff{SQL: joinPgDeltaFiles(result.Files), Files: result.Files, Debug: debugCapture}, nil } output, err := differ(ctx, shadowConfig, config, schema, options...) if err != nil { diff --git a/apps/cli-go/internal/db/diff/diff_test.go b/apps/cli-go/internal/db/diff/diff_test.go index 92fd01164b..aff3242699 100644 --- a/apps/cli-go/internal/db/diff/diff_test.go +++ b/apps/cli-go/internal/db/diff/diff_test.go @@ -224,12 +224,23 @@ func TestRun(t *testing.T) { Reply("CREATE FUNCTION"). Query(tableSQL). Reply("CREATE TABLE") + // pg-delta bypasses the injected DiffFunc and runs the real edge-runtime + // pipeline, so stub the seam DiffDatabase uses (mirrors exportCatalogPgDelta). + // The migra differ must never be reached on this path. + originalDiffPgDelta := diffPgDeltaRefDetailed + t.Cleanup(func() { diffPgDeltaRefDetailed = originalDiffPgDelta }) diffCalled := false - differ := func(_ context.Context, _, target pgconn.Config, schema []string, _ ...func(*pgx.ConnConfig)) (string, error) { + diffPgDeltaRefDetailed = func(_ context.Context, _, targetRef string, schema []string, _ string, _ ...func(*pgx.ConnConfig)) (PgDeltaDiffResult, error) { diffCalled = true - assert.Equal(t, "contrib_regression", target.Database) + assert.Contains(t, targetRef, "contrib_regression") assert.Equal(t, []string{"public"}, schema) - return generated, nil + return PgDeltaDiffResult{ + Files: []PgDeltaPlanFile{{Order: 1, Name: "schema_changes", TransactionMode: "transactional", SQL: generated}}, + }, nil + } + differ := func(context.Context, pgconn.Config, pgconn.Config, []string, ...func(*pgx.ConnConfig)) (string, error) { + t.Fatal("migra differ must not be called on the pg-delta path") + return "", nil } localConfig := pgconn.Config{ Host: utils.Config.Hostname, diff --git a/apps/cli-go/internal/db/diff/pgadmin.go b/apps/cli-go/internal/db/diff/pgadmin.go index d53cd4660e..9f1a692971 100644 --- a/apps/cli-go/internal/db/diff/pgadmin.go +++ b/apps/cli-go/internal/db/diff/pgadmin.go @@ -5,6 +5,7 @@ import ( _ "embed" "fmt" "os" + "time" "github.com/jackc/pgconn" "github.com/spf13/afero" @@ -17,13 +18,26 @@ import ( var warnDiff = `WARNING: The diff tool is not foolproof, so you may need to manually rearrange and modify the generated migration. Run ` + utils.Aqua("supabase db reset") + ` to verify that the new migration does not generate errors.` -func SaveDiff(out, file string, fsys afero.Fs) error { +func SaveDiff(result DatabaseDiff, file string, fsys afero.Fs) error { + out := result.SQL if len(out) < 2 { fmt.Fprintln(os.Stderr, "No schema changes found") } else if len(file) > 0 { - path := new.GetMigrationPath(utils.GetCurrentTimestamp(), file) - if err := utils.WriteFile(path, []byte(out), fsys); err != nil { - return err + // A pg-delta plan that crosses a transaction boundary yields more than one + // ordered unit; writing them into a single migration file would later fail + // when `db push`/`reset` applies it as one transaction. Write one migration + // file per unit in that case (Go's `WritePgDeltaMigrations`). The migra / + // pgadmin engines and single-unit pg-delta plans keep the exact single-file + // path, byte-identical to before. + if len(result.Files) > 1 { + if _, err := WritePgDeltaMigrations(result.Files, time.Now(), file, fsys); err != nil { + return err + } + } else { + path := new.GetMigrationPath(utils.GetCurrentTimestamp(), file) + if err := utils.WriteFile(path, []byte(out), fsys); err != nil { + return err + } } fmt.Fprintln(os.Stderr, warnDiff) } else { @@ -44,7 +58,7 @@ func RunPgAdmin(ctx context.Context, schema []string, file string, config pgconn return err } - return SaveDiff(output, file, fsys) + return SaveDiff(DatabaseDiff{SQL: output}, file, fsys) } var output string diff --git a/apps/cli-go/internal/db/diff/pgadmin_test.go b/apps/cli-go/internal/db/diff/pgadmin_test.go new file mode 100644 index 0000000000..7bf65e7d78 --- /dev/null +++ b/apps/cli-go/internal/db/diff/pgadmin_test.go @@ -0,0 +1,88 @@ +package diff + +import ( + "testing" + + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/supabase/cli/internal/utils" +) + +func TestSaveDiff(t *testing.T) { + t.Run("reports no changes on empty diff", func(t *testing.T) { + fsys := afero.NewMemMapFs() + require.NoError(t, SaveDiff(DatabaseDiff{SQL: ""}, "my_diff", fsys)) + // Nothing written when there are no schema changes. + entries, err := afero.ReadDir(fsys, utils.MigrationsDir) + assert.Error(t, err) + assert.Empty(t, entries) + }) + + t.Run("writes a single migration file for a single-unit plan", func(t *testing.T) { + fsys := afero.NewMemMapFs() + files := []PgDeltaPlanFile{ + {Order: 1, Name: "schema_changes", TransactionMode: "transactional", SQL: "create table a ();"}, + } + result := DatabaseDiff{SQL: joinPgDeltaFiles(files), Files: files} + require.NoError(t, SaveDiff(result, "my_diff", fsys)) + entries, err := afero.ReadDir(fsys, utils.MigrationsDir) + require.NoError(t, err) + require.Len(t, entries, 1) + // A single-unit plan keeps the plain `_.sql` name and the exact + // diff SQL, byte-identical to the pre-multi-file behavior (no trailing newline + // added, no unit-name suffix). + assert.Regexp(t, `^\d{14}_my_diff\.sql$`, entries[0].Name()) + contents, err := afero.ReadFile(fsys, utils.MigrationsDir+"/"+entries[0].Name()) + require.NoError(t, err) + assert.Equal(t, "create table a ();", string(contents)) + }) + + t.Run("writes one migration file per unit for a multi-unit plan", func(t *testing.T) { + fsys := afero.NewMemMapFs() + files := []PgDeltaPlanFile{ + {Order: 1, Name: "schema_changes", TransactionMode: "transactional", SQL: "alter type mood add value 'ok';"}, + {Order: 2, Name: "after_enum_values", TransactionMode: "transactional", SQL: "insert into t values ('ok');"}, + } + result := DatabaseDiff{SQL: joinPgDeltaFiles(files), Files: files} + require.NoError(t, SaveDiff(result, "my_diff", fsys)) + entries, err := afero.ReadDir(fsys, utils.MigrationsDir) + require.NoError(t, err) + require.Len(t, entries, 2) + // Multi-unit plans split into one ordered file per unit, each suffixed with the + // unit name, so `db push`/`reset` applies each unit as its own transaction. + assert.Regexp(t, `^\d{14}_my_diff_schema_changes\.sql$`, entries[0].Name()) + assert.Regexp(t, `^\d{14}_my_diff_after_enum_values\.sql$`, entries[1].Name()) + }) + + t.Run("prints diff to stdout when no file is given", func(t *testing.T) { + fsys := afero.NewMemMapFs() + require.NoError(t, SaveDiff(DatabaseDiff{SQL: "create table a ();"}, "", fsys)) + entries, _ := afero.ReadDir(fsys, utils.MigrationsDir) + assert.Empty(t, entries) + }) + + t.Run("creates nested parent directories for a nested single-unit name", func(t *testing.T) { + fsys := afero.NewMemMapFs() + require.NoError(t, SaveDiff(DatabaseDiff{SQL: "create table a ();"}, "snapshots/remote", fsys)) + matches, err := afero.Glob(fsys, utils.MigrationsDir+"/*_snapshots/remote.sql") + require.NoError(t, err) + require.Len(t, matches, 1) + contents, err := afero.ReadFile(fsys, matches[0]) + require.NoError(t, err) + assert.Equal(t, "create table a ();", string(contents)) + }) + + t.Run("creates nested parent directories for a nested multi-unit name", func(t *testing.T) { + fsys := afero.NewMemMapFs() + files := []PgDeltaPlanFile{ + {Order: 1, Name: "schema_changes", TransactionMode: "transactional", SQL: "alter type mood add value 'ok';"}, + {Order: 2, Name: "after_enum_values", TransactionMode: "transactional", SQL: "insert into t values ('ok');"}, + } + result := DatabaseDiff{SQL: joinPgDeltaFiles(files), Files: files} + require.NoError(t, SaveDiff(result, "snapshots/remote", fsys)) + matches, err := afero.Glob(fsys, utils.MigrationsDir+"/*_snapshots/remote_*.sql") + require.NoError(t, err) + require.Len(t, matches, 2) + }) +} diff --git a/apps/cli-go/internal/db/diff/pgdelta.go b/apps/cli-go/internal/db/diff/pgdelta.go index d9d5edfc46..5267f0c6dd 100644 --- a/apps/cli-go/internal/db/diff/pgdelta.go +++ b/apps/cli-go/internal/db/diff/pgdelta.go @@ -43,6 +43,34 @@ type DeclarativeOutput struct { Files []DeclarativeFile `json:"files"` } +// PgDeltaPlanFile is one execution-aware migration unit rendered by pg-delta's +// renderPlanFiles: a numbered SQL file whose header comments record the unit +// number, transaction mode and boundary reason. +type PgDeltaPlanFile struct { + Order int `json:"order"` + Name string `json:"name"` + TransactionMode string `json:"transactionMode"` + SQL string `json:"sql"` +} + +// PgDeltaDiffOutput is the top-level diff envelope emitted by templates/pgdelta.ts. +type PgDeltaDiffOutput struct { + Version int `json:"version"` + Files []PgDeltaPlanFile `json:"files"` +} + +// joinPgDeltaFiles flattens the per-unit files back into a single SQL string for +// callers (db diff, declarative sync) that consume one blob. The per-unit header +// comments keep the transaction boundaries visible in the reviewed output; empty +// files produce an empty string, preserving "no changes" detection. +func joinPgDeltaFiles(files []PgDeltaPlanFile) string { + blocks := make([]string, len(files)) + for i, file := range files { + blocks[i] = file.SQL + } + return strings.Join(blocks, "\n\n") +} + func isPostgresURL(ref string) bool { return strings.HasPrefix(ref, "postgres://") || strings.HasPrefix(ref, "postgresql://") } @@ -101,7 +129,7 @@ func DiffPgDeltaRef(ctx context.Context, sourceRef, targetRef string, schema []s if err != nil { return "", err } - return result.SQL, nil + return joinPgDeltaFiles(result.Files), nil } // DiffPgDeltaRefDetailed is like DiffPgDeltaRef but also returns edge-runtime stderr. @@ -136,15 +164,37 @@ func DiffPgDeltaRefDetailed(ctx context.Context, sourceRef, targetRef string, sc if err := utils.RunEdgeRuntimeScript(ctx, env, script, binds, "error diffing schema", &stdout, &stderr, utils.PgDeltaNpmRegistryOption()); err != nil { return PgDeltaDiffResult{}, err } - return PgDeltaDiffResult{ - SQL: stdout.String(), - Stderr: stderr.String(), - }, nil + return parsePgDeltaDiffOutput(stdout.String(), stderr.String()) +} + +// parsePgDeltaDiffOutput turns the pg-delta diff script's stdout envelope into a +// result. The template always prints the envelope on the success path, even for +// an empty plan (`{"version":1,"files":[]}`); a truly empty stdout means no +// envelope was produced, which we surface as "no changes" (empty Files) rather +// than an error. Non-empty stdout that is not valid envelope JSON is a parse +// error carrying the edge-runtime stderr for diagnosis. +func parsePgDeltaDiffOutput(stdout, stderr string) (PgDeltaDiffResult, error) { + result := PgDeltaDiffResult{Stderr: stderr} + if len(strings.TrimSpace(stdout)) == 0 { + return result, nil + } + var envelope PgDeltaDiffOutput + if err := json.Unmarshal([]byte(stdout), &envelope); err != nil { + return PgDeltaDiffResult{}, errors.Errorf("failed to parse pg-delta diff output: %w:\n%s", err, stderr) + } + result.Files = envelope.Files + return result, nil } // exportCatalogPgDelta is overridden in tests to mock catalog export. var exportCatalogPgDelta = ExportCatalogPgDelta +// diffPgDeltaRefDetailed is the seam DiffDatabase uses for the pg-delta engine. +// Tests override it to stub the real edge-runtime pipeline (which the injected +// DiffFunc differ cannot, since pg-delta bypasses differ), the same pattern as +// exportCatalogPgDelta above. +var diffPgDeltaRefDetailed = DiffPgDeltaRefDetailed + // DeclarativeExportPgDelta exports target schema as declarative file payloads // while keeping a config-based API for existing call sites. func DeclarativeExportPgDelta(ctx context.Context, source, target pgconn.Config, schema []string, formatOptions string, options ...func(*pgx.ConnConfig)) (DeclarativeOutput, error) { diff --git a/apps/cli-go/internal/db/diff/pgdelta_debug.go b/apps/cli-go/internal/db/diff/pgdelta_debug.go index b901edd5f7..1439018e80 100644 --- a/apps/cli-go/internal/db/diff/pgdelta_debug.go +++ b/apps/cli-go/internal/db/diff/pgdelta_debug.go @@ -17,9 +17,10 @@ func IsPgDeltaDebugEnabled() bool { } } -// PgDeltaDiffResult holds pg-delta diff output and edge-runtime stderr. +// PgDeltaDiffResult holds the parsed pg-delta diff envelope (one file per +// execution-aware plan unit) and the edge-runtime stderr. type PgDeltaDiffResult struct { - SQL string + Files []PgDeltaPlanFile Stderr string } @@ -31,7 +32,10 @@ type PgDeltaDebugCapture struct { // DatabaseDiff is the result of diffing a target database against a shadow baseline. type DatabaseDiff struct { - SQL string + SQL string + // Files carries the per-unit pg-delta plan files (empty for the migra engine). + // SQL is the flattened join of these, kept for callers that consume one blob. + Files []PgDeltaPlanFile Debug *PgDeltaDebugCapture } diff --git a/apps/cli-go/internal/db/diff/pgdelta_migrations.go b/apps/cli-go/internal/db/diff/pgdelta_migrations.go new file mode 100644 index 0000000000..c8f4a8b244 --- /dev/null +++ b/apps/cli-go/internal/db/diff/pgdelta_migrations.go @@ -0,0 +1,108 @@ +package diff + +import ( + "os" + "path/filepath" + "time" + + "github.com/go-errors/errors" + "github.com/spf13/afero" + "github.com/supabase/cli/internal/migration/new" + "github.com/supabase/cli/internal/utils" +) + +// maxVersionCollisionAttempts bounds the base-timestamp bump retry so a directory +// already full of same-second migrations can't spin forever. +const maxVersionCollisionAttempts = 60 + +// WrittenMigration is a migration file produced by a diff/pull, paired with the +// version to record in the remote migration history. +type WrittenMigration struct { + Path string + Version string +} + +// WritePgDeltaMigrations writes one ordered migration file per plan unit. A +// single-unit plan (the common case) keeps the exact `_.sql` filename; +// multi-unit plans append the unit name and give each file a strictly increasing +// timestamp (real time arithmetic on the base, never string increment) so their +// execution order and migration-history order stay stable. +// +// Before writing anything, the FULL set of generated filenames is collision-checked +// against the filesystem: if any target path already exists the base is advanced by +// one second and every version recomputed, so the set stays strictly ascending AND +// unique against pre-existing migrations. The base only ever moves forward — never +// backdated below the caller's wall clock, since backdating could sort a new file +// before pre-existing migrations. The resulting ≤N−1s future-dating is inherent to +// second-granularity versions and acceptable once uniqueness is enforced. +func WritePgDeltaMigrations(files []PgDeltaPlanFile, base time.Time, name string, fsys afero.Fs) (_ []WrittenMigration, err error) { + single := len(files) == 1 + buildSet := func(b time.Time) []WrittenMigration { + set := make([]WrittenMigration, len(files)) + for i, file := range files { + version := utils.GetVersionTimestamp(b.Add(time.Duration(i) * time.Second)) + fileName := name + if !single { + fileName = name + "_" + file.Name + } + set[i] = WrittenMigration{Path: new.GetMigrationPath(version, fileName), Version: version} + } + return set + } + + set := buildSet(base) + for attempt := 0; ; attempt++ { + collision := false + for _, w := range set { + exists, err := afero.Exists(fsys, w.Path) + if err != nil { + return nil, errors.Errorf("failed to check migration file: %w", err) + } + if exists { + collision = true + break + } + } + if !collision { + break + } + if attempt+1 >= maxVersionCollisionAttempts { + return nil, errors.Errorf("failed to find a unique migration version after %d attempts", maxVersionCollisionAttempts) + } + base = base.Add(time.Second) + set = buildSet(base) + } + + written := make([]WrittenMigration, 0, len(files)) + // Best-effort cleanup: if any open/write fails mid-loop, remove every file this + // invocation already wrote so a partial multi-file migration isn't left behind. + // A removal failure never masks the original error. + defer func() { + if err != nil { + for _, w := range written { + _ = fsys.Remove(w.Path) + } + } + }() + for i, file := range files { + w := set[i] + if err = utils.MkdirIfNotExistFS(fsys, filepath.Dir(w.Path)); err != nil { + return nil, err + } + // O_EXCL (not O_TRUNC): a race that created the file between the collision + // check and here must never silently overwrite an existing migration. + f, openErr := fsys.OpenFile(w.Path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644) + if openErr != nil { + err = errors.Errorf("failed to open migration file: %w", openErr) + return nil, err + } + if _, writeErr := f.WriteString(file.SQL + "\n"); writeErr != nil { + f.Close() + err = errors.Errorf("failed to write migration file: %w", writeErr) + return nil, err + } + f.Close() + written = append(written, w) + } + return written, nil +} diff --git a/apps/cli-go/internal/db/diff/pgdelta_migrations_test.go b/apps/cli-go/internal/db/diff/pgdelta_migrations_test.go new file mode 100644 index 0000000000..16ace1b7ff --- /dev/null +++ b/apps/cli-go/internal/db/diff/pgdelta_migrations_test.go @@ -0,0 +1,133 @@ +package diff + +import ( + "os" + "testing" + "time" + + "github.com/go-errors/errors" + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/supabase/cli/internal/migration/new" +) + +// failOnNthOpenFs fails the Nth create-for-write OpenFile so a mid-loop write +// failure can be exercised deterministically. Stat/mkdir/read calls pass through. +type failOnNthOpenFs struct { + afero.Fs + failOn int + count int +} + +func (f *failOnNthOpenFs) OpenFile(name string, flag int, perm os.FileMode) (afero.File, error) { + if flag&os.O_CREATE != 0 { + f.count++ + if f.count == f.failOn { + return nil, errors.New("simulated open failure") + } + } + return f.Fs.OpenFile(name, flag, perm) +} + +func TestWritePgDeltaMigrations(t *testing.T) { + base := time.Date(2026, 7, 17, 15, 18, 48, 0, time.UTC) + + t.Run("writes a single unit with the unchanged name", func(t *testing.T) { + fsys := afero.NewMemMapFs() + files := []PgDeltaPlanFile{ + {Order: 1, Name: "schema_changes", TransactionMode: "transactional", SQL: "-- unit 1\n\ncreate table a ();"}, + } + written, err := WritePgDeltaMigrations(files, base, "remote_schema", fsys) + require.NoError(t, err) + require.Len(t, written, 1) + assert.Equal(t, "20260717151848", written[0].Version) + expectedPath := new.GetMigrationPath("20260717151848", "remote_schema") + assert.Equal(t, expectedPath, written[0].Path) + contents, err := afero.ReadFile(fsys, expectedPath) + require.NoError(t, err) + assert.Equal(t, "-- unit 1\n\ncreate table a ();\n", string(contents)) + }) + + t.Run("writes one ordered file per unit with strictly increasing versions", func(t *testing.T) { + fsys := afero.NewMemMapFs() + files := []PgDeltaPlanFile{ + {Order: 1, Name: "schema_changes", TransactionMode: "transactional", SQL: "-- unit 1\n\nalter type mood add value 'ok';"}, + {Order: 2, Name: "after_enum_values", TransactionMode: "transactional", SQL: "-- unit 2\n\ninsert into t values ('ok');"}, + {Order: 3, Name: "non_transactional", TransactionMode: "none", SQL: "-- unit 3\n\ncreate index concurrently i on t (c);"}, + } + written, err := WritePgDeltaMigrations(files, base, "remote_schema", fsys) + require.NoError(t, err) + require.Len(t, written, 3) + + wantVersions := []string{"20260717151848", "20260717151849", "20260717151850"} + wantNames := []string{"remote_schema_schema_changes", "remote_schema_after_enum_values", "remote_schema_non_transactional"} + for i, w := range written { + assert.Equal(t, wantVersions[i], w.Version) + assert.Equal(t, new.GetMigrationPath(wantVersions[i], wantNames[i]), w.Path) + contents, err := afero.ReadFile(fsys, w.Path) + require.NoError(t, err) + assert.Equal(t, files[i].SQL+"\n", string(contents)) + } + // Versions are strictly increasing so history + execution order stay stable. + assert.True(t, written[0].Version < written[1].Version) + assert.True(t, written[1].Version < written[2].Version) + }) + + t.Run("creates nested parent directories for a nested migration name", func(t *testing.T) { + fsys := afero.NewMemMapFs() + files := []PgDeltaPlanFile{ + {Order: 1, Name: "schema_changes", TransactionMode: "transactional", SQL: "create table a ();"}, + {Order: 2, Name: "after_enum_values", TransactionMode: "transactional", SQL: "insert into t values ('ok');"}, + } + written, err := WritePgDeltaMigrations(files, base, "snapshots/remote", fsys) + require.NoError(t, err) + require.Len(t, written, 2) + for i, w := range written { + contents, err := afero.ReadFile(fsys, w.Path) + require.NoError(t, err) + assert.Equal(t, files[i].SQL+"\n", string(contents)) + } + }) + + t.Run("bumps the base version when a target file already exists", func(t *testing.T) { + fsys := afero.NewMemMapFs() + files := []PgDeltaPlanFile{ + {Order: 1, Name: "schema_changes", TransactionMode: "transactional", SQL: "create table a ();"}, + {Order: 2, Name: "after_enum_values", TransactionMode: "transactional", SQL: "insert into t values ('ok');"}, + } + // Pre-existing migration at the first version the base would otherwise use. + existing := new.GetMigrationPath("20260717151848", "remote_schema_schema_changes") + require.NoError(t, afero.WriteFile(fsys, existing, []byte("-- pre-existing\n"), 0644)) + + written, err := WritePgDeltaMigrations(files, base, "remote_schema", fsys) + require.NoError(t, err) + require.Len(t, written, 2) + // The whole set advances one second so it skips the colliding version and + // stays strictly ascending against the pre-existing file. + assert.Equal(t, "20260717151849", written[0].Version) + assert.Equal(t, "20260717151850", written[1].Version) + assert.True(t, written[0].Version < written[1].Version) + // The pre-existing file is untouched (never overwritten). + contents, err := afero.ReadFile(fsys, existing) + require.NoError(t, err) + assert.Equal(t, "-- pre-existing\n", string(contents)) + }) + + t.Run("removes already-written files when a later write fails", func(t *testing.T) { + fsys := &failOnNthOpenFs{Fs: afero.NewMemMapFs(), failOn: 2} + files := []PgDeltaPlanFile{ + {Order: 1, Name: "schema_changes", TransactionMode: "transactional", SQL: "create table a ();"}, + {Order: 2, Name: "after_enum_values", TransactionMode: "transactional", SQL: "insert into t values ('ok');"}, + } + written, err := WritePgDeltaMigrations(files, base, "remote_schema", fsys) + require.Error(t, err) + assert.Nil(t, written) + // The first unit's file was written then removed on the failure, so nothing + // from this invocation is left behind. + first := new.GetMigrationPath("20260717151848", "remote_schema_schema_changes") + exists, statErr := afero.Exists(fsys, first) + require.NoError(t, statErr) + assert.False(t, exists) + }) +} diff --git a/apps/cli-go/internal/db/diff/pgdelta_test.go b/apps/cli-go/internal/db/diff/pgdelta_test.go index 671414a069..ad312273c5 100644 --- a/apps/cli-go/internal/db/diff/pgdelta_test.go +++ b/apps/cli-go/internal/db/diff/pgdelta_test.go @@ -32,3 +32,40 @@ func TestContainerRef(t *testing.T) { assert.Equal(t, "/workspace/supabase/.temp/pgdelta/catalog-baseline-17.6.1.106.json", containerRef(ref)) }) } + +func TestParsePgDeltaDiffOutput(t *testing.T) { + t.Run("parses a multi-file envelope", func(t *testing.T) { + stdout := `{"version":1,"files":[` + + `{"order":1,"name":"schema_changes","transactionMode":"transactional","sql":"-- unit 1\n\nCREATE TABLE a ();"},` + + `{"order":2,"name":"after_enum_values","transactionMode":"transactional","sql":"-- unit 2\n\nINSERT INTO a VALUES (1);"}` + + `]}` + result, err := parsePgDeltaDiffOutput(stdout, "debug stderr") + assert.NoError(t, err) + assert.Equal(t, "debug stderr", result.Stderr) + assert.Len(t, result.Files, 2) + assert.Equal(t, PgDeltaPlanFile{Order: 1, Name: "schema_changes", TransactionMode: "transactional", SQL: "-- unit 1\n\nCREATE TABLE a ();"}, result.Files[0]) + assert.Equal(t, "after_enum_values", result.Files[1].Name) + // The flattened join keeps unit boundaries visible via header comments. + assert.Equal(t, "-- unit 1\n\nCREATE TABLE a ();\n\n-- unit 2\n\nINSERT INTO a VALUES (1);", joinPgDeltaFiles(result.Files)) + }) + + t.Run("treats an empty envelope as no changes", func(t *testing.T) { + result, err := parsePgDeltaDiffOutput(`{"version":1,"files":[]}`, "") + assert.NoError(t, err) + assert.Empty(t, result.Files) + assert.Equal(t, "", joinPgDeltaFiles(result.Files)) + }) + + t.Run("treats empty stdout as no changes", func(t *testing.T) { + result, err := parsePgDeltaDiffOutput(" \n", "") + assert.NoError(t, err) + assert.Empty(t, result.Files) + }) + + t.Run("fails on malformed json and embeds stderr", func(t *testing.T) { + _, err := parsePgDeltaDiffOutput("not json", "boom on the edge runtime") + assert.Error(t, err) + assert.ErrorContains(t, err, "failed to parse pg-delta diff output") + assert.ErrorContains(t, err, "boom on the edge runtime") + }) +} diff --git a/apps/cli-go/internal/db/diff/templates/pgdelta.ts b/apps/cli-go/internal/db/diff/templates/pgdelta.ts index 0fd9d00e30..2a9d2d3f2a 100644 --- a/apps/cli-go/internal/db/diff/templates/pgdelta.ts +++ b/apps/cli-go/internal/db/diff/templates/pgdelta.ts @@ -1,7 +1,7 @@ import { createPlan, deserializeCatalog, - formatSqlStatements, + renderPlanFiles, } from "npm:@supabase/pg-delta@1.0.0-alpha.20"; import { supabase } from "npm:@supabase/pg-delta@1.0.0-alpha.20/integrations/supabase"; @@ -32,10 +32,19 @@ if (includedSchemas) { } const formatOptionsRaw = Deno.env.get("FORMAT_OPTIONS"); -let formatOptions = undefined; -if (formatOptionsRaw) { - formatOptions = JSON.parse(formatOptionsRaw); -} +const parsedFormatOptions = formatOptionsRaw ? JSON.parse(formatOptionsRaw) : undefined; +// Format the emitted SQL by default with the same sensible settings the +// declarative export uses (`exportDeclarativeSchema` in @supabase/pg-delta: +// `{ ...DEFAULT_OPTIONS, maxWidth: 180, keywordCase: "upper", ...userOptions }`), +// so `db pull` / `db diff` produce readable migrations even when config sets no +// `[experimental.pgdelta] format_options`. The formatter fills DEFAULT_OPTIONS +// for missing keys itself, so only the two overrides are passed here. Setting +// `format_options = "null"` (parsed to `null`) is the explicit opt-out: raw, +// unformatted statements, mirroring declarative export's `formatOptions === null`. +const sqlFormatOptions = + parsedFormatOptions === null + ? undefined + : { maxWidth: 180, keywordCase: "upper", ...parsedFormatOptions }; try { const result = await createPlan( @@ -46,14 +55,32 @@ try { skipDefaultPrivilegeSubtraction: true, }, ); - let statements = result?.plan.statements ?? []; - if (formatOptions != null) { - statements = formatSqlStatements(statements, formatOptions); - } + // pg-delta >= 1.0.0-alpha.32 groups plan statements into execution-aware + // `units` with transaction boundaries. `renderPlanFiles` turns those into one + // numbered SQL file per unit (header comments included). `includeTransactions: + // false` because the CLI appliers already wrap each migration file in a single + // transaction (Go's implicit ExecBatch / the TS BEGIN/COMMIT wrap), so embedded + // BEGIN/COMMIT would nest; format options are applied per unit here instead of a + // manual `formatSqlStatements` pass. + const files = result + ? renderPlanFiles(result.plan, { + includeTransactions: false, + sqlFormatOptions, + }) + : []; + const envelope = files.map((file, index) => ({ + order: index + 1, + // The unit name is the rendered path minus its numeric prefix and `.sql` + // extension (e.g. `001_after_enum_values.sql` -> `after_enum_values`). + name: file.path.replace(/^\d+_/, "").replace(/\.sql$/, ""), + transactionMode: file.unit.transactionMode, + sql: file.sql, + })); if (Deno.env.get("PGDELTA_DEBUG")) { console.error( JSON.stringify({ - statementCount: statements.length, + statementCount: files.reduce((total, file) => total + file.unit.statements.length, 0), + fileCount: files.length, source: source ? "connected" : "null", target: target ? "connected" : "null", includedSchemas: includedSchemas ?? null, @@ -61,11 +88,13 @@ try { }), ); } - for (const sql of statements) { - console.log(`${sql};`); - } + console.log(JSON.stringify({ version: 1, files: envelope })); } catch (e) { console.error(e); + // Emit a sentinel so the CLI runner can distinguish a real script crash from a + // successful empty diff, even though the forced-exit non-zero code below is + // suppressed by the "main worker has been destroyed" handling. + console.error("PGDELTA_SCRIPT_ERROR"); // Force close event loop throw new Error(""); } diff --git a/apps/cli-go/internal/db/diff/templates/pgdelta_catalog_export.ts b/apps/cli-go/internal/db/diff/templates/pgdelta_catalog_export.ts index 6b7d426ce1..dfecc58da1 100644 --- a/apps/cli-go/internal/db/diff/templates/pgdelta_catalog_export.ts +++ b/apps/cli-go/internal/db/diff/templates/pgdelta_catalog_export.ts @@ -12,6 +12,10 @@ const role = Deno.env.get("ROLE") ?? undefined; if (!target) { console.error("TARGET is required"); + // Emit a sentinel so the CLI runner treats this as a real script crash rather + // than a successful empty catalog, even though the forced-exit non-zero code is + // suppressed by the "main worker has been destroyed" handling. + console.error("PGDELTA_SCRIPT_ERROR"); throw new Error(""); } const { pool, close } = await createManagedPool(target, { role }); @@ -21,6 +25,10 @@ try { console.log(stringifyCatalogSnapshot(serializeCatalog(catalog))); } catch (e) { console.error(e); + // Emit a sentinel so the CLI runner can distinguish a real script crash from a + // successful empty catalog, even though the forced-exit non-zero code below is + // suppressed by the "main worker has been destroyed" handling. + console.error("PGDELTA_SCRIPT_ERROR"); // Force close event loop throw new Error(""); } finally { diff --git a/apps/cli-go/internal/db/diff/templates/pgdelta_declarative_export.ts b/apps/cli-go/internal/db/diff/templates/pgdelta_declarative_export.ts index 4656e4690e..18820ff7b9 100644 --- a/apps/cli-go/internal/db/diff/templates/pgdelta_declarative_export.ts +++ b/apps/cli-go/internal/db/diff/templates/pgdelta_declarative_export.ts @@ -68,6 +68,10 @@ try { } } catch (e) { console.error(e); + // Emit a sentinel so the CLI runner can distinguish a real script crash from a + // successful empty export, even though the forced-exit non-zero code below is + // suppressed by the "main worker has been destroyed" handling. + console.error("PGDELTA_SCRIPT_ERROR"); // Force close event loop throw new Error(""); } diff --git a/apps/cli-go/internal/db/pull/pull.go b/apps/cli-go/internal/db/pull/pull.go index 07503687a7..21e0db87f0 100644 --- a/apps/cli-go/internal/db/pull/pull.go +++ b/apps/cli-go/internal/db/pull/pull.go @@ -11,6 +11,7 @@ import ( "path/filepath" "strconv" "strings" + "time" "github.com/go-errors/errors" "github.com/jackc/pgconn" @@ -59,21 +60,26 @@ func Run(ctx context.Context, schema []string, config pgconn.Config, name string // TODO: handle managed schemas return format.WriteStructuredSchemas(ctx, &buf, fsys) } - // 2. Pull schema - timestamp := utils.GetCurrentTimestamp() - path := new.GetMigrationPath(timestamp, name) - if err := run(ctx, schema, path, conn, usePgDeltaDiff, differ, fsys, options...); err != nil { + // 2. Pull schema. pg-delta plans with transaction boundaries produce more than + // one ordered migration file; migra always produces exactly one. + base := time.Now().UTC() + written, err := run(ctx, schema, base, name, conn, usePgDeltaDiff, differ, fsys, options...) + if err != nil { return err } - if err := ensureMigrationWritten(fsys, path); err != nil { - return err + if len(written) == 0 { + return errors.New(errInSync) + } + // 3. Insert a row to `schema_migrations` for every file written. + versions := make([]string, len(written)) + for i, w := range written { + fmt.Fprintln(os.Stderr, "Schema written to "+utils.Bold(w.Path)) + versions[i] = w.Version } - // 3. Insert a row to `schema_migrations` - fmt.Fprintln(os.Stderr, "Schema written to "+utils.Bold(path)) if shouldUpdate, err := utils.NewConsole().PromptYesNo(ctx, "Update remote migration history table?", true); err != nil { return err } else if shouldUpdate { - return repair.UpdateMigrationTable(ctx, conn, []string{timestamp}, repair.Applied, false, fsys) + return repair.UpdateMigrationTable(ctx, conn, versions, repair.Applied, false, fsys) } return nil } @@ -114,8 +120,10 @@ func pullDeclarativePgDelta(ctx context.Context, schema []string, config pgconn. return nil } -func run(ctx context.Context, schema []string, path string, conn *pgx.Conn, usePgDeltaDiff bool, differ diff.DiffFunc, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error { +func run(ctx context.Context, schema []string, base time.Time, name string, conn *pgx.Conn, usePgDeltaDiff bool, differ diff.DiffFunc, fsys afero.Fs, options ...func(*pgx.ConnConfig)) ([]diff.WrittenMigration, error) { config := conn.Config().Config + timestamp := utils.GetVersionTimestamp(base) + path := new.GetMigrationPath(timestamp, name) // 1. Assert `supabase/migrations` and `schema_migrations` are in sync. if err := assertRemoteInSync(ctx, conn, fsys); errors.Is(err, errMissing) { // pg_dump strips ownership when restored as a non-superuser, so platform @@ -126,19 +134,27 @@ func run(ctx context.Context, schema []string, path string, conn *pgx.Conn, useP if !usePgDeltaDiff { // Ignore schemas flag when working on the initial pull if err = dumpRemoteSchema(ctx, path, config, fsys); err != nil { - return err + return nil, err } } // For the legacy path this is a second pass that captures changes // pg_dump cannot emit (default privileges, managed schemas). For the // pg-delta path this is the only pass and produces the full schema. - err = swallowInitialInSync(diffRemoteSchema(ctx, nil, path, config, usePgDeltaDiff, differ, fsys, options...), fsys, path) - return err + written, err := diffRemoteSchema(ctx, nil, base, name, config, usePgDeltaDiff, differ, fsys, options...) + if err = swallowInitialInSync(err, fsys, path); err != nil { + return nil, err + } + // The migra initial pull seeds `path` with a pg_dump even when the follow-up + // diff is empty and swallowed above, so record that single migration. + if !usePgDeltaDiff && len(written) == 0 { + written = []diff.WrittenMigration{{Path: path, Version: timestamp}} + } + return written, nil } else if err != nil { - return err + return nil, err } // 2. Fetch remote schema changes - return diffRemoteSchema(ctx, schema, path, config, usePgDeltaDiff, differ, fsys, options...) + return diffRemoteSchema(ctx, schema, base, name, config, usePgDeltaDiff, differ, fsys, options...) } func dumpRemoteSchema(ctx context.Context, path string, config pgconn.Config, fsys afero.Fs) error { @@ -157,7 +173,7 @@ func dumpRemoteSchema(ctx context.Context, path string, config pgconn.Config, fs }) } -func diffRemoteSchema(ctx context.Context, schema []string, path string, config pgconn.Config, usePgDeltaDiff bool, differ diff.DiffFunc, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error { +func diffRemoteSchema(ctx context.Context, schema []string, base time.Time, name string, config pgconn.Config, usePgDeltaDiff bool, differ diff.DiffFunc, fsys afero.Fs, options ...func(*pgx.ConnConfig)) ([]diff.WrittenMigration, error) { // Diff remote db (source) & shadow db (target) and write it as a new migration. result, err := diff.DiffDatabase(ctx, schema, config, os.Stderr, fsys, differ, usePgDeltaDiff, options...) if err != nil { @@ -166,37 +182,47 @@ func diffRemoteSchema(ctx context.Context, schema []string, path string, config // so the whole db pull workflow is self-healing, not just the dump pass. poolerConfig, ok := dump.PoolerFallbackConfig(ctx, config, err) if !ok { - return err + return nil, err } if result, err = diff.DiffDatabase(ctx, schema, poolerConfig, os.Stderr, fsys, differ, usePgDeltaDiff, options...); err != nil { - return err + return nil, err } } - output := result.SQL - if trimmed := strings.TrimSpace(output); len(trimmed) == 0 { - if usePgDeltaDiff && diff.IsPgDeltaDebugEnabled() { - if debugDir, debugErr := saveEmptyPgDeltaPullDebug(ctx, config, result.Debug, fsys, options...); debugErr != nil { - fmt.Fprintf(os.Stderr, "Warning: failed to save pg-delta debug bundle: %v\n", debugErr) - } else if len(debugDir) > 0 { - return errors.Errorf("%w (debug bundle: %s)", errInSync, debugDir) + // pg-delta path: one migration file per execution-aware plan unit. + if usePgDeltaDiff { + if len(result.Files) == 0 { + if diff.IsPgDeltaDebugEnabled() { + if debugDir, debugErr := saveEmptyPgDeltaPullDebug(ctx, config, result.Debug, fsys, options...); debugErr != nil { + fmt.Fprintf(os.Stderr, "Warning: failed to save pg-delta debug bundle: %v\n", debugErr) + } else if len(debugDir) > 0 { + return nil, errors.Errorf("%w (debug bundle: %s)", errInSync, debugDir) + } } + return nil, errors.New(errInSync) } - return errors.New(errInSync) + return diff.WritePgDeltaMigrations(result.Files, base, name, fsys) } + // migra path: a single migration file, appended when seeded by dumpRemoteSchema. + output := result.SQL + if trimmed := strings.TrimSpace(output); len(trimmed) == 0 { + return nil, errors.New(errInSync) + } + timestamp := utils.GetVersionTimestamp(base) + path := new.GetMigrationPath(timestamp, name) if err := utils.MkdirIfNotExistFS(fsys, filepath.Dir(path)); err != nil { - return err + return nil, err } // Append to existing migration file when we run this after dumpRemoteSchema; - // for the pg-delta path this is the only writer and creates the file fresh. + // for a non-initial pull this creates the file fresh. f, err := fsys.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644) if err != nil { - return errors.Errorf("failed to open migration file: %w", err) + return nil, errors.Errorf("failed to open migration file: %w", err) } defer f.Close() if _, err := f.WriteString(output); err != nil { - return errors.Errorf("failed to write migration file: %w", err) + return nil, errors.Errorf("failed to write migration file: %w", err) } - return nil + return []diff.WrittenMigration{{Path: path, Version: timestamp}}, nil } func assertRemoteInSync(ctx context.Context, conn *pgx.Conn, fsys afero.Fs) error { diff --git a/apps/cli-go/internal/db/pull/pull_test.go b/apps/cli-go/internal/db/pull/pull_test.go index e9fbae9198..ec3e784f83 100644 --- a/apps/cli-go/internal/db/pull/pull_test.go +++ b/apps/cli-go/internal/db/pull/pull_test.go @@ -6,6 +6,7 @@ import ( "os" "path/filepath" "testing" + "time" "github.com/h2non/gock" "github.com/jackc/pgconn" @@ -14,6 +15,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/supabase/cli/internal/db/diff" + "github.com/supabase/cli/internal/migration/new" "github.com/supabase/cli/internal/testing/apitest" "github.com/supabase/cli/internal/testing/fstest" "github.com/supabase/cli/internal/utils" @@ -75,11 +77,13 @@ func TestPullSchema(t *testing.T) { conn.Query(migration.LIST_MIGRATION_VERSION). Reply("SELECT 0") // Run test - err := run(context.Background(), nil, "0_test.sql", conn.MockClient(t), false, diff.DiffSchemaMigra, fsys) + base := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC) + path := new.GetMigrationPath(utils.GetVersionTimestamp(base), "test") + _, err := run(context.Background(), nil, base, "test", conn.MockClient(t), false, diff.DiffSchemaMigra, fsys) // Check error assert.ErrorIs(t, err, errNetwork) assert.Empty(t, apitest.ListUnmatchedRequests()) - contents, err := afero.ReadFile(fsys, "0_test.sql") + contents, err := afero.ReadFile(fsys, path) assert.NoError(t, err) assert.Equal(t, []byte("test"), contents) }) @@ -102,12 +106,14 @@ func TestPullSchema(t *testing.T) { conn.Query(migration.LIST_MIGRATION_VERSION). Reply("SELECT 0") // Run test with usePgDeltaDiff=true - err := run(context.Background(), nil, "0_test.sql", conn.MockClient(t), true, diff.DiffPgDelta, fsys) + base := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC) + path := new.GetMigrationPath(utils.GetVersionTimestamp(base), "test") + _, err := run(context.Background(), nil, base, "test", conn.MockClient(t), true, diff.DiffPgDelta, fsys) // Failure must come from shadow-creation image inspect (proving we // reached the diff step), not from pg_dump. assert.ErrorIs(t, err, errNetwork) assert.Empty(t, apitest.ListUnmatchedRequests()) - exists, err := afero.Exists(fsys, "0_test.sql") + exists, err := afero.Exists(fsys, path) assert.NoError(t, err) assert.False(t, exists, "pg_dump should be skipped for pg-delta diff engine") }) @@ -129,7 +135,8 @@ func TestPullSchema(t *testing.T) { conn.Query(migration.LIST_MIGRATION_VERSION). Reply("SELECT 1", []any{"0"}) // Run test - err := run(context.Background(), []string{"public"}, "", conn.MockClient(t), false, diff.DiffSchemaMigra, fsys) + base := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC) + _, err := run(context.Background(), []string{"public"}, base, "test", conn.MockClient(t), false, diff.DiffSchemaMigra, fsys) // Check error assert.ErrorContains(t, err, "network error") assert.Empty(t, apitest.ListUnmatchedRequests()) diff --git a/apps/cli-go/internal/utils/edgeruntime.go b/apps/cli-go/internal/utils/edgeruntime.go index c81169ea6f..8e54afa628 100644 --- a/apps/cli-go/internal/utils/edgeruntime.go +++ b/apps/cli-go/internal/utils/edgeruntime.go @@ -13,6 +13,18 @@ import ( "github.com/spf13/viper" ) +// EdgeRuntimeScriptErrorSentinel is printed to stderr by the pg-delta Deno +// templates when their body throws (see the `catch` blocks in +// internal/db/diff/templates/*.ts). The templates force the edge-runtime worker +// to exit by throwing on both the success and failure paths, and that non-zero +// exit is otherwise suppressed here when stderr contains "main worker has been +// destroyed". Without a distinct marker a crashed script would be +// indistinguishable from a successful empty diff, so `db pull` would report "No +// schema changes found" while the real error (e.g. a permission-denied catalog +// query) was silently swallowed. Templates and tests must reference this exact +// string. See supabase/cli#5826. +const EdgeRuntimeScriptErrorSentinel = "PGDELTA_SCRIPT_ERROR" + // edgeRuntimeFile is a single file dropped into the edge-runtime container's // working directory before the configured command is run. type edgeRuntimeFile struct { @@ -119,6 +131,14 @@ func RunEdgeRuntimeScript(ctx context.Context, env []string, script string, bind ); err != nil && !strings.Contains(stderr.String(), "main worker has been destroyed") { return errors.Errorf("%s: %w:\n%s", errPrefix, err, stderr.String()) } + // The templates suppress their own non-zero exit (they throw to force the + // worker to exit, which surfaces as "main worker has been destroyed"), so a + // script crash can slip past the check above. Treat the sentinel — printed + // only by the templates' catch blocks — as a hard failure so the real error, + // collected in stderr, reaches the user instead of looking like an empty diff. + if strings.Contains(stderr.String(), EdgeRuntimeScriptErrorSentinel) { + return errors.Errorf("%s: error running script:\n%s", errPrefix, stderr.String()) + } return nil } diff --git a/apps/cli-go/internal/utils/edgeruntime_test.go b/apps/cli-go/internal/utils/edgeruntime_test.go index 471a518ee6..2497630255 100644 --- a/apps/cli-go/internal/utils/edgeruntime_test.go +++ b/apps/cli-go/internal/utils/edgeruntime_test.go @@ -1,14 +1,97 @@ package utils import ( + "bytes" + "context" + "io" + "net/http" "strconv" "strings" "testing" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/pkg/stdcopy" + "github.com/h2non/gock" + "github.com/spf13/viper" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/supabase/cli/internal/testing/apitest" ) +// mockEdgeRuntimeLogs registers the docker responses RunEdgeRuntimeScript needs: +// a one-shot log read multiplexing stdout+stderr, an inspect reporting the exit +// code, and the container delete. Mirrors apitest.MockDockerErrorLogs but also +// carries stdout so we can assert the success path preserves the script output. +func mockEdgeRuntimeLogs(t *testing.T, containerID, stdout, stderr string, exitCode int) { + t.Helper() + var body bytes.Buffer + if len(stdout) > 0 { + _, err := io.Copy(stdcopy.NewStdWriter(&body, stdcopy.Stdout), strings.NewReader(stdout)) + require.NoError(t, err) + } + if len(stderr) > 0 { + _, err := io.Copy(stdcopy.NewStdWriter(&body, stdcopy.Stderr), strings.NewReader(stderr)) + require.NoError(t, err) + } + gock.New(Docker.DaemonHost()). + Get("/v"+Docker.ClientVersion()+"/containers/"+containerID+"/logs"). + Reply(http.StatusOK). + SetHeader("Content-Type", "application/vnd.docker.raw-stream"). + Body(&body) + gock.New(Docker.DaemonHost()). + Get("/v" + Docker.ClientVersion() + "/containers/" + containerID + "/json"). + Reply(http.StatusOK). + JSON(container.InspectResponse{ContainerJSONBase: &container.ContainerJSONBase{ + State: &container.State{ExitCode: exitCode}, + }}) + gock.New(Docker.DaemonHost()). + Delete("/v" + Docker.ClientVersion() + "/containers/" + containerID). + Reply(http.StatusOK) +} + +func TestRunEdgeRuntimeScript(t *testing.T) { + const containerID = "test-edge-runtime" + imageUrl := GetRegistryImageUrl(Config.EdgeRuntime.Image) + + t.Run("surfaces the real error when the script crashes behind the worker-destroyed message", func(t *testing.T) { + viper.Set("INTERNAL_IMAGE_REGISTRY", "docker.io") + t.Cleanup(func() { viper.Set("INTERNAL_IMAGE_REGISTRY", "") }) + require.NoError(t, apitest.MockDocker(Docker)) + defer gock.OffAll() + apitest.MockDockerStart(Docker, imageUrl, containerID) + // The pg-delta template throws to force the worker to exit (surfacing as a + // non-zero exit + "main worker has been destroyed"), and its catch block + // prints the real error and the sentinel. This must NOT look like an empty diff. + stderr := "error: permission denied for table pg_user_mapping\n" + + EdgeRuntimeScriptErrorSentinel + "\n" + + "worker boot error\nmain worker has been destroyed\n" + mockEdgeRuntimeLogs(t, containerID, "", stderr, 1) + + var stdout, stderrBuf bytes.Buffer + err := RunEdgeRuntimeScript(context.Background(), nil, "console.log('x')", nil, "error diffing schema", &stdout, &stderrBuf) + require.Error(t, err) + assert.Contains(t, err.Error(), "error diffing schema: error running script:") + // The real, actionable error must reach the user, not "No schema changes found". + assert.Contains(t, err.Error(), "permission denied for table pg_user_mapping") + }) + + t.Run("still ignores a worker-destroyed exit when no sentinel is present", func(t *testing.T) { + viper.Set("INTERNAL_IMAGE_REGISTRY", "docker.io") + t.Cleanup(func() { viper.Set("INTERNAL_IMAGE_REGISTRY", "") }) + require.NoError(t, apitest.MockDocker(Docker)) + defer gock.OffAll() + apitest.MockDockerStart(Docker, imageUrl, containerID) + // Success path: the template forces the worker to exit after writing output, + // so the exit is non-zero with "main worker has been destroyed" but no sentinel. + mockEdgeRuntimeLogs(t, containerID, "ALTER TABLE x;\n", "main worker has been destroyed\n", 1) + + var stdout, stderrBuf bytes.Buffer + err := RunEdgeRuntimeScript(context.Background(), nil, "console.log('x')", nil, "error diffing schema", &stdout, &stderrBuf) + require.NoError(t, err) + assert.Equal(t, "ALTER TABLE x;\n", stdout.String()) + }) +} + func TestBuildEdgeRuntimeEntrypoint(t *testing.T) { t.Run("emits a single heredoc when only the script is provided", func(t *testing.T) { got := buildEdgeRuntimeEntrypoint( diff --git a/apps/cli-go/internal/utils/misc.go b/apps/cli-go/internal/utils/misc.go index 8e9870531b..3faf385966 100644 --- a/apps/cli-go/internal/utils/misc.go +++ b/apps/cli-go/internal/utils/misc.go @@ -129,7 +129,14 @@ func IsPgDeltaEnabled() bool { func GetCurrentTimestamp() string { // Magic number: https://stackoverflow.com/q/45160822. - return time.Now().UTC().Format(layoutVersion) + return GetVersionTimestamp(time.Now()) +} + +// GetVersionTimestamp formats t as a migration version (UTC `YYYYMMDDHHMMSS`). +// Callers that write several ordered migration files in one pass add real time +// offsets to a shared base rather than incrementing the formatted string. +func GetVersionTimestamp(t time.Time) string { + return t.UTC().Format(layoutVersion) } func GetCurrentBranchFS(fsys afero.Fs) (string, error) { diff --git a/apps/cli-go/pkg/config/pgdelta_version.go b/apps/cli-go/pkg/config/pgdelta_version.go index 26be07f8a0..a38641ae7c 100644 --- a/apps/cli-go/pkg/config/pgdelta_version.go +++ b/apps/cli-go/pkg/config/pgdelta_version.go @@ -4,7 +4,7 @@ import "strings" // DefaultPgDeltaNpmVersion is the npm dist-tag/version used for @supabase/pg-delta // when supabase/.temp/pgdelta-version is absent or empty. -const DefaultPgDeltaNpmVersion = "1.0.0-alpha.27" +const DefaultPgDeltaNpmVersion = "1.0.0-alpha.32" const pgDeltaNpmVersionPlaceholder = "1.0.0-alpha.20" diff --git a/apps/cli/src/legacy/commands/db/diff/diff.handler.ts b/apps/cli/src/legacy/commands/db/diff/diff.handler.ts index c7dcdb5fad..72bc7c2e58 100644 --- a/apps/cli/src/legacy/commands/db/diff/diff.handler.ts +++ b/apps/cli/src/legacy/commands/db/diff/diff.handler.ts @@ -27,6 +27,7 @@ import { legacyGetMigrationPath, } from "../../../shared/legacy-migration-file.ts"; import { legacyDiffMigra } from "../shared/legacy-migra.ts"; +import { legacyWritePgDeltaMigrations } from "../shared/legacy-pgdelta-migrations.write.ts"; import { type LegacyPgDeltaContext, legacyDiffPgDelta } from "../shared/legacy-pgdelta.ts"; import { LegacyDeclarativeSeam } from "../shared/legacy-pgdelta.seam.service.ts"; import type { LegacyDbDiffFlags } from "./diff.command.ts"; @@ -406,7 +407,7 @@ export const legacyDbDiff = Effect.fn("legacy.db.diff")(function* (flags: Legacy projectRef: connType === "linked" ? linkedRef : undefined, }); - const out = yield* Effect.gen(function* () { + const diffResult = yield* Effect.gen(function* () { const target = shadow.targetUrlOverride ?? targetUrl; yield* output.raw( flags.schema.length > 0 @@ -421,15 +422,22 @@ export const legacyDbDiff = Effect.fn("legacy.db.diff")(function* (flags: Legacy schema: flags.schema, formatOptions, }); - return result.sql; + // Keep the per-unit plan files so a multi-unit plan can be written as one + // migration file each (Go's `DatabaseDiff.Files`); `sql` stays the flattened + // join for stdout review + machine payloads. + return { sql: result.sql, files: result.files }; } - return yield* legacyDiffMigra(ctx, { + const sql = yield* legacyDiffMigra(ctx, { source: shadow.sourceUrl, target, schema: flags.schema, connectOptions: { isLocal: resolved.isLocal, dnsResolver }, }); + // The migra engine has no execution-aware plan units, so it always writes a + // single migration file (Go's `SaveDiff` single-file path). + return { sql, files: undefined }; }).pipe(Effect.ensuring(seam.removeShadowContainer(shadow.container))); + const out = diffResult.sql; // Detect the branch from the resolved workdir, not the caller's CWD: Go // chdirs into --workdir in PersistentPreRunE before GetGitBranch @@ -444,27 +452,46 @@ export const legacyDbDiff = Effect.fn("legacy.db.diff")(function* (flags: Legacy // Go's `SaveDiff` (`pgadmin.go:20`) + the drop-statement warning (`diff.go:44`). const engine = useDelta ? "pg-delta" : "migra"; const drops = legacyFindDropStatements(out); - let writtenFile: string | null = null; + const writtenFiles: Array = []; if (out.length < 2) { yield* output.raw("No schema changes found\n", "stderr"); // Go's `SaveDiff` gates the file write on `len(file) > 0` (`pgadmin.go`), so // an empty `--file=""` (e.g. an unset shell var) falls through to stdout // rather than writing a `_.sql` migration with no name. } else if (Option.isSome(flags.file) && flags.file.value.length > 0) { - const timestamp = legacyFormatMigrationTimestamp(yield* Clock.currentTimeMillis); - const migrationPath = legacyGetMigrationPath( - path, - cliConfig.workdir, - timestamp, - flags.file.value, - ); - yield* legacyMakeDir(fs, path.dirname(migrationPath)).pipe( - Effect.mapError((cause) => new LegacyDbDiffWriteError({ message: cause.message })), - ); - yield* fs - .writeFileString(migrationPath, out) - .pipe(Effect.mapError((cause) => new LegacyDbDiffWriteError({ message: cause.message }))); - writtenFile = migrationPath; + const fileName = flags.file.value; + // A pg-delta plan that crosses a transaction boundary yields more than one + // ordered unit; writing them into a single migration file would later fail + // when `db push`/`reset` applies it as one transaction. Write one migration + // file per unit in that case via the shared writer (Go's + // `WritePgDeltaMigrations`, `internal/db/diff/pgdelta_migrations.go`): each + // file appends the unit name and gets a strictly increasing timestamp, the + // full set is collision-checked against existing migrations, and every file is + // written exclusively so a pre-existing migration is never overwritten. A + // single-unit plan (and the migra engine) keeps the exact `_.sql` + // file (Go's `utils.WriteFile`), byte-identical to before. + const planFiles = diffResult.files ?? []; + if (planFiles.length > 1) { + const writtenUnits = yield* legacyWritePgDeltaMigrations(fs, path, { + workdir: cliConfig.workdir, + baseMillis: yield* Clock.currentTimeMillis, + name: fileName, + files: planFiles.map((file) => ({ name: file.name, sql: file.sql })), + }).pipe(Effect.mapError((cause) => new LegacyDbDiffWriteError({ message: cause.message }))); + for (const unit of writtenUnits) writtenFiles.push(unit.path); + } else { + const timestamp = legacyFormatMigrationTimestamp(yield* Clock.currentTimeMillis); + const migrationPath = legacyGetMigrationPath(path, cliConfig.workdir, timestamp, fileName); + // Create parent dirs per written path (mirroring Go's `utils.WriteFile`), so a + // nested `--file snapshots/remote` name creates `_snapshots/` first. + yield* legacyMakeDir(fs, path.dirname(migrationPath)).pipe( + Effect.mapError((cause) => new LegacyDbDiffWriteError({ message: cause.message })), + ); + yield* fs + .writeFileString(migrationPath, out) + .pipe(Effect.mapError((cause) => new LegacyDbDiffWriteError({ message: cause.message }))); + writtenFiles.push(migrationPath); + } yield* output.raw(`${warnDiff}\n`, "stderr"); } else if (output.format === "text") { yield* output.raw(`${out}\n`); @@ -479,7 +506,12 @@ export const legacyDbDiff = Effect.fn("legacy.db.diff")(function* (flags: Legacy if (output.format !== "text") { yield* output.success("Diff complete.", { diff: out, - file: writtenFile, + // `file` keeps the first written path for released consumers that read the + // string field (null when nothing was written); `files` lists EVERY written + // migration path in write order (a pg-delta plan writes one file per unit), + // mirroring pull's `schemaFiles` so machine callers see all of them. + file: writtenFiles[0] ?? null, + files: writtenFiles, schemas: flags.schema, engine, dropStatements: drops, diff --git a/apps/cli/src/legacy/commands/db/diff/diff.integration.test.ts b/apps/cli/src/legacy/commands/db/diff/diff.integration.test.ts index 0278e7e41e..f4e6fb4ea6 100644 --- a/apps/cli/src/legacy/commands/db/diff/diff.integration.test.ts +++ b/apps/cli/src/legacy/commands/db/diff/diff.integration.test.ts @@ -1,10 +1,11 @@ -import { existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs"; +import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { BunServices } from "@effect/platform-bun"; import { describe, expect, it } from "@effect/vitest"; import { Effect, Exit, Layer, Option } from "effect"; import { + legacyFailWriteStringOnNthCallFsLayer, mockLegacyCliConfig, mockLegacyLinkedProjectCacheTracked, mockLegacyTelemetryStateTracked, @@ -35,10 +36,15 @@ interface SetupOpts { readonly isLocal?: boolean; readonly linkedRef?: string; readonly diffSql?: string; + // When set, the pg-delta edge mock emits a multi-unit plan envelope (one file + // per entry) instead of the single-unit wrap of `diffSql`. + readonly diffFiles?: ReadonlyArray<{ readonly name: string; readonly sql: string }>; readonly targetOverride?: string; readonly oom?: boolean; // edge-runtime OOMs; the bash fallback returns `diffSql` readonly delegateStdout?: string; // stdout returned by a captured Go-delegate run readonly networkId?: string; // --network-id value forwarded to docker runs + // When set, the Nth `writeFileString` fails, exercising cleanup-on-failure. + readonly failWriteOnCall?: number; } function setup(workdir: string, opts: SetupOpts = {}) { @@ -87,7 +93,28 @@ function setup(workdir: string, opts: SetupOpts = {}) { new LegacyEdgeRuntimeScriptError({ message: "Fatal JavaScript out of memory" }), ); } - return Effect.succeed({ stdout: opts.diffSql ?? "", stderr: "" }); + const diffSql = opts.diffSql ?? ""; + // The pg-delta diff script (uniquely identified by `renderPlanFiles`) prints a + // JSON envelope with one file per plan unit; wrap the test's raw SQL into a + // single-unit envelope so `legacyDiffPgDelta` parses it. The migra script + // returns raw SQL unchanged. + const isPgDelta = runOpts.script.includes("renderPlanFiles"); + const planFiles = + opts.diffFiles !== undefined + ? opts.diffFiles.map((file, i) => ({ + order: i + 1, + name: file.name, + transactionMode: "transactional", + sql: file.sql, + })) + : diffSql.length > 0 + ? [{ order: 1, name: "schema_changes", transactionMode: "transactional", sql: diffSql }] + : []; + const stdout = + isPgDelta && planFiles.length > 0 + ? JSON.stringify({ version: 1, files: planFiles }) + : diffSql; + return Effect.succeed({ stdout, stderr: "" }); }, }); @@ -141,7 +168,7 @@ function setup(workdir: string, opts: SetupOpts = {}) { }), }); - const layer = Layer.mergeAll( + const baseLayer = Layer.mergeAll( out.layer, telemetry.layer, cache.layer, @@ -164,6 +191,12 @@ function setup(workdir: string, opts: SetupOpts = {}) { mockRuntimeInfo(), BunServices.layer, ); + // Merged last so its `FileSystem` overrides `BunServices` (last-wins); `Path` + // still resolves from `BunServices`. + const layer = + opts.failWriteOnCall === undefined + ? baseLayer + : Layer.merge(baseLayer, legacyFailWriteStringOnNthCallFsLayer(opts.failWriteOnCall)); return { layer, @@ -438,6 +471,122 @@ describe("legacy db diff", () => { }).pipe(Effect.provide(s.layer)); }); + it.effect("writes one migration file per unit for a multi-unit pg-delta plan", () => { + // A pg-delta plan that crosses a transaction boundary yields more than one + // ordered unit; writing them into one migration would fail when db push/reset + // applies it as a single transaction. Each unit becomes its own file (Go's + // WritePgDeltaMigrations), named `_` with strictly increasing + // timestamps, and the machine payload's `files` lists them all. + const s = setup(tmp.current, { + format: "json", + diffFiles: [ + { name: "schema_changes", sql: "alter type mood add value 'ok';" }, + { name: "after_enum_values", sql: "insert into t values ('ok');" }, + ], + }); + return Effect.gen(function* () { + yield* legacyDbDiff(flags({ usePgDelta: Option.some(true), file: Option.some("my_diff") })); + const dir = join(tmp.current, "supabase", "migrations"); + const files = readdirSync(dir).sort(); + expect(files).toHaveLength(2); + expect(files[0]).toMatch(/^\d{14}_my_diff_schema_changes\.sql$/); + expect(files[1]).toMatch(/^\d{14}_my_diff_after_enum_values\.sql$/); + // Each unit's file carries only that unit's SQL, terminated with a newline. + expect(readFileSync(join(dir, files[0]!), "utf8")).toBe("alter type mood add value 'ok';\n"); + const success = s.out.messages.find((m) => m.type === "success"); + const data = success?.data as { file: string; files: ReadonlyArray }; + expect(data.files).toHaveLength(2); + // `file` stays the first written path for released string-field consumers. + expect(data.file).toBe(data.files[0]); + }).pipe(Effect.provide(s.layer)); + }); + + it.effect("creates nested parent directories for a nested single-unit --file name", () => { + // `db diff -f snapshots/remote` must create the `_snapshots/` parent dir + // before writing, mirroring Go's `utils.WriteFile`. + const s = setup(tmp.current, { diffSql: "create table g ();\n" }); + return Effect.gen(function* () { + yield* legacyDbDiff(flags({ file: Option.some("snapshots/remote") })); + const migrationsRoot = join(tmp.current, "supabase", "migrations"); + const dirs = readdirSync(migrationsRoot); + expect(dirs).toHaveLength(1); + expect(dirs[0]).toMatch(/^\d{14}_snapshots$/); + expect(readdirSync(join(migrationsRoot, dirs[0]!))).toEqual(["remote.sql"]); + }).pipe(Effect.provide(s.layer)); + }); + + it.effect("creates nested parent directories for a nested multi-unit --file name", () => { + const s = setup(tmp.current, { + format: "json", + diffFiles: [ + { name: "schema_changes", sql: "alter type mood add value 'ok';" }, + { name: "after_enum_values", sql: "insert into t values ('ok');" }, + ], + }); + return Effect.gen(function* () { + yield* legacyDbDiff( + flags({ usePgDelta: Option.some(true), file: Option.some("snapshots/remote") }), + ); + const success = s.out.messages.find((m) => m.type === "success"); + const data = success?.data as { files: ReadonlyArray }; + expect(data.files).toHaveLength(2); + for (const written of data.files) expect(existsSync(written)).toBe(true); + expect(data.files[0]).toMatch(/\d{14}_snapshots\/remote_schema_changes\.sql$/u); + expect(data.files[1]).toMatch(/\d{14}_snapshots\/remote_after_enum_values\.sql$/u); + }).pipe(Effect.provide(s.layer)); + }); + + it.effect("bumps the version set when a target migration file already exists", () => { + // The full generated set is collision-checked before writing; if any target + // exists the base advances one second so the new files stay strictly ascending + // AND never overwrite the pre-existing migration. + const s = setup(tmp.current, { + format: "json", + diffFiles: [ + { name: "schema_changes", sql: "a" }, + { name: "after_enum_values", sql: "b" }, + ], + }); + return Effect.gen(function* () { + const dir = join(tmp.current, "supabase", "migrations"); + mkdirSync(dir, { recursive: true }); + // TestClock starts at epoch 0, so the first version the writer tries is + // 19700101000000; pre-seed a colliding file at that version. + const clashing = join(dir, "19700101000000_my_diff_schema_changes.sql"); + writeFileSync(clashing, "-- pre-existing\n"); + yield* legacyDbDiff(flags({ usePgDelta: Option.some(true), file: Option.some("my_diff") })); + expect(readdirSync(dir).sort()).toEqual([ + "19700101000000_my_diff_schema_changes.sql", + "19700101000001_my_diff_schema_changes.sql", + "19700101000002_my_diff_after_enum_values.sql", + ]); + // The pre-existing file was never overwritten. + expect(readFileSync(clashing, "utf8")).toBe("-- pre-existing\n"); + }).pipe(Effect.provide(s.layer)); + }); + + it.effect("removes already-written unit files when a later unit write fails", () => { + // A mid-loop write failure best-effort removes every file this invocation + // already wrote, so no partial multi-file migration is left behind. + const s = setup(tmp.current, { + format: "json", + failWriteOnCall: 2, + diffFiles: [ + { name: "schema_changes", sql: "a" }, + { name: "after_enum_values", sql: "b" }, + ], + }); + return Effect.gen(function* () { + const exit = yield* legacyDbDiff( + flags({ usePgDelta: Option.some(true), file: Option.some("my_diff") }), + ).pipe(Effect.exit); + expect(Exit.isFailure(exit)).toBe(true); + const dir = join(tmp.current, "supabase", "migrations"); + const remaining = existsSync(dir) ? readdirSync(dir) : []; + expect(remaining).toEqual([]); + }).pipe(Effect.provide(s.layer)); + }); + it.effect("explicit --from local --to linked prints the diff to stdout", () => { const s = setup(tmp.current, { isLocal: false, diffSql: "create table e ();\n" }); return Effect.gen(function* () { diff --git a/apps/cli/src/legacy/commands/db/pull/pull.handler.ts b/apps/cli/src/legacy/commands/db/pull/pull.handler.ts index e95fc934f2..858bb50c1a 100644 --- a/apps/cli/src/legacy/commands/db/pull/pull.handler.ts +++ b/apps/cli/src/legacy/commands/db/pull/pull.handler.ts @@ -44,6 +44,7 @@ import { legacyShouldUsePgDelta, } from "../shared/legacy-diff-engine.ts"; import { legacyDiffMigra } from "../shared/legacy-migra.ts"; +import { legacyWritePgDeltaMigrations } from "../shared/legacy-pgdelta-migrations.write.ts"; import { type LegacyDumpOptions, legacyBuildSchemaDumpEnv } from "../shared/legacy-pg-dump.env.ts"; import { legacyStreamPgDump } from "../shared/legacy-pg-dump.run.ts"; import { @@ -428,7 +429,8 @@ export const legacyDbPull = Effect.fn("legacy.db.pull")(function* (flags: Legacy } // Migration-file path (Go's `pull.run`). - const timestamp = legacyFormatMigrationTimestamp(yield* Clock.currentTimeMillis); + const nowMillis = yield* Clock.currentTimeMillis; + const timestamp = legacyFormatMigrationTimestamp(nowMillis); const migrationPath = legacyGetMigrationPath(path, cliConfig.workdir, timestamp, name); const remote = yield* legacyListRemoteMigrations(session); @@ -620,6 +622,7 @@ export const legacyDbPull = Effect.fn("legacy.db.pull")(function* (flags: Legacy }); return { sql: result.sql, + files: result.files, capture: debug ? { sourceCatalog, stderr: result.stderr } : undefined, }; } @@ -629,7 +632,7 @@ export const legacyDbPull = Effect.fn("legacy.db.pull")(function* (flags: Legacy schema: diffSchema, connectOptions: { isLocal: resolved.isLocal, dnsResolver }, }); - return { sql, capture: undefined }; + return { sql, files: undefined, capture: undefined }; }), ); }).pipe(Effect.ensuring(seam.removeShadowContainer(shadow.container))); @@ -678,55 +681,87 @@ export const legacyDbPull = Effect.fn("legacy.db.pull")(function* (flags: Legacy ); } - if (!diffEmpty) { - if (seededFromDump) { - // Append the migra diff to the dump-seeded file (Go's `diffRemoteSchema` - // opens the migration file `O_APPEND`, `pull.go:191`). - yield* Effect.scoped( - Effect.gen(function* () { - const file = yield* fs.open(migrationPath, { flag: "a" }).pipe( - Effect.mapError( - (cause) => - new LegacyDbPullWriteError({ - message: `failed to open migration file: ${cause.message}`, - }), - ), - ); - yield* file.writeAll(new TextEncoder().encode(out)).pipe( - Effect.mapError( - (cause) => - new LegacyDbPullWriteError({ - message: `failed to write migration file: ${cause.message}`, - }), - ), - ); - }), - ); - } else { - yield* legacyMakeDir(fs, path.dirname(migrationPath)).pipe( - Effect.mapError((cause) => new LegacyDbPullWriteError({ message: cause.message })), - ); - yield* fs.writeFileString(migrationPath, out).pipe( - Effect.mapError( - (cause) => - new LegacyDbPullWriteError({ - message: `failed to write migration file: ${cause.message}`, - }), - ), + // Build the list of migration files to record in the remote history. The + // migra engine writes exactly one file (the dump-seeded or freshly written + // migrationPath); the pg-delta engine writes one ordered file per + // execution-aware plan unit. + const writtenMigrations: Array<{ path: string; version: string }> = []; + if (usePgDeltaDiff) { + // pg-delta: one migration file per plan unit via the shared writer. A + // single-unit plan (the common case) keeps the exact `_.sql` + // filename; multi-unit plans append the unit name and give each file a + // strictly increasing timestamp so execution + migration-history order stay + // stable. The full set is collision-checked against existing migrations and + // each file is written exclusively so a pre-existing migration is never + // overwritten. Mirrors Go's `WritePgDeltaMigrations` + // (`internal/db/diff/pgdelta_migrations.go`). Empty plans are handled by the + // `diffEmpty` in-sync branch above, so `planFiles` is non-empty here. + const planFiles = diffOutcome.files ?? []; + const writtenUnits = yield* legacyWritePgDeltaMigrations(fs, path, { + workdir: cliConfig.workdir, + baseMillis: nowMillis, + name, + files: planFiles.map((file) => ({ name: file.name, sql: file.sql })), + }).pipe( + Effect.mapError((cause) => new LegacyDbPullWriteError({ message: cause.message })), + ); + for (const unit of writtenUnits) { + writtenMigrations.push({ path: unit.path, version: unit.version }); + } + } else { + if (!diffEmpty) { + if (seededFromDump) { + // Append the migra diff to the dump-seeded file (Go's `diffRemoteSchema` + // opens the migration file `O_APPEND`, `pull.go:191`). + yield* Effect.scoped( + Effect.gen(function* () { + const file = yield* fs.open(migrationPath, { flag: "a" }).pipe( + Effect.mapError( + (cause) => + new LegacyDbPullWriteError({ + message: `failed to open migration file: ${cause.message}`, + }), + ), + ); + yield* file.writeAll(new TextEncoder().encode(out)).pipe( + Effect.mapError( + (cause) => + new LegacyDbPullWriteError({ + message: `failed to write migration file: ${cause.message}`, + }), + ), + ); + }), + ); + } else { + yield* legacyMakeDir(fs, path.dirname(migrationPath)).pipe( + Effect.mapError((cause) => new LegacyDbPullWriteError({ message: cause.message })), + ); + yield* fs.writeFileString(migrationPath, out).pipe( + Effect.mapError( + (cause) => + new LegacyDbPullWriteError({ + message: `failed to write migration file: ${cause.message}`, + }), + ), + ); + } + } + + // Go's `ensureMigrationWritten` (`pull.go:68,263-268`): a dump that produced + // nothing followed by an empty diff leaves the file empty → in sync. + if (seededFromDump && !seedWroteBytes && diffEmpty) { + return yield* Effect.fail( + new LegacyDbPullInSyncError({ message: "No schema changes found" }), ); } + writtenMigrations.push({ path: migrationPath, version: timestamp }); } - // Go's `ensureMigrationWritten` (`pull.go:68,263-268`): a dump that produced - // nothing followed by an empty diff leaves the file empty → in sync. - if (seededFromDump && !seedWroteBytes && diffEmpty) { - return yield* Effect.fail( - new LegacyDbPullInSyncError({ message: "No schema changes found" }), - ); + for (const written of writtenMigrations) { + yield* output.raw(`Schema written to ${legacyBold(written.path)}\n`, "stderr"); } - yield* output.raw(`Schema written to ${legacyBold(migrationPath)}\n`, "stderr"); - // Prompt to update the remote migration history table. Go calls // `PromptYesNo(ctx, "Update remote migration history table?", true)` // (`internal/db/pull/pull.go:73`), which returns the default (`true`) on @@ -739,14 +774,19 @@ export const legacyDbPull = Effect.fn("legacy.db.pull")(function* (flags: Legacy // (`console.go:64-82`), and otherwise prompts on a real TTY. const shouldUpdate = yield* legacyPromptYesNo(output, yes, updateHistoryTitle, true); if (shouldUpdate) { - yield* legacyUpdateMigrationHistory(session, fs, path, migrationPath, timestamp); + yield* legacyUpdateMigrationHistory(session, fs, path, writtenMigrations); remoteHistoryUpdated = true; } if (output.format !== "text") { yield* output.success("Schema pulled.", { declarative: false, - schemaWritten: migrationPath, + // `schemaWritten` keeps the first written path for released consumers that + // read the string field; `schemaFiles` lists EVERY written migration path + // in write order (a pg-delta plan writes one file per unit), so machine + // callers see all of them, not just the first. + schemaWritten: writtenMigrations[0]?.path ?? migrationPath, + schemaFiles: writtenMigrations.map((written) => written.path), remoteHistoryUpdated, engine: usePgDeltaDiff ? "pg-delta" : "migra", }); diff --git a/apps/cli/src/legacy/commands/db/pull/pull.integration.test.ts b/apps/cli/src/legacy/commands/db/pull/pull.integration.test.ts index 1e15768888..248c0be283 100644 --- a/apps/cli/src/legacy/commands/db/pull/pull.integration.test.ts +++ b/apps/cli/src/legacy/commands/db/pull/pull.integration.test.ts @@ -5,6 +5,7 @@ import { describe, expect, it } from "@effect/vitest"; import { Effect, Exit, Layer, Option } from "effect"; import { + legacyFailWriteStringOnNthCallFsLayer, mockLegacyCliConfig, mockLegacyLinkedProjectCacheTracked, mockLegacyTelemetryStateTracked, @@ -44,6 +45,21 @@ const EXPORT_JSON = JSON.stringify({ files: [{ path: "schemas/public/t.sql", order: 0, statements: 1, sql: "create table t ();" }], }); +// Builds the pg-delta diff envelope printed by `templates/pgdelta.ts`: one file +// per execution-aware plan unit (`{version:1,files:[{order,name,transactionMode,sql}]}`). +const pgDeltaDiffEnvelope = ( + units: ReadonlyArray<{ name: string; sql: string; transactionMode?: string }>, +): string => + JSON.stringify({ + version: 1, + files: units.map((unit, index) => ({ + order: index + 1, + name: unit.name, + transactionMode: unit.transactionMode ?? "transactional", + sql: unit.sql, + })), + }); + interface SetupOpts { readonly format?: OutputFormat; readonly remoteVersions?: ReadonlyArray; @@ -78,6 +94,8 @@ interface SetupOpts { // `--declarative` and `--use-pg-delta` are present, to replay pflag's // last-occurrence-wins ordering; defaults to empty. readonly args?: ReadonlyArray; + // When set, the Nth `writeFileString` fails, exercising cleanup-on-failure. + readonly failWriteOnCall?: number; } function setup(workdir: string, opts: SetupOpts = {}) { @@ -222,7 +240,7 @@ function setup(workdir: string, opts: SetupOpts = {}) { }), }); - const layer = Layer.mergeAll( + const baseLayer = Layer.mergeAll( out.layer, telemetry.layer, cache.layer, @@ -250,6 +268,12 @@ function setup(workdir: string, opts: SetupOpts = {}) { mockRuntimeInfo(), BunServices.layer, ); + // Merged last so its `FileSystem` overrides `BunServices` (last-wins); `Path` + // still resolves from `BunServices`. + const layer = + opts.failWriteOnCall === undefined + ? baseLayer + : Layer.merge(baseLayer, legacyFailWriteStringOnNthCallFsLayer(opts.failWriteOnCall)); return { layer, @@ -303,20 +327,158 @@ describe("legacy db pull", () => { seedMigration(tmp.current, "20240101000000"); const s = setup(tmp.current, { remoteVersions: ["20240101000000"], - edgeStdout: "create table remote ();\n", + edgeStdout: pgDeltaDiffEnvelope([ + { + name: "schema_changes", + sql: "-- Migration unit 1: schema_changes\n\ncreate table remote ();", + }, + ]), yes: true, }); return Effect.gen(function* () { yield* legacyDbPull(flags({ diffEngine: Option.some("pg-delta") })); const dir = join(tmp.current, "supabase", "migrations"); expect(existsSync(join(dir, `${"20240101000000"}_local.sql`))).toBe(true); - // A new timestamped remote_schema migration was written. + // A single-unit plan keeps the unchanged `_remote_schema.sql` filename. + const written = readdirSync(dir).filter((f) => f.endsWith("_remote_schema.sql")); + expect(written).toHaveLength(1); + expect(readFileSync(join(dir, written[0] ?? ""), "utf8")).toContain( + "create table remote ();", + ); expect(streamText(s.out, "stderr")).toContain("Schema written to"); expect(s.historyUpserts.length).toBe(1); expect(streamText(s.out, "stdout")).toContain("Finished supabase db pull."); }).pipe(Effect.provide(s.layer)); }); + it.effect( + "a pg-delta plan with transaction boundaries writes one ordered migration file per unit", + () => { + // pg-delta plans that cross a transaction boundary (e.g. ALTER TYPE ... ADD + // VALUE then a statement using the new value) come back as several units; each + // is written to its own migration file with a strictly increasing timestamp and + // recorded in the remote history. Mirrors Go's `writePgDeltaMigrations`. + seedMigration(tmp.current, "20240101000000"); + const s = setup(tmp.current, { + remoteVersions: ["20240101000000"], + edgeStdout: pgDeltaDiffEnvelope([ + { name: "schema_changes", sql: "-- unit 1\n\nalter type mood add value 'ok';" }, + { name: "after_enum_values", sql: "-- unit 2\n\ninsert into t values ('ok');" }, + { + name: "non_transactional", + transactionMode: "none", + sql: "-- unit 3\n\ncreate index concurrently i on t (c);", + }, + ]), + yes: true, + }); + return Effect.gen(function* () { + yield* legacyDbPull(flags({ diffEngine: Option.some("pg-delta") })); + const dir = join(tmp.current, "supabase", "migrations"); + const written = readdirSync(dir) + .filter((f) => f !== "20240101000000_local.sql") + .sort(); + expect(written).toHaveLength(3); + // Multi-unit plans append the unit name and carry strictly increasing versions. + expect(written[0]).toMatch(/^\d{14}_remote_schema_schema_changes\.sql$/u); + expect(written[1]).toMatch(/^\d{14}_remote_schema_after_enum_values\.sql$/u); + expect(written[2]).toMatch(/^\d{14}_remote_schema_non_transactional\.sql$/u); + const versions = written.map((f) => f.slice(0, 14)); + expect((versions[0] ?? "") < (versions[1] ?? "")).toBe(true); + expect((versions[1] ?? "") < (versions[2] ?? "")).toBe(true); + expect(readFileSync(join(dir, written[2] ?? ""), "utf8")).toContain( + "create index concurrently i on t (c);", + ); + // One "Schema written to" line and one history upsert per unit. + expect(streamText(s.out, "stderr").match(/Schema written to/gu)).toHaveLength(3); + expect(s.historyUpserts.length).toBe(3); + // Go's UpdateMigrationTable prints all versions space-separated. + expect(streamText(s.out, "stderr")).toContain( + `Repaired migration history: [${versions.join(" ")}] => applied`, + ); + }).pipe(Effect.provide(s.layer)); + }, + ); + + it.effect( + "a multi-unit pg-delta pull reports every written migration path in the json payload", + () => { + // The structured payload must list ALL written migration files in write order, + // not just the first (`schemaWritten`). A pg-delta plan writes one file per unit. + seedMigration(tmp.current, "20240101000000"); + const s = setup(tmp.current, { + format: "json", + remoteVersions: ["20240101000000"], + edgeStdout: pgDeltaDiffEnvelope([ + { name: "schema_changes", sql: "-- unit 1\n\nalter type mood add value 'ok';" }, + { name: "after_enum_values", sql: "-- unit 2\n\ninsert into t values ('ok');" }, + { + name: "non_transactional", + transactionMode: "none", + sql: "-- unit 3\n\ncreate index concurrently i on t (c);", + }, + ]), + }); + return Effect.gen(function* () { + yield* legacyDbPull(flags({ diffEngine: Option.some("pg-delta") })); + const success = s.out.messages.find((m) => m.type === "success"); + const data = success?.data as + | { schemaWritten?: string; schemaFiles?: Array } + | undefined; + expect(data?.schemaFiles).toHaveLength(3); + // Paths appear in write order, each carrying its unit name. + expect(data?.schemaFiles?.[0]).toMatch(/_remote_schema_schema_changes\.sql$/u); + expect(data?.schemaFiles?.[1]).toMatch(/_remote_schema_after_enum_values\.sql$/u); + expect(data?.schemaFiles?.[2]).toMatch(/_remote_schema_non_transactional\.sql$/u); + // `schemaWritten` stays the first written path (unchanged string contract). + expect(data?.schemaWritten).toBe(data?.schemaFiles?.[0]); + }).pipe(Effect.provide(s.layer)); + }, + ); + + it.effect("removes already-written unit files when a later pg-delta unit write fails", () => { + // A mid-loop write failure best-effort removes every migration file this + // invocation already wrote, so no partial multi-file pull is left behind. + seedMigration(tmp.current, "20240101000000"); + const s = setup(tmp.current, { + failWriteOnCall: 2, + remoteVersions: ["20240101000000"], + edgeStdout: pgDeltaDiffEnvelope([ + { name: "schema_changes", sql: "alter type mood add value 'ok';" }, + { name: "after_enum_values", sql: "insert into t values ('ok');" }, + ]), + yes: true, + }); + return Effect.gen(function* () { + const exit = yield* legacyDbPull(flags({ diffEngine: Option.some("pg-delta") })).pipe( + Effect.exit, + ); + expect(Exit.isFailure(exit)).toBe(true); + const dir = join(tmp.current, "supabase", "migrations"); + // Only the pre-seeded local migration remains; the first written unit was + // rolled back and the failing second unit never landed. + expect(readdirSync(dir)).toEqual(["20240101000000_local.sql"]); + // No history rows were upserted because the write failed before the prompt. + expect(s.historyUpserts.length).toBe(0); + }).pipe(Effect.provide(s.layer)); + }); + + it.effect("a malformed pg-delta diff envelope surfaces a parse error, not 'in sync'", () => { + seedMigration(tmp.current, "20240101000000"); + const s = setup(tmp.current, { + remoteVersions: ["20240101000000"], + edgeStdout: "not a valid envelope{", + yes: true, + }); + return Effect.gen(function* () { + const error = yield* legacyDbPull(flags({ diffEngine: Option.some("pg-delta") })).pipe( + Effect.flip, + ); + expect(error.message).toContain("failed to parse pg-delta diff output"); + expect(error.message).not.toContain("No schema changes found"); + }).pipe(Effect.provide(s.layer)); + }); + it.effect("pulls with the default migra engine", () => { seedMigration(tmp.current, "20240101000000"); const s = setup(tmp.current, { @@ -527,8 +689,14 @@ describe("legacy db pull", () => { remoteHistoryUpdated: true, engine: "migra", }); - const data = success?.data as { schemaWritten?: string } | undefined; + const data = success?.data as + | { schemaWritten?: string; schemaFiles?: Array } + | undefined; expect(data?.schemaWritten).toMatch(/_remote_schema\.sql$/u); + // The single-unit case lists exactly one written migration path, and it is the + // same path as the singular `schemaWritten` field. + expect(data?.schemaFiles).toHaveLength(1); + expect(data?.schemaFiles?.[0]).toBe(data?.schemaWritten); }).pipe(Effect.provide(s.layer)); }); @@ -1045,7 +1213,7 @@ describe("legacy db pull", () => { writeFileSync(join(tmp.current, "supabase", ".env"), "SUPABASE_EXPERIMENTAL_PG_DELTA=true\n"); const s = setup(tmp.current, { remoteVersions: ["20240101000000"], - edgeStdout: "create table remote ();\n", + edgeStdout: pgDeltaDiffEnvelope([{ name: "schema_changes", sql: "create table remote ();" }]), yes: true, }); return Effect.gen(function* () { @@ -1158,7 +1326,7 @@ describe("legacy db pull", () => { ); const s = setup(tmp.current, { remoteVersions: ["20240101000000"], - edgeStdout: "create table remote ();\n", + edgeStdout: pgDeltaDiffEnvelope([{ name: "schema_changes", sql: "create table remote ();" }]), yes: true, resolvedRef: "abcdefghijklmnopqrst", }); @@ -1180,7 +1348,7 @@ describe("legacy db pull", () => { const s = setup(tmp.current, { remoteVersions: ["20240101000000"], edgeFailFirstWith: "error diffing schema:\nfailed to connect: network is unreachable", - edgeStdout: "create table remote ();\n", + edgeStdout: pgDeltaDiffEnvelope([{ name: "schema_changes", sql: "create table remote ();" }]), yes: true, poolerAvailable: true, }); diff --git a/apps/cli/src/legacy/commands/db/pull/pull.sync.integration.test.ts b/apps/cli/src/legacy/commands/db/pull/pull.sync.integration.test.ts new file mode 100644 index 0000000000..05b3423db6 --- /dev/null +++ b/apps/cli/src/legacy/commands/db/pull/pull.sync.integration.test.ts @@ -0,0 +1,100 @@ +import { mkdtempSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { BunServices } from "@effect/platform-bun"; +import { describe, expect, it } from "@effect/vitest"; +import { Effect, FileSystem, Path } from "effect"; + +import { mockOutput } from "../../../../../tests/helpers/mocks.ts"; +import { LegacyDbExecError } from "../../../shared/legacy-db-connection.errors.ts"; +import type { LegacyDbSession } from "../../../shared/legacy-db-connection.service.ts"; +import { LegacyDbPullWriteError } from "./pull.errors.ts"; +import { legacyUpdateMigrationHistory, type LegacyPulledMigration } from "./pull.sync.ts"; + +// Records exec statements and successful upserts in one ordered log so the tests +// can assert the transaction envelope (BEGIN / UPSERT / COMMIT / ROLLBACK) around +// the version writes. `failUpsertAt` fails the Nth upsert to simulate a dropped +// connection mid-loop. +function mockSession(opts: { readonly failUpsertAt?: number } = {}) { + const calls: Array = []; + let upsertCount = 0; + const session: LegacyDbSession = { + exec: (sql: string) => Effect.sync(() => void calls.push(sql)), + query: (sql: string) => { + if (/INSERT INTO supabase_migrations/u.test(sql)) { + upsertCount += 1; + if (opts.failUpsertAt === upsertCount) { + return Effect.fail(new LegacyDbExecError({ message: "connection reset by peer" })); + } + calls.push("UPSERT"); + } + return Effect.succeed([] as ReadonlyArray>); + }, + extensionExists: () => Effect.die("extensionExists unused"), + copyToCsv: () => Effect.die("copyToCsv unused"), + queryRaw: () => Effect.die("queryRaw unused"), + }; + return { session, calls }; +} + +function writeMigrations(dir: string): ReadonlyArray { + const migrations: ReadonlyArray = [ + { path: join(dir, "20240101000000_a.sql"), version: "20240101000000" }, + { path: join(dir, "20240101000001_b.sql"), version: "20240101000001" }, + ]; + writeFileSync(migrations[0]!.path, "create table a ();"); + writeFileSync(migrations[1]!.path, "create table b ();"); + return migrations; +} + +describe("legacyUpdateMigrationHistory", () => { + it.effect("wraps the upserts in one BEGIN + N upserts + COMMIT transaction", () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const dir = mkdtempSync(join(tmpdir(), "pull-sync-")); + const migrations = writeMigrations(dir); + const out = mockOutput(); + const { session, calls } = mockSession(); + + yield* legacyUpdateMigrationHistory(session, fs, path, migrations).pipe( + Effect.provide(out.layer), + ); + + // The create-table setup runs its own BEGIN/COMMIT first; the upsert + // transaction is the trailing envelope around every version write. + expect(calls).not.toContain("ROLLBACK"); + expect(calls.slice(-4)).toEqual(["BEGIN", "UPSERT", "UPSERT", "COMMIT"]); + // The success line is byte-identical to Go's repair output. + expect(out.stderrText).toContain( + "Repaired migration history: [20240101000000 20240101000001] => applied", + ); + }).pipe(Effect.provide(BunServices.layer)), + ); + + it.effect("rolls back and surfaces the error when an upsert fails mid-loop", () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const dir = mkdtempSync(join(tmpdir(), "pull-sync-")); + const migrations = writeMigrations(dir); + const out = mockOutput(); + const { session, calls } = mockSession({ failUpsertAt: 2 }); + + const error = yield* legacyUpdateMigrationHistory(session, fs, path, migrations).pipe( + Effect.provide(out.layer), + Effect.flip, + ); + + // First upsert applied, second failed → the upsert transaction ends in + // ROLLBACK, never COMMIT (the create-table setup's own COMMIT ran earlier). + expect(calls.slice(-3)).toEqual(["BEGIN", "UPSERT", "ROLLBACK"]); + expect(calls[calls.length - 1]).toBe("ROLLBACK"); + // Error message shape stays byte-identical to the pre-transaction version. + expect(error).toBeInstanceOf(LegacyDbPullWriteError); + expect(error.message).toBe("failed to update migration table: connection reset by peer"); + // No success line when the repair failed. + expect(out.stderrText).not.toContain("Repaired migration history"); + }).pipe(Effect.provide(BunServices.layer)), + ); +}); diff --git a/apps/cli/src/legacy/commands/db/pull/pull.sync.ts b/apps/cli/src/legacy/commands/db/pull/pull.sync.ts index 3e251e4c1b..7d00895074 100644 --- a/apps/cli/src/legacy/commands/db/pull/pull.sync.ts +++ b/apps/cli/src/legacy/commands/db/pull/pull.sync.ts @@ -10,50 +10,80 @@ import { import { legacySplitAndTrim } from "../../../shared/legacy-sql-split.ts"; import { LegacyDbPullWriteError } from "./pull.errors.ts"; +/** A pulled migration file paired with the version to record in the history. */ +export interface LegacyPulledMigration { + readonly path: string; + readonly version: string; +} + /** - * Records the pulled migration as applied in `supabase_migrations.schema_migrations` - * WITHOUT re-executing it (the schema already exists on the remote). Mirrors Go's - * `repair.UpdateMigrationTable(conn, [version], Applied, false, fsys)` + * Records the pulled migration(s) as applied in + * `supabase_migrations.schema_migrations` WITHOUT re-executing them (the schema + * already exists on the remote). Mirrors Go's + * `repair.UpdateMigrationTable(conn, versions, Applied, false, fsys)` * (`internal/migration/repair/repair.go:58`): create the history table, then UPSERT - * the version row with the migration's name + statements. + * each version row with the migration's name + statements. A pg-delta pull whose + * plan crosses a transaction boundary writes several ordered files, so several + * versions are recorded in one pass. */ export const legacyUpdateMigrationHistory = ( session: LegacyDbSession, fs: FileSystem.FileSystem, path: Path.Path, - migrationPath: string, - timestamp: string, + migrations: ReadonlyArray, ) => Effect.gen(function* () { const output = yield* Output; - const match = MIGRATE_FILE_PATTERN.exec(path.basename(migrationPath)); - if (match === null || match[1] !== timestamp) { - // Go resolves the repair file by globbing `_*.sql` against the - // migrations dir and fails with `os.ErrNotExist` when nothing matches - // (`repair.GetMigrationFile`, `internal/migration/repair/repair.go:90-99`). - // The glob is anchored on the GENERATED `timestamp` and `*` never crosses a - // path separator, so a migration name with a separator (`supabase db pull - // dir/...`) writes a nested file the glob can't reach — even when the nested - // basename is itself a valid migration filename (`dir/20250101000000_backfill` - // → basename `20250101000000_backfill.sql`, which DOES match the regex but - // carries the user's nested timestamp, not the generated one). Require the - // basename to both match the pattern AND carry the generated timestamp, - // mirroring Go's anchored glob, rather than trusting `path.basename`. - return yield* Effect.fail( - new LegacyDbPullWriteError({ - message: `glob supabase/migrations/${timestamp}_*.sql: file does not exist`, - }), - ); + // Resolve each file the way Go's `repair.GetMigrationFile` globs + // `_*.sql` against the migrations dir, failing with `os.ErrNotExist` + // when nothing matches (`internal/migration/repair/repair.go:90-99`). The glob + // is anchored on the GENERATED version and `*` never crosses a path separator, + // so a migration name with a separator writes a nested file the glob can't + // reach — require the basename to both match the pattern AND carry the + // generated version rather than trusting `path.basename`. + const resolved: Array<{ version: string; name: string; migrationPath: string }> = []; + for (const migration of migrations) { + const match = MIGRATE_FILE_PATTERN.exec(path.basename(migration.path)); + if (match === null || match[1] !== migration.version) { + return yield* Effect.fail( + new LegacyDbPullWriteError({ + message: `glob supabase/migrations/${migration.version}_*.sql: file does not exist`, + }), + ); + } + resolved.push({ + version: migration.version, + name: match[2] ?? "", + migrationPath: migration.path, + }); } - // Guarded above: match[1] === timestamp, so use the generated timestamp - // directly (avoids re-deriving a `string | undefined` from the regex group). - const version = timestamp; - const name = match[2] ?? ""; yield* Effect.gen(function* () { - const content = yield* fs.readFileString(migrationPath); - const statements = legacySplitAndTrim(content); + // Create the history schema/table first, in its OWN transaction — Go runs + // `CreateMigrationTable` before the upsert batch (`repair.go:59`). Keeping it + // outside the upsert transaction below avoids nesting BEGINs + // (`legacyCreateMigrationTable` issues its own BEGIN/COMMIT). yield* legacyCreateMigrationTable(session); - yield* session.query(UPSERT_MIGRATION_VERSION, [version, name, statements]); + // Record every version in ONE explicit transaction: a mid-loop failure + // (dropped connection, unreadable migration file) must record NONE of them. + // Go queues all upserts in a single `pgx.Batch` (`repair.go:63-83`), which + // Postgres executes as one implicit transaction; without a transaction here + // each UPSERT autocommits, so a failure partway through would leave partial + // remote history that fails the next pull's sync check. + yield* Effect.gen(function* () { + yield* session.exec("BEGIN"); + for (const entry of resolved) { + const content = yield* fs.readFileString(entry.migrationPath); + const statements = legacySplitAndTrim(content); + yield* session.query(UPSERT_MIGRATION_VERSION, [entry.version, entry.name, statements]); + } + yield* session.exec("COMMIT"); + }).pipe( + // Roll back on ANY failure inside the transaction — including a migration + // file read that fails after BEGIN. `Effect.ignore` keeps a ROLLBACK + // failure from masking the original error (`tapError` re-raises the + // original). Mirrors `legacyCreateMigrationTable`'s rollback handling. + Effect.tapError(() => session.exec("ROLLBACK").pipe(Effect.ignore)), + ); }).pipe( Effect.mapError( (cause) => @@ -63,8 +93,9 @@ export const legacyUpdateMigrationHistory = ( ), ); // Match Go's `repair.UpdateMigrationTable(..., repairAll=false, ...)`, which - // prints `Repaired migration history: [] => applied` to stderr - // (`internal/migration/repair/repair.go`). Plain text on stderr, so it does - // not interfere with machine-output payloads on stdout. - yield* output.raw(`Repaired migration history: [${version}] => applied\n`, "stderr"); + // prints `Repaired migration history: [ ...] => applied` to stderr + // (Go's `%v` over the `[]string` of versions, space-separated). Plain text on + // stderr, so it does not interfere with machine-output payloads on stdout. + const versions = resolved.map((entry) => entry.version).join(" "); + yield* output.raw(`Repaired migration history: [${versions}] => applied\n`, "stderr"); }); diff --git a/apps/cli/src/legacy/commands/db/schema/declarative/declarative.orchestrate.integration.test.ts b/apps/cli/src/legacy/commands/db/schema/declarative/declarative.orchestrate.integration.test.ts index ad474a9a77..2b581df3b0 100644 --- a/apps/cli/src/legacy/commands/db/schema/declarative/declarative.orchestrate.integration.test.ts +++ b/apps/cli/src/legacy/commands/db/schema/declarative/declarative.orchestrate.integration.test.ts @@ -41,7 +41,20 @@ function mockEdge(stdout: string) { const layer = Layer.succeed(LegacyEdgeRuntimeScript, { run: (opts: LegacyEdgeRuntimeRunOpts) => { calls.push(opts); - return Effect.succeed({ stdout, stderr: "" }); + // The pg-delta diff script (uniquely identified by `renderPlanFiles`) prints a + // JSON envelope with one file per plan unit; wrap the test's raw SQL into a + // single-unit envelope so `legacyDiffPgDelta` parses it. Other scripts + // (declarative export) return their stdout unchanged. + const wrapped = + opts.script.includes("renderPlanFiles") && stdout.length > 0 + ? JSON.stringify({ + version: 1, + files: [ + { order: 1, name: "schema_changes", transactionMode: "transactional", sql: stdout }, + ], + }) + : stdout; + return Effect.succeed({ stdout: wrapped, stderr: "" }); }, }); return { layer, calls }; diff --git a/apps/cli/src/legacy/commands/db/schema/declarative/sync/sync.integration.test.ts b/apps/cli/src/legacy/commands/db/schema/declarative/sync/sync.integration.test.ts index 44dd7548b2..fa04d716dd 100644 --- a/apps/cli/src/legacy/commands/db/schema/declarative/sync/sync.integration.test.ts +++ b/apps/cli/src/legacy/commands/db/schema/declarative/sync/sync.integration.test.ts @@ -96,15 +96,33 @@ function setup(workdir: string, opts: SetupOpts = {}) { removeShadowContainer: () => Effect.void, }); const edge = Layer.succeed(LegacyEdgeRuntimeScript, { - run: (runOpts: LegacyEdgeRuntimeRunOpts) => - Effect.succeed({ - stdout: - opts.exportJson !== undefined && - runOpts.errPrefix === "error exporting declarative schema" - ? opts.exportJson - : (opts.diffSql ?? ""), - stderr: "", - }), + run: (runOpts: LegacyEdgeRuntimeRunOpts) => { + if ( + opts.exportJson !== undefined && + runOpts.errPrefix === "error exporting declarative schema" + ) { + return Effect.succeed({ stdout: opts.exportJson, stderr: "" }); + } + const diffSql = opts.diffSql ?? ""; + // The pg-delta diff script (uniquely identified by `renderPlanFiles`) prints a + // JSON envelope with one file per plan unit; wrap the test's raw SQL into a + // single-unit envelope so `legacyDiffPgDelta` parses it. + const stdout = + runOpts.script.includes("renderPlanFiles") && diffSql.length > 0 + ? JSON.stringify({ + version: 1, + files: [ + { + order: 1, + name: "schema_changes", + transactionMode: "transactional", + sql: diffSql, + }, + ], + }) + : diffSql; + return Effect.succeed({ stdout, stderr: "" }); + }, }); const dbExec: string[] = []; const dbConn = Layer.succeed(LegacyDbConnection, { diff --git a/apps/cli/src/legacy/commands/db/shared/legacy-pgdelta-migrations.write.ts b/apps/cli/src/legacy/commands/db/shared/legacy-pgdelta-migrations.write.ts new file mode 100644 index 0000000000..e02b5c720f --- /dev/null +++ b/apps/cli/src/legacy/commands/db/shared/legacy-pgdelta-migrations.write.ts @@ -0,0 +1,135 @@ +import { Data, Effect, type FileSystem, type Path } from "effect"; + +import { legacyMakeDir } from "../../../shared/legacy-make-dir.ts"; +import { + legacyFormatMigrationTimestamp, + legacyGetMigrationPath, +} from "../../../shared/legacy-migration-file.ts"; + +/** A migration file written by a diff/pull, paired with its history version. */ +export interface LegacyWrittenMigration { + readonly path: string; + readonly version: string; +} + +/** + * A write failure from `legacyWritePgDeltaMigrations`. Callers map this to their + * own command-domain write error (`LegacyDbDiffWriteError` / `LegacyDbPullWriteError`). + */ +export class LegacyPgDeltaMigrationWriteError extends Data.TaggedError( + "LegacyPgDeltaMigrationWriteError", +)<{ + readonly message: string; +}> {} + +/** + * Bounds the base-timestamp bump retry so a directory already full of same-second + * migrations can't spin forever. Mirrors Go's `maxVersionCollisionAttempts`. + */ +const MAX_VERSION_COLLISION_ATTEMPTS = 60; + +/** + * Port of Go's `WritePgDeltaMigrations` (`apps/cli-go/internal/db/diff/pgdelta_migrations.go`). + * + * Writes one ordered migration file per plan unit. A single-unit plan (the common + * case) keeps the exact `_.sql` filename; multi-unit plans append the + * unit name and give each file a strictly increasing timestamp (real time + * arithmetic on the base millis, never string increment) so their execution order + * and migration-history order stay stable. + * + * Before writing anything the FULL set of generated filenames is collision-checked + * against the filesystem: if any target path already exists the base is advanced by + * one second and every version recomputed, so the set stays strictly ascending AND + * unique against pre-existing migrations. The base only ever moves forward — never + * backdated below the caller's wall clock, since backdating could sort a new file + * before pre-existing migrations. The resulting ≤N−1s future-dating is inherent to + * second-granularity versions and acceptable once uniqueness is enforced. + * + * Each file is written with the exclusive `"wx"` flag so a race between the + * collision check and the write can still never silently overwrite an existing + * migration. If any open/write fails mid-loop, every file already written by THIS + * invocation is best-effort removed before the error surfaces (a removal failure + * never masks the original error). + */ +export const legacyWritePgDeltaMigrations = ( + fs: FileSystem.FileSystem, + pathSvc: Path.Path, + opts: { + readonly workdir: string; + readonly baseMillis: number; + readonly name: string; + readonly files: ReadonlyArray<{ readonly name: string; readonly sql: string }>; + }, +): Effect.Effect, LegacyPgDeltaMigrationWriteError> => + Effect.gen(function* () { + const { workdir, name, files } = opts; + const single = files.length === 1; + const buildSet = (baseMillis: number): Array => + files.map((file, i) => { + const version = legacyFormatMigrationTimestamp(baseMillis + i * 1000); + const unitName = single ? name : `${name}_${file.name}`; + return { path: legacyGetMigrationPath(pathSvc, workdir, version, unitName), version }; + }); + + let baseMillis = opts.baseMillis; + let set = buildSet(baseMillis); + for (let attempt = 0; ; attempt++) { + let collision = false; + for (const w of set) { + const exists = yield* fs.exists(w.path).pipe( + Effect.mapError( + (cause) => + new LegacyPgDeltaMigrationWriteError({ + message: `failed to check migration file: ${cause.message}`, + }), + ), + ); + if (exists) { + collision = true; + break; + } + } + if (!collision) break; + if (attempt + 1 >= MAX_VERSION_COLLISION_ATTEMPTS) { + return yield* Effect.fail( + new LegacyPgDeltaMigrationWriteError({ + message: `failed to find a unique migration version after ${MAX_VERSION_COLLISION_ATTEMPTS} attempts`, + }), + ); + } + baseMillis += 1000; + set = buildSet(baseMillis); + } + + const written: Array = []; + const writeAll = Effect.gen(function* () { + for (let i = 0; i < files.length; i++) { + const w = set[i]!; + const file = files[i]!; + yield* legacyMakeDir(fs, pathSvc.dirname(w.path)).pipe( + Effect.mapError( + (cause) => new LegacyPgDeltaMigrationWriteError({ message: cause.message }), + ), + ); + yield* fs.writeFileString(w.path, `${file.sql}\n`, { flag: "wx" }).pipe( + Effect.mapError( + (cause) => + new LegacyPgDeltaMigrationWriteError({ + message: + cause.reason._tag === "AlreadyExists" + ? `failed to open migration file: ${cause.message}` + : `failed to write migration file: ${cause.message}`, + }), + ), + ); + written.push(w); + } + return written; + }); + + return yield* writeAll.pipe( + Effect.tapError(() => + Effect.forEach(written, (w) => fs.remove(w.path).pipe(Effect.ignore), { discard: true }), + ), + ); + }); diff --git a/apps/cli/src/legacy/commands/db/shared/legacy-pgdelta.deno-templates.ts b/apps/cli/src/legacy/commands/db/shared/legacy-pgdelta.deno-templates.ts index e43e9828a8..5a6fb7060b 100644 --- a/apps/cli/src/legacy/commands/db/shared/legacy-pgdelta.deno-templates.ts +++ b/apps/cli/src/legacy/commands/db/shared/legacy-pgdelta.deno-templates.ts @@ -15,15 +15,15 @@ /** `templates/pgdelta.ts` — diffs SOURCE→TARGET and prints SQL statements. */ export const legacyPgDeltaDiffScript = - 'import {\n createPlan,\n deserializeCatalog,\n formatSqlStatements,\n} from "npm:@supabase/pg-delta@1.0.0-alpha.20";\nimport { supabase } from "npm:@supabase/pg-delta@1.0.0-alpha.20/integrations/supabase";\n\nasync function resolveInput(ref: string | undefined) {\n if (!ref) {\n return null;\n }\n if (ref.startsWith("postgres://") || ref.startsWith("postgresql://")) {\n return ref;\n }\n const json = await Deno.readTextFile(ref);\n return deserializeCatalog(JSON.parse(json));\n}\n\nconst source = Deno.env.get("SOURCE");\nconst target = Deno.env.get("TARGET");\n\nconst includedSchemas = Deno.env.get("INCLUDED_SCHEMAS");\nif (includedSchemas) {\n const schemas = includedSchemas.split(",");\n const schemaFilter = {\n or: [{ "*/schema": schemas }, { "schema/name": schemas }],\n };\n // CompositionPattern `and` is valid FilterDSL; Deno\'s structural typing is strict on `or` branches.\n supabase.filter = {\n and: [supabase.filter!, schemaFilter],\n } as typeof supabase.filter;\n}\n\nconst formatOptionsRaw = Deno.env.get("FORMAT_OPTIONS");\nlet formatOptions = undefined;\nif (formatOptionsRaw) {\n formatOptions = JSON.parse(formatOptionsRaw);\n}\n\ntry {\n const result = await createPlan(\n await resolveInput(source),\n await resolveInput(target),\n {\n ...supabase,\n skipDefaultPrivilegeSubtraction: true,\n },\n );\n let statements = result?.plan.statements ?? [];\n if (formatOptions != null) {\n statements = formatSqlStatements(statements, formatOptions);\n }\n if (Deno.env.get("PGDELTA_DEBUG")) {\n console.error(\n JSON.stringify({\n statementCount: statements.length,\n source: source ? "connected" : "null",\n target: target ? "connected" : "null",\n includedSchemas: includedSchemas ?? null,\n skipDefaultPrivilegeSubtraction: true,\n }),\n );\n }\n for (const sql of statements) {\n console.log(`${sql};`);\n }\n} catch (e) {\n console.error(e);\n // Force close event loop\n throw new Error("");\n}\n// Force close the event loop on the success path too. When SOURCE/TARGET are\n// live database URLs the plan opens connections whose keepalive handles can keep\n// the Edge Runtime worker alive after the diff has been written, so the container\n// never exits and the CLI — which follows this container\'s logs — hangs\n// indefinitely at 0% CPU (supabase/pg-toolbelt#312).\nthrow new Error("");\n'; + 'import {\n createPlan,\n deserializeCatalog,\n renderPlanFiles,\n} from "npm:@supabase/pg-delta@1.0.0-alpha.20";\nimport { supabase } from "npm:@supabase/pg-delta@1.0.0-alpha.20/integrations/supabase";\n\nasync function resolveInput(ref: string | undefined) {\n if (!ref) {\n return null;\n }\n if (ref.startsWith("postgres://") || ref.startsWith("postgresql://")) {\n return ref;\n }\n const json = await Deno.readTextFile(ref);\n return deserializeCatalog(JSON.parse(json));\n}\n\nconst source = Deno.env.get("SOURCE");\nconst target = Deno.env.get("TARGET");\n\nconst includedSchemas = Deno.env.get("INCLUDED_SCHEMAS");\nif (includedSchemas) {\n const schemas = includedSchemas.split(",");\n const schemaFilter = {\n or: [{ "*/schema": schemas }, { "schema/name": schemas }],\n };\n // CompositionPattern `and` is valid FilterDSL; Deno\'s structural typing is strict on `or` branches.\n supabase.filter = {\n and: [supabase.filter!, schemaFilter],\n } as typeof supabase.filter;\n}\n\nconst formatOptionsRaw = Deno.env.get("FORMAT_OPTIONS");\nconst parsedFormatOptions = formatOptionsRaw ? JSON.parse(formatOptionsRaw) : undefined;\n// Format the emitted SQL by default with the same sensible settings the\n// declarative export uses (`exportDeclarativeSchema` in @supabase/pg-delta:\n// `{ ...DEFAULT_OPTIONS, maxWidth: 180, keywordCase: "upper", ...userOptions }`),\n// so `db pull` / `db diff` produce readable migrations even when config sets no\n// `[experimental.pgdelta] format_options`. The formatter fills DEFAULT_OPTIONS\n// for missing keys itself, so only the two overrides are passed here. Setting\n// `format_options = "null"` (parsed to `null`) is the explicit opt-out: raw,\n// unformatted statements, mirroring declarative export\'s `formatOptions === null`.\nconst sqlFormatOptions =\n parsedFormatOptions === null\n ? undefined\n : { maxWidth: 180, keywordCase: "upper", ...parsedFormatOptions };\n\ntry {\n const result = await createPlan(\n await resolveInput(source),\n await resolveInput(target),\n {\n ...supabase,\n skipDefaultPrivilegeSubtraction: true,\n },\n );\n // pg-delta >= 1.0.0-alpha.32 groups plan statements into execution-aware\n // `units` with transaction boundaries. `renderPlanFiles` turns those into one\n // numbered SQL file per unit (header comments included). `includeTransactions:\n // false` because the CLI appliers already wrap each migration file in a single\n // transaction (Go\'s implicit ExecBatch / the TS BEGIN/COMMIT wrap), so embedded\n // BEGIN/COMMIT would nest; format options are applied per unit here instead of a\n // manual `formatSqlStatements` pass.\n const files = result\n ? renderPlanFiles(result.plan, {\n includeTransactions: false,\n sqlFormatOptions,\n })\n : [];\n const envelope = files.map((file, index) => ({\n order: index + 1,\n // The unit name is the rendered path minus its numeric prefix and `.sql`\n // extension (e.g. `001_after_enum_values.sql` -> `after_enum_values`).\n name: file.path.replace(/^\\d+_/, "").replace(/\\.sql$/, ""),\n transactionMode: file.unit.transactionMode,\n sql: file.sql,\n }));\n if (Deno.env.get("PGDELTA_DEBUG")) {\n console.error(\n JSON.stringify({\n statementCount: files.reduce((total, file) => total + file.unit.statements.length, 0),\n fileCount: files.length,\n source: source ? "connected" : "null",\n target: target ? "connected" : "null",\n includedSchemas: includedSchemas ?? null,\n skipDefaultPrivilegeSubtraction: true,\n }),\n );\n }\n console.log(JSON.stringify({ version: 1, files: envelope }));\n} catch (e) {\n console.error(e);\n // Emit a sentinel so the CLI runner can distinguish a real script crash from a\n // successful empty diff, even though the forced-exit non-zero code below is\n // suppressed by the "main worker has been destroyed" handling.\n console.error("PGDELTA_SCRIPT_ERROR");\n // Force close event loop\n throw new Error("");\n}\n// Force close the event loop on the success path too. When SOURCE/TARGET are\n// live database URLs the plan opens connections whose keepalive handles can keep\n// the Edge Runtime worker alive after the diff has been written, so the container\n// never exits and the CLI — which follows this container\'s logs — hangs\n// indefinitely at 0% CPU (supabase/pg-toolbelt#312).\nthrow new Error("");\n'; /** `templates/pgdelta_declarative_export.ts` — exports declarative file payloads. */ export const legacyPgDeltaDeclarativeExportScript = - '// This script is executed inside Edge Runtime by the CLI to export a target\n// schema as declarative file payloads. It accepts either live DB URLs or\n// catalog-file references for SOURCE/TARGET, which enables cached sync flows.\nimport {\n createPlan,\n deserializeCatalog,\n exportDeclarativeSchema,\n} from "npm:@supabase/pg-delta@1.0.0-alpha.20";\nimport { supabase } from "npm:@supabase/pg-delta@1.0.0-alpha.20/integrations/supabase";\n\nasync function resolveInput(ref: string | undefined) {\n if (!ref) {\n return null;\n }\n if (ref.startsWith("postgres://") || ref.startsWith("postgresql://")) {\n return ref;\n }\n const json = await Deno.readTextFile(ref);\n return deserializeCatalog(JSON.parse(json));\n}\n\nconst source = Deno.env.get("SOURCE");\nconst target = Deno.env.get("TARGET");\n\nconst includedSchemas = Deno.env.get("INCLUDED_SCHEMAS");\nif (includedSchemas) {\n const schemas = includedSchemas.split(",");\n const schemaFilter = {\n or: [{ "*/schema": schemas }, { "schema/name": schemas }],\n };\n supabase.filter = {\n and: [supabase.filter!, schemaFilter],\n } as unknown as typeof supabase.filter;\n}\n\nconst formatOptionsRaw = Deno.env.get("FORMAT_OPTIONS");\nlet formatOptions = undefined;\nif (formatOptionsRaw) {\n formatOptions = JSON.parse(formatOptionsRaw);\n}\ntry {\n const result = await createPlan(\n await resolveInput(source),\n await resolveInput(target),\n {\n ...supabase,\n skipDefaultPrivilegeSubtraction: true,\n },\n );\n if (!result) {\n console.log(\n JSON.stringify({\n version: 1,\n mode: "declarative",\n files: [],\n }),\n );\n } else {\n const output = exportDeclarativeSchema(result, {\n integration: supabase,\n formatOptions,\n });\n console.log(\n JSON.stringify(output, (_key, value) =>\n typeof value === "bigint" ? Number(value) : value,\n ),\n );\n }\n} catch (e) {\n console.error(e);\n // Force close event loop\n throw new Error("");\n}\n// Force close the event loop on the success path too. When SOURCE/TARGET are\n// live database URLs the plan opens connections whose keepalive handles can keep\n// the Edge Runtime worker alive after the export has been written, so the\n// container never exits and the CLI — which follows this container\'s logs —\n// hangs indefinitely at 0% CPU (supabase/pg-toolbelt#312).\nthrow new Error("");\n'; + '// This script is executed inside Edge Runtime by the CLI to export a target\n// schema as declarative file payloads. It accepts either live DB URLs or\n// catalog-file references for SOURCE/TARGET, which enables cached sync flows.\nimport {\n createPlan,\n deserializeCatalog,\n exportDeclarativeSchema,\n} from "npm:@supabase/pg-delta@1.0.0-alpha.20";\nimport { supabase } from "npm:@supabase/pg-delta@1.0.0-alpha.20/integrations/supabase";\n\nasync function resolveInput(ref: string | undefined) {\n if (!ref) {\n return null;\n }\n if (ref.startsWith("postgres://") || ref.startsWith("postgresql://")) {\n return ref;\n }\n const json = await Deno.readTextFile(ref);\n return deserializeCatalog(JSON.parse(json));\n}\n\nconst source = Deno.env.get("SOURCE");\nconst target = Deno.env.get("TARGET");\n\nconst includedSchemas = Deno.env.get("INCLUDED_SCHEMAS");\nif (includedSchemas) {\n const schemas = includedSchemas.split(",");\n const schemaFilter = {\n or: [{ "*/schema": schemas }, { "schema/name": schemas }],\n };\n supabase.filter = {\n and: [supabase.filter!, schemaFilter],\n } as unknown as typeof supabase.filter;\n}\n\nconst formatOptionsRaw = Deno.env.get("FORMAT_OPTIONS");\nlet formatOptions = undefined;\nif (formatOptionsRaw) {\n formatOptions = JSON.parse(formatOptionsRaw);\n}\ntry {\n const result = await createPlan(\n await resolveInput(source),\n await resolveInput(target),\n {\n ...supabase,\n skipDefaultPrivilegeSubtraction: true,\n },\n );\n if (!result) {\n console.log(\n JSON.stringify({\n version: 1,\n mode: "declarative",\n files: [],\n }),\n );\n } else {\n const output = exportDeclarativeSchema(result, {\n integration: supabase,\n formatOptions,\n });\n console.log(\n JSON.stringify(output, (_key, value) =>\n typeof value === "bigint" ? Number(value) : value,\n ),\n );\n }\n} catch (e) {\n console.error(e);\n // Emit a sentinel so the CLI runner can distinguish a real script crash from a\n // successful empty export, even though the forced-exit non-zero code below is\n // suppressed by the "main worker has been destroyed" handling.\n console.error("PGDELTA_SCRIPT_ERROR");\n // Force close event loop\n throw new Error("");\n}\n// Force close the event loop on the success path too. When SOURCE/TARGET are\n// live database URLs the plan opens connections whose keepalive handles can keep\n// the Edge Runtime worker alive after the export has been written, so the\n// container never exits and the CLI — which follows this container\'s logs —\n// hangs indefinitely at 0% CPU (supabase/pg-toolbelt#312).\nthrow new Error("");\n'; /** `templates/pgdelta_catalog_export.ts` — serializes a catalog snapshot for caching. */ export const legacyPgDeltaCatalogExportScript = - '// This script serializes a database catalog for caching/reuse in declarative\n// sync workflows, so later diff/export operations can run from file references.\nimport {\n createManagedPool,\n extractCatalog,\n serializeCatalog,\n stringifyCatalogSnapshot,\n} from "npm:@supabase/pg-delta@1.0.0-alpha.20";\n\nconst target = Deno.env.get("TARGET");\nconst role = Deno.env.get("ROLE") ?? undefined;\n\nif (!target) {\n console.error("TARGET is required");\n throw new Error("");\n}\nconst { pool, close } = await createManagedPool(target, { role });\n\ntry {\n const catalog = await extractCatalog(pool);\n console.log(stringifyCatalogSnapshot(serializeCatalog(catalog)));\n} catch (e) {\n console.error(e);\n // Force close event loop\n throw new Error("");\n} finally {\n await close();\n}\n// Force close the event loop on the success path too. The connection pool can\n// leave keepalive handles registered even after close() resolves, which keeps\n// the Edge Runtime worker (and therefore the container) alive after the catalog\n// has already been written to stdout. The CLI streams this container\'s logs with\n// Follow:true, so a worker that never exits hangs the parent `__catalog`\n// subprocess — and the declarative-sync command that spawned it — indefinitely\n// at 0% CPU (supabase/pg-toolbelt#312).\nthrow new Error("");\n'; + '// This script serializes a database catalog for caching/reuse in declarative\n// sync workflows, so later diff/export operations can run from file references.\nimport {\n createManagedPool,\n extractCatalog,\n serializeCatalog,\n stringifyCatalogSnapshot,\n} from "npm:@supabase/pg-delta@1.0.0-alpha.20";\n\nconst target = Deno.env.get("TARGET");\nconst role = Deno.env.get("ROLE") ?? undefined;\n\nif (!target) {\n console.error("TARGET is required");\n // Emit a sentinel so the CLI runner treats this as a real script crash rather\n // than a successful empty catalog, even though the forced-exit non-zero code is\n // suppressed by the "main worker has been destroyed" handling.\n console.error("PGDELTA_SCRIPT_ERROR");\n throw new Error("");\n}\nconst { pool, close } = await createManagedPool(target, { role });\n\ntry {\n const catalog = await extractCatalog(pool);\n console.log(stringifyCatalogSnapshot(serializeCatalog(catalog)));\n} catch (e) {\n console.error(e);\n // Emit a sentinel so the CLI runner can distinguish a real script crash from a\n // successful empty catalog, even though the forced-exit non-zero code below is\n // suppressed by the "main worker has been destroyed" handling.\n console.error("PGDELTA_SCRIPT_ERROR");\n // Force close event loop\n throw new Error("");\n} finally {\n await close();\n}\n// Force close the event loop on the success path too. The connection pool can\n// leave keepalive handles registered even after close() resolves, which keeps\n// the Edge Runtime worker (and therefore the container) alive after the catalog\n// has already been written to stdout. The CLI streams this container\'s logs with\n// Follow:true, so a worker that never exits hangs the parent `__catalog`\n// subprocess — and the declarative-sync command that spawned it — indefinitely\n// at 0% CPU (supabase/pg-toolbelt#312).\nthrow new Error("");\n'; /** `internal/pgdelta/templates/pgdelta_declarative_apply.ts` — applies declarative files to TARGET. */ export const legacyPgDeltaDeclarativeApplyScript = @@ -35,7 +35,7 @@ export const legacyPgDeltaDeclarativeApplyScript = * config field) is absent or empty. Mirrors Go's `DefaultPgDeltaNpmVersion` * (`apps/cli-go/pkg/config/pgdelta_version.go:7`). */ -export const LEGACY_DEFAULT_PG_DELTA_NPM_VERSION = "1.0.0-alpha.27"; +export const LEGACY_DEFAULT_PG_DELTA_NPM_VERSION = "1.0.0-alpha.32"; /** * The literal version baked into the embedded templates above, replaced by diff --git a/apps/cli/src/legacy/commands/db/shared/legacy-pgdelta.errors.ts b/apps/cli/src/legacy/commands/db/shared/legacy-pgdelta.errors.ts index b52d173fbe..6e12afd324 100644 --- a/apps/cli/src/legacy/commands/db/shared/legacy-pgdelta.errors.ts +++ b/apps/cli/src/legacy/commands/db/shared/legacy-pgdelta.errors.ts @@ -48,6 +48,15 @@ export class LegacyDeclarativeParseOutputError extends Data.TaggedError( readonly message: string; }> {} +/** + * Parsing the pg-delta diff envelope failed. Byte-matches Go's + * `"failed to parse pg-delta diff output: " + err + ":\n" + stderr` + * (`apps/cli-go/internal/db/diff/pgdelta.go`, `parsePgDeltaDiffOutput`). + */ +export class LegacyPgDeltaDiffParseError extends Data.TaggedError("LegacyPgDeltaDiffParseError")<{ + readonly message: string; +}> {} + /** * Materializing the declarative export on disk failed. Byte-matches Go's * `WriteDeclarativeSchemas` errors (`declarative.go:239`): diff --git a/apps/cli/src/legacy/commands/db/shared/legacy-pgdelta.integration.test.ts b/apps/cli/src/legacy/commands/db/shared/legacy-pgdelta.integration.test.ts index f6c816c461..c64f9f624a 100644 --- a/apps/cli/src/legacy/commands/db/shared/legacy-pgdelta.integration.test.ts +++ b/apps/cli/src/legacy/commands/db/shared/legacy-pgdelta.integration.test.ts @@ -58,7 +58,20 @@ describe("legacyDiffPgDelta", () => { it.effect( "returns the SQL + stderr and passes the interpolated diff script + env + binds", () => { - const edge = fakeEdgeRuntime({ stdout: "ALTER TABLE x;\n", stderr: "warn" }); + const edge = fakeEdgeRuntime({ + stdout: JSON.stringify({ + version: 1, + files: [ + { + order: 1, + name: "schema_changes", + transactionMode: "transactional", + sql: "-- unit 1\n\nALTER TABLE x;", + }, + ], + }), + stderr: "warn", + }); return legacyDiffPgDelta(CTX, { targetRef: "postgresql://u:p@127.0.0.1:54320/postgres?connect_timeout=10", sourceRef: "supabase/.temp/catalog.json", @@ -67,7 +80,10 @@ describe("legacyDiffPgDelta", () => { }).pipe( Effect.tap((result) => Effect.sync(() => { - expect(result.sql).toBe("ALTER TABLE x;\n"); + // The envelope is parsed into per-unit files and a flattened SQL join. + expect(result.sql).toBe("-- unit 1\n\nALTER TABLE x;"); + expect(result.files).toHaveLength(1); + expect(result.files[0]?.name).toBe("schema_changes"); expect(result.stderr).toBe("warn"); const opts = edge.calls[0]!; expect(opts.errPrefix).toBe("error diffing schema"); @@ -139,6 +155,27 @@ describe("legacyDiffPgDelta", () => { Effect.provide(Layer.mergeAll(edge.layer, probe, BunServices.layer)), ); }); + + it.effect("fails with LegacyPgDeltaDiffParseError on a malformed envelope", () => { + const edge = fakeEdgeRuntime({ stdout: "not json{", stderr: "boom" }); + return legacyDiffPgDelta(CTX, { + targetRef: "postgresql://t", + sourceRef: "", + schema: [], + formatOptions: "", + }).pipe( + Effect.exit, + Effect.tap((exit) => + Effect.sync(() => { + expect(failError(exit)?.constructor.name).toBe("LegacyPgDeltaDiffParseError"); + const message = (failError(exit) as { message: string }).message; + expect(message).toContain("failed to parse pg-delta diff output"); + expect(message).toContain("boom"); + }), + ), + Effect.provide(Layer.mergeAll(edge.layer, probe, BunServices.layer)), + ); + }); }); describe("legacyDeclarativeExportPgDelta", () => { diff --git a/apps/cli/src/legacy/commands/db/shared/legacy-pgdelta.ts b/apps/cli/src/legacy/commands/db/shared/legacy-pgdelta.ts index a7b49c230d..4dc3de042f 100644 --- a/apps/cli/src/legacy/commands/db/shared/legacy-pgdelta.ts +++ b/apps/cli/src/legacy/commands/db/shared/legacy-pgdelta.ts @@ -19,6 +19,7 @@ import { LegacyDeclarativeEdgeRuntimeError, LegacyDeclarativeEmptyOutputError, LegacyDeclarativeParseOutputError, + LegacyPgDeltaDiffParseError, } from "./legacy-pgdelta.errors.ts"; const PG_DELTA_NPM_REGISTRY_ENV = "PGDELTA_NPM_REGISTRY"; @@ -38,9 +39,32 @@ export interface LegacyDeclarativeOutput { readonly files: ReadonlyArray; } -/** Result of a pg-delta diff: the SQL statements plus edge-runtime stderr. */ +/** + * One execution-aware migration unit from a pg-delta diff plan. Mirrors Go's + * `PgDeltaPlanFile` (`internal/db/diff/pgdelta.go`): a numbered SQL file whose + * header comments record the unit number, transaction mode and boundary reason. + */ +interface LegacyPgDeltaPlanFile { + readonly order: number; + readonly name: string; + readonly transactionMode: string; + readonly sql: string; +} + +/** The pg-delta diff envelope. Mirrors Go's `PgDeltaDiffOutput`. */ +interface LegacyPgDeltaDiffOutput { + readonly version: number; + readonly files: ReadonlyArray; +} + +/** + * Result of a pg-delta diff: the per-unit plan `files`, a `sql` flattening of + * them (kept for `db diff` / declarative callers that consume one blob), and the + * edge-runtime `stderr`. + */ interface LegacyPgDeltaDiffResult { readonly sql: string; + readonly files: ReadonlyArray; readonly stderr: string; } @@ -189,7 +213,27 @@ export const legacyDiffPgDelta = Effect.fnUntraced(function* ( denoVersion: ctx.denoVersion, }) .pipe(Effect.mapError(toDeclarativeEdgeRuntimeError)); - return { sql: result.stdout, stderr: result.stderr } satisfies LegacyPgDeltaDiffResult; + // The template always prints the diff envelope on the success path, even for an + // empty plan (`{"version":1,"files":[]}`); a truly empty stdout means no envelope + // was produced, which we surface as "no changes" rather than a parse error. + // Mirrors Go's `parsePgDeltaDiffOutput` (`internal/db/diff/pgdelta.go`). + if (result.stdout.trim().length === 0) { + return { sql: "", files: [], stderr: result.stderr } satisfies LegacyPgDeltaDiffResult; + } + const envelope = yield* Effect.try({ + try: () => JSON.parse(result.stdout) as LegacyPgDeltaDiffOutput, + catch: (cause) => + new LegacyPgDeltaDiffParseError({ + message: `failed to parse pg-delta diff output: ${ + cause instanceof Error ? cause.message : String(cause) + }:\n${result.stderr}`, + }), + }); + const files = envelope.files ?? []; + // Flatten to one blob for callers that need it; unit header comments keep the + // transaction boundaries visible (mirrors Go's `joinPgDeltaFiles`). + const sql = files.map((file) => file.sql).join("\n\n"); + return { sql, files, stderr: result.stderr } satisfies LegacyPgDeltaDiffResult; }); /** diff --git a/apps/cli/src/legacy/shared/legacy-db-connection.sql-pg.layer.ts b/apps/cli/src/legacy/shared/legacy-db-connection.sql-pg.layer.ts index f291140c54..7553b1a3e0 100644 --- a/apps/cli/src/legacy/shared/legacy-db-connection.sql-pg.layer.ts +++ b/apps/cli/src/legacy/shared/legacy-db-connection.sql-pg.layer.ts @@ -2,8 +2,9 @@ import { readFileSync } from "node:fs"; import * as net from "node:net"; import type { ConnectionOptions } from "node:tls"; import { PgClient } from "@effect/sql-pg"; -import { Duration, Effect, Layer, Redacted, type Scope } from "effect"; +import { Duration, Effect, Exit, Layer, Scope } from "effect"; import * as Reactivity from "effect/unstable/reactivity/Reactivity"; +import { ConnectionError, SqlError } from "effect/unstable/sql/SqlError"; // `pg` is also `@effect/sql-pg`'s transitive driver; we depend on it directly only // for the COPY protocol (which `@effect/sql-pg` does not expose). Keep the direct // `pg` version constraint in package.json aligned with the one `@effect/sql-pg` @@ -27,10 +28,16 @@ import { legacyResolveHostsOverHttps } from "./legacy-db-dns.ts"; // node-postgres honors `queryMode: "extended"` to force the Parse/Bind/Execute // protocol (`pg/lib/query.js` `requiresPreparation`), but `@types/pg` doesn't declare // it. Augment `QueryConfig` so `queryRaw` can request it without an `as` cast. +// pg-pool likewise honors a `verify(client, callback)` option — run for every +// brand-new physical connection before it is handed to a waiting checkout +// (`pg-pool/index.js` `_acquireClient`) — that `@types/pg` doesn't declare either. declare module "pg" { interface QueryConfig { queryMode?: "extended" | "simple"; } + interface PoolConfig { + verify?: (client: import("pg").PoolClient, callback: (err?: Error) => void) => void; + } } // Go's role step-down (`apps/cli-go/internal/utils/connect.go:200-220`, @@ -327,6 +334,178 @@ export function legacySslConfigsFor( return [legacySslOptionFor(sslmode, false, servername, caCert, clientCert)]; } +/** + * The raw `pg.ClientConfig` for a dial target, choosing the connection-string form + * whenever a libpq `options`/`runtimeParams` payload must reach the server (see + * `legacyBuildConnectionUrl`) and discrete fields otherwise (to avoid round-tripping + * the password through a URL). `copyToCsv` / `queryRaw` reuse it to open a dedicated + * node-postgres client for the COPY protocol and full result metadata (neither is + * surfaced by `@effect/sql-pg`), against whichever target the primary connection won. + */ +export function legacyBuildRawPgConfig( + cfg: LegacyPgConnInput, + host: string, + port: number, + sslOption: boolean | ConnectionOptions | undefined, + connectTimeoutSeconds: number, +): Pg.ClientConfig { + const hasOptions = legacyMergedConnectionOptions(cfg) !== undefined; + return { + ...(hasOptions + ? { connectionString: legacyBuildConnectionUrl(cfg, host, port) } + : { host, port, user: cfg.user, password: cfg.password, database: cfg.database }), + ...(sslOption === undefined ? {} : { ssl: sslOption }), + connectionTimeoutMillis: connectTimeoutSeconds * 1000, + }; +} + +/** + * The `pg.PoolConfig` for the primary pooled connection. Extends the raw client + * config with the two pool controls this layer depends on: + * + * - `max: 1` — a single physical connection, so a session-scoped `SET SESSION ROLE` + * (the remote step-down) and any session GUCs persist across `exec`/`query` calls. + * - `idleTimeoutMillis: 0` — node-postgres documents `0` as disabling auto-reaping of + * idle connections. `PgClient.make` leaves this unset (→ node-postgres default + * 10_000ms), which during a long-idle `db pull` (shadow-DB provisioning + diff + + * interactive confirm prompt) reaps the stepped-down connection after 10s idle and + * transparently redials a fresh one for the final migration-table write; that fresh + * connection never ran the step-down, so the DDL executes as the bare `cli_login_*` + * role and fails with `permission denied` (42501). Disabling the reaper keeps the + * single stepped-down connection alive for the whole session. + * + * `application_name` mirrors `PgClient.make`'s default so the server-side session name + * is unchanged from the previous `PgClient.make` path. + * + * When `stepDownRequired`, the pool also gets the `verify` step-down hook so every + * new physical connection runs `SET SESSION ROLE postgres` before it is handed out + * (see `legacyPoolStepDownVerify`). + */ +export function legacyBuildPoolConfig( + cfg: LegacyPgConnInput, + host: string, + port: number, + sslOption: boolean | ConnectionOptions | undefined, + connectTimeoutSeconds: number, + stepDownRequired: boolean, +): Pg.PoolConfig { + return { + ...legacyBuildRawPgConfig(cfg, host, port, sslOption, connectTimeoutSeconds), + idleTimeoutMillis: 0, + max: 1, + application_name: "@effect/sql-pg", + ...(stepDownRequired ? { verify: legacyPoolStepDownVerify } : {}), + }; +} + +// Minimal structural views of `pg.Pool` / `pg.PoolClient` used by the pool hooks, +// so the wiring can be unit-tested with a tiny fake at the driver boundary instead +// of a live pool. A real `pg.Pool`/`pg.PoolClient` satisfies these (their `on` +// overloads and `query` signatures are wider). +interface LegacyStepDownClient { + readonly query: (sql: string) => Promise; +} +interface LegacyPoolErrorSource { + readonly on: (event: "error", listener: (error: Error) => void) => unknown; +} + +/** + * pg-pool `verify` hook that re-runs the remote role step-down on EVERY new + * physical connection, matching Go's `AfterConnect` (`connect.go:355-361`) — + * including the silent redial after a dropped connection, which the post-connect + * one-shot in `connect` can't reach. pg-pool invokes `verify` for a brand-new + * client BEFORE resolving the pending checkout (`pg-pool/index.js` + * `_acquireClient`), so the `SET` completes before any caller query runs on that + * connection. (A `"connect"`-listener `client.query()` would instead overlap + * pg-pool's own synchronous dispatch of the checked-out query — node-postgres' + * "Calling client.query() when the client is already executing a query" + * deprecation, whose internal queueing pg@9 removes.) A failure propagates to + * the checkout and fails the caller's query, like Go's `AfterConnect` failing + * the connect. + */ +export function legacyPoolStepDownVerify( + client: LegacyStepDownClient, + callback: (err?: Error) => void, +): void { + client.query(SET_SESSION_ROLE).then( + () => callback(), + (error) => callback(error instanceof Error ? error : new Error(String(error))), + ); +} + +/** + * Swallow the pool's async background errors, like `PgClient.make`: an idle + * client's connection-level failure emits `"error"` on the pool and would crash + * the process without a listener; the next checkout simply redials (and the + * redial re-runs the `verify` step-down). + */ +export function legacyInstallPoolErrorSwallow(pool: LegacyPoolErrorSource): void { + pool.on("error", () => {}); +} + +// Minimal structural view of the `pg.Pool` surface `legacyAcquireProbedPool` +// drives — a real `pg.Pool` satisfies it (its `on`/`query`/`end` signatures are +// wider), and a tiny fake can stand in at the driver boundary for unit tests. +interface LegacyProbePool extends LegacyPoolErrorSource { + readonly query: (sql: string) => Promise; + readonly end: () => Promise; +} + +/** + * Acquire a `pg` pool and probe it with `SELECT 1`, guaranteeing the pool is + * `.end()`ed on EVERY non-success path — a probe rejection, a + * `connectTimeoutSeconds` timeout, or an interruption. + * + * The pool.end() finalizer is registered the MOMENT the pool is constructed, via + * `Effect.acquireRelease` with a synchronous (never-failing) resource step, BEFORE + * the probe runs. So even if the probe rejects or the outer timeout fires (a + * black-holed host), the finalizer is already installed and closes the pool along + * with its in-flight dial (sockets/timers) when the scope unwinds. Upstream + * `@effect/sql-pg`'s `PgClient.make` instead probes INSIDE the acquireRelease + * acquire, so a failed/timed-out probe never installs the finalizer and leaks the + * pool — we deliberately diverge to fix that leak while keeping the SqlError / + * ConnectionError shapes identical. + * + * The probe pool is generic so tests can inject a lightweight fake at the driver + * boundary; a real `pg.Pool` is returned unchanged for `PgClient.fromPool`. + */ +export const legacyAcquireProbedPool =

( + makePool: () => P, + connectTimeoutSeconds: number, +): Effect.Effect => + Effect.gen(function* () { + const pool = yield* Effect.acquireRelease(Effect.sync(makePool), (pool) => + Effect.promise(() => pool.end()).pipe(Effect.timeoutOption(1000)), + ); + legacyInstallPoolErrorSwallow(pool); + yield* Effect.tryPromise({ + try: () => pool.query("SELECT 1"), + catch: (cause) => + new SqlError({ + reason: new ConnectionError({ + cause, + message: "PgClient: Failed to connect", + operation: "connect", + }), + }), + }).pipe( + Effect.timeoutOrElse({ + duration: Duration.seconds(connectTimeoutSeconds), + orElse: () => + Effect.fail( + new SqlError({ + reason: new ConnectionError({ + cause: new Error("Connection timed out"), + message: "PgClient: Connection timed out", + operation: "connect", + }), + }), + ), + }), + ); + return pool; + }); + /** * Default `LegacyDbConnection` layer, backed by `@effect/sql-pg` (pure-JS `pg` * driver, no native addon — bundles under `bun build --compile`). Each @@ -360,53 +539,46 @@ const connect = ( dialTargets.push({ dialHost, port, servername: dialHost === host ? undefined : host }); } } - // Route through the connection string whenever a libpq `options` param OR - // parsed `runtimeParams` are present, so both reach the live connection. - const hasOptions = legacyMergedConnectionOptions(cfg) !== undefined; // Connect timeout parity: Go's `ToPostgresURL` always sets `connect_timeout`, // defaulting to 10s (`connect.go:24-28`); `ConnectLocalPostgres` uses 2s for // local (`connect.go:143-145`). A DSN/`PGCONNECT_TIMEOUT` value (>0) overrides // both. Without this a black-holed host would hang to the OS/driver default. const connectTimeoutSeconds = cfg.connectTimeoutSeconds ?? (isLocal ? 2 : 10); + // Whether the remote step-down runs on this connection. Go installs the + // `AfterConnect` hook only on the remote path (`ConnectByConfigStream`, + // `connect.go:342-362`), not `ConnectLocalPostgres`, so gate on `!isLocal`. + const stepDownRequired = !isLocal && needsRoleStepDown(cfg.user); + // Build the primary connection over a self-managed `pg.Pool` (via + // `PgClient.fromPool`) rather than `PgClient.make`, so we control two pool + // behaviors `PgClient.make` does not expose: `idleTimeoutMillis: 0` (never reap + // the single pooled connection — see `legacyBuildPoolConfig`; the fix for the + // `db pull` step-down loss) and the per-connection role step-down `verify` hook + // (see `legacyPoolStepDownVerify`). The `acquire` uses `legacyAcquireProbedPool`: + // probe with `SELECT 1`, bound the connect by `connectTimeoutSeconds`, and close + // the pool on scope exit AND on every failure/timeout (the leak `PgClient.make` + // has). `probe` (below) runs each attempt in a forked scope so a failed fallback + // attempt's pool closes immediately, before the next host is dialed. const makeClient = ( dialHost: string, port: number, sslOption: boolean | ConnectionOptions | undefined, - ) => - PgClient.make({ - // When a libpq `options` param is present, route everything through the - // connection string so it reaches the server (see `buildConnectionUrl`); - // otherwise pass discrete fields to avoid round-tripping the password. - ...(hasOptions - ? { url: Redacted.make(legacyBuildConnectionUrl(cfg, dialHost, port)) } - : { - host: dialHost, + ) => { + const acquire = legacyAcquireProbedPool( + () => + new Pg.Pool( + legacyBuildPoolConfig( + cfg, + dialHost, port, - username: cfg.user, - password: Redacted.make(cfg.password), - database: cfg.database, - }), - // TLS parity with Go (`internal/utils/connect.go`): see `legacySslOptionFor`. - ...(sslOption === undefined ? {} : { ssl: sslOption }), - connectTimeout: Duration.seconds(connectTimeoutSeconds), - maxConnections: 1, - }).pipe(Effect.provide(Reactivity.layer)); - - // The raw `pg.ClientConfig` for the same dial target, mirroring `makeClient`'s - // discrete-vs-url choice. `copyToCsv` uses it to open a dedicated node-postgres - // connection for the COPY protocol (which `@effect/sql-pg` does not expose), - // against whichever target the primary connection won. - const buildRawPgConfig = ( - dialHost: string, - port: number, - sslOption: boolean | ConnectionOptions | undefined, - ): Pg.ClientConfig => ({ - ...(hasOptions - ? { connectionString: legacyBuildConnectionUrl(cfg, dialHost, port) } - : { host: dialHost, port, user: cfg.user, password: cfg.password, database: cfg.database }), - ...(sslOption === undefined ? {} : { ssl: sslOption }), - connectionTimeoutMillis: connectTimeoutSeconds * 1000, - }); + sslOption, + connectTimeoutSeconds, + stepDownRequired, + ), + ), + connectTimeoutSeconds, + ); + return PgClient.fromPool({ acquire }).pipe(Effect.provide(Reactivity.layer)); + }; // Go's `ConnectByUrl` calls `SetConnectSuggestion(err)` on every connect failure // (`connect.go:187`), mapping the driver error to an actionable hint that replaces @@ -483,7 +655,7 @@ const connect = ( // failed attempt used TLS (`pgconn.go:182`, gated on `fc.TLSConfig != nil`); // a TLS config is any non-plaintext `ssl` value. usedTls: ssl !== undefined && ssl !== false, - rawConfig: buildRawPgConfig(dialHost, port, ssl), + rawConfig: legacyBuildRawPgConfig(cfg, dialHost, port, ssl, connectTimeoutSeconds), }), ), ); @@ -500,10 +672,26 @@ const connect = ( // follow-up query. The winning attempt's `rawConfig` is carried out so `copyToCsv` // can reuse the exact dial target the primary connection succeeded against. const probe = (attempt: (typeof attempts)[number]) => - attempt.client.pipe( - Effect.tap((candidate) => candidate`select 1`), - Effect.map((candidate) => ({ candidate, rawConfig: attempt.rawConfig })), - ); + Effect.gen(function* () { + // Run each attempt's pool in its OWN scope, forked from the session scope. + // Forking (not a detached `Scope.make`) means an abandoned winner — e.g. a + // later step-down failure — is still closed when the session scope unwinds. + // On a probe FAILURE we close the child immediately, so a failed fallback + // attempt's pool (and its in-flight dial) is released before the next host is + // dialed, not left open until session end. On SUCCESS we leave the child open + // (a child of the session scope), so the winning pool lives for the whole + // session and closes with it. + const sessionScope = yield* Scope.Scope; + const attemptScope = yield* Scope.fork(sessionScope); + return yield* attempt.client.pipe( + Effect.tap((candidate) => candidate`select 1`), + Effect.map((candidate) => ({ candidate, rawConfig: attempt.rawConfig })), + Scope.provide(attemptScope), + Effect.onExit((exit) => + Exit.isSuccess(exit) ? Effect.void : Scope.close(attemptScope, exit), + ), + ); + }); const lastIndex = attempts.length - 1; const { candidate: client, rawConfig: winningRawConfig } = yield* attempts .slice(0, lastIndex) @@ -521,9 +709,13 @@ const connect = ( // Step down from the temp/privileged login role before any further SQL — but // only for remote connections: Go installs this hook in `ConnectByConfigStream`, // not `ConnectLocalPostgres`, so a local `--db-url` using `supabase_admin`/ - // `cli_login_*` must not run it. `maxConnections: 1` guarantees the single - // physical connection is reused, so the session-scoped role persists for `exec`. - if (!isLocal && needsRoleStepDown(cfg.user)) { + // `cli_login_*` must not run it. The pool's `"connect"` hook already ran this on + // the physical connection (and on any silent redial); this explicit one-shot is + // the fail-fast path — the hook swallows errors, so a real role-privilege problem + // only surfaces here, as `LegacyDbConnectError: failed to set session role: ...` + // (Go parity). `max: 1` + `idleTimeoutMillis: 0` keep the stepped-down connection + // alive so the session-scoped role persists for every later `exec`/`query`. + if (stepDownRequired) { yield* client.unsafe(SET_SESSION_ROLE).pipe( Effect.asVoid, Effect.mapError( @@ -563,7 +755,7 @@ const connect = ( try: () => fresh.connect(), catch: toConnectError, }); - if (!isLocal && needsRoleStepDown(cfg.user)) { + if (stepDownRequired) { yield* Effect.tryPromise({ try: () => fresh.query(SET_SESSION_ROLE), catch: (error) => diff --git a/apps/cli/src/legacy/shared/legacy-db-connection.sql-pg.unit.test.ts b/apps/cli/src/legacy/shared/legacy-db-connection.sql-pg.unit.test.ts index c3407c8018..334ecda513 100644 --- a/apps/cli/src/legacy/shared/legacy-db-connection.sql-pg.unit.test.ts +++ b/apps/cli/src/legacy/shared/legacy-db-connection.sql-pg.unit.test.ts @@ -1,7 +1,14 @@ +import { EventEmitter } from "node:events"; +import { Effect, Exit } from "effect"; import { describe, expect, it } from "vitest"; import { + legacyAcquireProbedPool, legacyBuildConnectionUrl, + legacyBuildPoolConfig, + legacyBuildRawPgConfig, + legacyInstallPoolErrorSwallow, + legacyPoolStepDownVerify, legacyIsTerminalConnectError, legacyIsUnixSocketHost, legacyMergedConnectionOptions, @@ -271,6 +278,185 @@ describe("legacySslConfigsFor (pgconn fallback list)", () => { }); }); +describe("legacyBuildRawPgConfig", () => { + const base = { user: "postgres", password: "pw", port: 5432, database: "postgres", host: "h" }; + + it("uses discrete fields (no connection string) when no options/runtimeParams", () => { + const c = legacyBuildRawPgConfig( + { ...base }, + "db.example.com", + 5432, + { rejectUnauthorized: false }, + 10, + ); + expect(c).toMatchObject({ + host: "db.example.com", + port: 5432, + user: "postgres", + password: "pw", + database: "postgres", + connectionTimeoutMillis: 10_000, + ssl: { rejectUnauthorized: false }, + }); + expect(c.connectionString).toBeUndefined(); + }); + + it("routes through a connection string when a libpq options payload is present", () => { + const c = legacyBuildRawPgConfig( + { ...base, options: "reference=abc" }, + "db.example.com", + 6543, + false, + 2, + ); + expect(c.connectionString).toContain("@db.example.com:6543/"); + expect(c.connectionString).toContain("options=reference%3Dabc"); + expect(c.host).toBeUndefined(); + expect(c.connectionTimeoutMillis).toBe(2000); + expect(c.ssl).toBe(false); + }); + + it("omits the ssl field entirely when the ssl option is undefined", () => { + const c = legacyBuildRawPgConfig({ ...base }, "h", 5432, undefined, 5); + expect("ssl" in c).toBe(false); + }); +}); + +describe("legacyBuildPoolConfig", () => { + const base = { user: "postgres", password: "pw", port: 5432, database: "postgres", host: "h" }; + + it("disables idle reaping (idleTimeoutMillis 0) and pins one connection (max 1) for a remote config", () => { + // The `db pull` bug: `PgClient.make` left idleTimeoutMillis unset (node-postgres + // default 10s), reaping the stepped-down connection during a long idle. + const c = legacyBuildPoolConfig( + { ...base }, + "db.example.com", + 5432, + { rejectUnauthorized: false }, + 10, + true, + ); + expect(c.idleTimeoutMillis).toBe(0); + expect(c.max).toBe(1); + expect(c.application_name).toBe("@effect/sql-pg"); + // carries the raw client config fields through + expect(c).toMatchObject({ host: "db.example.com", connectionTimeoutMillis: 10_000 }); + }); + + it("disables idle reaping and pins one connection for a local (plaintext) config too", () => { + const c = legacyBuildPoolConfig({ ...base }, "127.0.0.1", 54322, false, 2, false); + expect(c.idleTimeoutMillis).toBe(0); + expect(c.max).toBe(1); + expect(c.ssl).toBe(false); + }); + + it("installs the step-down verify hook only when required", () => { + // Every NEW physical connection (initial + silent redials) runs the step-down + // before pg-pool hands it to a checkout, mirroring Go's per-connection + // AfterConnect. + const remote = legacyBuildPoolConfig({ ...base }, "db.example.com", 5432, false, 10, true); + expect(remote.verify).toBe(legacyPoolStepDownVerify); + const local = legacyBuildPoolConfig({ ...base }, "127.0.0.1", 54322, false, 2, false); + expect("verify" in local).toBe(false); + }); +}); + +describe("legacyPoolStepDownVerify", () => { + it("runs SET SESSION ROLE postgres and reports success to the pool", async () => { + const queries: Array = []; + const client = { query: (sql: string) => (queries.push(sql), Promise.resolve()) }; + const done = await new Promise((resolve) => { + legacyPoolStepDownVerify(client, resolve); + }); + expect(queries).toEqual(["SET SESSION ROLE postgres"]); + expect(done).toBeUndefined(); + }); + + it("propagates a failing step-down to the pool callback so the checkout fails (Go AfterConnect parity)", async () => { + const failure = new Error("permission denied to set role"); + const client = { query: () => Promise.reject(failure) }; + const done = await new Promise((resolve) => { + legacyPoolStepDownVerify(client, resolve); + }); + expect(done).toBe(failure); + }); + + it("wraps a non-Error rejection into an Error for the pool callback", async () => { + const client = { query: () => Promise.reject("boom") }; + const done = await new Promise((resolve) => { + legacyPoolStepDownVerify(client, resolve); + }); + expect(done).toBeInstanceOf(Error); + expect(String(done)).toContain("boom"); + }); +}); + +describe("legacyInstallPoolErrorSwallow", () => { + it("swallows pool background errors so a dropped idle client never crashes the process", () => { + const pool = new EventEmitter(); + legacyInstallPoolErrorSwallow(pool); + expect(pool.listenerCount("error")).toBe(1); + expect(() => pool.emit("error", new Error("idle client boom"))).not.toThrow(); + }); +}); + +describe("legacyAcquireProbedPool", () => { + // Tiny fake at the driver boundary: records query/end calls. Standing in for a + // real `pg.Pool` proves the pool is always `.end()`ed — the leak this fixes. + function makeFakePool(query: () => Promise) { + const calls = { query: 0, end: 0 }; + const pool = { + query: (_sql: string) => { + calls.query += 1; + return query(); + }, + end: () => { + calls.end += 1; + return Promise.resolve(); + }, + on: (_event: "error", _listener: (error: Error) => void) => undefined, + }; + return { pool, calls }; + } + + it("ends the pool when the connect probe rejects", async () => { + const fake = makeFakePool(() => Promise.reject(new Error("ECONNREFUSED"))); + const exit = await Effect.runPromiseExit( + legacyAcquireProbedPool(() => fake.pool, 2).pipe(Effect.scoped), + ); + expect(Exit.isFailure(exit)).toBe(true); + expect(fake.calls.query).toBe(1); + // The finalizer, installed the moment the pool exists, closes it on failure. + expect(fake.calls.end).toBe(1); + }); + + it("ends the pool when the connect probe times out (black-holed host)", async () => { + // A never-resolving probe models a black-holed host: `timeoutOrElse` fires and + // the already-installed finalizer still closes the pool + its in-flight dial. + const fake = makeFakePool(() => new Promise(() => {})); + const exit = await Effect.runPromiseExit( + legacyAcquireProbedPool(() => fake.pool, 0.05).pipe(Effect.scoped), + ); + expect(Exit.isFailure(exit)).toBe(true); + expect(fake.calls.end).toBe(1); + }); + + it("keeps the pool open until the scope closes on a successful probe", async () => { + const fake = makeFakePool(() => Promise.resolve({ rows: [{ "?column?": 1 }] })); + const observed = await Effect.runPromise( + Effect.gen(function* () { + const pool = yield* legacyAcquireProbedPool(() => fake.pool, 2); + // While the scope is open the pool is live and not yet ended. + return { isSamePool: pool === fake.pool, endWhileOpen: fake.calls.end }; + }).pipe(Effect.scoped), + ); + expect(observed.isSamePool).toBe(true); + expect(observed.endWhileOpen).toBe(0); + // Closing the scope ends the pool exactly once. + expect(fake.calls.end).toBe(1); + }); +}); + describe("legacyIsUnixSocketHost", () => { it("treats an absolute path as a unix socket and a hostname/IP as not", () => { expect(legacyIsUnixSocketHost("/var/run/postgresql")).toBe(true); diff --git a/apps/cli/src/legacy/shared/legacy-edge-runtime-script.layer.integration.test.ts b/apps/cli/src/legacy/shared/legacy-edge-runtime-script.layer.integration.test.ts new file mode 100644 index 0000000000..dc7a3fb052 --- /dev/null +++ b/apps/cli/src/legacy/shared/legacy-edge-runtime-script.layer.integration.test.ts @@ -0,0 +1,123 @@ +import { describe, expect, it } from "@effect/vitest"; +import { BunServices } from "@effect/platform-bun"; +import { Effect, Exit, Layer, Option } from "effect"; + +import { LegacyDebugFlag, LegacyNetworkIdFlag } from "../../shared/legacy/global-flags.ts"; +import { RuntimeInfo } from "../../shared/runtime/runtime-info.service.ts"; +import { LegacyCliConfig } from "../config/legacy-cli-config.service.ts"; +import { LegacyDockerRun } from "./legacy-docker-run.service.ts"; +import { legacyEdgeRuntimeScriptLayer } from "./legacy-edge-runtime-script.layer.ts"; +import { LegacyEdgeRuntimeScript } from "./legacy-edge-runtime-script.service.ts"; + +// Fakes a `docker run --rm` capture: the pg-delta scripts throw to force the +// worker to exit, so a real diff always comes back with a non-zero exit and +// "main worker has been destroyed" in stderr — the crash path only differs by +// the sentinel line the templates' catch blocks add. +function fakeDocker(result: { exitCode: number; stdout?: string; stderr?: string }) { + return Layer.succeed(LegacyDockerRun, { + runCapture: () => + Effect.succeed({ + exitCode: result.exitCode, + stdout: new TextEncoder().encode(result.stdout ?? ""), + stderr: result.stderr ?? "", + }), + run: () => Effect.die("unused"), + runStream: () => Effect.die("unused"), + }); +} + +// `workdir` points at a directory without `supabase/.temp/edge-runtime-version`, +// so the image resolver falls back to the default tag (the read is orElseSucceed). +const cliConfig = Layer.succeed(LegacyCliConfig, { + profile: "supabase", + apiUrl: "https://api.supabase.com", + projectHost: "supabase.co", + poolerHost: "supabase.co", + dashboardUrl: "https://supabase.com/dashboard", + accessToken: Option.none(), + projectId: Option.none(), + workdir: "/nonexistent-workdir", + userAgent: "test", +}); + +function setup(result: { exitCode: number; stdout?: string; stderr?: string }) { + return legacyEdgeRuntimeScriptLayer.pipe( + Layer.provideMerge( + Layer.mergeAll( + fakeDocker(result), + cliConfig, + Layer.succeed(RuntimeInfo, { + cwd: "/nonexistent-workdir", + platform: "darwin", + arch: "arm64", + homeDir: "/home/test", + execPath: "/usr/bin/bun", + pid: 1, + }), + Layer.succeed(LegacyDebugFlag, false), + Layer.succeed(LegacyNetworkIdFlag, Option.none()), + BunServices.layer, + ), + ), + ); +} + +const runScript = Effect.fnUntraced(function* () { + const edge = yield* LegacyEdgeRuntimeScript; + return yield* edge.run({ + script: "console.log('x')", + env: {}, + binds: [], + errPrefix: "error diffing schema", + denoVersion: 2, + }); +}); + +describe("legacyEdgeRuntimeScriptLayer sentinel handling", () => { + it.effect( + "fails with the real error when the script crashes behind the worker-destroyed message", + () => { + // The catch block emits the real error, the sentinel, and the worker is torn + // down — all with a non-zero exit. This must surface, not read as an empty diff. + const stderr = + "error: permission denied for table pg_user_mapping\n" + + "PGDELTA_SCRIPT_ERROR\n" + + "worker boot error\nmain worker has been destroyed\n"; + return runScript().pipe( + Effect.exit, + Effect.tap((exit) => + Effect.sync(() => { + expect(Exit.isFailure(exit)).toBe(true); + const error = Exit.isFailure(exit) + ? exit.cause.reasons.find((r) => r._tag === "Fail")?.error + : undefined; + const message = (error as { message: string } | undefined)?.message ?? ""; + expect(message).toContain("error diffing schema: error running script:"); + // The actionable permission error must reach the user. + expect(message).toContain("permission denied for table pg_user_mapping"); + }), + ), + Effect.provide(setup({ exitCode: 1, stdout: "", stderr })), + ); + }, + ); + + it.effect("still succeeds on a worker-destroyed exit when no sentinel is present", () => { + // Success path: the template forces the worker to exit after writing output, so + // the exit is non-zero with "main worker has been destroyed" but no sentinel. + return runScript().pipe( + Effect.tap((res) => + Effect.sync(() => { + expect(res.stdout).toBe("ALTER TABLE x;\n"); + }), + ), + Effect.provide( + setup({ + exitCode: 1, + stdout: "ALTER TABLE x;\n", + stderr: "main worker has been destroyed\n", + }), + ), + ); + }); +}); diff --git a/apps/cli/src/legacy/shared/legacy-edge-runtime-script.layer.ts b/apps/cli/src/legacy/shared/legacy-edge-runtime-script.layer.ts index 5b7d273c78..3fa93dad83 100644 --- a/apps/cli/src/legacy/shared/legacy-edge-runtime-script.layer.ts +++ b/apps/cli/src/legacy/shared/legacy-edge-runtime-script.layer.ts @@ -10,6 +10,7 @@ import { LegacyDockerRun } from "./legacy-docker-run.service.ts"; import { legacyResolveEdgeRuntimeImage } from "./legacy-edge-runtime-image.ts"; import { LegacyEdgeRuntimeScriptError } from "./legacy-edge-runtime-script.errors.ts"; import { + LEGACY_EDGE_RUNTIME_SCRIPT_ERROR_SENTINEL, LegacyEdgeRuntimeScript, legacyBuildEdgeRuntimeEntrypoint, legacyBuildEdgeRuntimeStartCmd, @@ -132,6 +133,21 @@ export const legacyEdgeRuntimeScriptLayer = Layer.effect( ); } + // The pg-delta templates force the worker to exit by throwing, so a + // script crash is masked by the "main worker has been destroyed" + // suppression above. The sentinel — printed only by the templates' + // catch blocks — marks a real failure so the collected stderr (which + // holds the real error) reaches the user instead of looking like an + // empty diff. Byte-for-byte port of Go's check in + // apps/cli-go/internal/utils/edgeruntime.go. + if (result.stderr.includes(LEGACY_EDGE_RUNTIME_SCRIPT_ERROR_SENTINEL)) { + return yield* Effect.fail( + new LegacyEdgeRuntimeScriptError({ + message: `${opts.errPrefix}: error running script:\n${result.stderr}`, + }), + ); + } + return { stdout: new TextDecoder().decode(result.stdout), stderr: result.stderr, diff --git a/apps/cli/src/legacy/shared/legacy-edge-runtime-script.service.ts b/apps/cli/src/legacy/shared/legacy-edge-runtime-script.service.ts index 8f6e970817..8d679b8be6 100644 --- a/apps/cli/src/legacy/shared/legacy-edge-runtime-script.service.ts +++ b/apps/cli/src/legacy/shared/legacy-edge-runtime-script.service.ts @@ -2,6 +2,19 @@ import { Context, type Effect, Option } from "effect"; import type { LegacyEdgeRuntimeScriptError } from "./legacy-edge-runtime-script.errors.ts"; +/** + * Printed to stderr by the pg-delta Deno templates when their body throws (see + * the `catch` blocks in `apps/cli-go/internal/db/diff/templates/*.ts`). The + * templates force the edge-runtime worker to exit by throwing on both the + * success and failure paths, and that non-zero exit is otherwise suppressed when + * stderr contains `"main worker has been destroyed"`. Without a distinct marker + * a crashed script is indistinguishable from a successful empty diff, so + * `db pull` reports "No schema changes found" while the real error is swallowed. + * Byte-for-byte mirror of Go's `EdgeRuntimeScriptErrorSentinel` + * (`apps/cli-go/internal/utils/edgeruntime.go`). See supabase/cli#5826. + */ +export const LEGACY_EDGE_RUNTIME_SCRIPT_ERROR_SENTINEL = "PGDELTA_SCRIPT_ERROR"; + /** A file dropped alongside `index.ts` in the container's working directory. */ export interface LegacyEdgeRuntimeFile { readonly name: string; diff --git a/apps/cli/tests/helpers/legacy-mocks.ts b/apps/cli/tests/helpers/legacy-mocks.ts index d8ab445cc2..e02e241fbb 100644 --- a/apps/cli/tests/helpers/legacy-mocks.ts +++ b/apps/cli/tests/helpers/legacy-mocks.ts @@ -4,7 +4,8 @@ import { join } from "node:path"; import { BunServices } from "@effect/platform-bun"; import { type ApiClient, makeApiClient, type SupabaseApiConfigError } from "@supabase/api/effect"; -import { Effect, Layer, Option, Redacted } from "effect"; +import { Effect, FileSystem, Layer, Option, Redacted } from "effect"; +import { PlatformError, SystemError } from "effect/PlatformError"; import * as HttpClient from "effect/unstable/http/HttpClient"; import * as HttpClientError from "effect/unstable/http/HttpClientError"; import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse"; @@ -642,6 +643,47 @@ export function useLegacyTempWorkdir(prefix = "supabase-legacy-test-"): { }; } +// --------------------------------------------------------------------------- +// Failing filesystem — wraps the real Bun `FileSystem` and fails the Nth +// `writeFileString` call with a `PlatformError`, so cleanup-on-failure paths +// (e.g. the pg-delta multi-file migration writer) can be exercised +// deterministically. Every other call delegates to the real filesystem, so +// config reads / earlier writes behave normally. Merge this AFTER +// `BunServices.layer` (last-wins) so it overrides only `FileSystem`; `Path` +// still comes from `BunServices`. +// --------------------------------------------------------------------------- + +export function legacyFailWriteStringOnNthCallFsLayer( + failOnCall: number, +): Layer.Layer { + return Layer.effect( + FileSystem.FileSystem, + Effect.map(FileSystem.FileSystem, (real) => { + let calls = 0; + return FileSystem.FileSystem.of({ + ...real, + writeFileString: (path, data, options) => { + calls += 1; + if (calls === failOnCall) { + return Effect.fail( + new PlatformError( + new SystemError({ + _tag: "Unknown", + module: "FileSystem", + method: "writeFileString", + pathOrDescriptor: path, + description: "simulated write failure", + }), + ), + ); + } + return real.writeFileString(path, data, options); + }, + }); + }), + ).pipe(Layer.provide(BunServices.layer)); +} + // --------------------------------------------------------------------------- // Runtime composition — bundles the entire Layer.mergeAll(...) graph that // every native-port integration test re-builds, including the easy-to-mis-wire From 2b8083c085249414946af72758361e79c4522c0d Mon Sep 17 00:00:00 2001 From: "supabase-cli-releaser[bot]" <246109035+supabase-cli-releaser[bot]@users.noreply.github.com> Date: Tue, 21 Jul 2026 15:38:26 +0200 Subject: [PATCH 67/79] chore: sync API types from infrastructure (#5897) This PR was automatically created to sync API types from the infrastructure repository. Changes were detected in the generated API code after syncing with the latest spec from infrastructure. --------- Co-authored-by: supabase-cli-releaser[bot] <246109035+supabase-cli-releaser[bot]@users.noreply.github.com> Co-authored-by: avallete --- apps/cli-go/internal/sso/create/create.go | 10 +- .../internal/sso/internal/saml/files_test.go | 18 +- apps/cli-go/internal/sso/update/update.go | 10 +- apps/cli-go/pkg/api/client.gen.go | 3195 ++++++++++---- apps/cli-go/pkg/api/types.gen.go | 3775 ++++++++++++++++- 5 files changed, 5964 insertions(+), 1044 deletions(-) diff --git a/apps/cli-go/internal/sso/create/create.go b/apps/cli-go/internal/sso/create/create.go index acf3eb14e5..3716e5ee73 100644 --- a/apps/cli-go/internal/sso/create/create.go +++ b/apps/cli-go/internal/sso/create/create.go @@ -52,15 +52,7 @@ func Run(ctx context.Context, params RunParams) error { } if params.AttributeMapping != "" { - body.AttributeMapping = &struct { - Keys map[string]struct { - Array *bool "json:\"array,omitempty\"" - Default *any "json:\"default,omitempty\"" - Name *string "json:\"name,omitempty\"" - Names *[]string "json:\"names,omitempty\"" - } "json:\"keys\"" - }{} - if err := saml.ReadAttributeMappingFile(Fs, params.AttributeMapping, body.AttributeMapping); err != nil { + if err := saml.ReadAttributeMappingFile(Fs, params.AttributeMapping, &body.AttributeMapping); err != nil { return err } } diff --git a/apps/cli-go/internal/sso/internal/saml/files_test.go b/apps/cli-go/internal/sso/internal/saml/files_test.go index cc5c59b854..cd512a599e 100644 --- a/apps/cli-go/internal/sso/internal/saml/files_test.go +++ b/apps/cli-go/internal/sso/internal/saml/files_test.go @@ -22,7 +22,7 @@ func TestReadAttributeMappingFile(t *testing.T) { fs := afero.NewMemMapFs() require.NoError(t, afero.WriteFile(fs, "/not-valid-json", []byte("not-valid-JSON"), 0755)) var body api.CreateProviderBody - err := ReadAttributeMappingFile(fs, "/not-valid-json", body.AttributeMapping) + err := ReadAttributeMappingFile(fs, "/not-valid-json", &body.AttributeMapping) assert.ErrorContains(t, err, "failed to parse attribute mapping") }) @@ -30,24 +30,16 @@ func TestReadAttributeMappingFile(t *testing.T) { fs := afero.NewMemMapFs() data := `{"keys":{"abc":{"names":["x","y","z"],"default":2,"name":"k"}}}` require.NoError(t, afero.WriteFile(fs, "/valid-json", []byte(data), 0755)) - body := api.CreateProviderBody{ - AttributeMapping: &struct { - Keys map[string]struct { - Array *bool "json:\"array,omitempty\"" - Default *any "json:\"default,omitempty\"" - Name *string "json:\"name,omitempty\"" - Names *[]string "json:\"names,omitempty\"" - } "json:\"keys\"" - }{}, - } - err := ReadAttributeMappingFile(fs, "/valid-json", body.AttributeMapping) + var body api.CreateProviderBody + err := ReadAttributeMappingFile(fs, "/valid-json", &body.AttributeMapping) assert.NoError(t, err) + require.NotNil(t, body.AttributeMapping) assert.Len(t, body.AttributeMapping.Keys, 1) value := body.AttributeMapping.Keys["abc"] assert.Equal(t, cast.Ptr("k"), value.Name) assert.Equal(t, &[]string{"x", "y", "z"}, value.Names) assert.NotNil(t, value.Default) - assert.Equal(t, float64(2), *value.Default) + assert.Equal(t, float64(2), value.Default) }) } diff --git a/apps/cli-go/internal/sso/update/update.go b/apps/cli-go/internal/sso/update/update.go index 4e54b8961e..6a4dc9b294 100644 --- a/apps/cli-go/internal/sso/update/update.go +++ b/apps/cli-go/internal/sso/update/update.go @@ -74,15 +74,7 @@ func Run(ctx context.Context, params RunParams) error { } if params.AttributeMapping != "" { - body.AttributeMapping = &struct { - Keys map[string]struct { - Array *bool "json:\"array,omitempty\"" - Default *any "json:\"default,omitempty\"" - Name *string "json:\"name,omitempty\"" - Names *[]string "json:\"names,omitempty\"" - } "json:\"keys\"" - }{} - if err := saml.ReadAttributeMappingFile(Fs, params.AttributeMapping, body.AttributeMapping); err != nil { + if err := saml.ReadAttributeMappingFile(Fs, params.AttributeMapping, &body.AttributeMapping); err != nil { return err } } diff --git a/apps/cli-go/pkg/api/client.gen.go b/apps/cli-go/pkg/api/client.gen.go index 17a4a14ad1..b8684b994f 100644 --- a/apps/cli-go/pkg/api/client.gen.go +++ b/apps/cli-go/pkg/api/client.gen.go @@ -1,6 +1,6 @@ // Package api provides primitives to interact with the openapi HTTP API. // -// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.4.1 DO NOT EDIT. +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.7.1 DO NOT EDIT. package api import ( @@ -3477,7 +3477,7 @@ func NewV1DeleteABranchRequest(server string, branchIdOrRef string, params *V1De var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "branch_id_or_ref", runtime.ParamLocationPath, branchIdOrRef) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "branch_id_or_ref", branchIdOrRef, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -3498,28 +3498,33 @@ func NewV1DeleteABranchRequest(server string, branchIdOrRef string, params *V1De } if params != nil { + // queryValues collects non-styled parameters (passthrough, JSON) + // that are safe to round-trip through url.Values.Encode(). queryValues := queryURL.Query() + // rawQueryFragments collects pre-encoded query fragments from + // styled parameters, preserving literal commas as delimiters + // per the OpenAPI spec (e.g. "color=blue,black,brown"). + var rawQueryFragments []string if params.Force != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "force", runtime.ParamLocationQuery, *params.Force); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "force", *params.Force, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "boolean", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } } - queryURL.RawQuery = queryValues.Encode() + if encoded := queryValues.Encode(); encoded != "" { + rawQueryFragments = append(rawQueryFragments, encoded) + } + queryURL.RawQuery = strings.Join(rawQueryFragments, "&") } - req, err := http.NewRequest("DELETE", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodDelete, queryURL.String(), nil) if err != nil { return nil, err } @@ -3533,7 +3538,7 @@ func NewV1GetABranchConfigRequest(server string, branchIdOrRef string) (*http.Re var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "branch_id_or_ref", runtime.ParamLocationPath, branchIdOrRef) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "branch_id_or_ref", branchIdOrRef, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -3553,7 +3558,7 @@ func NewV1GetABranchConfigRequest(server string, branchIdOrRef string) (*http.Re return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -3578,7 +3583,7 @@ func NewV1UpdateABranchConfigRequestWithBody(server string, branchIdOrRef string var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "branch_id_or_ref", runtime.ParamLocationPath, branchIdOrRef) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "branch_id_or_ref", branchIdOrRef, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -3598,7 +3603,7 @@ func NewV1UpdateABranchConfigRequestWithBody(server string, branchIdOrRef string return nil, err } - req, err := http.NewRequest("PATCH", queryURL.String(), body) + req, err := http.NewRequest(http.MethodPatch, queryURL.String(), body) if err != nil { return nil, err } @@ -3614,7 +3619,7 @@ func NewV1DiffABranchRequest(server string, branchIdOrRef string, params *V1Diff var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "branch_id_or_ref", runtime.ParamLocationPath, branchIdOrRef) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "branch_id_or_ref", branchIdOrRef, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -3635,19 +3640,21 @@ func NewV1DiffABranchRequest(server string, branchIdOrRef string, params *V1Diff } if params != nil { + // queryValues collects non-styled parameters (passthrough, JSON) + // that are safe to round-trip through url.Values.Encode(). queryValues := queryURL.Query() + // rawQueryFragments collects pre-encoded query fragments from + // styled parameters, preserving literal commas as delimiters + // per the OpenAPI spec (e.g. "color=blue,black,brown"). + var rawQueryFragments []string if params.IncludedSchemas != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "included_schemas", runtime.ParamLocationQuery, *params.IncludedSchemas); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "included_schemas", *params.IncludedSchemas, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } @@ -3655,24 +3662,23 @@ func NewV1DiffABranchRequest(server string, branchIdOrRef string, params *V1Diff if params.Pgdelta != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "pgdelta", runtime.ParamLocationQuery, *params.Pgdelta); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "pgdelta", *params.Pgdelta, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "boolean", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } } - queryURL.RawQuery = queryValues.Encode() + if encoded := queryValues.Encode(); encoded != "" { + rawQueryFragments = append(rawQueryFragments, encoded) + } + queryURL.RawQuery = strings.Join(rawQueryFragments, "&") } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -3697,7 +3703,7 @@ func NewV1MergeABranchRequestWithBody(server string, branchIdOrRef string, conte var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "branch_id_or_ref", runtime.ParamLocationPath, branchIdOrRef) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "branch_id_or_ref", branchIdOrRef, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -3717,7 +3723,7 @@ func NewV1MergeABranchRequestWithBody(server string, branchIdOrRef string, conte return nil, err } - req, err := http.NewRequest("POST", queryURL.String(), body) + req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) if err != nil { return nil, err } @@ -3744,7 +3750,7 @@ func NewV1PushABranchRequestWithBody(server string, branchIdOrRef string, conten var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "branch_id_or_ref", runtime.ParamLocationPath, branchIdOrRef) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "branch_id_or_ref", branchIdOrRef, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -3764,7 +3770,7 @@ func NewV1PushABranchRequestWithBody(server string, branchIdOrRef string, conten return nil, err } - req, err := http.NewRequest("POST", queryURL.String(), body) + req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) if err != nil { return nil, err } @@ -3791,7 +3797,7 @@ func NewV1ResetABranchRequestWithBody(server string, branchIdOrRef string, conte var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "branch_id_or_ref", runtime.ParamLocationPath, branchIdOrRef) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "branch_id_or_ref", branchIdOrRef, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -3811,7 +3817,7 @@ func NewV1ResetABranchRequestWithBody(server string, branchIdOrRef string, conte return nil, err } - req, err := http.NewRequest("POST", queryURL.String(), body) + req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) if err != nil { return nil, err } @@ -3827,7 +3833,7 @@ func NewV1RestoreABranchRequest(server string, branchIdOrRef string) (*http.Requ var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "branch_id_or_ref", runtime.ParamLocationPath, branchIdOrRef) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "branch_id_or_ref", branchIdOrRef, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -3847,7 +3853,7 @@ func NewV1RestoreABranchRequest(server string, branchIdOrRef string) (*http.Requ return nil, err } - req, err := http.NewRequest("POST", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodPost, queryURL.String(), nil) if err != nil { return nil, err } @@ -3875,55 +3881,45 @@ func NewV1AuthorizeUserRequest(server string, params *V1AuthorizeUserParams) (*h } if params != nil { + // queryValues collects non-styled parameters (passthrough, JSON) + // that are safe to round-trip through url.Values.Encode(). queryValues := queryURL.Query() + // rawQueryFragments collects pre-encoded query fragments from + // styled parameters, preserving literal commas as delimiters + // per the OpenAPI spec (e.g. "color=blue,black,brown"). + var rawQueryFragments []string - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "client_id", runtime.ParamLocationQuery, params.ClientId); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "client_id", params.ClientId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: "uuid"}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "response_type", runtime.ParamLocationQuery, params.ResponseType); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "response_type", params.ResponseType, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "redirect_uri", runtime.ParamLocationQuery, params.RedirectUri); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "redirect_uri", params.RedirectUri, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } if params.Scope != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "scope", runtime.ParamLocationQuery, *params.Scope); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "scope", *params.Scope, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } @@ -3931,15 +3927,11 @@ func NewV1AuthorizeUserRequest(server string, params *V1AuthorizeUserParams) (*h if params.State != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "state", runtime.ParamLocationQuery, *params.State); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "state", *params.State, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } @@ -3947,15 +3939,11 @@ func NewV1AuthorizeUserRequest(server string, params *V1AuthorizeUserParams) (*h if params.ResponseMode != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "response_mode", runtime.ParamLocationQuery, *params.ResponseMode); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "response_mode", *params.ResponseMode, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } @@ -3963,15 +3951,11 @@ func NewV1AuthorizeUserRequest(server string, params *V1AuthorizeUserParams) (*h if params.CodeChallenge != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "code_challenge", runtime.ParamLocationQuery, *params.CodeChallenge); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "code_challenge", *params.CodeChallenge, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } @@ -3979,15 +3963,11 @@ func NewV1AuthorizeUserRequest(server string, params *V1AuthorizeUserParams) (*h if params.CodeChallengeMethod != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "code_challenge_method", runtime.ParamLocationQuery, *params.CodeChallengeMethod); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "code_challenge_method", *params.CodeChallengeMethod, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } @@ -3995,15 +3975,11 @@ func NewV1AuthorizeUserRequest(server string, params *V1AuthorizeUserParams) (*h if params.OrganizationSlug != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "organization_slug", runtime.ParamLocationQuery, *params.OrganizationSlug); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "organization_slug", *params.OrganizationSlug, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } @@ -4011,15 +3987,11 @@ func NewV1AuthorizeUserRequest(server string, params *V1AuthorizeUserParams) (*h if params.TargetFlow != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "target_flow", runtime.ParamLocationQuery, *params.TargetFlow); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "target_flow", *params.TargetFlow, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } @@ -4027,24 +3999,23 @@ func NewV1AuthorizeUserRequest(server string, params *V1AuthorizeUserParams) (*h if params.Resource != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "resource", runtime.ParamLocationQuery, *params.Resource); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "resource", *params.Resource, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: "uri"}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } } - queryURL.RawQuery = queryValues.Encode() + if encoded := queryValues.Encode(); encoded != "" { + rawQueryFragments = append(rawQueryFragments, encoded) + } + queryURL.RawQuery = strings.Join(rawQueryFragments, "&") } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -4072,67 +4043,53 @@ func NewV1OauthAuthorizeProjectClaimRequest(server string, params *V1OauthAuthor } if params != nil { + // queryValues collects non-styled parameters (passthrough, JSON) + // that are safe to round-trip through url.Values.Encode(). queryValues := queryURL.Query() + // rawQueryFragments collects pre-encoded query fragments from + // styled parameters, preserving literal commas as delimiters + // per the OpenAPI spec (e.g. "color=blue,black,brown"). + var rawQueryFragments []string - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "project_ref", runtime.ParamLocationQuery, params.ProjectRef); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "project_ref", params.ProjectRef, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "client_id", runtime.ParamLocationQuery, params.ClientId); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "client_id", params.ClientId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: "uuid"}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "response_type", runtime.ParamLocationQuery, params.ResponseType); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "response_type", params.ResponseType, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "redirect_uri", runtime.ParamLocationQuery, params.RedirectUri); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "redirect_uri", params.RedirectUri, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } if params.State != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "state", runtime.ParamLocationQuery, *params.State); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "state", *params.State, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } @@ -4140,15 +4097,11 @@ func NewV1OauthAuthorizeProjectClaimRequest(server string, params *V1OauthAuthor if params.ResponseMode != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "response_mode", runtime.ParamLocationQuery, *params.ResponseMode); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "response_mode", *params.ResponseMode, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } @@ -4156,15 +4109,11 @@ func NewV1OauthAuthorizeProjectClaimRequest(server string, params *V1OauthAuthor if params.CodeChallenge != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "code_challenge", runtime.ParamLocationQuery, *params.CodeChallenge); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "code_challenge", *params.CodeChallenge, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } @@ -4172,24 +4121,23 @@ func NewV1OauthAuthorizeProjectClaimRequest(server string, params *V1OauthAuthor if params.CodeChallengeMethod != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "code_challenge_method", runtime.ParamLocationQuery, *params.CodeChallengeMethod); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "code_challenge_method", *params.CodeChallengeMethod, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } } - queryURL.RawQuery = queryValues.Encode() + if encoded := queryValues.Encode(); encoded != "" { + rawQueryFragments = append(rawQueryFragments, encoded) + } + queryURL.RawQuery = strings.Join(rawQueryFragments, "&") } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -4227,7 +4175,7 @@ func NewV1RevokeTokenRequestWithBody(server string, contentType string, body io. return nil, err } - req, err := http.NewRequest("POST", queryURL.String(), body) + req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) if err != nil { return nil, err } @@ -4267,7 +4215,7 @@ func NewV1ExchangeOauthTokenRequestWithBody(server string, contentType string, b return nil, err } - req, err := http.NewRequest("POST", queryURL.String(), body) + req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) if err != nil { return nil, err } @@ -4296,7 +4244,7 @@ func NewV1ListAllOrganizationsRequest(server string) (*http.Request, error) { return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -4334,7 +4282,7 @@ func NewV1CreateAnOrganizationRequestWithBody(server string, contentType string, return nil, err } - req, err := http.NewRequest("POST", queryURL.String(), body) + req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) if err != nil { return nil, err } @@ -4350,7 +4298,7 @@ func NewV1GetAnOrganizationRequest(server string, slug string) (*http.Request, e var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "slug", runtime.ParamLocationPath, slug) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "slug", slug, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -4370,7 +4318,7 @@ func NewV1GetAnOrganizationRequest(server string, slug string) (*http.Request, e return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -4384,7 +4332,7 @@ func NewV1GetOrganizationEntitlementsRequest(server string, slug string) (*http. var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "slug", runtime.ParamLocationPath, slug) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "slug", slug, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -4404,7 +4352,7 @@ func NewV1GetOrganizationEntitlementsRequest(server string, slug string) (*http. return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -4418,7 +4366,7 @@ func NewV1ListOrganizationMembersRequest(server string, slug string) (*http.Requ var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "slug", runtime.ParamLocationPath, slug) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "slug", slug, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -4438,7 +4386,7 @@ func NewV1ListOrganizationMembersRequest(server string, slug string) (*http.Requ return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -4452,14 +4400,14 @@ func NewV1GetOrganizationProjectClaimRequest(server string, slug string, token s var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "slug", runtime.ParamLocationPath, slug) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "slug", slug, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } var pathParam1 string - pathParam1, err = runtime.StyleParamWithLocation("simple", false, "token", runtime.ParamLocationPath, token) + pathParam1, err = runtime.StyleParamWithOptions("simple", false, "token", token, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -4479,7 +4427,7 @@ func NewV1GetOrganizationProjectClaimRequest(server string, slug string, token s return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -4493,14 +4441,14 @@ func NewV1ClaimProjectForOrganizationRequest(server string, slug string, token s var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "slug", runtime.ParamLocationPath, slug) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "slug", slug, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } var pathParam1 string - pathParam1, err = runtime.StyleParamWithLocation("simple", false, "token", runtime.ParamLocationPath, token) + pathParam1, err = runtime.StyleParamWithOptions("simple", false, "token", token, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -4520,7 +4468,7 @@ func NewV1ClaimProjectForOrganizationRequest(server string, slug string, token s return nil, err } - req, err := http.NewRequest("POST", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodPost, queryURL.String(), nil) if err != nil { return nil, err } @@ -4534,7 +4482,7 @@ func NewV1GetAllProjectsForOrganizationRequest(server string, slug string, param var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "slug", runtime.ParamLocationPath, slug) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "slug", slug, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -4555,19 +4503,21 @@ func NewV1GetAllProjectsForOrganizationRequest(server string, slug string, param } if params != nil { + // queryValues collects non-styled parameters (passthrough, JSON) + // that are safe to round-trip through url.Values.Encode(). queryValues := queryURL.Query() + // rawQueryFragments collects pre-encoded query fragments from + // styled parameters, preserving literal commas as delimiters + // per the OpenAPI spec (e.g. "color=blue,black,brown"). + var rawQueryFragments []string if params.Offset != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "offset", runtime.ParamLocationQuery, *params.Offset); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "offset", *params.Offset, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "integer", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } @@ -4575,15 +4525,11 @@ func NewV1GetAllProjectsForOrganizationRequest(server string, slug string, param if params.Limit != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "limit", runtime.ParamLocationQuery, *params.Limit); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "limit", *params.Limit, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "integer", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } @@ -4591,15 +4537,11 @@ func NewV1GetAllProjectsForOrganizationRequest(server string, slug string, param if params.Search != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "search", runtime.ParamLocationQuery, *params.Search); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "search", *params.Search, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } @@ -4607,15 +4549,11 @@ func NewV1GetAllProjectsForOrganizationRequest(server string, slug string, param if params.Sort != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "sort", runtime.ParamLocationQuery, *params.Sort); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "sort", *params.Sort, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } @@ -4623,24 +4561,23 @@ func NewV1GetAllProjectsForOrganizationRequest(server string, slug string, param if params.Statuses != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "statuses", runtime.ParamLocationQuery, *params.Statuses); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "statuses", *params.Statuses, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } } - queryURL.RawQuery = queryValues.Encode() + if encoded := queryValues.Encode(); encoded != "" { + rawQueryFragments = append(rawQueryFragments, encoded) + } + queryURL.RawQuery = strings.Join(rawQueryFragments, "&") } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -4667,7 +4604,7 @@ func NewV1GetProfileRequest(server string) (*http.Request, error) { return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -4694,7 +4631,7 @@ func NewV1ListAllProjectsRequest(server string) (*http.Request, error) { return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -4732,7 +4669,7 @@ func NewV1CreateAProjectRequestWithBody(server string, contentType string, body return nil, err } - req, err := http.NewRequest("POST", queryURL.String(), body) + req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) if err != nil { return nil, err } @@ -4762,31 +4699,29 @@ func NewV1GetAvailableRegionsRequest(server string, params *V1GetAvailableRegion } if params != nil { + // queryValues collects non-styled parameters (passthrough, JSON) + // that are safe to round-trip through url.Values.Encode(). queryValues := queryURL.Query() + // rawQueryFragments collects pre-encoded query fragments from + // styled parameters, preserving literal commas as delimiters + // per the OpenAPI spec (e.g. "color=blue,black,brown"). + var rawQueryFragments []string - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "organization_slug", runtime.ParamLocationQuery, params.OrganizationSlug); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "organization_slug", params.OrganizationSlug, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } if params.Continent != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "continent", runtime.ParamLocationQuery, *params.Continent); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "continent", *params.Continent, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } @@ -4794,24 +4729,23 @@ func NewV1GetAvailableRegionsRequest(server string, params *V1GetAvailableRegion if params.DesiredInstanceSize != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "desired_instance_size", runtime.ParamLocationQuery, *params.DesiredInstanceSize); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "desired_instance_size", *params.DesiredInstanceSize, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } } - queryURL.RawQuery = queryValues.Encode() + if encoded := queryValues.Encode(); encoded != "" { + rawQueryFragments = append(rawQueryFragments, encoded) + } + queryURL.RawQuery = strings.Join(rawQueryFragments, "&") } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -4825,7 +4759,7 @@ func NewV1DeleteAProjectRequest(server string, ref string) (*http.Request, error var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -4845,7 +4779,7 @@ func NewV1DeleteAProjectRequest(server string, ref string) (*http.Request, error return nil, err } - req, err := http.NewRequest("DELETE", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodDelete, queryURL.String(), nil) if err != nil { return nil, err } @@ -4859,7 +4793,7 @@ func NewV1GetProjectRequest(server string, ref string) (*http.Request, error) { var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -4879,7 +4813,7 @@ func NewV1GetProjectRequest(server string, ref string) (*http.Request, error) { return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -4904,7 +4838,7 @@ func NewV1UpdateAProjectRequestWithBody(server string, ref string, contentType s var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -4924,7 +4858,7 @@ func NewV1UpdateAProjectRequestWithBody(server string, ref string, contentType s return nil, err } - req, err := http.NewRequest("PATCH", queryURL.String(), body) + req, err := http.NewRequest(http.MethodPatch, queryURL.String(), body) if err != nil { return nil, err } @@ -4940,7 +4874,7 @@ func NewV1ListActionRunsRequest(server string, ref string, params *V1ListActionR var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -4961,19 +4895,21 @@ func NewV1ListActionRunsRequest(server string, ref string, params *V1ListActionR } if params != nil { + // queryValues collects non-styled parameters (passthrough, JSON) + // that are safe to round-trip through url.Values.Encode(). queryValues := queryURL.Query() + // rawQueryFragments collects pre-encoded query fragments from + // styled parameters, preserving literal commas as delimiters + // per the OpenAPI spec (e.g. "color=blue,black,brown"). + var rawQueryFragments []string if params.Offset != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "offset", runtime.ParamLocationQuery, *params.Offset); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "offset", *params.Offset, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "number", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } @@ -4981,24 +4917,23 @@ func NewV1ListActionRunsRequest(server string, ref string, params *V1ListActionR if params.Limit != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "limit", runtime.ParamLocationQuery, *params.Limit); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "limit", *params.Limit, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "number", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } } - queryURL.RawQuery = queryValues.Encode() + if encoded := queryValues.Encode(); encoded != "" { + rawQueryFragments = append(rawQueryFragments, encoded) + } + queryURL.RawQuery = strings.Join(rawQueryFragments, "&") } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -5012,7 +4947,7 @@ func NewV1CountActionRunsRequest(server string, ref string) (*http.Request, erro var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -5032,7 +4967,7 @@ func NewV1CountActionRunsRequest(server string, ref string) (*http.Request, erro return nil, err } - req, err := http.NewRequest("HEAD", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodHead, queryURL.String(), nil) if err != nil { return nil, err } @@ -5046,14 +4981,14 @@ func NewV1GetActionRunRequest(server string, ref string, runId string) (*http.Re var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } var pathParam1 string - pathParam1, err = runtime.StyleParamWithLocation("simple", false, "run_id", runtime.ParamLocationPath, runId) + pathParam1, err = runtime.StyleParamWithOptions("simple", false, "run_id", runId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -5073,7 +5008,7 @@ func NewV1GetActionRunRequest(server string, ref string, runId string) (*http.Re return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -5087,14 +5022,14 @@ func NewV1GetActionRunLogsRequest(server string, ref string, runId string) (*htt var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } var pathParam1 string - pathParam1, err = runtime.StyleParamWithLocation("simple", false, "run_id", runtime.ParamLocationPath, runId) + pathParam1, err = runtime.StyleParamWithOptions("simple", false, "run_id", runId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -5114,7 +5049,7 @@ func NewV1GetActionRunLogsRequest(server string, ref string, runId string) (*htt return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -5139,14 +5074,14 @@ func NewV1UpdateActionRunStatusRequestWithBody(server string, ref string, runId var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } var pathParam1 string - pathParam1, err = runtime.StyleParamWithLocation("simple", false, "run_id", runtime.ParamLocationPath, runId) + pathParam1, err = runtime.StyleParamWithOptions("simple", false, "run_id", runId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -5166,7 +5101,7 @@ func NewV1UpdateActionRunStatusRequestWithBody(server string, ref string, runId return nil, err } - req, err := http.NewRequest("PATCH", queryURL.String(), body) + req, err := http.NewRequest(http.MethodPatch, queryURL.String(), body) if err != nil { return nil, err } @@ -5182,7 +5117,7 @@ func NewV1GetPerformanceAdvisorsRequest(server string, ref string) (*http.Reques var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -5202,7 +5137,7 @@ func NewV1GetPerformanceAdvisorsRequest(server string, ref string) (*http.Reques return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -5216,7 +5151,7 @@ func NewV1GetSecurityAdvisorsRequest(server string, ref string, params *V1GetSec var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -5237,28 +5172,33 @@ func NewV1GetSecurityAdvisorsRequest(server string, ref string, params *V1GetSec } if params != nil { + // queryValues collects non-styled parameters (passthrough, JSON) + // that are safe to round-trip through url.Values.Encode(). queryValues := queryURL.Query() + // rawQueryFragments collects pre-encoded query fragments from + // styled parameters, preserving literal commas as delimiters + // per the OpenAPI spec (e.g. "color=blue,black,brown"). + var rawQueryFragments []string if params.LintType != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "lint_type", runtime.ParamLocationQuery, *params.LintType); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "lint_type", *params.LintType, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } } - queryURL.RawQuery = queryValues.Encode() + if encoded := queryValues.Encode(); encoded != "" { + rawQueryFragments = append(rawQueryFragments, encoded) + } + queryURL.RawQuery = strings.Join(rawQueryFragments, "&") } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -5272,7 +5212,7 @@ func NewV1GetProjectFunctionCombinedStatsRequest(server string, ref string, para var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -5293,36 +5233,37 @@ func NewV1GetProjectFunctionCombinedStatsRequest(server string, ref string, para } if params != nil { + // queryValues collects non-styled parameters (passthrough, JSON) + // that are safe to round-trip through url.Values.Encode(). queryValues := queryURL.Query() + // rawQueryFragments collects pre-encoded query fragments from + // styled parameters, preserving literal commas as delimiters + // per the OpenAPI spec (e.g. "color=blue,black,brown"). + var rawQueryFragments []string - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "interval", runtime.ParamLocationQuery, params.Interval); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "interval", params.Interval, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "function_id", runtime.ParamLocationQuery, params.FunctionId); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "function_id", params.FunctionId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } - queryURL.RawQuery = queryValues.Encode() + if encoded := queryValues.Encode(); encoded != "" { + rawQueryFragments = append(rawQueryFragments, encoded) + } + queryURL.RawQuery = strings.Join(rawQueryFragments, "&") } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -5336,7 +5277,7 @@ func NewV1GetProjectLogsRequest(server string, ref string, params *V1GetProjectL var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -5357,19 +5298,21 @@ func NewV1GetProjectLogsRequest(server string, ref string, params *V1GetProjectL } if params != nil { + // queryValues collects non-styled parameters (passthrough, JSON) + // that are safe to round-trip through url.Values.Encode(). queryValues := queryURL.Query() + // rawQueryFragments collects pre-encoded query fragments from + // styled parameters, preserving literal commas as delimiters + // per the OpenAPI spec (e.g. "color=blue,black,brown"). + var rawQueryFragments []string if params.Sql != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "sql", runtime.ParamLocationQuery, *params.Sql); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "sql", *params.Sql, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } @@ -5377,15 +5320,11 @@ func NewV1GetProjectLogsRequest(server string, ref string, params *V1GetProjectL if params.IsoTimestampStart != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "iso_timestamp_start", runtime.ParamLocationQuery, *params.IsoTimestampStart); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "iso_timestamp_start", *params.IsoTimestampStart, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: "date-time"}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } @@ -5393,24 +5332,23 @@ func NewV1GetProjectLogsRequest(server string, ref string, params *V1GetProjectL if params.IsoTimestampEnd != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "iso_timestamp_end", runtime.ParamLocationQuery, *params.IsoTimestampEnd); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "iso_timestamp_end", *params.IsoTimestampEnd, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: "date-time"}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } } - queryURL.RawQuery = queryValues.Encode() + if encoded := queryValues.Encode(); encoded != "" { + rawQueryFragments = append(rawQueryFragments, encoded) + } + queryURL.RawQuery = strings.Join(rawQueryFragments, "&") } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -5424,7 +5362,7 @@ func NewV1GetProjectLogsAllRequest(server string, ref string, params *V1GetProje var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -5445,19 +5383,21 @@ func NewV1GetProjectLogsAllRequest(server string, ref string, params *V1GetProje } if params != nil { + // queryValues collects non-styled parameters (passthrough, JSON) + // that are safe to round-trip through url.Values.Encode(). queryValues := queryURL.Query() + // rawQueryFragments collects pre-encoded query fragments from + // styled parameters, preserving literal commas as delimiters + // per the OpenAPI spec (e.g. "color=blue,black,brown"). + var rawQueryFragments []string if params.Sql != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "sql", runtime.ParamLocationQuery, *params.Sql); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "sql", *params.Sql, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } @@ -5465,15 +5405,11 @@ func NewV1GetProjectLogsAllRequest(server string, ref string, params *V1GetProje if params.IsoTimestampStart != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "iso_timestamp_start", runtime.ParamLocationQuery, *params.IsoTimestampStart); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "iso_timestamp_start", *params.IsoTimestampStart, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: "date-time"}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } @@ -5481,24 +5417,23 @@ func NewV1GetProjectLogsAllRequest(server string, ref string, params *V1GetProje if params.IsoTimestampEnd != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "iso_timestamp_end", runtime.ParamLocationQuery, *params.IsoTimestampEnd); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "iso_timestamp_end", *params.IsoTimestampEnd, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: "date-time"}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } } - queryURL.RawQuery = queryValues.Encode() + if encoded := queryValues.Encode(); encoded != "" { + rawQueryFragments = append(rawQueryFragments, encoded) + } + queryURL.RawQuery = strings.Join(rawQueryFragments, "&") } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -5512,7 +5447,7 @@ func NewV1ScrapeProjectMetricsRequest(server string, ref string) (*http.Request, var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -5532,7 +5467,7 @@ func NewV1ScrapeProjectMetricsRequest(server string, ref string) (*http.Request, return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -5546,7 +5481,7 @@ func NewV1GetProjectUsageApiCountRequest(server string, ref string, params *V1Ge var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -5567,28 +5502,33 @@ func NewV1GetProjectUsageApiCountRequest(server string, ref string, params *V1Ge } if params != nil { + // queryValues collects non-styled parameters (passthrough, JSON) + // that are safe to round-trip through url.Values.Encode(). queryValues := queryURL.Query() + // rawQueryFragments collects pre-encoded query fragments from + // styled parameters, preserving literal commas as delimiters + // per the OpenAPI spec (e.g. "color=blue,black,brown"). + var rawQueryFragments []string if params.Interval != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "interval", runtime.ParamLocationQuery, *params.Interval); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "interval", *params.Interval, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } } - queryURL.RawQuery = queryValues.Encode() + if encoded := queryValues.Encode(); encoded != "" { + rawQueryFragments = append(rawQueryFragments, encoded) + } + queryURL.RawQuery = strings.Join(rawQueryFragments, "&") } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -5602,7 +5542,7 @@ func NewV1GetProjectUsageRequestCountRequest(server string, ref string) (*http.R var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -5622,7 +5562,7 @@ func NewV1GetProjectUsageRequestCountRequest(server string, ref string) (*http.R return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -5636,7 +5576,7 @@ func NewV1GetProjectApiKeysRequest(server string, ref string, params *V1GetProje var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -5657,28 +5597,33 @@ func NewV1GetProjectApiKeysRequest(server string, ref string, params *V1GetProje } if params != nil { + // queryValues collects non-styled parameters (passthrough, JSON) + // that are safe to round-trip through url.Values.Encode(). queryValues := queryURL.Query() + // rawQueryFragments collects pre-encoded query fragments from + // styled parameters, preserving literal commas as delimiters + // per the OpenAPI spec (e.g. "color=blue,black,brown"). + var rawQueryFragments []string if params.Reveal != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "reveal", runtime.ParamLocationQuery, *params.Reveal); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "reveal", *params.Reveal, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "boolean", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } } - queryURL.RawQuery = queryValues.Encode() + if encoded := queryValues.Encode(); encoded != "" { + rawQueryFragments = append(rawQueryFragments, encoded) + } + queryURL.RawQuery = strings.Join(rawQueryFragments, "&") } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -5703,7 +5648,7 @@ func NewV1CreateProjectApiKeyRequestWithBody(server string, ref string, params * var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -5724,28 +5669,33 @@ func NewV1CreateProjectApiKeyRequestWithBody(server string, ref string, params * } if params != nil { + // queryValues collects non-styled parameters (passthrough, JSON) + // that are safe to round-trip through url.Values.Encode(). queryValues := queryURL.Query() + // rawQueryFragments collects pre-encoded query fragments from + // styled parameters, preserving literal commas as delimiters + // per the OpenAPI spec (e.g. "color=blue,black,brown"). + var rawQueryFragments []string if params.Reveal != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "reveal", runtime.ParamLocationQuery, *params.Reveal); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "reveal", *params.Reveal, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "boolean", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } } - queryURL.RawQuery = queryValues.Encode() + if encoded := queryValues.Encode(); encoded != "" { + rawQueryFragments = append(rawQueryFragments, encoded) + } + queryURL.RawQuery = strings.Join(rawQueryFragments, "&") } - req, err := http.NewRequest("POST", queryURL.String(), body) + req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) if err != nil { return nil, err } @@ -5761,7 +5711,7 @@ func NewV1GetProjectLegacyApiKeysRequest(server string, ref string) (*http.Reque var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -5781,7 +5731,7 @@ func NewV1GetProjectLegacyApiKeysRequest(server string, ref string) (*http.Reque return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -5795,7 +5745,7 @@ func NewV1UpdateProjectLegacyApiKeysRequest(server string, ref string, params *V var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -5816,24 +5766,29 @@ func NewV1UpdateProjectLegacyApiKeysRequest(server string, ref string, params *V } if params != nil { + // queryValues collects non-styled parameters (passthrough, JSON) + // that are safe to round-trip through url.Values.Encode(). queryValues := queryURL.Query() + // rawQueryFragments collects pre-encoded query fragments from + // styled parameters, preserving literal commas as delimiters + // per the OpenAPI spec (e.g. "color=blue,black,brown"). + var rawQueryFragments []string - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "enabled", runtime.ParamLocationQuery, params.Enabled); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "enabled", params.Enabled, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "boolean", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } - queryURL.RawQuery = queryValues.Encode() + if encoded := queryValues.Encode(); encoded != "" { + rawQueryFragments = append(rawQueryFragments, encoded) + } + queryURL.RawQuery = strings.Join(rawQueryFragments, "&") } - req, err := http.NewRequest("PUT", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodPut, queryURL.String(), nil) if err != nil { return nil, err } @@ -5847,14 +5802,14 @@ func NewV1DeleteProjectApiKeyRequest(server string, ref string, id openapi_types var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } var pathParam1 string - pathParam1, err = runtime.StyleParamWithLocation("simple", false, "id", runtime.ParamLocationPath, id) + pathParam1, err = runtime.StyleParamWithOptions("simple", false, "id", id, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) if err != nil { return nil, err } @@ -5875,19 +5830,21 @@ func NewV1DeleteProjectApiKeyRequest(server string, ref string, id openapi_types } if params != nil { + // queryValues collects non-styled parameters (passthrough, JSON) + // that are safe to round-trip through url.Values.Encode(). queryValues := queryURL.Query() + // rawQueryFragments collects pre-encoded query fragments from + // styled parameters, preserving literal commas as delimiters + // per the OpenAPI spec (e.g. "color=blue,black,brown"). + var rawQueryFragments []string if params.Reveal != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "reveal", runtime.ParamLocationQuery, *params.Reveal); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "reveal", *params.Reveal, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "boolean", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } @@ -5895,15 +5852,11 @@ func NewV1DeleteProjectApiKeyRequest(server string, ref string, id openapi_types if params.WasCompromised != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "was_compromised", runtime.ParamLocationQuery, *params.WasCompromised); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "was_compromised", *params.WasCompromised, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "boolean", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } @@ -5911,24 +5864,23 @@ func NewV1DeleteProjectApiKeyRequest(server string, ref string, id openapi_types if params.Reason != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "reason", runtime.ParamLocationQuery, *params.Reason); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "reason", *params.Reason, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } } - queryURL.RawQuery = queryValues.Encode() + if encoded := queryValues.Encode(); encoded != "" { + rawQueryFragments = append(rawQueryFragments, encoded) + } + queryURL.RawQuery = strings.Join(rawQueryFragments, "&") } - req, err := http.NewRequest("DELETE", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodDelete, queryURL.String(), nil) if err != nil { return nil, err } @@ -5942,14 +5894,14 @@ func NewV1GetProjectApiKeyRequest(server string, ref string, id openapi_types.UU var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } var pathParam1 string - pathParam1, err = runtime.StyleParamWithLocation("simple", false, "id", runtime.ParamLocationPath, id) + pathParam1, err = runtime.StyleParamWithOptions("simple", false, "id", id, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) if err != nil { return nil, err } @@ -5970,28 +5922,33 @@ func NewV1GetProjectApiKeyRequest(server string, ref string, id openapi_types.UU } if params != nil { + // queryValues collects non-styled parameters (passthrough, JSON) + // that are safe to round-trip through url.Values.Encode(). queryValues := queryURL.Query() + // rawQueryFragments collects pre-encoded query fragments from + // styled parameters, preserving literal commas as delimiters + // per the OpenAPI spec (e.g. "color=blue,black,brown"). + var rawQueryFragments []string if params.Reveal != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "reveal", runtime.ParamLocationQuery, *params.Reveal); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "reveal", *params.Reveal, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "boolean", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } } - queryURL.RawQuery = queryValues.Encode() + if encoded := queryValues.Encode(); encoded != "" { + rawQueryFragments = append(rawQueryFragments, encoded) + } + queryURL.RawQuery = strings.Join(rawQueryFragments, "&") } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -6016,14 +5973,14 @@ func NewV1UpdateProjectApiKeyRequestWithBody(server string, ref string, id opena var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } var pathParam1 string - pathParam1, err = runtime.StyleParamWithLocation("simple", false, "id", runtime.ParamLocationPath, id) + pathParam1, err = runtime.StyleParamWithOptions("simple", false, "id", id, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) if err != nil { return nil, err } @@ -6044,28 +6001,33 @@ func NewV1UpdateProjectApiKeyRequestWithBody(server string, ref string, id opena } if params != nil { + // queryValues collects non-styled parameters (passthrough, JSON) + // that are safe to round-trip through url.Values.Encode(). queryValues := queryURL.Query() + // rawQueryFragments collects pre-encoded query fragments from + // styled parameters, preserving literal commas as delimiters + // per the OpenAPI spec (e.g. "color=blue,black,brown"). + var rawQueryFragments []string if params.Reveal != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "reveal", runtime.ParamLocationQuery, *params.Reveal); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "reveal", *params.Reveal, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "boolean", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } } - queryURL.RawQuery = queryValues.Encode() + if encoded := queryValues.Encode(); encoded != "" { + rawQueryFragments = append(rawQueryFragments, encoded) + } + queryURL.RawQuery = strings.Join(rawQueryFragments, "&") } - req, err := http.NewRequest("PATCH", queryURL.String(), body) + req, err := http.NewRequest(http.MethodPatch, queryURL.String(), body) if err != nil { return nil, err } @@ -6081,7 +6043,7 @@ func NewV1ListProjectAddonsRequest(server string, ref string) (*http.Request, er var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -6101,7 +6063,7 @@ func NewV1ListProjectAddonsRequest(server string, ref string) (*http.Request, er return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -6126,7 +6088,7 @@ func NewV1ApplyProjectAddonRequestWithBody(server string, ref string, contentTyp var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -6146,7 +6108,7 @@ func NewV1ApplyProjectAddonRequestWithBody(server string, ref string, contentTyp return nil, err } - req, err := http.NewRequest("PATCH", queryURL.String(), body) + req, err := http.NewRequest(http.MethodPatch, queryURL.String(), body) if err != nil { return nil, err } @@ -6164,14 +6126,14 @@ func NewV1RemoveProjectAddonRequest(server string, ref string, addonVariant stru var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } var pathParam1 string - pathParam1, err = runtime.StyleParamWithLocation("simple", false, "addon_variant", runtime.ParamLocationPath, addonVariant) + pathParam1, err = runtime.StyleParamWithOptions("simple", false, "addon_variant", addonVariant, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "", Format: ""}) if err != nil { return nil, err } @@ -6191,7 +6153,7 @@ func NewV1RemoveProjectAddonRequest(server string, ref string, addonVariant stru return nil, err } - req, err := http.NewRequest("DELETE", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodDelete, queryURL.String(), nil) if err != nil { return nil, err } @@ -6205,7 +6167,7 @@ func NewV1DisablePreviewBranchingRequest(server string, ref string) (*http.Reque var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -6225,7 +6187,7 @@ func NewV1DisablePreviewBranchingRequest(server string, ref string) (*http.Reque return nil, err } - req, err := http.NewRequest("DELETE", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodDelete, queryURL.String(), nil) if err != nil { return nil, err } @@ -6239,7 +6201,7 @@ func NewV1ListAllBranchesRequest(server string, ref string) (*http.Request, erro var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -6259,7 +6221,7 @@ func NewV1ListAllBranchesRequest(server string, ref string) (*http.Request, erro return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -6284,7 +6246,7 @@ func NewV1CreateABranchRequestWithBody(server string, ref string, contentType st var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -6304,7 +6266,7 @@ func NewV1CreateABranchRequestWithBody(server string, ref string, contentType st return nil, err } - req, err := http.NewRequest("POST", queryURL.String(), body) + req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) if err != nil { return nil, err } @@ -6320,14 +6282,14 @@ func NewV1GetABranchRequest(server string, ref string, name string) (*http.Reque var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } var pathParam1 string - pathParam1, err = runtime.StyleParamWithLocation("simple", false, "name", runtime.ParamLocationPath, name) + pathParam1, err = runtime.StyleParamWithOptions("simple", false, "name", name, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -6347,7 +6309,7 @@ func NewV1GetABranchRequest(server string, ref string, name string) (*http.Reque return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -6361,7 +6323,7 @@ func NewV1DeleteProjectClaimTokenRequest(server string, ref string) (*http.Reque var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -6381,7 +6343,7 @@ func NewV1DeleteProjectClaimTokenRequest(server string, ref string) (*http.Reque return nil, err } - req, err := http.NewRequest("DELETE", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodDelete, queryURL.String(), nil) if err != nil { return nil, err } @@ -6395,7 +6357,7 @@ func NewV1GetProjectClaimTokenRequest(server string, ref string) (*http.Request, var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -6415,7 +6377,7 @@ func NewV1GetProjectClaimTokenRequest(server string, ref string) (*http.Request, return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -6429,7 +6391,7 @@ func NewV1CreateProjectClaimTokenRequest(server string, ref string) (*http.Reque var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -6449,7 +6411,7 @@ func NewV1CreateProjectClaimTokenRequest(server string, ref string) (*http.Reque return nil, err } - req, err := http.NewRequest("POST", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodPost, queryURL.String(), nil) if err != nil { return nil, err } @@ -6463,7 +6425,7 @@ func NewV1DeleteLoginRolesRequest(server string, ref string) (*http.Request, err var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -6483,7 +6445,7 @@ func NewV1DeleteLoginRolesRequest(server string, ref string) (*http.Request, err return nil, err } - req, err := http.NewRequest("DELETE", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodDelete, queryURL.String(), nil) if err != nil { return nil, err } @@ -6508,7 +6470,7 @@ func NewV1CreateLoginRoleRequestWithBody(server string, ref string, contentType var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -6528,7 +6490,7 @@ func NewV1CreateLoginRoleRequestWithBody(server string, ref string, contentType return nil, err } - req, err := http.NewRequest("POST", queryURL.String(), body) + req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) if err != nil { return nil, err } @@ -6544,7 +6506,7 @@ func NewV1GetAuthServiceConfigRequest(server string, ref string) (*http.Request, var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -6564,7 +6526,7 @@ func NewV1GetAuthServiceConfigRequest(server string, ref string) (*http.Request, return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -6589,7 +6551,7 @@ func NewV1UpdateAuthServiceConfigRequestWithBody(server string, ref string, cont var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -6609,7 +6571,7 @@ func NewV1UpdateAuthServiceConfigRequestWithBody(server string, ref string, cont return nil, err } - req, err := http.NewRequest("PATCH", queryURL.String(), body) + req, err := http.NewRequest(http.MethodPatch, queryURL.String(), body) if err != nil { return nil, err } @@ -6625,7 +6587,7 @@ func NewV1GetProjectSigningKeysRequest(server string, ref string) (*http.Request var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -6645,7 +6607,7 @@ func NewV1GetProjectSigningKeysRequest(server string, ref string) (*http.Request return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -6670,7 +6632,7 @@ func NewV1CreateProjectSigningKeyRequestWithBody(server string, ref string, cont var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -6690,7 +6652,7 @@ func NewV1CreateProjectSigningKeyRequestWithBody(server string, ref string, cont return nil, err } - req, err := http.NewRequest("POST", queryURL.String(), body) + req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) if err != nil { return nil, err } @@ -6706,7 +6668,7 @@ func NewV1GetLegacySigningKeyRequest(server string, ref string) (*http.Request, var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -6726,7 +6688,7 @@ func NewV1GetLegacySigningKeyRequest(server string, ref string) (*http.Request, return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -6740,7 +6702,7 @@ func NewV1CreateLegacySigningKeyRequest(server string, ref string) (*http.Reques var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -6760,7 +6722,7 @@ func NewV1CreateLegacySigningKeyRequest(server string, ref string) (*http.Reques return nil, err } - req, err := http.NewRequest("POST", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodPost, queryURL.String(), nil) if err != nil { return nil, err } @@ -6774,14 +6736,14 @@ func NewV1RemoveProjectSigningKeyRequest(server string, ref string, id openapi_t var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } var pathParam1 string - pathParam1, err = runtime.StyleParamWithLocation("simple", false, "id", runtime.ParamLocationPath, id) + pathParam1, err = runtime.StyleParamWithOptions("simple", false, "id", id, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) if err != nil { return nil, err } @@ -6801,7 +6763,7 @@ func NewV1RemoveProjectSigningKeyRequest(server string, ref string, id openapi_t return nil, err } - req, err := http.NewRequest("DELETE", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodDelete, queryURL.String(), nil) if err != nil { return nil, err } @@ -6815,14 +6777,14 @@ func NewV1GetProjectSigningKeyRequest(server string, ref string, id openapi_type var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } var pathParam1 string - pathParam1, err = runtime.StyleParamWithLocation("simple", false, "id", runtime.ParamLocationPath, id) + pathParam1, err = runtime.StyleParamWithOptions("simple", false, "id", id, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) if err != nil { return nil, err } @@ -6842,7 +6804,7 @@ func NewV1GetProjectSigningKeyRequest(server string, ref string, id openapi_type return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -6867,14 +6829,14 @@ func NewV1UpdateProjectSigningKeyRequestWithBody(server string, ref string, id o var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } var pathParam1 string - pathParam1, err = runtime.StyleParamWithLocation("simple", false, "id", runtime.ParamLocationPath, id) + pathParam1, err = runtime.StyleParamWithOptions("simple", false, "id", id, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) if err != nil { return nil, err } @@ -6894,7 +6856,7 @@ func NewV1UpdateProjectSigningKeyRequestWithBody(server string, ref string, id o return nil, err } - req, err := http.NewRequest("PATCH", queryURL.String(), body) + req, err := http.NewRequest(http.MethodPatch, queryURL.String(), body) if err != nil { return nil, err } @@ -6910,7 +6872,7 @@ func NewV1ListAllSsoProviderRequest(server string, ref string) (*http.Request, e var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -6930,7 +6892,7 @@ func NewV1ListAllSsoProviderRequest(server string, ref string) (*http.Request, e return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -6955,7 +6917,7 @@ func NewV1CreateASsoProviderRequestWithBody(server string, ref string, contentTy var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -6975,7 +6937,7 @@ func NewV1CreateASsoProviderRequestWithBody(server string, ref string, contentTy return nil, err } - req, err := http.NewRequest("POST", queryURL.String(), body) + req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) if err != nil { return nil, err } @@ -6991,14 +6953,14 @@ func NewV1DeleteASsoProviderRequest(server string, ref string, providerId openap var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } var pathParam1 string - pathParam1, err = runtime.StyleParamWithLocation("simple", false, "provider_id", runtime.ParamLocationPath, providerId) + pathParam1, err = runtime.StyleParamWithOptions("simple", false, "provider_id", providerId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) if err != nil { return nil, err } @@ -7018,7 +6980,7 @@ func NewV1DeleteASsoProviderRequest(server string, ref string, providerId openap return nil, err } - req, err := http.NewRequest("DELETE", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodDelete, queryURL.String(), nil) if err != nil { return nil, err } @@ -7032,14 +6994,14 @@ func NewV1GetASsoProviderRequest(server string, ref string, providerId openapi_t var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } var pathParam1 string - pathParam1, err = runtime.StyleParamWithLocation("simple", false, "provider_id", runtime.ParamLocationPath, providerId) + pathParam1, err = runtime.StyleParamWithOptions("simple", false, "provider_id", providerId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) if err != nil { return nil, err } @@ -7059,7 +7021,7 @@ func NewV1GetASsoProviderRequest(server string, ref string, providerId openapi_t return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -7084,14 +7046,14 @@ func NewV1UpdateASsoProviderRequestWithBody(server string, ref string, providerI var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } var pathParam1 string - pathParam1, err = runtime.StyleParamWithLocation("simple", false, "provider_id", runtime.ParamLocationPath, providerId) + pathParam1, err = runtime.StyleParamWithOptions("simple", false, "provider_id", providerId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) if err != nil { return nil, err } @@ -7111,7 +7073,7 @@ func NewV1UpdateASsoProviderRequestWithBody(server string, ref string, providerI return nil, err } - req, err := http.NewRequest("PUT", queryURL.String(), body) + req, err := http.NewRequest(http.MethodPut, queryURL.String(), body) if err != nil { return nil, err } @@ -7127,7 +7089,7 @@ func NewV1ListProjectTpaIntegrationsRequest(server string, ref string) (*http.Re var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -7147,7 +7109,7 @@ func NewV1ListProjectTpaIntegrationsRequest(server string, ref string) (*http.Re return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -7172,7 +7134,7 @@ func NewV1CreateProjectTpaIntegrationRequestWithBody(server string, ref string, var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -7192,7 +7154,7 @@ func NewV1CreateProjectTpaIntegrationRequestWithBody(server string, ref string, return nil, err } - req, err := http.NewRequest("POST", queryURL.String(), body) + req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) if err != nil { return nil, err } @@ -7208,14 +7170,14 @@ func NewV1DeleteProjectTpaIntegrationRequest(server string, ref string, tpaId op var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } var pathParam1 string - pathParam1, err = runtime.StyleParamWithLocation("simple", false, "tpa_id", runtime.ParamLocationPath, tpaId) + pathParam1, err = runtime.StyleParamWithOptions("simple", false, "tpa_id", tpaId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) if err != nil { return nil, err } @@ -7235,7 +7197,7 @@ func NewV1DeleteProjectTpaIntegrationRequest(server string, ref string, tpaId op return nil, err } - req, err := http.NewRequest("DELETE", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodDelete, queryURL.String(), nil) if err != nil { return nil, err } @@ -7249,14 +7211,14 @@ func NewV1GetProjectTpaIntegrationRequest(server string, ref string, tpaId opena var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } var pathParam1 string - pathParam1, err = runtime.StyleParamWithLocation("simple", false, "tpa_id", runtime.ParamLocationPath, tpaId) + pathParam1, err = runtime.StyleParamWithOptions("simple", false, "tpa_id", tpaId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) if err != nil { return nil, err } @@ -7276,7 +7238,7 @@ func NewV1GetProjectTpaIntegrationRequest(server string, ref string, tpaId opena return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -7290,7 +7252,7 @@ func NewV1GetProjectPgbouncerConfigRequest(server string, ref string) (*http.Req var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -7310,7 +7272,7 @@ func NewV1GetProjectPgbouncerConfigRequest(server string, ref string) (*http.Req return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -7324,7 +7286,7 @@ func NewV1GetPoolerConfigRequest(server string, ref string) (*http.Request, erro var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -7344,7 +7306,7 @@ func NewV1GetPoolerConfigRequest(server string, ref string) (*http.Request, erro return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -7369,7 +7331,7 @@ func NewV1UpdatePoolerConfigRequestWithBody(server string, ref string, contentTy var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -7389,7 +7351,7 @@ func NewV1UpdatePoolerConfigRequestWithBody(server string, ref string, contentTy return nil, err } - req, err := http.NewRequest("PATCH", queryURL.String(), body) + req, err := http.NewRequest(http.MethodPatch, queryURL.String(), body) if err != nil { return nil, err } @@ -7405,7 +7367,7 @@ func NewV1GetPostgresConfigRequest(server string, ref string) (*http.Request, er var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -7425,7 +7387,7 @@ func NewV1GetPostgresConfigRequest(server string, ref string) (*http.Request, er return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -7450,7 +7412,7 @@ func NewV1UpdatePostgresConfigRequestWithBody(server string, ref string, content var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -7470,7 +7432,7 @@ func NewV1UpdatePostgresConfigRequestWithBody(server string, ref string, content return nil, err } - req, err := http.NewRequest("PUT", queryURL.String(), body) + req, err := http.NewRequest(http.MethodPut, queryURL.String(), body) if err != nil { return nil, err } @@ -7486,7 +7448,7 @@ func NewV1GetDatabaseDiskRequest(server string, ref string) (*http.Request, erro var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -7506,7 +7468,7 @@ func NewV1GetDatabaseDiskRequest(server string, ref string) (*http.Request, erro return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -7531,7 +7493,7 @@ func NewV1ModifyDatabaseDiskRequestWithBody(server string, ref string, contentTy var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -7551,7 +7513,7 @@ func NewV1ModifyDatabaseDiskRequestWithBody(server string, ref string, contentTy return nil, err } - req, err := http.NewRequest("POST", queryURL.String(), body) + req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) if err != nil { return nil, err } @@ -7567,7 +7529,7 @@ func NewV1GetProjectDiskAutoscaleConfigRequest(server string, ref string) (*http var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -7587,7 +7549,7 @@ func NewV1GetProjectDiskAutoscaleConfigRequest(server string, ref string) (*http return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -7601,7 +7563,7 @@ func NewV1GetDiskUtilizationRequest(server string, ref string) (*http.Request, e var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -7621,7 +7583,7 @@ func NewV1GetDiskUtilizationRequest(server string, ref string) (*http.Request, e return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -7635,7 +7597,7 @@ func NewV1GetRealtimeConfigRequest(server string, ref string) (*http.Request, er var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -7655,7 +7617,7 @@ func NewV1GetRealtimeConfigRequest(server string, ref string) (*http.Request, er return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -7680,7 +7642,7 @@ func NewV1UpdateRealtimeConfigRequestWithBody(server string, ref string, content var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -7700,7 +7662,7 @@ func NewV1UpdateRealtimeConfigRequestWithBody(server string, ref string, content return nil, err } - req, err := http.NewRequest("PATCH", queryURL.String(), body) + req, err := http.NewRequest(http.MethodPatch, queryURL.String(), body) if err != nil { return nil, err } @@ -7716,7 +7678,7 @@ func NewV1ShutdownRealtimeRequest(server string, ref string) (*http.Request, err var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -7736,7 +7698,7 @@ func NewV1ShutdownRealtimeRequest(server string, ref string) (*http.Request, err return nil, err } - req, err := http.NewRequest("POST", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodPost, queryURL.String(), nil) if err != nil { return nil, err } @@ -7750,7 +7712,7 @@ func NewV1GetStorageConfigRequest(server string, ref string) (*http.Request, err var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -7770,7 +7732,7 @@ func NewV1GetStorageConfigRequest(server string, ref string) (*http.Request, err return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -7795,7 +7757,7 @@ func NewV1UpdateStorageConfigRequestWithBody(server string, ref string, contentT var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -7815,7 +7777,7 @@ func NewV1UpdateStorageConfigRequestWithBody(server string, ref string, contentT return nil, err } - req, err := http.NewRequest("PATCH", queryURL.String(), body) + req, err := http.NewRequest(http.MethodPatch, queryURL.String(), body) if err != nil { return nil, err } @@ -7831,7 +7793,7 @@ func NewV1DeleteHostnameConfigRequest(server string, ref string, params *V1Delet var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -7852,28 +7814,33 @@ func NewV1DeleteHostnameConfigRequest(server string, ref string, params *V1Delet } if params != nil { + // queryValues collects non-styled parameters (passthrough, JSON) + // that are safe to round-trip through url.Values.Encode(). queryValues := queryURL.Query() + // rawQueryFragments collects pre-encoded query fragments from + // styled parameters, preserving literal commas as delimiters + // per the OpenAPI spec (e.g. "color=blue,black,brown"). + var rawQueryFragments []string if params.RemoveAddon != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "remove_addon", runtime.ParamLocationQuery, *params.RemoveAddon); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "remove_addon", *params.RemoveAddon, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "boolean", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } } - queryURL.RawQuery = queryValues.Encode() + if encoded := queryValues.Encode(); encoded != "" { + rawQueryFragments = append(rawQueryFragments, encoded) + } + queryURL.RawQuery = strings.Join(rawQueryFragments, "&") } - req, err := http.NewRequest("DELETE", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodDelete, queryURL.String(), nil) if err != nil { return nil, err } @@ -7887,7 +7854,7 @@ func NewV1GetHostnameConfigRequest(server string, ref string) (*http.Request, er var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -7907,7 +7874,7 @@ func NewV1GetHostnameConfigRequest(server string, ref string) (*http.Request, er return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -7921,7 +7888,7 @@ func NewV1ActivateCustomHostnameRequest(server string, ref string) (*http.Reques var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -7941,7 +7908,7 @@ func NewV1ActivateCustomHostnameRequest(server string, ref string) (*http.Reques return nil, err } - req, err := http.NewRequest("POST", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodPost, queryURL.String(), nil) if err != nil { return nil, err } @@ -7966,7 +7933,7 @@ func NewV1UpdateHostnameConfigRequestWithBody(server string, ref string, content var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -7986,7 +7953,7 @@ func NewV1UpdateHostnameConfigRequestWithBody(server string, ref string, content return nil, err } - req, err := http.NewRequest("POST", queryURL.String(), body) + req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) if err != nil { return nil, err } @@ -8002,7 +7969,7 @@ func NewV1VerifyDnsConfigRequest(server string, ref string) (*http.Request, erro var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -8022,7 +7989,7 @@ func NewV1VerifyDnsConfigRequest(server string, ref string) (*http.Request, erro return nil, err } - req, err := http.NewRequest("POST", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodPost, queryURL.String(), nil) if err != nil { return nil, err } @@ -8036,7 +8003,7 @@ func NewV1ListAllBackupsRequest(server string, ref string) (*http.Request, error var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -8056,7 +8023,7 @@ func NewV1ListAllBackupsRequest(server string, ref string) (*http.Request, error return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -8081,7 +8048,7 @@ func NewV1RestorePhysicalBackupRequestWithBody(server string, ref string, conten var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -8101,7 +8068,7 @@ func NewV1RestorePhysicalBackupRequestWithBody(server string, ref string, conten return nil, err } - req, err := http.NewRequest("POST", queryURL.String(), body) + req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) if err != nil { return nil, err } @@ -8128,7 +8095,7 @@ func NewV1RestorePitrBackupRequestWithBody(server string, ref string, contentTyp var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -8148,7 +8115,7 @@ func NewV1RestorePitrBackupRequestWithBody(server string, ref string, contentTyp return nil, err } - req, err := http.NewRequest("POST", queryURL.String(), body) + req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) if err != nil { return nil, err } @@ -8164,7 +8131,7 @@ func NewV1GetRestorePointRequest(server string, ref string, params *V1GetRestore var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -8185,28 +8152,33 @@ func NewV1GetRestorePointRequest(server string, ref string, params *V1GetRestore } if params != nil { + // queryValues collects non-styled parameters (passthrough, JSON) + // that are safe to round-trip through url.Values.Encode(). queryValues := queryURL.Query() + // rawQueryFragments collects pre-encoded query fragments from + // styled parameters, preserving literal commas as delimiters + // per the OpenAPI spec (e.g. "color=blue,black,brown"). + var rawQueryFragments []string if params.Name != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "name", runtime.ParamLocationQuery, *params.Name); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "name", *params.Name, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } } - queryURL.RawQuery = queryValues.Encode() + if encoded := queryValues.Encode(); encoded != "" { + rawQueryFragments = append(rawQueryFragments, encoded) + } + queryURL.RawQuery = strings.Join(rawQueryFragments, "&") } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -8231,7 +8203,7 @@ func NewV1CreateRestorePointRequestWithBody(server string, ref string, contentTy var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -8251,7 +8223,7 @@ func NewV1CreateRestorePointRequestWithBody(server string, ref string, contentTy return nil, err } - req, err := http.NewRequest("POST", queryURL.String(), body) + req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) if err != nil { return nil, err } @@ -8267,7 +8239,7 @@ func NewV1GetBackupScheduleRequest(server string, ref string) (*http.Request, er var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -8287,7 +8259,7 @@ func NewV1GetBackupScheduleRequest(server string, ref string) (*http.Request, er return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -8312,7 +8284,7 @@ func NewV1UpdateBackupScheduleRequestWithBody(server string, ref string, content var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -8332,7 +8304,7 @@ func NewV1UpdateBackupScheduleRequestWithBody(server string, ref string, content return nil, err } - req, err := http.NewRequest("PATCH", queryURL.String(), body) + req, err := http.NewRequest(http.MethodPatch, queryURL.String(), body) if err != nil { return nil, err } @@ -8359,7 +8331,7 @@ func NewV1UndoRequestWithBody(server string, ref string, contentType string, bod var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -8379,7 +8351,7 @@ func NewV1UndoRequestWithBody(server string, ref string, contentType string, bod return nil, err } - req, err := http.NewRequest("POST", queryURL.String(), body) + req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) if err != nil { return nil, err } @@ -8395,7 +8367,7 @@ func NewV1GetDatabaseMetadataRequest(server string, ref string) (*http.Request, var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -8415,7 +8387,7 @@ func NewV1GetDatabaseMetadataRequest(server string, ref string) (*http.Request, return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -8429,7 +8401,7 @@ func NewV1GetJitAccessRequest(server string, ref string) (*http.Request, error) var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -8449,7 +8421,7 @@ func NewV1GetJitAccessRequest(server string, ref string) (*http.Request, error) return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -8474,7 +8446,7 @@ func NewV1AuthorizeJitAccessRequestWithBody(server string, ref string, contentTy var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -8494,7 +8466,7 @@ func NewV1AuthorizeJitAccessRequestWithBody(server string, ref string, contentTy return nil, err } - req, err := http.NewRequest("POST", queryURL.String(), body) + req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) if err != nil { return nil, err } @@ -8521,7 +8493,7 @@ func NewV1UpdateJitAccessRequestWithBody(server string, ref string, contentType var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -8541,7 +8513,7 @@ func NewV1UpdateJitAccessRequestWithBody(server string, ref string, contentType return nil, err } - req, err := http.NewRequest("PUT", queryURL.String(), body) + req, err := http.NewRequest(http.MethodPut, queryURL.String(), body) if err != nil { return nil, err } @@ -8568,7 +8540,7 @@ func NewV1InviteExternalJitAccessRequestWithBody(server string, ref string, cont var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -8588,7 +8560,7 @@ func NewV1InviteExternalJitAccessRequestWithBody(server string, ref string, cont return nil, err } - req, err := http.NewRequest("POST", queryURL.String(), body) + req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) if err != nil { return nil, err } @@ -8615,7 +8587,7 @@ func NewV1AcceptInviteExternalJitAccessRequestWithBody(server string, ref string var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -8635,7 +8607,7 @@ func NewV1AcceptInviteExternalJitAccessRequestWithBody(server string, ref string return nil, err } - req, err := http.NewRequest("POST", queryURL.String(), body) + req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) if err != nil { return nil, err } @@ -8651,14 +8623,14 @@ func NewV1DeleteInviteExternalJitAccessRequest(server string, ref string, invite var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } var pathParam1 string - pathParam1, err = runtime.StyleParamWithLocation("simple", false, "invite_id", runtime.ParamLocationPath, inviteId) + pathParam1, err = runtime.StyleParamWithOptions("simple", false, "invite_id", inviteId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) if err != nil { return nil, err } @@ -8678,7 +8650,7 @@ func NewV1DeleteInviteExternalJitAccessRequest(server string, ref string, invite return nil, err } - req, err := http.NewRequest("DELETE", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodDelete, queryURL.String(), nil) if err != nil { return nil, err } @@ -8692,7 +8664,7 @@ func NewV1ListJitAccessRequest(server string, ref string) (*http.Request, error) var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -8712,7 +8684,7 @@ func NewV1ListJitAccessRequest(server string, ref string) (*http.Request, error) return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -8726,14 +8698,14 @@ func NewV1DeleteJitAccessRequest(server string, ref string, userId openapi_types var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } var pathParam1 string - pathParam1, err = runtime.StyleParamWithLocation("simple", false, "user_id", runtime.ParamLocationPath, userId) + pathParam1, err = runtime.StyleParamWithOptions("simple", false, "user_id", userId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) if err != nil { return nil, err } @@ -8753,7 +8725,7 @@ func NewV1DeleteJitAccessRequest(server string, ref string, userId openapi_types return nil, err } - req, err := http.NewRequest("DELETE", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodDelete, queryURL.String(), nil) if err != nil { return nil, err } @@ -8767,7 +8739,7 @@ func NewV1RollbackMigrationsRequest(server string, ref string, params *V1Rollbac var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -8788,24 +8760,29 @@ func NewV1RollbackMigrationsRequest(server string, ref string, params *V1Rollbac } if params != nil { + // queryValues collects non-styled parameters (passthrough, JSON) + // that are safe to round-trip through url.Values.Encode(). queryValues := queryURL.Query() + // rawQueryFragments collects pre-encoded query fragments from + // styled parameters, preserving literal commas as delimiters + // per the OpenAPI spec (e.g. "color=blue,black,brown"). + var rawQueryFragments []string - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "gte", runtime.ParamLocationQuery, params.Gte); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "gte", params.Gte, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } - queryURL.RawQuery = queryValues.Encode() + if encoded := queryValues.Encode(); encoded != "" { + rawQueryFragments = append(rawQueryFragments, encoded) + } + queryURL.RawQuery = strings.Join(rawQueryFragments, "&") } - req, err := http.NewRequest("DELETE", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodDelete, queryURL.String(), nil) if err != nil { return nil, err } @@ -8819,7 +8796,7 @@ func NewV1ListMigrationHistoryRequest(server string, ref string) (*http.Request, var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -8839,7 +8816,7 @@ func NewV1ListMigrationHistoryRequest(server string, ref string) (*http.Request, return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -8864,7 +8841,7 @@ func NewV1ApplyAMigrationRequestWithBody(server string, ref string, params *V1Ap var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -8884,7 +8861,7 @@ func NewV1ApplyAMigrationRequestWithBody(server string, ref string, params *V1Ap return nil, err } - req, err := http.NewRequest("POST", queryURL.String(), body) + req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) if err != nil { return nil, err } @@ -8896,7 +8873,7 @@ func NewV1ApplyAMigrationRequestWithBody(server string, ref string, params *V1Ap if params.IdempotencyKey != nil { var headerParam0 string - headerParam0, err = runtime.StyleParamWithLocation("simple", false, "Idempotency-Key", runtime.ParamLocationHeader, *params.IdempotencyKey) + headerParam0, err = runtime.StyleParamWithOptions("simple", false, "Idempotency-Key", *params.IdempotencyKey, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationHeader, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -8926,7 +8903,7 @@ func NewV1UpsertAMigrationRequestWithBody(server string, ref string, params *V1U var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -8946,7 +8923,7 @@ func NewV1UpsertAMigrationRequestWithBody(server string, ref string, params *V1U return nil, err } - req, err := http.NewRequest("PUT", queryURL.String(), body) + req, err := http.NewRequest(http.MethodPut, queryURL.String(), body) if err != nil { return nil, err } @@ -8958,7 +8935,7 @@ func NewV1UpsertAMigrationRequestWithBody(server string, ref string, params *V1U if params.IdempotencyKey != nil { var headerParam0 string - headerParam0, err = runtime.StyleParamWithLocation("simple", false, "Idempotency-Key", runtime.ParamLocationHeader, *params.IdempotencyKey) + headerParam0, err = runtime.StyleParamWithOptions("simple", false, "Idempotency-Key", *params.IdempotencyKey, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationHeader, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -8977,14 +8954,14 @@ func NewV1GetAMigrationRequest(server string, ref string, version string) (*http var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } var pathParam1 string - pathParam1, err = runtime.StyleParamWithLocation("simple", false, "version", runtime.ParamLocationPath, version) + pathParam1, err = runtime.StyleParamWithOptions("simple", false, "version", version, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -9004,7 +8981,7 @@ func NewV1GetAMigrationRequest(server string, ref string, version string) (*http return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -9029,14 +9006,14 @@ func NewV1PatchAMigrationRequestWithBody(server string, ref string, version stri var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } var pathParam1 string - pathParam1, err = runtime.StyleParamWithLocation("simple", false, "version", runtime.ParamLocationPath, version) + pathParam1, err = runtime.StyleParamWithOptions("simple", false, "version", version, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -9056,7 +9033,7 @@ func NewV1PatchAMigrationRequestWithBody(server string, ref string, version stri return nil, err } - req, err := http.NewRequest("PATCH", queryURL.String(), body) + req, err := http.NewRequest(http.MethodPatch, queryURL.String(), body) if err != nil { return nil, err } @@ -9072,7 +9049,7 @@ func NewV1GetDatabaseOpenapiRequest(server string, ref string, params *V1GetData var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -9093,28 +9070,33 @@ func NewV1GetDatabaseOpenapiRequest(server string, ref string, params *V1GetData } if params != nil { + // queryValues collects non-styled parameters (passthrough, JSON) + // that are safe to round-trip through url.Values.Encode(). queryValues := queryURL.Query() + // rawQueryFragments collects pre-encoded query fragments from + // styled parameters, preserving literal commas as delimiters + // per the OpenAPI spec (e.g. "color=blue,black,brown"). + var rawQueryFragments []string if params.Schema != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "schema", runtime.ParamLocationQuery, *params.Schema); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "schema", *params.Schema, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } } - queryURL.RawQuery = queryValues.Encode() + if encoded := queryValues.Encode(); encoded != "" { + rawQueryFragments = append(rawQueryFragments, encoded) + } + queryURL.RawQuery = strings.Join(rawQueryFragments, "&") } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -9139,7 +9121,7 @@ func NewV1UpdateDatabasePasswordRequestWithBody(server string, ref string, conte var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -9159,7 +9141,7 @@ func NewV1UpdateDatabasePasswordRequestWithBody(server string, ref string, conte return nil, err } - req, err := http.NewRequest("PATCH", queryURL.String(), body) + req, err := http.NewRequest(http.MethodPatch, queryURL.String(), body) if err != nil { return nil, err } @@ -9186,7 +9168,7 @@ func NewV1RunAQueryRequestWithBody(server string, ref string, contentType string var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -9206,7 +9188,7 @@ func NewV1RunAQueryRequestWithBody(server string, ref string, contentType string return nil, err } - req, err := http.NewRequest("POST", queryURL.String(), body) + req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) if err != nil { return nil, err } @@ -9233,7 +9215,7 @@ func NewV1ReadOnlyQueryRequestWithBody(server string, ref string, contentType st var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -9253,7 +9235,7 @@ func NewV1ReadOnlyQueryRequestWithBody(server string, ref string, contentType st return nil, err } - req, err := http.NewRequest("POST", queryURL.String(), body) + req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) if err != nil { return nil, err } @@ -9269,7 +9251,7 @@ func NewV1EnableDatabaseWebhookRequest(server string, ref string) (*http.Request var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -9289,7 +9271,7 @@ func NewV1EnableDatabaseWebhookRequest(server string, ref string) (*http.Request return nil, err } - req, err := http.NewRequest("POST", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodPost, queryURL.String(), nil) if err != nil { return nil, err } @@ -9303,7 +9285,7 @@ func NewV1ListAllFunctionsRequest(server string, ref string) (*http.Request, err var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -9323,7 +9305,7 @@ func NewV1ListAllFunctionsRequest(server string, ref string) (*http.Request, err return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -9348,7 +9330,7 @@ func NewV1CreateAFunctionRequestWithBody(server string, ref string, params *V1Cr var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -9369,19 +9351,21 @@ func NewV1CreateAFunctionRequestWithBody(server string, ref string, params *V1Cr } if params != nil { + // queryValues collects non-styled parameters (passthrough, JSON) + // that are safe to round-trip through url.Values.Encode(). queryValues := queryURL.Query() + // rawQueryFragments collects pre-encoded query fragments from + // styled parameters, preserving literal commas as delimiters + // per the OpenAPI spec (e.g. "color=blue,black,brown"). + var rawQueryFragments []string if params.Slug != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "slug", runtime.ParamLocationQuery, *params.Slug); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "slug", *params.Slug, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } @@ -9389,15 +9373,11 @@ func NewV1CreateAFunctionRequestWithBody(server string, ref string, params *V1Cr if params.Name != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "name", runtime.ParamLocationQuery, *params.Name); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "name", *params.Name, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } @@ -9405,15 +9385,11 @@ func NewV1CreateAFunctionRequestWithBody(server string, ref string, params *V1Cr if params.VerifyJwt != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "verify_jwt", runtime.ParamLocationQuery, *params.VerifyJwt); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "verify_jwt", *params.VerifyJwt, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "boolean", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } @@ -9421,15 +9397,11 @@ func NewV1CreateAFunctionRequestWithBody(server string, ref string, params *V1Cr if params.ImportMap != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "import_map", runtime.ParamLocationQuery, *params.ImportMap); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "import_map", *params.ImportMap, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "boolean", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } @@ -9437,15 +9409,11 @@ func NewV1CreateAFunctionRequestWithBody(server string, ref string, params *V1Cr if params.EntrypointPath != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "entrypoint_path", runtime.ParamLocationQuery, *params.EntrypointPath); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "entrypoint_path", *params.EntrypointPath, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } @@ -9453,15 +9421,11 @@ func NewV1CreateAFunctionRequestWithBody(server string, ref string, params *V1Cr if params.ImportMapPath != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "import_map_path", runtime.ParamLocationQuery, *params.ImportMapPath); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "import_map_path", *params.ImportMapPath, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } @@ -9469,24 +9433,23 @@ func NewV1CreateAFunctionRequestWithBody(server string, ref string, params *V1Cr if params.EzbrSha256 != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "ezbr_sha256", runtime.ParamLocationQuery, *params.EzbrSha256); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "ezbr_sha256", *params.EzbrSha256, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } } - queryURL.RawQuery = queryValues.Encode() + if encoded := queryValues.Encode(); encoded != "" { + rawQueryFragments = append(rawQueryFragments, encoded) + } + queryURL.RawQuery = strings.Join(rawQueryFragments, "&") } - req, err := http.NewRequest("POST", queryURL.String(), body) + req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) if err != nil { return nil, err } @@ -9513,7 +9476,7 @@ func NewV1BulkUpdateFunctionsRequestWithBody(server string, ref string, contentT var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -9533,7 +9496,7 @@ func NewV1BulkUpdateFunctionsRequestWithBody(server string, ref string, contentT return nil, err } - req, err := http.NewRequest("PUT", queryURL.String(), body) + req, err := http.NewRequest(http.MethodPut, queryURL.String(), body) if err != nil { return nil, err } @@ -9549,7 +9512,7 @@ func NewV1DeployAFunctionRequestWithBody(server string, ref string, params *V1De var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -9570,19 +9533,21 @@ func NewV1DeployAFunctionRequestWithBody(server string, ref string, params *V1De } if params != nil { + // queryValues collects non-styled parameters (passthrough, JSON) + // that are safe to round-trip through url.Values.Encode(). queryValues := queryURL.Query() + // rawQueryFragments collects pre-encoded query fragments from + // styled parameters, preserving literal commas as delimiters + // per the OpenAPI spec (e.g. "color=blue,black,brown"). + var rawQueryFragments []string if params.Slug != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "slug", runtime.ParamLocationQuery, *params.Slug); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "slug", *params.Slug, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } @@ -9590,24 +9555,23 @@ func NewV1DeployAFunctionRequestWithBody(server string, ref string, params *V1De if params.BundleOnly != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "bundleOnly", runtime.ParamLocationQuery, *params.BundleOnly); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "bundleOnly", *params.BundleOnly, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "boolean", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } } - queryURL.RawQuery = queryValues.Encode() + if encoded := queryValues.Encode(); encoded != "" { + rawQueryFragments = append(rawQueryFragments, encoded) + } + queryURL.RawQuery = strings.Join(rawQueryFragments, "&") } - req, err := http.NewRequest("POST", queryURL.String(), body) + req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) if err != nil { return nil, err } @@ -9623,14 +9587,14 @@ func NewV1DeleteAFunctionRequest(server string, ref string, functionSlug string) var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } var pathParam1 string - pathParam1, err = runtime.StyleParamWithLocation("simple", false, "function_slug", runtime.ParamLocationPath, functionSlug) + pathParam1, err = runtime.StyleParamWithOptions("simple", false, "function_slug", functionSlug, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -9650,7 +9614,7 @@ func NewV1DeleteAFunctionRequest(server string, ref string, functionSlug string) return nil, err } - req, err := http.NewRequest("DELETE", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodDelete, queryURL.String(), nil) if err != nil { return nil, err } @@ -9664,14 +9628,14 @@ func NewV1GetAFunctionRequest(server string, ref string, functionSlug string) (* var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } var pathParam1 string - pathParam1, err = runtime.StyleParamWithLocation("simple", false, "function_slug", runtime.ParamLocationPath, functionSlug) + pathParam1, err = runtime.StyleParamWithOptions("simple", false, "function_slug", functionSlug, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -9691,7 +9655,7 @@ func NewV1GetAFunctionRequest(server string, ref string, functionSlug string) (* return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -9716,14 +9680,14 @@ func NewV1UpdateAFunctionRequestWithBody(server string, ref string, functionSlug var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } var pathParam1 string - pathParam1, err = runtime.StyleParamWithLocation("simple", false, "function_slug", runtime.ParamLocationPath, functionSlug) + pathParam1, err = runtime.StyleParamWithOptions("simple", false, "function_slug", functionSlug, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -9744,19 +9708,21 @@ func NewV1UpdateAFunctionRequestWithBody(server string, ref string, functionSlug } if params != nil { + // queryValues collects non-styled parameters (passthrough, JSON) + // that are safe to round-trip through url.Values.Encode(). queryValues := queryURL.Query() + // rawQueryFragments collects pre-encoded query fragments from + // styled parameters, preserving literal commas as delimiters + // per the OpenAPI spec (e.g. "color=blue,black,brown"). + var rawQueryFragments []string if params.Slug != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "slug", runtime.ParamLocationQuery, *params.Slug); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "slug", *params.Slug, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } @@ -9764,15 +9730,11 @@ func NewV1UpdateAFunctionRequestWithBody(server string, ref string, functionSlug if params.Name != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "name", runtime.ParamLocationQuery, *params.Name); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "name", *params.Name, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } @@ -9780,15 +9742,11 @@ func NewV1UpdateAFunctionRequestWithBody(server string, ref string, functionSlug if params.VerifyJwt != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "verify_jwt", runtime.ParamLocationQuery, *params.VerifyJwt); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "verify_jwt", *params.VerifyJwt, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "boolean", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } @@ -9796,15 +9754,11 @@ func NewV1UpdateAFunctionRequestWithBody(server string, ref string, functionSlug if params.ImportMap != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "import_map", runtime.ParamLocationQuery, *params.ImportMap); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "import_map", *params.ImportMap, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "boolean", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } @@ -9812,15 +9766,11 @@ func NewV1UpdateAFunctionRequestWithBody(server string, ref string, functionSlug if params.EntrypointPath != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "entrypoint_path", runtime.ParamLocationQuery, *params.EntrypointPath); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "entrypoint_path", *params.EntrypointPath, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } @@ -9828,15 +9778,11 @@ func NewV1UpdateAFunctionRequestWithBody(server string, ref string, functionSlug if params.ImportMapPath != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "import_map_path", runtime.ParamLocationQuery, *params.ImportMapPath); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "import_map_path", *params.ImportMapPath, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } @@ -9844,24 +9790,23 @@ func NewV1UpdateAFunctionRequestWithBody(server string, ref string, functionSlug if params.EzbrSha256 != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "ezbr_sha256", runtime.ParamLocationQuery, *params.EzbrSha256); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "ezbr_sha256", *params.EzbrSha256, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } } - queryURL.RawQuery = queryValues.Encode() + if encoded := queryValues.Encode(); encoded != "" { + rawQueryFragments = append(rawQueryFragments, encoded) + } + queryURL.RawQuery = strings.Join(rawQueryFragments, "&") } - req, err := http.NewRequest("PATCH", queryURL.String(), body) + req, err := http.NewRequest(http.MethodPatch, queryURL.String(), body) if err != nil { return nil, err } @@ -9877,14 +9822,14 @@ func NewV1GetAFunctionBodyRequest(server string, ref string, functionSlug string var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } var pathParam1 string - pathParam1, err = runtime.StyleParamWithLocation("simple", false, "function_slug", runtime.ParamLocationPath, functionSlug) + pathParam1, err = runtime.StyleParamWithOptions("simple", false, "function_slug", functionSlug, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -9904,7 +9849,7 @@ func NewV1GetAFunctionBodyRequest(server string, ref string, functionSlug string return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -9918,7 +9863,7 @@ func NewV1GetServicesHealthRequest(server string, ref string, params *V1GetServi var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -9939,40 +9884,45 @@ func NewV1GetServicesHealthRequest(server string, ref string, params *V1GetServi } if params != nil { + // queryValues collects non-styled parameters (passthrough, JSON) + // that are safe to round-trip through url.Values.Encode(). queryValues := queryURL.Query() + // rawQueryFragments collects pre-encoded query fragments from + // styled parameters, preserving literal commas as delimiters + // per the OpenAPI spec (e.g. "color=blue,black,brown"). + var rawQueryFragments []string - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "services", runtime.ParamLocationQuery, params.Services); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { - return nil, err - } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) + if params.Services != nil { + + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "services", params.Services, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "array", Format: ""}); err != nil { + return nil, err + } else { + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } + } if params.TimeoutMs != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "timeout_ms", runtime.ParamLocationQuery, *params.TimeoutMs); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "timeout_ms", *params.TimeoutMs, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "integer", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } } - queryURL.RawQuery = queryValues.Encode() + if encoded := queryValues.Encode(); encoded != "" { + rawQueryFragments = append(rawQueryFragments, encoded) + } + queryURL.RawQuery = strings.Join(rawQueryFragments, "&") } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -9986,7 +9936,7 @@ func NewV1GetJitAccessConfigRequest(server string, ref string) (*http.Request, e var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -10006,7 +9956,7 @@ func NewV1GetJitAccessConfigRequest(server string, ref string) (*http.Request, e return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -10031,7 +9981,7 @@ func NewV1UpdateJitAccessConfigRequestWithBody(server string, ref string, conten var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -10051,7 +10001,7 @@ func NewV1UpdateJitAccessConfigRequestWithBody(server string, ref string, conten return nil, err } - req, err := http.NewRequest("PUT", queryURL.String(), body) + req, err := http.NewRequest(http.MethodPut, queryURL.String(), body) if err != nil { return nil, err } @@ -10078,7 +10028,7 @@ func NewV1DeleteNetworkBansRequestWithBody(server string, ref string, contentTyp var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -10098,7 +10048,7 @@ func NewV1DeleteNetworkBansRequestWithBody(server string, ref string, contentTyp return nil, err } - req, err := http.NewRequest("DELETE", queryURL.String(), body) + req, err := http.NewRequest(http.MethodDelete, queryURL.String(), body) if err != nil { return nil, err } @@ -10114,7 +10064,7 @@ func NewV1ListAllNetworkBansRequest(server string, ref string) (*http.Request, e var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -10134,7 +10084,7 @@ func NewV1ListAllNetworkBansRequest(server string, ref string) (*http.Request, e return nil, err } - req, err := http.NewRequest("POST", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodPost, queryURL.String(), nil) if err != nil { return nil, err } @@ -10148,7 +10098,7 @@ func NewV1ListAllNetworkBansEnrichedRequest(server string, ref string) (*http.Re var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -10168,7 +10118,7 @@ func NewV1ListAllNetworkBansEnrichedRequest(server string, ref string) (*http.Re return nil, err } - req, err := http.NewRequest("POST", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodPost, queryURL.String(), nil) if err != nil { return nil, err } @@ -10182,7 +10132,7 @@ func NewV1GetNetworkRestrictionsRequest(server string, ref string) (*http.Reques var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -10202,7 +10152,7 @@ func NewV1GetNetworkRestrictionsRequest(server string, ref string) (*http.Reques return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -10227,7 +10177,7 @@ func NewV1PatchNetworkRestrictionsRequestWithBody(server string, ref string, con var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -10247,7 +10197,7 @@ func NewV1PatchNetworkRestrictionsRequestWithBody(server string, ref string, con return nil, err } - req, err := http.NewRequest("PATCH", queryURL.String(), body) + req, err := http.NewRequest(http.MethodPatch, queryURL.String(), body) if err != nil { return nil, err } @@ -10274,7 +10224,7 @@ func NewV1UpdateNetworkRestrictionsRequestWithBody(server string, ref string, co var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -10294,7 +10244,7 @@ func NewV1UpdateNetworkRestrictionsRequestWithBody(server string, ref string, co return nil, err } - req, err := http.NewRequest("POST", queryURL.String(), body) + req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) if err != nil { return nil, err } @@ -10310,7 +10260,7 @@ func NewV1PauseAProjectRequest(server string, ref string) (*http.Request, error) var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -10330,7 +10280,7 @@ func NewV1PauseAProjectRequest(server string, ref string) (*http.Request, error) return nil, err } - req, err := http.NewRequest("POST", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodPost, queryURL.String(), nil) if err != nil { return nil, err } @@ -10344,7 +10294,7 @@ func NewV1GetPgsodiumConfigRequest(server string, ref string) (*http.Request, er var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -10364,7 +10314,7 @@ func NewV1GetPgsodiumConfigRequest(server string, ref string) (*http.Request, er return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -10389,7 +10339,7 @@ func NewV1UpdatePgsodiumConfigRequestWithBody(server string, ref string, content var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -10409,7 +10359,7 @@ func NewV1UpdatePgsodiumConfigRequestWithBody(server string, ref string, content return nil, err } - req, err := http.NewRequest("PUT", queryURL.String(), body) + req, err := http.NewRequest(http.MethodPut, queryURL.String(), body) if err != nil { return nil, err } @@ -10425,7 +10375,7 @@ func NewV1GetPostgrestServiceConfigRequest(server string, ref string) (*http.Req var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -10445,7 +10395,7 @@ func NewV1GetPostgrestServiceConfigRequest(server string, ref string) (*http.Req return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -10470,7 +10420,7 @@ func NewV1UpdatePostgrestServiceConfigRequestWithBody(server string, ref string, var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -10490,7 +10440,7 @@ func NewV1UpdatePostgrestServiceConfigRequestWithBody(server string, ref string, return nil, err } - req, err := http.NewRequest("PATCH", queryURL.String(), body) + req, err := http.NewRequest(http.MethodPatch, queryURL.String(), body) if err != nil { return nil, err } @@ -10517,7 +10467,7 @@ func NewV1RemoveAReadReplicaRequestWithBody(server string, ref string, contentTy var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -10537,7 +10487,7 @@ func NewV1RemoveAReadReplicaRequestWithBody(server string, ref string, contentTy return nil, err } - req, err := http.NewRequest("POST", queryURL.String(), body) + req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) if err != nil { return nil, err } @@ -10564,7 +10514,7 @@ func NewV1SetupAReadReplicaRequestWithBody(server string, ref string, contentTyp var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -10584,7 +10534,7 @@ func NewV1SetupAReadReplicaRequestWithBody(server string, ref string, contentTyp return nil, err } - req, err := http.NewRequest("POST", queryURL.String(), body) + req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) if err != nil { return nil, err } @@ -10600,7 +10550,7 @@ func NewV1GetReadonlyModeStatusRequest(server string, ref string) (*http.Request var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -10620,7 +10570,7 @@ func NewV1GetReadonlyModeStatusRequest(server string, ref string) (*http.Request return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -10634,7 +10584,7 @@ func NewV1DisableReadonlyModeTemporarilyRequest(server string, ref string) (*htt var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -10654,7 +10604,7 @@ func NewV1DisableReadonlyModeTemporarilyRequest(server string, ref string) (*htt return nil, err } - req, err := http.NewRequest("POST", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodPost, queryURL.String(), nil) if err != nil { return nil, err } @@ -10668,7 +10618,7 @@ func NewV1RestartAProjectRequest(server string, ref string) (*http.Request, erro var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -10688,7 +10638,7 @@ func NewV1RestartAProjectRequest(server string, ref string) (*http.Request, erro return nil, err } - req, err := http.NewRequest("POST", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodPost, queryURL.String(), nil) if err != nil { return nil, err } @@ -10702,7 +10652,7 @@ func NewV1ListAvailableRestoreVersionsRequest(server string, ref string) (*http. var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -10722,7 +10672,7 @@ func NewV1ListAvailableRestoreVersionsRequest(server string, ref string) (*http. return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -10736,7 +10686,7 @@ func NewV1RestoreAProjectRequest(server string, ref string) (*http.Request, erro var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -10756,7 +10706,7 @@ func NewV1RestoreAProjectRequest(server string, ref string) (*http.Request, erro return nil, err } - req, err := http.NewRequest("POST", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodPost, queryURL.String(), nil) if err != nil { return nil, err } @@ -10770,7 +10720,7 @@ func NewV1CancelAProjectRestorationRequest(server string, ref string) (*http.Req var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -10790,7 +10740,7 @@ func NewV1CancelAProjectRestorationRequest(server string, ref string) (*http.Req return nil, err } - req, err := http.NewRequest("POST", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodPost, queryURL.String(), nil) if err != nil { return nil, err } @@ -10815,7 +10765,7 @@ func NewV1BulkDeleteSecretsRequestWithBody(server string, ref string, contentTyp var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -10835,7 +10785,7 @@ func NewV1BulkDeleteSecretsRequestWithBody(server string, ref string, contentTyp return nil, err } - req, err := http.NewRequest("DELETE", queryURL.String(), body) + req, err := http.NewRequest(http.MethodDelete, queryURL.String(), body) if err != nil { return nil, err } @@ -10851,7 +10801,7 @@ func NewV1ListAllSecretsRequest(server string, ref string) (*http.Request, error var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -10871,7 +10821,7 @@ func NewV1ListAllSecretsRequest(server string, ref string) (*http.Request, error return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -10896,7 +10846,7 @@ func NewV1BulkCreateSecretsRequestWithBody(server string, ref string, contentTyp var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -10916,7 +10866,7 @@ func NewV1BulkCreateSecretsRequestWithBody(server string, ref string, contentTyp return nil, err } - req, err := http.NewRequest("POST", queryURL.String(), body) + req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) if err != nil { return nil, err } @@ -10932,7 +10882,7 @@ func NewV1GetSslEnforcementConfigRequest(server string, ref string) (*http.Reque var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -10952,7 +10902,7 @@ func NewV1GetSslEnforcementConfigRequest(server string, ref string) (*http.Reque return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -10977,7 +10927,7 @@ func NewV1UpdateSslEnforcementConfigRequestWithBody(server string, ref string, c var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -10997,7 +10947,7 @@ func NewV1UpdateSslEnforcementConfigRequestWithBody(server string, ref string, c return nil, err } - req, err := http.NewRequest("PUT", queryURL.String(), body) + req, err := http.NewRequest(http.MethodPut, queryURL.String(), body) if err != nil { return nil, err } @@ -11013,7 +10963,7 @@ func NewV1ListAllBucketsRequest(server string, ref string) (*http.Request, error var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -11033,7 +10983,7 @@ func NewV1ListAllBucketsRequest(server string, ref string) (*http.Request, error return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -11047,7 +10997,7 @@ func NewV1GenerateTypescriptTypesRequest(server string, ref string, params *V1Ge var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -11068,28 +11018,33 @@ func NewV1GenerateTypescriptTypesRequest(server string, ref string, params *V1Ge } if params != nil { + // queryValues collects non-styled parameters (passthrough, JSON) + // that are safe to round-trip through url.Values.Encode(). queryValues := queryURL.Query() + // rawQueryFragments collects pre-encoded query fragments from + // styled parameters, preserving literal commas as delimiters + // per the OpenAPI spec (e.g. "color=blue,black,brown"). + var rawQueryFragments []string if params.IncludedSchemas != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "included_schemas", runtime.ParamLocationQuery, *params.IncludedSchemas); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "included_schemas", *params.IncludedSchemas, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } } - queryURL.RawQuery = queryValues.Encode() + if encoded := queryValues.Encode(); encoded != "" { + rawQueryFragments = append(rawQueryFragments, encoded) + } + queryURL.RawQuery = strings.Join(rawQueryFragments, "&") } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -11114,7 +11069,7 @@ func NewV1UpgradePostgresVersionRequestWithBody(server string, ref string, conte var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -11134,7 +11089,7 @@ func NewV1UpgradePostgresVersionRequestWithBody(server string, ref string, conte return nil, err } - req, err := http.NewRequest("POST", queryURL.String(), body) + req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) if err != nil { return nil, err } @@ -11150,7 +11105,7 @@ func NewV1GetPostgresUpgradeEligibilityRequest(server string, ref string) (*http var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -11170,7 +11125,7 @@ func NewV1GetPostgresUpgradeEligibilityRequest(server string, ref string) (*http return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -11184,7 +11139,7 @@ func NewV1GetPostgresUpgradeStatusRequest(server string, ref string, params *V1G var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -11205,28 +11160,33 @@ func NewV1GetPostgresUpgradeStatusRequest(server string, ref string, params *V1G } if params != nil { + // queryValues collects non-styled parameters (passthrough, JSON) + // that are safe to round-trip through url.Values.Encode(). queryValues := queryURL.Query() + // rawQueryFragments collects pre-encoded query fragments from + // styled parameters, preserving literal commas as delimiters + // per the OpenAPI spec (e.g. "color=blue,black,brown"). + var rawQueryFragments []string if params.TrackingId != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "tracking_id", runtime.ParamLocationQuery, *params.TrackingId); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "tracking_id", *params.TrackingId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } } - queryURL.RawQuery = queryValues.Encode() + if encoded := queryValues.Encode(); encoded != "" { + rawQueryFragments = append(rawQueryFragments, encoded) + } + queryURL.RawQuery = strings.Join(rawQueryFragments, "&") } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -11240,7 +11200,7 @@ func NewV1DeactivateVanitySubdomainConfigRequest(server string, ref string) (*ht var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -11260,7 +11220,7 @@ func NewV1DeactivateVanitySubdomainConfigRequest(server string, ref string) (*ht return nil, err } - req, err := http.NewRequest("DELETE", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodDelete, queryURL.String(), nil) if err != nil { return nil, err } @@ -11274,7 +11234,7 @@ func NewV1GetVanitySubdomainConfigRequest(server string, ref string) (*http.Requ var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -11294,7 +11254,7 @@ func NewV1GetVanitySubdomainConfigRequest(server string, ref string) (*http.Requ return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -11319,7 +11279,7 @@ func NewV1ActivateVanitySubdomainConfigRequestWithBody(server string, ref string var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -11339,7 +11299,7 @@ func NewV1ActivateVanitySubdomainConfigRequestWithBody(server string, ref string return nil, err } - req, err := http.NewRequest("POST", queryURL.String(), body) + req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) if err != nil { return nil, err } @@ -11366,7 +11326,7 @@ func NewV1CheckVanitySubdomainAvailabilityRequestWithBody(server string, ref str var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "ref", runtime.ParamLocationPath, ref) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "ref", ref, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -11386,7 +11346,7 @@ func NewV1CheckVanitySubdomainAvailabilityRequestWithBody(server string, ref str return nil, err } - req, err := http.NewRequest("POST", queryURL.String(), body) + req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) if err != nil { return nil, err } @@ -11416,19 +11376,21 @@ func NewV1ListAllSnippetsRequest(server string, params *V1ListAllSnippetsParams) } if params != nil { + // queryValues collects non-styled parameters (passthrough, JSON) + // that are safe to round-trip through url.Values.Encode(). queryValues := queryURL.Query() + // rawQueryFragments collects pre-encoded query fragments from + // styled parameters, preserving literal commas as delimiters + // per the OpenAPI spec (e.g. "color=blue,black,brown"). + var rawQueryFragments []string if params.ProjectRef != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "project_ref", runtime.ParamLocationQuery, *params.ProjectRef); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "project_ref", *params.ProjectRef, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } @@ -11436,15 +11398,11 @@ func NewV1ListAllSnippetsRequest(server string, params *V1ListAllSnippetsParams) if params.Cursor != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "cursor", runtime.ParamLocationQuery, *params.Cursor); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "cursor", *params.Cursor, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } @@ -11452,15 +11410,11 @@ func NewV1ListAllSnippetsRequest(server string, params *V1ListAllSnippetsParams) if params.Limit != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "limit", runtime.ParamLocationQuery, *params.Limit); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "limit", *params.Limit, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } @@ -11468,15 +11422,11 @@ func NewV1ListAllSnippetsRequest(server string, params *V1ListAllSnippetsParams) if params.SortBy != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "sort_by", runtime.ParamLocationQuery, *params.SortBy); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "sort_by", *params.SortBy, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } @@ -11484,24 +11434,23 @@ func NewV1ListAllSnippetsRequest(server string, params *V1ListAllSnippetsParams) if params.SortOrder != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "sort_order", runtime.ParamLocationQuery, *params.SortOrder); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "sort_order", *params.SortOrder, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) } } } - queryURL.RawQuery = queryValues.Encode() + if encoded := queryValues.Encode(); encoded != "" { + rawQueryFragments = append(rawQueryFragments, encoded) + } + queryURL.RawQuery = strings.Join(rawQueryFragments, "&") } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -11515,7 +11464,7 @@ func NewV1GetASnippetRequest(server string, id openapi_types.UUID) (*http.Reques var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "id", runtime.ParamLocationPath, id) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "id", id, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) if err != nil { return nil, err } @@ -11535,7 +11484,7 @@ func NewV1GetASnippetRequest(server string, id openapi_types.UUID) (*http.Reques return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -12239,6 +12188,14 @@ func (r V1DeleteABranchResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1DeleteABranchResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1GetABranchConfigResponse struct { Body []byte HTTPResponse *http.Response @@ -12261,6 +12218,14 @@ func (r V1GetABranchConfigResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1GetABranchConfigResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1UpdateABranchConfigResponse struct { Body []byte HTTPResponse *http.Response @@ -12283,6 +12248,14 @@ func (r V1UpdateABranchConfigResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1UpdateABranchConfigResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1DiffABranchResponse struct { Body []byte HTTPResponse *http.Response @@ -12304,6 +12277,14 @@ func (r V1DiffABranchResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1DiffABranchResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1MergeABranchResponse struct { Body []byte HTTPResponse *http.Response @@ -12326,6 +12307,14 @@ func (r V1MergeABranchResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1MergeABranchResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1PushABranchResponse struct { Body []byte HTTPResponse *http.Response @@ -12348,6 +12337,14 @@ func (r V1PushABranchResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1PushABranchResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1ResetABranchResponse struct { Body []byte HTTPResponse *http.Response @@ -12370,6 +12367,14 @@ func (r V1ResetABranchResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1ResetABranchResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1RestoreABranchResponse struct { Body []byte HTTPResponse *http.Response @@ -12392,6 +12397,14 @@ func (r V1RestoreABranchResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1RestoreABranchResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1AuthorizeUserResponse struct { Body []byte HTTPResponse *http.Response @@ -12413,6 +12426,14 @@ func (r V1AuthorizeUserResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1AuthorizeUserResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1OauthAuthorizeProjectClaimResponse struct { Body []byte HTTPResponse *http.Response @@ -12434,6 +12455,14 @@ func (r V1OauthAuthorizeProjectClaimResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1OauthAuthorizeProjectClaimResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1RevokeTokenResponse struct { Body []byte HTTPResponse *http.Response @@ -12455,6 +12484,14 @@ func (r V1RevokeTokenResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1RevokeTokenResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1ExchangeOauthTokenResponse struct { Body []byte HTTPResponse *http.Response @@ -12477,6 +12514,14 @@ func (r V1ExchangeOauthTokenResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1ExchangeOauthTokenResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1ListAllOrganizationsResponse struct { Body []byte HTTPResponse *http.Response @@ -12499,6 +12544,14 @@ func (r V1ListAllOrganizationsResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1ListAllOrganizationsResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1CreateAnOrganizationResponse struct { Body []byte HTTPResponse *http.Response @@ -12521,6 +12574,14 @@ func (r V1CreateAnOrganizationResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1CreateAnOrganizationResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1GetAnOrganizationResponse struct { Body []byte HTTPResponse *http.Response @@ -12543,6 +12604,14 @@ func (r V1GetAnOrganizationResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1GetAnOrganizationResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1GetOrganizationEntitlementsResponse struct { Body []byte HTTPResponse *http.Response @@ -12565,6 +12634,14 @@ func (r V1GetOrganizationEntitlementsResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1GetOrganizationEntitlementsResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1ListOrganizationMembersResponse struct { Body []byte HTTPResponse *http.Response @@ -12587,6 +12664,14 @@ func (r V1ListOrganizationMembersResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1ListOrganizationMembersResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1GetOrganizationProjectClaimResponse struct { Body []byte HTTPResponse *http.Response @@ -12609,6 +12694,14 @@ func (r V1GetOrganizationProjectClaimResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1GetOrganizationProjectClaimResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1ClaimProjectForOrganizationResponse struct { Body []byte HTTPResponse *http.Response @@ -12630,6 +12723,14 @@ func (r V1ClaimProjectForOrganizationResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1ClaimProjectForOrganizationResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1GetAllProjectsForOrganizationResponse struct { Body []byte HTTPResponse *http.Response @@ -12652,6 +12753,14 @@ func (r V1GetAllProjectsForOrganizationResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1GetAllProjectsForOrganizationResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1GetProfileResponse struct { Body []byte HTTPResponse *http.Response @@ -12674,6 +12783,14 @@ func (r V1GetProfileResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1GetProfileResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1ListAllProjectsResponse struct { Body []byte HTTPResponse *http.Response @@ -12696,6 +12813,14 @@ func (r V1ListAllProjectsResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1ListAllProjectsResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1CreateAProjectResponse struct { Body []byte HTTPResponse *http.Response @@ -12718,6 +12843,14 @@ func (r V1CreateAProjectResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1CreateAProjectResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1GetAvailableRegionsResponse struct { Body []byte HTTPResponse *http.Response @@ -12740,6 +12873,14 @@ func (r V1GetAvailableRegionsResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1GetAvailableRegionsResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1DeleteAProjectResponse struct { Body []byte HTTPResponse *http.Response @@ -12762,6 +12903,14 @@ func (r V1DeleteAProjectResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1DeleteAProjectResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1GetProjectResponse struct { Body []byte HTTPResponse *http.Response @@ -12784,6 +12933,14 @@ func (r V1GetProjectResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1GetProjectResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1UpdateAProjectResponse struct { Body []byte HTTPResponse *http.Response @@ -12806,6 +12963,14 @@ func (r V1UpdateAProjectResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1UpdateAProjectResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1ListActionRunsResponse struct { Body []byte HTTPResponse *http.Response @@ -12828,6 +12993,14 @@ func (r V1ListActionRunsResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1ListActionRunsResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1CountActionRunsResponse struct { Body []byte HTTPResponse *http.Response @@ -12849,6 +13022,14 @@ func (r V1CountActionRunsResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1CountActionRunsResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1GetActionRunResponse struct { Body []byte HTTPResponse *http.Response @@ -12871,6 +13052,14 @@ func (r V1GetActionRunResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1GetActionRunResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1GetActionRunLogsResponse struct { Body []byte HTTPResponse *http.Response @@ -12892,6 +13081,14 @@ func (r V1GetActionRunLogsResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1GetActionRunLogsResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1UpdateActionRunStatusResponse struct { Body []byte HTTPResponse *http.Response @@ -12914,6 +13111,14 @@ func (r V1UpdateActionRunStatusResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1UpdateActionRunStatusResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1GetPerformanceAdvisorsResponse struct { Body []byte HTTPResponse *http.Response @@ -12936,8 +13141,16 @@ func (r V1GetPerformanceAdvisorsResponse) StatusCode() int { return 0 } -type V1GetSecurityAdvisorsResponse struct { - Body []byte +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1GetPerformanceAdvisorsResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + +type V1GetSecurityAdvisorsResponse struct { + Body []byte HTTPResponse *http.Response JSON200 *V1ProjectAdvisorsResponse } @@ -12958,6 +13171,14 @@ func (r V1GetSecurityAdvisorsResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1GetSecurityAdvisorsResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1GetProjectFunctionCombinedStatsResponse struct { Body []byte HTTPResponse *http.Response @@ -12980,6 +13201,14 @@ func (r V1GetProjectFunctionCombinedStatsResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1GetProjectFunctionCombinedStatsResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1GetProjectLogsResponse struct { Body []byte HTTPResponse *http.Response @@ -13002,6 +13231,14 @@ func (r V1GetProjectLogsResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1GetProjectLogsResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1GetProjectLogsAllResponse struct { Body []byte HTTPResponse *http.Response @@ -13024,6 +13261,14 @@ func (r V1GetProjectLogsAllResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1GetProjectLogsAllResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1ScrapeProjectMetricsResponse struct { Body []byte HTTPResponse *http.Response @@ -13045,6 +13290,14 @@ func (r V1ScrapeProjectMetricsResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1ScrapeProjectMetricsResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1GetProjectUsageApiCountResponse struct { Body []byte HTTPResponse *http.Response @@ -13067,6 +13320,14 @@ func (r V1GetProjectUsageApiCountResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1GetProjectUsageApiCountResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1GetProjectUsageRequestCountResponse struct { Body []byte HTTPResponse *http.Response @@ -13089,6 +13350,14 @@ func (r V1GetProjectUsageRequestCountResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1GetProjectUsageRequestCountResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1GetProjectApiKeysResponse struct { Body []byte HTTPResponse *http.Response @@ -13111,6 +13380,14 @@ func (r V1GetProjectApiKeysResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1GetProjectApiKeysResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1CreateProjectApiKeyResponse struct { Body []byte HTTPResponse *http.Response @@ -13133,6 +13410,14 @@ func (r V1CreateProjectApiKeyResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1CreateProjectApiKeyResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1GetProjectLegacyApiKeysResponse struct { Body []byte HTTPResponse *http.Response @@ -13155,6 +13440,14 @@ func (r V1GetProjectLegacyApiKeysResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1GetProjectLegacyApiKeysResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1UpdateProjectLegacyApiKeysResponse struct { Body []byte HTTPResponse *http.Response @@ -13177,6 +13470,14 @@ func (r V1UpdateProjectLegacyApiKeysResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1UpdateProjectLegacyApiKeysResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1DeleteProjectApiKeyResponse struct { Body []byte HTTPResponse *http.Response @@ -13199,6 +13500,14 @@ func (r V1DeleteProjectApiKeyResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1DeleteProjectApiKeyResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1GetProjectApiKeyResponse struct { Body []byte HTTPResponse *http.Response @@ -13221,6 +13530,14 @@ func (r V1GetProjectApiKeyResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1GetProjectApiKeyResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1UpdateProjectApiKeyResponse struct { Body []byte HTTPResponse *http.Response @@ -13243,6 +13560,14 @@ func (r V1UpdateProjectApiKeyResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1UpdateProjectApiKeyResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1ListProjectAddonsResponse struct { Body []byte HTTPResponse *http.Response @@ -13265,6 +13590,14 @@ func (r V1ListProjectAddonsResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1ListProjectAddonsResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1ApplyProjectAddonResponse struct { Body []byte HTTPResponse *http.Response @@ -13286,6 +13619,14 @@ func (r V1ApplyProjectAddonResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1ApplyProjectAddonResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1RemoveProjectAddonResponse struct { Body []byte HTTPResponse *http.Response @@ -13307,6 +13648,14 @@ func (r V1RemoveProjectAddonResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1RemoveProjectAddonResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1DisablePreviewBranchingResponse struct { Body []byte HTTPResponse *http.Response @@ -13328,6 +13677,14 @@ func (r V1DisablePreviewBranchingResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1DisablePreviewBranchingResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1ListAllBranchesResponse struct { Body []byte HTTPResponse *http.Response @@ -13350,6 +13707,14 @@ func (r V1ListAllBranchesResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1ListAllBranchesResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1CreateABranchResponse struct { Body []byte HTTPResponse *http.Response @@ -13372,6 +13737,14 @@ func (r V1CreateABranchResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1CreateABranchResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1GetABranchResponse struct { Body []byte HTTPResponse *http.Response @@ -13394,6 +13767,14 @@ func (r V1GetABranchResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1GetABranchResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1DeleteProjectClaimTokenResponse struct { Body []byte HTTPResponse *http.Response @@ -13415,6 +13796,14 @@ func (r V1DeleteProjectClaimTokenResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1DeleteProjectClaimTokenResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1GetProjectClaimTokenResponse struct { Body []byte HTTPResponse *http.Response @@ -13437,6 +13826,14 @@ func (r V1GetProjectClaimTokenResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1GetProjectClaimTokenResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1CreateProjectClaimTokenResponse struct { Body []byte HTTPResponse *http.Response @@ -13459,6 +13856,14 @@ func (r V1CreateProjectClaimTokenResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1CreateProjectClaimTokenResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1DeleteLoginRolesResponse struct { Body []byte HTTPResponse *http.Response @@ -13481,6 +13886,14 @@ func (r V1DeleteLoginRolesResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1DeleteLoginRolesResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1CreateLoginRoleResponse struct { Body []byte HTTPResponse *http.Response @@ -13503,6 +13916,14 @@ func (r V1CreateLoginRoleResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1CreateLoginRoleResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1GetAuthServiceConfigResponse struct { Body []byte HTTPResponse *http.Response @@ -13525,6 +13946,14 @@ func (r V1GetAuthServiceConfigResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1GetAuthServiceConfigResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1UpdateAuthServiceConfigResponse struct { Body []byte HTTPResponse *http.Response @@ -13547,6 +13976,14 @@ func (r V1UpdateAuthServiceConfigResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1UpdateAuthServiceConfigResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1GetProjectSigningKeysResponse struct { Body []byte HTTPResponse *http.Response @@ -13569,6 +14006,14 @@ func (r V1GetProjectSigningKeysResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1GetProjectSigningKeysResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1CreateProjectSigningKeyResponse struct { Body []byte HTTPResponse *http.Response @@ -13591,6 +14036,14 @@ func (r V1CreateProjectSigningKeyResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1CreateProjectSigningKeyResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1GetLegacySigningKeyResponse struct { Body []byte HTTPResponse *http.Response @@ -13613,6 +14066,14 @@ func (r V1GetLegacySigningKeyResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1GetLegacySigningKeyResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1CreateLegacySigningKeyResponse struct { Body []byte HTTPResponse *http.Response @@ -13635,6 +14096,14 @@ func (r V1CreateLegacySigningKeyResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1CreateLegacySigningKeyResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1RemoveProjectSigningKeyResponse struct { Body []byte HTTPResponse *http.Response @@ -13657,6 +14126,14 @@ func (r V1RemoveProjectSigningKeyResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1RemoveProjectSigningKeyResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1GetProjectSigningKeyResponse struct { Body []byte HTTPResponse *http.Response @@ -13679,6 +14156,14 @@ func (r V1GetProjectSigningKeyResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1GetProjectSigningKeyResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1UpdateProjectSigningKeyResponse struct { Body []byte HTTPResponse *http.Response @@ -13701,6 +14186,14 @@ func (r V1UpdateProjectSigningKeyResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1UpdateProjectSigningKeyResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1ListAllSsoProviderResponse struct { Body []byte HTTPResponse *http.Response @@ -13723,6 +14216,14 @@ func (r V1ListAllSsoProviderResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1ListAllSsoProviderResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1CreateASsoProviderResponse struct { Body []byte HTTPResponse *http.Response @@ -13745,6 +14246,14 @@ func (r V1CreateASsoProviderResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1CreateASsoProviderResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1DeleteASsoProviderResponse struct { Body []byte HTTPResponse *http.Response @@ -13767,6 +14276,14 @@ func (r V1DeleteASsoProviderResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1DeleteASsoProviderResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1GetASsoProviderResponse struct { Body []byte HTTPResponse *http.Response @@ -13789,6 +14306,14 @@ func (r V1GetASsoProviderResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1GetASsoProviderResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1UpdateASsoProviderResponse struct { Body []byte HTTPResponse *http.Response @@ -13811,6 +14336,14 @@ func (r V1UpdateASsoProviderResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1UpdateASsoProviderResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1ListProjectTpaIntegrationsResponse struct { Body []byte HTTPResponse *http.Response @@ -13833,6 +14366,14 @@ func (r V1ListProjectTpaIntegrationsResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1ListProjectTpaIntegrationsResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1CreateProjectTpaIntegrationResponse struct { Body []byte HTTPResponse *http.Response @@ -13855,6 +14396,14 @@ func (r V1CreateProjectTpaIntegrationResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1CreateProjectTpaIntegrationResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1DeleteProjectTpaIntegrationResponse struct { Body []byte HTTPResponse *http.Response @@ -13877,6 +14426,14 @@ func (r V1DeleteProjectTpaIntegrationResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1DeleteProjectTpaIntegrationResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1GetProjectTpaIntegrationResponse struct { Body []byte HTTPResponse *http.Response @@ -13899,6 +14456,14 @@ func (r V1GetProjectTpaIntegrationResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1GetProjectTpaIntegrationResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1GetProjectPgbouncerConfigResponse struct { Body []byte HTTPResponse *http.Response @@ -13921,6 +14486,14 @@ func (r V1GetProjectPgbouncerConfigResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1GetProjectPgbouncerConfigResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1GetPoolerConfigResponse struct { Body []byte HTTPResponse *http.Response @@ -13943,6 +14516,14 @@ func (r V1GetPoolerConfigResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1GetPoolerConfigResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1UpdatePoolerConfigResponse struct { Body []byte HTTPResponse *http.Response @@ -13965,6 +14546,14 @@ func (r V1UpdatePoolerConfigResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1UpdatePoolerConfigResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1GetPostgresConfigResponse struct { Body []byte HTTPResponse *http.Response @@ -13987,6 +14576,14 @@ func (r V1GetPostgresConfigResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1GetPostgresConfigResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1UpdatePostgresConfigResponse struct { Body []byte HTTPResponse *http.Response @@ -14009,6 +14606,14 @@ func (r V1UpdatePostgresConfigResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1UpdatePostgresConfigResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1GetDatabaseDiskResponse struct { Body []byte HTTPResponse *http.Response @@ -14031,6 +14636,14 @@ func (r V1GetDatabaseDiskResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1GetDatabaseDiskResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1ModifyDatabaseDiskResponse struct { Body []byte HTTPResponse *http.Response @@ -14052,6 +14665,14 @@ func (r V1ModifyDatabaseDiskResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1ModifyDatabaseDiskResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1GetProjectDiskAutoscaleConfigResponse struct { Body []byte HTTPResponse *http.Response @@ -14074,6 +14695,14 @@ func (r V1GetProjectDiskAutoscaleConfigResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1GetProjectDiskAutoscaleConfigResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1GetDiskUtilizationResponse struct { Body []byte HTTPResponse *http.Response @@ -14096,6 +14725,14 @@ func (r V1GetDiskUtilizationResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1GetDiskUtilizationResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1GetRealtimeConfigResponse struct { Body []byte HTTPResponse *http.Response @@ -14118,6 +14755,14 @@ func (r V1GetRealtimeConfigResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1GetRealtimeConfigResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1UpdateRealtimeConfigResponse struct { Body []byte HTTPResponse *http.Response @@ -14139,6 +14784,14 @@ func (r V1UpdateRealtimeConfigResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1UpdateRealtimeConfigResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1ShutdownRealtimeResponse struct { Body []byte HTTPResponse *http.Response @@ -14160,6 +14813,14 @@ func (r V1ShutdownRealtimeResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1ShutdownRealtimeResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1GetStorageConfigResponse struct { Body []byte HTTPResponse *http.Response @@ -14182,6 +14843,14 @@ func (r V1GetStorageConfigResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1GetStorageConfigResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1UpdateStorageConfigResponse struct { Body []byte HTTPResponse *http.Response @@ -14203,6 +14872,14 @@ func (r V1UpdateStorageConfigResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1UpdateStorageConfigResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1DeleteHostnameConfigResponse struct { Body []byte HTTPResponse *http.Response @@ -14224,6 +14901,14 @@ func (r V1DeleteHostnameConfigResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1DeleteHostnameConfigResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1GetHostnameConfigResponse struct { Body []byte HTTPResponse *http.Response @@ -14246,6 +14931,14 @@ func (r V1GetHostnameConfigResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1GetHostnameConfigResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1ActivateCustomHostnameResponse struct { Body []byte HTTPResponse *http.Response @@ -14268,6 +14961,14 @@ func (r V1ActivateCustomHostnameResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1ActivateCustomHostnameResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1UpdateHostnameConfigResponse struct { Body []byte HTTPResponse *http.Response @@ -14290,6 +14991,14 @@ func (r V1UpdateHostnameConfigResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1UpdateHostnameConfigResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1VerifyDnsConfigResponse struct { Body []byte HTTPResponse *http.Response @@ -14312,6 +15021,14 @@ func (r V1VerifyDnsConfigResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1VerifyDnsConfigResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1ListAllBackupsResponse struct { Body []byte HTTPResponse *http.Response @@ -14334,6 +15051,14 @@ func (r V1ListAllBackupsResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1ListAllBackupsResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1RestorePhysicalBackupResponse struct { Body []byte HTTPResponse *http.Response @@ -14355,6 +15080,14 @@ func (r V1RestorePhysicalBackupResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1RestorePhysicalBackupResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1RestorePitrBackupResponse struct { Body []byte HTTPResponse *http.Response @@ -14376,6 +15109,14 @@ func (r V1RestorePitrBackupResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1RestorePitrBackupResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1GetRestorePointResponse struct { Body []byte HTTPResponse *http.Response @@ -14398,6 +15139,14 @@ func (r V1GetRestorePointResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1GetRestorePointResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1CreateRestorePointResponse struct { Body []byte HTTPResponse *http.Response @@ -14420,6 +15169,14 @@ func (r V1CreateRestorePointResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1CreateRestorePointResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1GetBackupScheduleResponse struct { Body []byte HTTPResponse *http.Response @@ -14443,6 +15200,14 @@ func (r V1GetBackupScheduleResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1GetBackupScheduleResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1UpdateBackupScheduleResponse struct { Body []byte HTTPResponse *http.Response @@ -14466,6 +15231,14 @@ func (r V1UpdateBackupScheduleResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1UpdateBackupScheduleResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1UndoResponse struct { Body []byte HTTPResponse *http.Response @@ -14487,6 +15260,14 @@ func (r V1UndoResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1UndoResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1GetDatabaseMetadataResponse struct { Body []byte HTTPResponse *http.Response @@ -14509,6 +15290,14 @@ func (r V1GetDatabaseMetadataResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1GetDatabaseMetadataResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1GetJitAccessResponse struct { Body []byte HTTPResponse *http.Response @@ -14531,6 +15320,14 @@ func (r V1GetJitAccessResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1GetJitAccessResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1AuthorizeJitAccessResponse struct { Body []byte HTTPResponse *http.Response @@ -14553,6 +15350,14 @@ func (r V1AuthorizeJitAccessResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1AuthorizeJitAccessResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1UpdateJitAccessResponse struct { Body []byte HTTPResponse *http.Response @@ -14575,6 +15380,14 @@ func (r V1UpdateJitAccessResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1UpdateJitAccessResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1InviteExternalJitAccessResponse struct { Body []byte HTTPResponse *http.Response @@ -14597,6 +15410,14 @@ func (r V1InviteExternalJitAccessResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1InviteExternalJitAccessResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1AcceptInviteExternalJitAccessResponse struct { Body []byte HTTPResponse *http.Response @@ -14619,6 +15440,14 @@ func (r V1AcceptInviteExternalJitAccessResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1AcceptInviteExternalJitAccessResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1DeleteInviteExternalJitAccessResponse struct { Body []byte HTTPResponse *http.Response @@ -14640,6 +15469,14 @@ func (r V1DeleteInviteExternalJitAccessResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1DeleteInviteExternalJitAccessResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1ListJitAccessResponse struct { Body []byte HTTPResponse *http.Response @@ -14662,6 +15499,14 @@ func (r V1ListJitAccessResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1ListJitAccessResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1DeleteJitAccessResponse struct { Body []byte HTTPResponse *http.Response @@ -14683,6 +15528,14 @@ func (r V1DeleteJitAccessResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1DeleteJitAccessResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1RollbackMigrationsResponse struct { Body []byte HTTPResponse *http.Response @@ -14704,6 +15557,14 @@ func (r V1RollbackMigrationsResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1RollbackMigrationsResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1ListMigrationHistoryResponse struct { Body []byte HTTPResponse *http.Response @@ -14726,6 +15587,14 @@ func (r V1ListMigrationHistoryResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1ListMigrationHistoryResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1ApplyAMigrationResponse struct { Body []byte HTTPResponse *http.Response @@ -14747,6 +15616,14 @@ func (r V1ApplyAMigrationResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1ApplyAMigrationResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1UpsertAMigrationResponse struct { Body []byte HTTPResponse *http.Response @@ -14768,6 +15645,14 @@ func (r V1UpsertAMigrationResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1UpsertAMigrationResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1GetAMigrationResponse struct { Body []byte HTTPResponse *http.Response @@ -14790,6 +15675,14 @@ func (r V1GetAMigrationResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1GetAMigrationResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1PatchAMigrationResponse struct { Body []byte HTTPResponse *http.Response @@ -14811,6 +15704,14 @@ func (r V1PatchAMigrationResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1PatchAMigrationResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1GetDatabaseOpenapiResponse struct { Body []byte HTTPResponse *http.Response @@ -14833,6 +15734,14 @@ func (r V1GetDatabaseOpenapiResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1GetDatabaseOpenapiResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1UpdateDatabasePasswordResponse struct { Body []byte HTTPResponse *http.Response @@ -14855,6 +15764,14 @@ func (r V1UpdateDatabasePasswordResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1UpdateDatabasePasswordResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1RunAQueryResponse struct { Body []byte HTTPResponse *http.Response @@ -14876,6 +15793,14 @@ func (r V1RunAQueryResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1RunAQueryResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1ReadOnlyQueryResponse struct { Body []byte HTTPResponse *http.Response @@ -14897,6 +15822,14 @@ func (r V1ReadOnlyQueryResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1ReadOnlyQueryResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1EnableDatabaseWebhookResponse struct { Body []byte HTTPResponse *http.Response @@ -14918,6 +15851,14 @@ func (r V1EnableDatabaseWebhookResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1EnableDatabaseWebhookResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1ListAllFunctionsResponse struct { Body []byte HTTPResponse *http.Response @@ -14940,6 +15881,14 @@ func (r V1ListAllFunctionsResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1ListAllFunctionsResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1CreateAFunctionResponse struct { Body []byte HTTPResponse *http.Response @@ -14962,6 +15911,14 @@ func (r V1CreateAFunctionResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1CreateAFunctionResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1BulkUpdateFunctionsResponse struct { Body []byte HTTPResponse *http.Response @@ -14984,6 +15941,14 @@ func (r V1BulkUpdateFunctionsResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1BulkUpdateFunctionsResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1DeployAFunctionResponse struct { Body []byte HTTPResponse *http.Response @@ -15006,6 +15971,14 @@ func (r V1DeployAFunctionResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1DeployAFunctionResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1DeleteAFunctionResponse struct { Body []byte HTTPResponse *http.Response @@ -15027,6 +16000,14 @@ func (r V1DeleteAFunctionResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1DeleteAFunctionResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1GetAFunctionResponse struct { Body []byte HTTPResponse *http.Response @@ -15049,6 +16030,14 @@ func (r V1GetAFunctionResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1GetAFunctionResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1UpdateAFunctionResponse struct { Body []byte HTTPResponse *http.Response @@ -15071,6 +16060,14 @@ func (r V1UpdateAFunctionResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1UpdateAFunctionResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1GetAFunctionBodyResponse struct { Body []byte HTTPResponse *http.Response @@ -15093,6 +16090,14 @@ func (r V1GetAFunctionBodyResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1GetAFunctionBodyResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1GetServicesHealthResponse struct { Body []byte HTTPResponse *http.Response @@ -15115,6 +16120,14 @@ func (r V1GetServicesHealthResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1GetServicesHealthResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1GetJitAccessConfigResponse struct { Body []byte HTTPResponse *http.Response @@ -15137,6 +16150,14 @@ func (r V1GetJitAccessConfigResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1GetJitAccessConfigResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1UpdateJitAccessConfigResponse struct { Body []byte HTTPResponse *http.Response @@ -15159,6 +16180,14 @@ func (r V1UpdateJitAccessConfigResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1UpdateJitAccessConfigResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1DeleteNetworkBansResponse struct { Body []byte HTTPResponse *http.Response @@ -15180,6 +16209,14 @@ func (r V1DeleteNetworkBansResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1DeleteNetworkBansResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1ListAllNetworkBansResponse struct { Body []byte HTTPResponse *http.Response @@ -15202,6 +16239,14 @@ func (r V1ListAllNetworkBansResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1ListAllNetworkBansResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1ListAllNetworkBansEnrichedResponse struct { Body []byte HTTPResponse *http.Response @@ -15224,6 +16269,14 @@ func (r V1ListAllNetworkBansEnrichedResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1ListAllNetworkBansEnrichedResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1GetNetworkRestrictionsResponse struct { Body []byte HTTPResponse *http.Response @@ -15246,6 +16299,14 @@ func (r V1GetNetworkRestrictionsResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1GetNetworkRestrictionsResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1PatchNetworkRestrictionsResponse struct { Body []byte HTTPResponse *http.Response @@ -15268,6 +16329,14 @@ func (r V1PatchNetworkRestrictionsResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1PatchNetworkRestrictionsResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1UpdateNetworkRestrictionsResponse struct { Body []byte HTTPResponse *http.Response @@ -15290,6 +16359,14 @@ func (r V1UpdateNetworkRestrictionsResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1UpdateNetworkRestrictionsResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1PauseAProjectResponse struct { Body []byte HTTPResponse *http.Response @@ -15311,6 +16388,14 @@ func (r V1PauseAProjectResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1PauseAProjectResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1GetPgsodiumConfigResponse struct { Body []byte HTTPResponse *http.Response @@ -15333,6 +16418,14 @@ func (r V1GetPgsodiumConfigResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1GetPgsodiumConfigResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1UpdatePgsodiumConfigResponse struct { Body []byte HTTPResponse *http.Response @@ -15355,6 +16448,14 @@ func (r V1UpdatePgsodiumConfigResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1UpdatePgsodiumConfigResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1GetPostgrestServiceConfigResponse struct { Body []byte HTTPResponse *http.Response @@ -15377,6 +16478,14 @@ func (r V1GetPostgrestServiceConfigResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1GetPostgrestServiceConfigResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1UpdatePostgrestServiceConfigResponse struct { Body []byte HTTPResponse *http.Response @@ -15399,6 +16508,14 @@ func (r V1UpdatePostgrestServiceConfigResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1UpdatePostgrestServiceConfigResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1RemoveAReadReplicaResponse struct { Body []byte HTTPResponse *http.Response @@ -15420,6 +16537,14 @@ func (r V1RemoveAReadReplicaResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1RemoveAReadReplicaResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1SetupAReadReplicaResponse struct { Body []byte HTTPResponse *http.Response @@ -15442,6 +16567,14 @@ func (r V1SetupAReadReplicaResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1SetupAReadReplicaResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1GetReadonlyModeStatusResponse struct { Body []byte HTTPResponse *http.Response @@ -15464,6 +16597,14 @@ func (r V1GetReadonlyModeStatusResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1GetReadonlyModeStatusResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1DisableReadonlyModeTemporarilyResponse struct { Body []byte HTTPResponse *http.Response @@ -15485,6 +16626,14 @@ func (r V1DisableReadonlyModeTemporarilyResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1DisableReadonlyModeTemporarilyResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1RestartAProjectResponse struct { Body []byte HTTPResponse *http.Response @@ -15506,6 +16655,14 @@ func (r V1RestartAProjectResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1RestartAProjectResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1ListAvailableRestoreVersionsResponse struct { Body []byte HTTPResponse *http.Response @@ -15528,6 +16685,14 @@ func (r V1ListAvailableRestoreVersionsResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1ListAvailableRestoreVersionsResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1RestoreAProjectResponse struct { Body []byte HTTPResponse *http.Response @@ -15549,6 +16714,14 @@ func (r V1RestoreAProjectResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1RestoreAProjectResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1CancelAProjectRestorationResponse struct { Body []byte HTTPResponse *http.Response @@ -15570,6 +16743,14 @@ func (r V1CancelAProjectRestorationResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1CancelAProjectRestorationResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1BulkDeleteSecretsResponse struct { Body []byte HTTPResponse *http.Response @@ -15591,6 +16772,14 @@ func (r V1BulkDeleteSecretsResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1BulkDeleteSecretsResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1ListAllSecretsResponse struct { Body []byte HTTPResponse *http.Response @@ -15613,6 +16802,14 @@ func (r V1ListAllSecretsResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1ListAllSecretsResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1BulkCreateSecretsResponse struct { Body []byte HTTPResponse *http.Response @@ -15634,6 +16831,14 @@ func (r V1BulkCreateSecretsResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1BulkCreateSecretsResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1GetSslEnforcementConfigResponse struct { Body []byte HTTPResponse *http.Response @@ -15656,6 +16861,14 @@ func (r V1GetSslEnforcementConfigResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1GetSslEnforcementConfigResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1UpdateSslEnforcementConfigResponse struct { Body []byte HTTPResponse *http.Response @@ -15678,6 +16891,14 @@ func (r V1UpdateSslEnforcementConfigResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1UpdateSslEnforcementConfigResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1ListAllBucketsResponse struct { Body []byte HTTPResponse *http.Response @@ -15700,6 +16921,14 @@ func (r V1ListAllBucketsResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1ListAllBucketsResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1GenerateTypescriptTypesResponse struct { Body []byte HTTPResponse *http.Response @@ -15722,6 +16951,14 @@ func (r V1GenerateTypescriptTypesResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1GenerateTypescriptTypesResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1UpgradePostgresVersionResponse struct { Body []byte HTTPResponse *http.Response @@ -15744,6 +16981,14 @@ func (r V1UpgradePostgresVersionResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1UpgradePostgresVersionResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1GetPostgresUpgradeEligibilityResponse struct { Body []byte HTTPResponse *http.Response @@ -15766,6 +17011,14 @@ func (r V1GetPostgresUpgradeEligibilityResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1GetPostgresUpgradeEligibilityResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1GetPostgresUpgradeStatusResponse struct { Body []byte HTTPResponse *http.Response @@ -15788,6 +17041,14 @@ func (r V1GetPostgresUpgradeStatusResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1GetPostgresUpgradeStatusResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1DeactivateVanitySubdomainConfigResponse struct { Body []byte HTTPResponse *http.Response @@ -15809,6 +17070,14 @@ func (r V1DeactivateVanitySubdomainConfigResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1DeactivateVanitySubdomainConfigResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1GetVanitySubdomainConfigResponse struct { Body []byte HTTPResponse *http.Response @@ -15832,6 +17101,14 @@ func (r V1GetVanitySubdomainConfigResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1GetVanitySubdomainConfigResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1ActivateVanitySubdomainConfigResponse struct { Body []byte HTTPResponse *http.Response @@ -15855,6 +17132,14 @@ func (r V1ActivateVanitySubdomainConfigResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1ActivateVanitySubdomainConfigResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1CheckVanitySubdomainAvailabilityResponse struct { Body []byte HTTPResponse *http.Response @@ -15878,6 +17163,14 @@ func (r V1CheckVanitySubdomainAvailabilityResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1CheckVanitySubdomainAvailabilityResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1ListAllSnippetsResponse struct { Body []byte HTTPResponse *http.Response @@ -15900,6 +17193,14 @@ func (r V1ListAllSnippetsResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1ListAllSnippetsResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type V1GetASnippetResponse struct { Body []byte HTTPResponse *http.Response @@ -15922,6 +17223,14 @@ func (r V1GetASnippetResponse) StatusCode() int { return 0 } +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r V1GetASnippetResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + // V1DeleteABranchWithResponse request returning *V1DeleteABranchResponse func (c *ClientWithResponses) V1DeleteABranchWithResponse(ctx context.Context, branchIdOrRef string, params *V1DeleteABranchParams, reqEditors ...RequestEditorFn) (*V1DeleteABranchResponse, error) { rsp, err := c.V1DeleteABranch(ctx, branchIdOrRef, params, reqEditors...) diff --git a/apps/cli-go/pkg/api/types.gen.go b/apps/cli-go/pkg/api/types.gen.go index bd0961de95..315c1560c9 100644 --- a/apps/cli-go/pkg/api/types.gen.go +++ b/apps/cli-go/pkg/api/types.gen.go @@ -1,6 +1,6 @@ // Package api provides primitives to interact with the openapi HTTP API. // -// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.4.1 DO NOT EDIT. +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.7.1 DO NOT EDIT. package api import ( @@ -14,9 +14,7 @@ import ( ) const ( - BearerScopes = "bearer.Scopes" - Fga_permissionsScopes = "fga_permissions.Scopes" - Oauth2Scopes = "oauth2.Scopes" + BearerScopes bearerContextKey = "bearer.Scopes" ) // Defines values for ActionRunResponseRunStepsName. @@ -30,6 +28,28 @@ const ( ActionRunResponseRunStepsNameSeed ActionRunResponseRunStepsName = "seed" ) +// Valid indicates whether the value is a known member of the ActionRunResponseRunStepsName enum. +func (e ActionRunResponseRunStepsName) Valid() bool { + switch e { + case ActionRunResponseRunStepsNameClone: + return true + case ActionRunResponseRunStepsNameConfigure: + return true + case ActionRunResponseRunStepsNameDeploy: + return true + case ActionRunResponseRunStepsNameHealth: + return true + case ActionRunResponseRunStepsNameMigrate: + return true + case ActionRunResponseRunStepsNamePull: + return true + case ActionRunResponseRunStepsNameSeed: + return true + default: + return false + } +} + // Defines values for ActionRunResponseRunStepsStatus. const ( ActionRunResponseRunStepsStatusCREATED ActionRunResponseRunStepsStatus = "CREATED" @@ -41,6 +61,28 @@ const ( ActionRunResponseRunStepsStatusRUNNING ActionRunResponseRunStepsStatus = "RUNNING" ) +// Valid indicates whether the value is a known member of the ActionRunResponseRunStepsStatus enum. +func (e ActionRunResponseRunStepsStatus) Valid() bool { + switch e { + case ActionRunResponseRunStepsStatusCREATED: + return true + case ActionRunResponseRunStepsStatusDEAD: + return true + case ActionRunResponseRunStepsStatusEXITED: + return true + case ActionRunResponseRunStepsStatusPAUSED: + return true + case ActionRunResponseRunStepsStatusREMOVING: + return true + case ActionRunResponseRunStepsStatusRESTARTING: + return true + case ActionRunResponseRunStepsStatusRUNNING: + return true + default: + return false + } +} + // Defines values for ApiKeyResponseType. const ( ApiKeyResponseTypeLegacy ApiKeyResponseType = "legacy" @@ -48,6 +90,20 @@ const ( ApiKeyResponseTypeSecret ApiKeyResponseType = "secret" ) +// Valid indicates whether the value is a known member of the ApiKeyResponseType enum. +func (e ApiKeyResponseType) Valid() bool { + switch e { + case ApiKeyResponseTypeLegacy: + return true + case ApiKeyResponseTypePublishable: + return true + case ApiKeyResponseTypeSecret: + return true + default: + return false + } +} + // Defines values for ApplyProjectAddonBodyAddonType. const ( ApplyProjectAddonBodyAddonTypeAuthMfaPhone ApplyProjectAddonBodyAddonType = "auth_mfa_phone" @@ -60,6 +116,30 @@ const ( ApplyProjectAddonBodyAddonTypePitr ApplyProjectAddonBodyAddonType = "pitr" ) +// Valid indicates whether the value is a known member of the ApplyProjectAddonBodyAddonType enum. +func (e ApplyProjectAddonBodyAddonType) Valid() bool { + switch e { + case ApplyProjectAddonBodyAddonTypeAuthMfaPhone: + return true + case ApplyProjectAddonBodyAddonTypeAuthMfaWebAuthn: + return true + case ApplyProjectAddonBodyAddonTypeComputeInstance: + return true + case ApplyProjectAddonBodyAddonTypeCustomDomain: + return true + case ApplyProjectAddonBodyAddonTypeEtlPipeline: + return true + case ApplyProjectAddonBodyAddonTypeIpv4: + return true + case ApplyProjectAddonBodyAddonTypeLogDrain: + return true + case ApplyProjectAddonBodyAddonTypePitr: + return true + default: + return false + } +} + // Defines values for ApplyProjectAddonBodyAddonVariant0. const ( ApplyProjectAddonBodyAddonVariant0Ci12xlarge ApplyProjectAddonBodyAddonVariant0 = "ci_12xlarge" @@ -82,11 +162,65 @@ const ( ApplyProjectAddonBodyAddonVariant0CiXlarge ApplyProjectAddonBodyAddonVariant0 = "ci_xlarge" ) +// Valid indicates whether the value is a known member of the ApplyProjectAddonBodyAddonVariant0 enum. +func (e ApplyProjectAddonBodyAddonVariant0) Valid() bool { + switch e { + case ApplyProjectAddonBodyAddonVariant0Ci12xlarge: + return true + case ApplyProjectAddonBodyAddonVariant0Ci16xlarge: + return true + case ApplyProjectAddonBodyAddonVariant0Ci24xlarge: + return true + case ApplyProjectAddonBodyAddonVariant0Ci24xlargeHighMemory: + return true + case ApplyProjectAddonBodyAddonVariant0Ci24xlargeOptimizedCpu: + return true + case ApplyProjectAddonBodyAddonVariant0Ci24xlargeOptimizedMemory: + return true + case ApplyProjectAddonBodyAddonVariant0Ci2xlarge: + return true + case ApplyProjectAddonBodyAddonVariant0Ci48xlarge: + return true + case ApplyProjectAddonBodyAddonVariant0Ci48xlargeHighMemory: + return true + case ApplyProjectAddonBodyAddonVariant0Ci48xlargeOptimizedCpu: + return true + case ApplyProjectAddonBodyAddonVariant0Ci48xlargeOptimizedMemory: + return true + case ApplyProjectAddonBodyAddonVariant0Ci4xlarge: + return true + case ApplyProjectAddonBodyAddonVariant0Ci8xlarge: + return true + case ApplyProjectAddonBodyAddonVariant0CiLarge: + return true + case ApplyProjectAddonBodyAddonVariant0CiMedium: + return true + case ApplyProjectAddonBodyAddonVariant0CiMicro: + return true + case ApplyProjectAddonBodyAddonVariant0CiSmall: + return true + case ApplyProjectAddonBodyAddonVariant0CiXlarge: + return true + default: + return false + } +} + // Defines values for ApplyProjectAddonBodyAddonVariant1. const ( ApplyProjectAddonBodyAddonVariant1CdDefault ApplyProjectAddonBodyAddonVariant1 = "cd_default" ) +// Valid indicates whether the value is a known member of the ApplyProjectAddonBodyAddonVariant1 enum. +func (e ApplyProjectAddonBodyAddonVariant1) Valid() bool { + switch e { + case ApplyProjectAddonBodyAddonVariant1CdDefault: + return true + default: + return false + } +} + // Defines values for ApplyProjectAddonBodyAddonVariant2. const ( ApplyProjectAddonBodyAddonVariant2Pitr14 ApplyProjectAddonBodyAddonVariant2 = "pitr_14" @@ -94,17 +228,53 @@ const ( ApplyProjectAddonBodyAddonVariant2Pitr7 ApplyProjectAddonBodyAddonVariant2 = "pitr_7" ) +// Valid indicates whether the value is a known member of the ApplyProjectAddonBodyAddonVariant2 enum. +func (e ApplyProjectAddonBodyAddonVariant2) Valid() bool { + switch e { + case ApplyProjectAddonBodyAddonVariant2Pitr14: + return true + case ApplyProjectAddonBodyAddonVariant2Pitr28: + return true + case ApplyProjectAddonBodyAddonVariant2Pitr7: + return true + default: + return false + } +} + // Defines values for ApplyProjectAddonBodyAddonVariant3. const ( ApplyProjectAddonBodyAddonVariant3Ipv4Default ApplyProjectAddonBodyAddonVariant3 = "ipv4_default" ) +// Valid indicates whether the value is a known member of the ApplyProjectAddonBodyAddonVariant3 enum. +func (e ApplyProjectAddonBodyAddonVariant3) Valid() bool { + switch e { + case ApplyProjectAddonBodyAddonVariant3Ipv4Default: + return true + default: + return false + } +} + // Defines values for AuthConfigResponseDbMaxPoolSizeUnit. const ( AuthConfigResponseDbMaxPoolSizeUnitConnections AuthConfigResponseDbMaxPoolSizeUnit = "connections" AuthConfigResponseDbMaxPoolSizeUnitPercent AuthConfigResponseDbMaxPoolSizeUnit = "percent" ) +// Valid indicates whether the value is a known member of the AuthConfigResponseDbMaxPoolSizeUnit enum. +func (e AuthConfigResponseDbMaxPoolSizeUnit) Valid() bool { + switch e { + case AuthConfigResponseDbMaxPoolSizeUnitConnections: + return true + case AuthConfigResponseDbMaxPoolSizeUnitPercent: + return true + default: + return false + } +} + // Defines values for AuthConfigResponsePasswordRequiredCharacters. const ( AuthConfigResponsePasswordRequiredCharactersAbcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 AuthConfigResponsePasswordRequiredCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ:0123456789" @@ -113,12 +283,40 @@ const ( AuthConfigResponsePasswordRequiredCharactersEmpty AuthConfigResponsePasswordRequiredCharacters = "" ) +// Valid indicates whether the value is a known member of the AuthConfigResponsePasswordRequiredCharacters enum. +func (e AuthConfigResponsePasswordRequiredCharacters) Valid() bool { + switch e { + case AuthConfigResponsePasswordRequiredCharactersAbcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789: + return true + case AuthConfigResponsePasswordRequiredCharactersAbcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567891: + return true + case AuthConfigResponsePasswordRequiredCharactersAbcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567892: + return true + case AuthConfigResponsePasswordRequiredCharactersEmpty: + return true + default: + return false + } +} + // Defines values for AuthConfigResponseSecurityCaptchaProvider. const ( AuthConfigResponseSecurityCaptchaProviderHcaptcha AuthConfigResponseSecurityCaptchaProvider = "hcaptcha" AuthConfigResponseSecurityCaptchaProviderTurnstile AuthConfigResponseSecurityCaptchaProvider = "turnstile" ) +// Valid indicates whether the value is a known member of the AuthConfigResponseSecurityCaptchaProvider enum. +func (e AuthConfigResponseSecurityCaptchaProvider) Valid() bool { + switch e { + case AuthConfigResponseSecurityCaptchaProviderHcaptcha: + return true + case AuthConfigResponseSecurityCaptchaProviderTurnstile: + return true + default: + return false + } +} + // Defines values for AuthConfigResponseSmsProvider. const ( AuthConfigResponseSmsProviderMessagebird AuthConfigResponseSmsProvider = "messagebird" @@ -128,11 +326,39 @@ const ( AuthConfigResponseSmsProviderVonage AuthConfigResponseSmsProvider = "vonage" ) +// Valid indicates whether the value is a known member of the AuthConfigResponseSmsProvider enum. +func (e AuthConfigResponseSmsProvider) Valid() bool { + switch e { + case AuthConfigResponseSmsProviderMessagebird: + return true + case AuthConfigResponseSmsProviderTextlocal: + return true + case AuthConfigResponseSmsProviderTwilio: + return true + case AuthConfigResponseSmsProviderTwilioVerify: + return true + case AuthConfigResponseSmsProviderVonage: + return true + default: + return false + } +} + // Defines values for BranchDeleteResponseMessage. const ( BranchDeleteResponseMessageOk BranchDeleteResponseMessage = "ok" ) +// Valid indicates whether the value is a known member of the BranchDeleteResponseMessage enum. +func (e BranchDeleteResponseMessage) Valid() bool { + switch e { + case BranchDeleteResponseMessageOk: + return true + default: + return false + } +} + // Defines values for BranchDetailResponseStatus. const ( BranchDetailResponseStatusACTIVEHEALTHY BranchDetailResponseStatus = "ACTIVE_HEALTHY" @@ -152,6 +378,44 @@ const ( BranchDetailResponseStatusUPGRADING BranchDetailResponseStatus = "UPGRADING" ) +// Valid indicates whether the value is a known member of the BranchDetailResponseStatus enum. +func (e BranchDetailResponseStatus) Valid() bool { + switch e { + case BranchDetailResponseStatusACTIVEHEALTHY: + return true + case BranchDetailResponseStatusACTIVEUNHEALTHY: + return true + case BranchDetailResponseStatusCOMINGUP: + return true + case BranchDetailResponseStatusGOINGDOWN: + return true + case BranchDetailResponseStatusINACTIVE: + return true + case BranchDetailResponseStatusINITFAILED: + return true + case BranchDetailResponseStatusPAUSEFAILED: + return true + case BranchDetailResponseStatusPAUSING: + return true + case BranchDetailResponseStatusREMOVED: + return true + case BranchDetailResponseStatusRESIZING: + return true + case BranchDetailResponseStatusRESTARTING: + return true + case BranchDetailResponseStatusRESTOREFAILED: + return true + case BranchDetailResponseStatusRESTORING: + return true + case BranchDetailResponseStatusUNKNOWN: + return true + case BranchDetailResponseStatusUPGRADING: + return true + default: + return false + } +} + // Defines values for BranchResponsePreviewProjectStatus. const ( BranchResponsePreviewProjectStatusACTIVEHEALTHY BranchResponsePreviewProjectStatus = "ACTIVE_HEALTHY" @@ -171,6 +435,44 @@ const ( BranchResponsePreviewProjectStatusUPGRADING BranchResponsePreviewProjectStatus = "UPGRADING" ) +// Valid indicates whether the value is a known member of the BranchResponsePreviewProjectStatus enum. +func (e BranchResponsePreviewProjectStatus) Valid() bool { + switch e { + case BranchResponsePreviewProjectStatusACTIVEHEALTHY: + return true + case BranchResponsePreviewProjectStatusACTIVEUNHEALTHY: + return true + case BranchResponsePreviewProjectStatusCOMINGUP: + return true + case BranchResponsePreviewProjectStatusGOINGDOWN: + return true + case BranchResponsePreviewProjectStatusINACTIVE: + return true + case BranchResponsePreviewProjectStatusINITFAILED: + return true + case BranchResponsePreviewProjectStatusPAUSEFAILED: + return true + case BranchResponsePreviewProjectStatusPAUSING: + return true + case BranchResponsePreviewProjectStatusREMOVED: + return true + case BranchResponsePreviewProjectStatusRESIZING: + return true + case BranchResponsePreviewProjectStatusRESTARTING: + return true + case BranchResponsePreviewProjectStatusRESTOREFAILED: + return true + case BranchResponsePreviewProjectStatusRESTORING: + return true + case BranchResponsePreviewProjectStatusUNKNOWN: + return true + case BranchResponsePreviewProjectStatusUPGRADING: + return true + default: + return false + } +} + // Defines values for BranchResponseStatus. const ( BranchResponseStatusCREATINGPROJECT BranchResponseStatus = "CREATING_PROJECT" @@ -181,16 +483,56 @@ const ( BranchResponseStatusRUNNINGMIGRATIONS BranchResponseStatus = "RUNNING_MIGRATIONS" ) +// Valid indicates whether the value is a known member of the BranchResponseStatus enum. +func (e BranchResponseStatus) Valid() bool { + switch e { + case BranchResponseStatusCREATINGPROJECT: + return true + case BranchResponseStatusFUNCTIONSDEPLOYED: + return true + case BranchResponseStatusFUNCTIONSFAILED: + return true + case BranchResponseStatusMIGRATIONSFAILED: + return true + case BranchResponseStatusMIGRATIONSPASSED: + return true + case BranchResponseStatusRUNNINGMIGRATIONS: + return true + default: + return false + } +} + // Defines values for BranchRestoreResponseMessage. const ( BranchRestorationInitiated BranchRestoreResponseMessage = "Branch restoration initiated" ) +// Valid indicates whether the value is a known member of the BranchRestoreResponseMessage enum. +func (e BranchRestoreResponseMessage) Valid() bool { + switch e { + case BranchRestorationInitiated: + return true + default: + return false + } +} + // Defines values for BranchUpdateResponseMessage. const ( BranchUpdateResponseMessageOk BranchUpdateResponseMessage = "ok" ) +// Valid indicates whether the value is a known member of the BranchUpdateResponseMessage enum. +func (e BranchUpdateResponseMessage) Valid() bool { + switch e { + case BranchUpdateResponseMessageOk: + return true + default: + return false + } +} + // Defines values for BulkUpdateFunctionBodyStatus. const ( BulkUpdateFunctionBodyStatusACTIVE BulkUpdateFunctionBodyStatus = "ACTIVE" @@ -198,6 +540,20 @@ const ( BulkUpdateFunctionBodyStatusTHROTTLED BulkUpdateFunctionBodyStatus = "THROTTLED" ) +// Valid indicates whether the value is a known member of the BulkUpdateFunctionBodyStatus enum. +func (e BulkUpdateFunctionBodyStatus) Valid() bool { + switch e { + case BulkUpdateFunctionBodyStatusACTIVE: + return true + case BulkUpdateFunctionBodyStatusREMOVED: + return true + case BulkUpdateFunctionBodyStatusTHROTTLED: + return true + default: + return false + } +} + // Defines values for BulkUpdateFunctionResponseFunctionsStatus. const ( BulkUpdateFunctionResponseFunctionsStatusACTIVE BulkUpdateFunctionResponseFunctionsStatus = "ACTIVE" @@ -205,12 +561,38 @@ const ( BulkUpdateFunctionResponseFunctionsStatusTHROTTLED BulkUpdateFunctionResponseFunctionsStatus = "THROTTLED" ) +// Valid indicates whether the value is a known member of the BulkUpdateFunctionResponseFunctionsStatus enum. +func (e BulkUpdateFunctionResponseFunctionsStatus) Valid() bool { + switch e { + case BulkUpdateFunctionResponseFunctionsStatusACTIVE: + return true + case BulkUpdateFunctionResponseFunctionsStatusREMOVED: + return true + case BulkUpdateFunctionResponseFunctionsStatusTHROTTLED: + return true + default: + return false + } +} + // Defines values for CreateApiKeyBodyType. const ( CreateApiKeyBodyTypePublishable CreateApiKeyBodyType = "publishable" CreateApiKeyBodyTypeSecret CreateApiKeyBodyType = "secret" ) +// Valid indicates whether the value is a known member of the CreateApiKeyBodyType enum. +func (e CreateApiKeyBodyType) Valid() bool { + switch e { + case CreateApiKeyBodyTypePublishable: + return true + case CreateApiKeyBodyTypeSecret: + return true + default: + return false + } +} + // Defines values for CreateBranchBodyDesiredInstanceSize. const ( CreateBranchBodyDesiredInstanceSizeLarge CreateBranchBodyDesiredInstanceSize = "large" @@ -235,6 +617,54 @@ const ( CreateBranchBodyDesiredInstanceSizeXlarge CreateBranchBodyDesiredInstanceSize = "xlarge" ) +// Valid indicates whether the value is a known member of the CreateBranchBodyDesiredInstanceSize enum. +func (e CreateBranchBodyDesiredInstanceSize) Valid() bool { + switch e { + case CreateBranchBodyDesiredInstanceSizeLarge: + return true + case CreateBranchBodyDesiredInstanceSizeMedium: + return true + case CreateBranchBodyDesiredInstanceSizeMicro: + return true + case CreateBranchBodyDesiredInstanceSizeN12xlarge: + return true + case CreateBranchBodyDesiredInstanceSizeN16xlarge: + return true + case CreateBranchBodyDesiredInstanceSizeN24xlarge: + return true + case CreateBranchBodyDesiredInstanceSizeN24xlargeHighMemory: + return true + case CreateBranchBodyDesiredInstanceSizeN24xlargeOptimizedCpu: + return true + case CreateBranchBodyDesiredInstanceSizeN24xlargeOptimizedMemory: + return true + case CreateBranchBodyDesiredInstanceSizeN2xlarge: + return true + case CreateBranchBodyDesiredInstanceSizeN48xlarge: + return true + case CreateBranchBodyDesiredInstanceSizeN48xlargeHighMemory: + return true + case CreateBranchBodyDesiredInstanceSizeN48xlargeOptimizedCpu: + return true + case CreateBranchBodyDesiredInstanceSizeN48xlargeOptimizedMemory: + return true + case CreateBranchBodyDesiredInstanceSizeN4xlarge: + return true + case CreateBranchBodyDesiredInstanceSizeN8xlarge: + return true + case CreateBranchBodyDesiredInstanceSizeNano: + return true + case CreateBranchBodyDesiredInstanceSizePico: + return true + case CreateBranchBodyDesiredInstanceSizeSmall: + return true + case CreateBranchBodyDesiredInstanceSizeXlarge: + return true + default: + return false + } +} + // Defines values for CreateBranchBodyPostgresEngine. const ( CreateBranchBodyPostgresEngineN15 CreateBranchBodyPostgresEngine = "15" @@ -242,6 +672,20 @@ const ( CreateBranchBodyPostgresEngineN17Oriole CreateBranchBodyPostgresEngine = "17-oriole" ) +// Valid indicates whether the value is a known member of the CreateBranchBodyPostgresEngine enum. +func (e CreateBranchBodyPostgresEngine) Valid() bool { + switch e { + case CreateBranchBodyPostgresEngineN15: + return true + case CreateBranchBodyPostgresEngineN17: + return true + case CreateBranchBodyPostgresEngineN17Oriole: + return true + default: + return false + } +} + // Defines values for CreateBranchBodyReleaseChannel. const ( CreateBranchBodyReleaseChannelAlpha CreateBranchBodyReleaseChannel = "alpha" @@ -252,6 +696,26 @@ const ( CreateBranchBodyReleaseChannelWithdrawn CreateBranchBodyReleaseChannel = "withdrawn" ) +// Valid indicates whether the value is a known member of the CreateBranchBodyReleaseChannel enum. +func (e CreateBranchBodyReleaseChannel) Valid() bool { + switch e { + case CreateBranchBodyReleaseChannelAlpha: + return true + case CreateBranchBodyReleaseChannelBeta: + return true + case CreateBranchBodyReleaseChannelGa: + return true + case CreateBranchBodyReleaseChannelInternal: + return true + case CreateBranchBodyReleaseChannelPreview: + return true + case CreateBranchBodyReleaseChannelWithdrawn: + return true + default: + return false + } +} + // Defines values for CreateProviderBodyNameIdFormat. const ( CreateProviderBodyNameIdFormatUrnOasisNamesTcSAML11NameidFormatEmailAddress CreateProviderBodyNameIdFormat = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" @@ -260,11 +724,37 @@ const ( CreateProviderBodyNameIdFormatUrnOasisNamesTcSAML20NameidFormatTransient CreateProviderBodyNameIdFormat = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient" ) +// Valid indicates whether the value is a known member of the CreateProviderBodyNameIdFormat enum. +func (e CreateProviderBodyNameIdFormat) Valid() bool { + switch e { + case CreateProviderBodyNameIdFormatUrnOasisNamesTcSAML11NameidFormatEmailAddress: + return true + case CreateProviderBodyNameIdFormatUrnOasisNamesTcSAML11NameidFormatUnspecified: + return true + case CreateProviderBodyNameIdFormatUrnOasisNamesTcSAML20NameidFormatPersistent: + return true + case CreateProviderBodyNameIdFormatUrnOasisNamesTcSAML20NameidFormatTransient: + return true + default: + return false + } +} + // Defines values for CreateProviderBodyType. const ( Saml CreateProviderBodyType = "saml" ) +// Valid indicates whether the value is a known member of the CreateProviderBodyType enum. +func (e CreateProviderBodyType) Valid() bool { + switch e { + case Saml: + return true + default: + return false + } +} + // Defines values for CreateSigningKeyBodyAlgorithm. const ( CreateSigningKeyBodyAlgorithmES256 CreateSigningKeyBodyAlgorithm = "ES256" @@ -273,131 +763,397 @@ const ( CreateSigningKeyBodyAlgorithmRS256 CreateSigningKeyBodyAlgorithm = "RS256" ) +// Valid indicates whether the value is a known member of the CreateSigningKeyBodyAlgorithm enum. +func (e CreateSigningKeyBodyAlgorithm) Valid() bool { + switch e { + case CreateSigningKeyBodyAlgorithmES256: + return true + case CreateSigningKeyBodyAlgorithmEdDSA: + return true + case CreateSigningKeyBodyAlgorithmHS256: + return true + case CreateSigningKeyBodyAlgorithmRS256: + return true + default: + return false + } +} + // Defines values for CreateSigningKeyBodyPrivateJwk0Alg. const ( CreateSigningKeyBodyPrivateJwk0AlgRS256 CreateSigningKeyBodyPrivateJwk0Alg = "RS256" ) +// Valid indicates whether the value is a known member of the CreateSigningKeyBodyPrivateJwk0Alg enum. +func (e CreateSigningKeyBodyPrivateJwk0Alg) Valid() bool { + switch e { + case CreateSigningKeyBodyPrivateJwk0AlgRS256: + return true + default: + return false + } +} + // Defines values for CreateSigningKeyBodyPrivateJwk0E. const ( AQAB CreateSigningKeyBodyPrivateJwk0E = "AQAB" ) +// Valid indicates whether the value is a known member of the CreateSigningKeyBodyPrivateJwk0E enum. +func (e CreateSigningKeyBodyPrivateJwk0E) Valid() bool { + switch e { + case AQAB: + return true + default: + return false + } +} + // Defines values for CreateSigningKeyBodyPrivateJwk0Ext. const ( CreateSigningKeyBodyPrivateJwk0ExtTrue CreateSigningKeyBodyPrivateJwk0Ext = true ) +// Valid indicates whether the value is a known member of the CreateSigningKeyBodyPrivateJwk0Ext enum. +func (e CreateSigningKeyBodyPrivateJwk0Ext) Valid() bool { + switch e { + case CreateSigningKeyBodyPrivateJwk0ExtTrue: + return true + default: + return false + } +} + // Defines values for CreateSigningKeyBodyPrivateJwk0KeyOps. const ( CreateSigningKeyBodyPrivateJwk0KeyOpsSign CreateSigningKeyBodyPrivateJwk0KeyOps = "sign" CreateSigningKeyBodyPrivateJwk0KeyOpsVerify CreateSigningKeyBodyPrivateJwk0KeyOps = "verify" ) +// Valid indicates whether the value is a known member of the CreateSigningKeyBodyPrivateJwk0KeyOps enum. +func (e CreateSigningKeyBodyPrivateJwk0KeyOps) Valid() bool { + switch e { + case CreateSigningKeyBodyPrivateJwk0KeyOpsSign: + return true + case CreateSigningKeyBodyPrivateJwk0KeyOpsVerify: + return true + default: + return false + } +} + // Defines values for CreateSigningKeyBodyPrivateJwk0Kty. const ( RSA CreateSigningKeyBodyPrivateJwk0Kty = "RSA" ) +// Valid indicates whether the value is a known member of the CreateSigningKeyBodyPrivateJwk0Kty enum. +func (e CreateSigningKeyBodyPrivateJwk0Kty) Valid() bool { + switch e { + case RSA: + return true + default: + return false + } +} + // Defines values for CreateSigningKeyBodyPrivateJwk0Use. const ( CreateSigningKeyBodyPrivateJwk0UseSig CreateSigningKeyBodyPrivateJwk0Use = "sig" ) +// Valid indicates whether the value is a known member of the CreateSigningKeyBodyPrivateJwk0Use enum. +func (e CreateSigningKeyBodyPrivateJwk0Use) Valid() bool { + switch e { + case CreateSigningKeyBodyPrivateJwk0UseSig: + return true + default: + return false + } +} + // Defines values for CreateSigningKeyBodyPrivateJwk1Alg. const ( CreateSigningKeyBodyPrivateJwk1AlgES256 CreateSigningKeyBodyPrivateJwk1Alg = "ES256" ) +// Valid indicates whether the value is a known member of the CreateSigningKeyBodyPrivateJwk1Alg enum. +func (e CreateSigningKeyBodyPrivateJwk1Alg) Valid() bool { + switch e { + case CreateSigningKeyBodyPrivateJwk1AlgES256: + return true + default: + return false + } +} + // Defines values for CreateSigningKeyBodyPrivateJwk1Crv. const ( P256 CreateSigningKeyBodyPrivateJwk1Crv = "P-256" ) +// Valid indicates whether the value is a known member of the CreateSigningKeyBodyPrivateJwk1Crv enum. +func (e CreateSigningKeyBodyPrivateJwk1Crv) Valid() bool { + switch e { + case P256: + return true + default: + return false + } +} + // Defines values for CreateSigningKeyBodyPrivateJwk1Ext. const ( CreateSigningKeyBodyPrivateJwk1ExtTrue CreateSigningKeyBodyPrivateJwk1Ext = true ) +// Valid indicates whether the value is a known member of the CreateSigningKeyBodyPrivateJwk1Ext enum. +func (e CreateSigningKeyBodyPrivateJwk1Ext) Valid() bool { + switch e { + case CreateSigningKeyBodyPrivateJwk1ExtTrue: + return true + default: + return false + } +} + // Defines values for CreateSigningKeyBodyPrivateJwk1KeyOps. const ( CreateSigningKeyBodyPrivateJwk1KeyOpsSign CreateSigningKeyBodyPrivateJwk1KeyOps = "sign" CreateSigningKeyBodyPrivateJwk1KeyOpsVerify CreateSigningKeyBodyPrivateJwk1KeyOps = "verify" ) +// Valid indicates whether the value is a known member of the CreateSigningKeyBodyPrivateJwk1KeyOps enum. +func (e CreateSigningKeyBodyPrivateJwk1KeyOps) Valid() bool { + switch e { + case CreateSigningKeyBodyPrivateJwk1KeyOpsSign: + return true + case CreateSigningKeyBodyPrivateJwk1KeyOpsVerify: + return true + default: + return false + } +} + // Defines values for CreateSigningKeyBodyPrivateJwk1Kty. const ( EC CreateSigningKeyBodyPrivateJwk1Kty = "EC" ) +// Valid indicates whether the value is a known member of the CreateSigningKeyBodyPrivateJwk1Kty enum. +func (e CreateSigningKeyBodyPrivateJwk1Kty) Valid() bool { + switch e { + case EC: + return true + default: + return false + } +} + // Defines values for CreateSigningKeyBodyPrivateJwk1Use. const ( CreateSigningKeyBodyPrivateJwk1UseSig CreateSigningKeyBodyPrivateJwk1Use = "sig" ) +// Valid indicates whether the value is a known member of the CreateSigningKeyBodyPrivateJwk1Use enum. +func (e CreateSigningKeyBodyPrivateJwk1Use) Valid() bool { + switch e { + case CreateSigningKeyBodyPrivateJwk1UseSig: + return true + default: + return false + } +} + // Defines values for CreateSigningKeyBodyPrivateJwk2Alg. const ( CreateSigningKeyBodyPrivateJwk2AlgEdDSA CreateSigningKeyBodyPrivateJwk2Alg = "EdDSA" ) +// Valid indicates whether the value is a known member of the CreateSigningKeyBodyPrivateJwk2Alg enum. +func (e CreateSigningKeyBodyPrivateJwk2Alg) Valid() bool { + switch e { + case CreateSigningKeyBodyPrivateJwk2AlgEdDSA: + return true + default: + return false + } +} + // Defines values for CreateSigningKeyBodyPrivateJwk2Crv. const ( Ed25519 CreateSigningKeyBodyPrivateJwk2Crv = "Ed25519" ) +// Valid indicates whether the value is a known member of the CreateSigningKeyBodyPrivateJwk2Crv enum. +func (e CreateSigningKeyBodyPrivateJwk2Crv) Valid() bool { + switch e { + case Ed25519: + return true + default: + return false + } +} + // Defines values for CreateSigningKeyBodyPrivateJwk2Ext. const ( CreateSigningKeyBodyPrivateJwk2ExtTrue CreateSigningKeyBodyPrivateJwk2Ext = true ) +// Valid indicates whether the value is a known member of the CreateSigningKeyBodyPrivateJwk2Ext enum. +func (e CreateSigningKeyBodyPrivateJwk2Ext) Valid() bool { + switch e { + case CreateSigningKeyBodyPrivateJwk2ExtTrue: + return true + default: + return false + } +} + // Defines values for CreateSigningKeyBodyPrivateJwk2KeyOps. const ( CreateSigningKeyBodyPrivateJwk2KeyOpsSign CreateSigningKeyBodyPrivateJwk2KeyOps = "sign" CreateSigningKeyBodyPrivateJwk2KeyOpsVerify CreateSigningKeyBodyPrivateJwk2KeyOps = "verify" ) +// Valid indicates whether the value is a known member of the CreateSigningKeyBodyPrivateJwk2KeyOps enum. +func (e CreateSigningKeyBodyPrivateJwk2KeyOps) Valid() bool { + switch e { + case CreateSigningKeyBodyPrivateJwk2KeyOpsSign: + return true + case CreateSigningKeyBodyPrivateJwk2KeyOpsVerify: + return true + default: + return false + } +} + // Defines values for CreateSigningKeyBodyPrivateJwk2Kty. const ( OKP CreateSigningKeyBodyPrivateJwk2Kty = "OKP" ) +// Valid indicates whether the value is a known member of the CreateSigningKeyBodyPrivateJwk2Kty enum. +func (e CreateSigningKeyBodyPrivateJwk2Kty) Valid() bool { + switch e { + case OKP: + return true + default: + return false + } +} + // Defines values for CreateSigningKeyBodyPrivateJwk2Use. const ( CreateSigningKeyBodyPrivateJwk2UseSig CreateSigningKeyBodyPrivateJwk2Use = "sig" ) +// Valid indicates whether the value is a known member of the CreateSigningKeyBodyPrivateJwk2Use enum. +func (e CreateSigningKeyBodyPrivateJwk2Use) Valid() bool { + switch e { + case CreateSigningKeyBodyPrivateJwk2UseSig: + return true + default: + return false + } +} + // Defines values for CreateSigningKeyBodyPrivateJwk3Alg. const ( HS256 CreateSigningKeyBodyPrivateJwk3Alg = "HS256" ) +// Valid indicates whether the value is a known member of the CreateSigningKeyBodyPrivateJwk3Alg enum. +func (e CreateSigningKeyBodyPrivateJwk3Alg) Valid() bool { + switch e { + case HS256: + return true + default: + return false + } +} + // Defines values for CreateSigningKeyBodyPrivateJwk3Ext. const ( CreateSigningKeyBodyPrivateJwk3ExtTrue CreateSigningKeyBodyPrivateJwk3Ext = true ) +// Valid indicates whether the value is a known member of the CreateSigningKeyBodyPrivateJwk3Ext enum. +func (e CreateSigningKeyBodyPrivateJwk3Ext) Valid() bool { + switch e { + case CreateSigningKeyBodyPrivateJwk3ExtTrue: + return true + default: + return false + } +} + // Defines values for CreateSigningKeyBodyPrivateJwk3KeyOps. const ( CreateSigningKeyBodyPrivateJwk3KeyOpsSign CreateSigningKeyBodyPrivateJwk3KeyOps = "sign" CreateSigningKeyBodyPrivateJwk3KeyOpsVerify CreateSigningKeyBodyPrivateJwk3KeyOps = "verify" ) +// Valid indicates whether the value is a known member of the CreateSigningKeyBodyPrivateJwk3KeyOps enum. +func (e CreateSigningKeyBodyPrivateJwk3KeyOps) Valid() bool { + switch e { + case CreateSigningKeyBodyPrivateJwk3KeyOpsSign: + return true + case CreateSigningKeyBodyPrivateJwk3KeyOpsVerify: + return true + default: + return false + } +} + // Defines values for CreateSigningKeyBodyPrivateJwk3Kty. const ( Oct CreateSigningKeyBodyPrivateJwk3Kty = "oct" ) +// Valid indicates whether the value is a known member of the CreateSigningKeyBodyPrivateJwk3Kty enum. +func (e CreateSigningKeyBodyPrivateJwk3Kty) Valid() bool { + switch e { + case Oct: + return true + default: + return false + } +} + // Defines values for CreateSigningKeyBodyPrivateJwk3Use. const ( CreateSigningKeyBodyPrivateJwk3UseSig CreateSigningKeyBodyPrivateJwk3Use = "sig" ) +// Valid indicates whether the value is a known member of the CreateSigningKeyBodyPrivateJwk3Use enum. +func (e CreateSigningKeyBodyPrivateJwk3Use) Valid() bool { + switch e { + case CreateSigningKeyBodyPrivateJwk3UseSig: + return true + default: + return false + } +} + // Defines values for CreateSigningKeyBodyStatus. const ( CreateSigningKeyBodyStatusInUse CreateSigningKeyBodyStatus = "in_use" CreateSigningKeyBodyStatusStandby CreateSigningKeyBodyStatus = "standby" ) +// Valid indicates whether the value is a known member of the CreateSigningKeyBodyStatus enum. +func (e CreateSigningKeyBodyStatus) Valid() bool { + switch e { + case CreateSigningKeyBodyStatusInUse: + return true + case CreateSigningKeyBodyStatusStandby: + return true + default: + return false + } +} + // Defines values for DatabaseUpgradeStatusResponseDatabaseUpgradeStatusError. const ( N1UpgradedInstanceLaunchFailed DatabaseUpgradeStatusResponseDatabaseUpgradeStatusError = "1_upgraded_instance_launch_failed" @@ -411,6 +1167,32 @@ const ( N9PostPhysicalBackupFailed DatabaseUpgradeStatusResponseDatabaseUpgradeStatusError = "9_post_physical_backup_failed" ) +// Valid indicates whether the value is a known member of the DatabaseUpgradeStatusResponseDatabaseUpgradeStatusError enum. +func (e DatabaseUpgradeStatusResponseDatabaseUpgradeStatusError) Valid() bool { + switch e { + case N1UpgradedInstanceLaunchFailed: + return true + case N2VolumeDetachchmentFromUpgradedInstanceFailed: + return true + case N3VolumeAttachmentToOriginalInstanceFailed: + return true + case N4DataUpgradeInitiationFailed: + return true + case N5DataUpgradeCompletionFailed: + return true + case N6VolumeDetachchmentFromOriginalInstanceFailed: + return true + case N7VolumeAttachmentToUpgradedInstanceFailed: + return true + case N8UpgradeCompletionFailed: + return true + case N9PostPhysicalBackupFailed: + return true + default: + return false + } +} + // Defines values for DatabaseUpgradeStatusResponseDatabaseUpgradeStatusProgress. const ( N0Requested DatabaseUpgradeStatusResponseDatabaseUpgradeStatusProgress = "0_requested" @@ -426,11 +1208,51 @@ const ( N9CompletedUpgrade DatabaseUpgradeStatusResponseDatabaseUpgradeStatusProgress = "9_completed_upgrade" ) +// Valid indicates whether the value is a known member of the DatabaseUpgradeStatusResponseDatabaseUpgradeStatusProgress enum. +func (e DatabaseUpgradeStatusResponseDatabaseUpgradeStatusProgress) Valid() bool { + switch e { + case N0Requested: + return true + case N10CompletedPostPhysicalBackup: + return true + case N1Started: + return true + case N2LaunchedUpgradedInstance: + return true + case N3DetachedVolumeFromUpgradedInstance: + return true + case N4AttachedVolumeToOriginalInstance: + return true + case N5InitiatedDataUpgrade: + return true + case N6CompletedDataUpgrade: + return true + case N7DetachedVolumeFromOriginalInstance: + return true + case N8AttachedVolumeToUpgradedInstance: + return true + case N9CompletedUpgrade: + return true + default: + return false + } +} + // Defines values for DeleteRolesResponseMessage. const ( DeleteRolesResponseMessageOk DeleteRolesResponseMessage = "ok" ) +// Valid indicates whether the value is a known member of the DeleteRolesResponseMessage enum. +func (e DeleteRolesResponseMessage) Valid() bool { + switch e { + case DeleteRolesResponseMessageOk: + return true + default: + return false + } +} + // Defines values for DeployFunctionResponseStatus. const ( DeployFunctionResponseStatusACTIVE DeployFunctionResponseStatus = "ACTIVE" @@ -438,26 +1260,80 @@ const ( DeployFunctionResponseStatusTHROTTLED DeployFunctionResponseStatus = "THROTTLED" ) +// Valid indicates whether the value is a known member of the DeployFunctionResponseStatus enum. +func (e DeployFunctionResponseStatus) Valid() bool { + switch e { + case DeployFunctionResponseStatusACTIVE: + return true + case DeployFunctionResponseStatusREMOVED: + return true + case DeployFunctionResponseStatusTHROTTLED: + return true + default: + return false + } +} + // Defines values for DiskRequestBodyAttributes0Type. const ( DiskRequestBodyAttributes0TypeGp3 DiskRequestBodyAttributes0Type = "gp3" ) +// Valid indicates whether the value is a known member of the DiskRequestBodyAttributes0Type enum. +func (e DiskRequestBodyAttributes0Type) Valid() bool { + switch e { + case DiskRequestBodyAttributes0TypeGp3: + return true + default: + return false + } +} + // Defines values for DiskRequestBodyAttributes1Type. const ( DiskRequestBodyAttributes1TypeIo2 DiskRequestBodyAttributes1Type = "io2" ) +// Valid indicates whether the value is a known member of the DiskRequestBodyAttributes1Type enum. +func (e DiskRequestBodyAttributes1Type) Valid() bool { + switch e { + case DiskRequestBodyAttributes1TypeIo2: + return true + default: + return false + } +} + // Defines values for DiskResponseAttributes0Type. const ( DiskResponseAttributes0TypeGp3 DiskResponseAttributes0Type = "gp3" ) +// Valid indicates whether the value is a known member of the DiskResponseAttributes0Type enum. +func (e DiskResponseAttributes0Type) Valid() bool { + switch e { + case DiskResponseAttributes0TypeGp3: + return true + default: + return false + } +} + // Defines values for DiskResponseAttributes1Type. const ( DiskResponseAttributes1TypeIo2 DiskResponseAttributes1Type = "io2" ) +// Valid indicates whether the value is a known member of the DiskResponseAttributes1Type enum. +func (e DiskResponseAttributes1Type) Valid() bool { + switch e { + case DiskResponseAttributes1TypeIo2: + return true + default: + return false + } +} + // Defines values for FunctionResponseStatus. const ( FunctionResponseStatusACTIVE FunctionResponseStatus = "ACTIVE" @@ -465,6 +1341,20 @@ const ( FunctionResponseStatusTHROTTLED FunctionResponseStatus = "THROTTLED" ) +// Valid indicates whether the value is a known member of the FunctionResponseStatus enum. +func (e FunctionResponseStatus) Valid() bool { + switch e { + case FunctionResponseStatusACTIVE: + return true + case FunctionResponseStatusREMOVED: + return true + case FunctionResponseStatusTHROTTLED: + return true + default: + return false + } +} + // Defines values for FunctionSlugResponseStatus. const ( FunctionSlugResponseStatusACTIVE FunctionSlugResponseStatus = "ACTIVE" @@ -472,6 +1362,20 @@ const ( FunctionSlugResponseStatusTHROTTLED FunctionSlugResponseStatus = "THROTTLED" ) +// Valid indicates whether the value is a known member of the FunctionSlugResponseStatus enum. +func (e FunctionSlugResponseStatus) Valid() bool { + switch e { + case FunctionSlugResponseStatusACTIVE: + return true + case FunctionSlugResponseStatusREMOVED: + return true + case FunctionSlugResponseStatusTHROTTLED: + return true + default: + return false + } +} + // Defines values for GetProjectAvailableRestoreVersionsResponseAvailableVersionsPostgresEngine. const ( GetProjectAvailableRestoreVersionsResponseAvailableVersionsPostgresEngineN13 GetProjectAvailableRestoreVersionsResponseAvailableVersionsPostgresEngine = "13" @@ -481,6 +1385,24 @@ const ( GetProjectAvailableRestoreVersionsResponseAvailableVersionsPostgresEngineN17Oriole GetProjectAvailableRestoreVersionsResponseAvailableVersionsPostgresEngine = "17-oriole" ) +// Valid indicates whether the value is a known member of the GetProjectAvailableRestoreVersionsResponseAvailableVersionsPostgresEngine enum. +func (e GetProjectAvailableRestoreVersionsResponseAvailableVersionsPostgresEngine) Valid() bool { + switch e { + case GetProjectAvailableRestoreVersionsResponseAvailableVersionsPostgresEngineN13: + return true + case GetProjectAvailableRestoreVersionsResponseAvailableVersionsPostgresEngineN14: + return true + case GetProjectAvailableRestoreVersionsResponseAvailableVersionsPostgresEngineN15: + return true + case GetProjectAvailableRestoreVersionsResponseAvailableVersionsPostgresEngineN17: + return true + case GetProjectAvailableRestoreVersionsResponseAvailableVersionsPostgresEngineN17Oriole: + return true + default: + return false + } +} + // Defines values for GetProjectAvailableRestoreVersionsResponseAvailableVersionsReleaseChannel. const ( GetProjectAvailableRestoreVersionsResponseAvailableVersionsReleaseChannelAlpha GetProjectAvailableRestoreVersionsResponseAvailableVersionsReleaseChannel = "alpha" @@ -491,23 +1413,77 @@ const ( GetProjectAvailableRestoreVersionsResponseAvailableVersionsReleaseChannelWithdrawn GetProjectAvailableRestoreVersionsResponseAvailableVersionsReleaseChannel = "withdrawn" ) +// Valid indicates whether the value is a known member of the GetProjectAvailableRestoreVersionsResponseAvailableVersionsReleaseChannel enum. +func (e GetProjectAvailableRestoreVersionsResponseAvailableVersionsReleaseChannel) Valid() bool { + switch e { + case GetProjectAvailableRestoreVersionsResponseAvailableVersionsReleaseChannelAlpha: + return true + case GetProjectAvailableRestoreVersionsResponseAvailableVersionsReleaseChannelBeta: + return true + case GetProjectAvailableRestoreVersionsResponseAvailableVersionsReleaseChannelGa: + return true + case GetProjectAvailableRestoreVersionsResponseAvailableVersionsReleaseChannelInternal: + return true + case GetProjectAvailableRestoreVersionsResponseAvailableVersionsReleaseChannelPreview: + return true + case GetProjectAvailableRestoreVersionsResponseAvailableVersionsReleaseChannelWithdrawn: + return true + default: + return false + } +} + // Defines values for JitAccessRequestRequestState. const ( JitAccessRequestRequestStateDisabled JitAccessRequestRequestState = "disabled" JitAccessRequestRequestStateEnabled JitAccessRequestRequestState = "enabled" ) +// Valid indicates whether the value is a known member of the JitAccessRequestRequestState enum. +func (e JitAccessRequestRequestState) Valid() bool { + switch e { + case JitAccessRequestRequestStateDisabled: + return true + case JitAccessRequestRequestStateEnabled: + return true + default: + return false + } +} + // Defines values for JitStateResponse0State. const ( JitStateResponse0StateDisabled JitStateResponse0State = "disabled" JitStateResponse0StateEnabled JitStateResponse0State = "enabled" ) +// Valid indicates whether the value is a known member of the JitStateResponse0State enum. +func (e JitStateResponse0State) Valid() bool { + switch e { + case JitStateResponse0StateDisabled: + return true + case JitStateResponse0StateEnabled: + return true + default: + return false + } +} + // Defines values for JitStateResponse1State. const ( Unavailable JitStateResponse1State = "unavailable" ) +// Valid indicates whether the value is a known member of the JitStateResponse1State enum. +func (e JitStateResponse1State) Valid() bool { + switch e { + case Unavailable: + return true + default: + return false + } +} + // Defines values for JitStateResponse1UnavailableReason. const ( PostgresUpgradeRequired JitStateResponse1UnavailableReason = "postgres_upgrade_required" @@ -515,6 +1491,20 @@ const ( TemporarilyUnavailable JitStateResponse1UnavailableReason = "temporarily_unavailable" ) +// Valid indicates whether the value is a known member of the JitStateResponse1UnavailableReason enum. +func (e JitStateResponse1UnavailableReason) Valid() bool { + switch e { + case PostgresUpgradeRequired: + return true + case SslEnforcementRequired: + return true + case TemporarilyUnavailable: + return true + default: + return false + } +} + // Defines values for ListActionRunResponseRunStepsName. const ( ListActionRunResponseRunStepsNameClone ListActionRunResponseRunStepsName = "clone" @@ -526,6 +1516,28 @@ const ( ListActionRunResponseRunStepsNameSeed ListActionRunResponseRunStepsName = "seed" ) +// Valid indicates whether the value is a known member of the ListActionRunResponseRunStepsName enum. +func (e ListActionRunResponseRunStepsName) Valid() bool { + switch e { + case ListActionRunResponseRunStepsNameClone: + return true + case ListActionRunResponseRunStepsNameConfigure: + return true + case ListActionRunResponseRunStepsNameDeploy: + return true + case ListActionRunResponseRunStepsNameHealth: + return true + case ListActionRunResponseRunStepsNameMigrate: + return true + case ListActionRunResponseRunStepsNamePull: + return true + case ListActionRunResponseRunStepsNameSeed: + return true + default: + return false + } +} + // Defines values for ListActionRunResponseRunStepsStatus. const ( ListActionRunResponseRunStepsStatusCREATED ListActionRunResponseRunStepsStatus = "CREATED" @@ -537,6 +1549,28 @@ const ( ListActionRunResponseRunStepsStatusRUNNING ListActionRunResponseRunStepsStatus = "RUNNING" ) +// Valid indicates whether the value is a known member of the ListActionRunResponseRunStepsStatus enum. +func (e ListActionRunResponseRunStepsStatus) Valid() bool { + switch e { + case ListActionRunResponseRunStepsStatusCREATED: + return true + case ListActionRunResponseRunStepsStatusDEAD: + return true + case ListActionRunResponseRunStepsStatusEXITED: + return true + case ListActionRunResponseRunStepsStatusPAUSED: + return true + case ListActionRunResponseRunStepsStatusREMOVING: + return true + case ListActionRunResponseRunStepsStatusRESTARTING: + return true + case ListActionRunResponseRunStepsStatusRUNNING: + return true + default: + return false + } +} + // Defines values for ListProjectAddonsResponseAvailableAddonsType. const ( ListProjectAddonsResponseAvailableAddonsTypeAuthMfaPhone ListProjectAddonsResponseAvailableAddonsType = "auth_mfa_phone" @@ -549,6 +1583,30 @@ const ( ListProjectAddonsResponseAvailableAddonsTypePitr ListProjectAddonsResponseAvailableAddonsType = "pitr" ) +// Valid indicates whether the value is a known member of the ListProjectAddonsResponseAvailableAddonsType enum. +func (e ListProjectAddonsResponseAvailableAddonsType) Valid() bool { + switch e { + case ListProjectAddonsResponseAvailableAddonsTypeAuthMfaPhone: + return true + case ListProjectAddonsResponseAvailableAddonsTypeAuthMfaWebAuthn: + return true + case ListProjectAddonsResponseAvailableAddonsTypeComputeInstance: + return true + case ListProjectAddonsResponseAvailableAddonsTypeCustomDomain: + return true + case ListProjectAddonsResponseAvailableAddonsTypeEtlPipeline: + return true + case ListProjectAddonsResponseAvailableAddonsTypeIpv4: + return true + case ListProjectAddonsResponseAvailableAddonsTypeLogDrain: + return true + case ListProjectAddonsResponseAvailableAddonsTypePitr: + return true + default: + return false + } +} + // Defines values for ListProjectAddonsResponseAvailableAddonsVariantsId0. const ( ListProjectAddonsResponseAvailableAddonsVariantsId0Ci12xlarge ListProjectAddonsResponseAvailableAddonsVariantsId0 = "ci_12xlarge" @@ -571,11 +1629,65 @@ const ( ListProjectAddonsResponseAvailableAddonsVariantsId0CiXlarge ListProjectAddonsResponseAvailableAddonsVariantsId0 = "ci_xlarge" ) +// Valid indicates whether the value is a known member of the ListProjectAddonsResponseAvailableAddonsVariantsId0 enum. +func (e ListProjectAddonsResponseAvailableAddonsVariantsId0) Valid() bool { + switch e { + case ListProjectAddonsResponseAvailableAddonsVariantsId0Ci12xlarge: + return true + case ListProjectAddonsResponseAvailableAddonsVariantsId0Ci16xlarge: + return true + case ListProjectAddonsResponseAvailableAddonsVariantsId0Ci24xlarge: + return true + case ListProjectAddonsResponseAvailableAddonsVariantsId0Ci24xlargeHighMemory: + return true + case ListProjectAddonsResponseAvailableAddonsVariantsId0Ci24xlargeOptimizedCpu: + return true + case ListProjectAddonsResponseAvailableAddonsVariantsId0Ci24xlargeOptimizedMemory: + return true + case ListProjectAddonsResponseAvailableAddonsVariantsId0Ci2xlarge: + return true + case ListProjectAddonsResponseAvailableAddonsVariantsId0Ci48xlarge: + return true + case ListProjectAddonsResponseAvailableAddonsVariantsId0Ci48xlargeHighMemory: + return true + case ListProjectAddonsResponseAvailableAddonsVariantsId0Ci48xlargeOptimizedCpu: + return true + case ListProjectAddonsResponseAvailableAddonsVariantsId0Ci48xlargeOptimizedMemory: + return true + case ListProjectAddonsResponseAvailableAddonsVariantsId0Ci4xlarge: + return true + case ListProjectAddonsResponseAvailableAddonsVariantsId0Ci8xlarge: + return true + case ListProjectAddonsResponseAvailableAddonsVariantsId0CiLarge: + return true + case ListProjectAddonsResponseAvailableAddonsVariantsId0CiMedium: + return true + case ListProjectAddonsResponseAvailableAddonsVariantsId0CiMicro: + return true + case ListProjectAddonsResponseAvailableAddonsVariantsId0CiSmall: + return true + case ListProjectAddonsResponseAvailableAddonsVariantsId0CiXlarge: + return true + default: + return false + } +} + // Defines values for ListProjectAddonsResponseAvailableAddonsVariantsId1. const ( ListProjectAddonsResponseAvailableAddonsVariantsId1CdDefault ListProjectAddonsResponseAvailableAddonsVariantsId1 = "cd_default" ) +// Valid indicates whether the value is a known member of the ListProjectAddonsResponseAvailableAddonsVariantsId1 enum. +func (e ListProjectAddonsResponseAvailableAddonsVariantsId1) Valid() bool { + switch e { + case ListProjectAddonsResponseAvailableAddonsVariantsId1CdDefault: + return true + default: + return false + } +} + // Defines values for ListProjectAddonsResponseAvailableAddonsVariantsId2. const ( ListProjectAddonsResponseAvailableAddonsVariantsId2Pitr14 ListProjectAddonsResponseAvailableAddonsVariantsId2 = "pitr_14" @@ -583,43 +1695,131 @@ const ( ListProjectAddonsResponseAvailableAddonsVariantsId2Pitr7 ListProjectAddonsResponseAvailableAddonsVariantsId2 = "pitr_7" ) +// Valid indicates whether the value is a known member of the ListProjectAddonsResponseAvailableAddonsVariantsId2 enum. +func (e ListProjectAddonsResponseAvailableAddonsVariantsId2) Valid() bool { + switch e { + case ListProjectAddonsResponseAvailableAddonsVariantsId2Pitr14: + return true + case ListProjectAddonsResponseAvailableAddonsVariantsId2Pitr28: + return true + case ListProjectAddonsResponseAvailableAddonsVariantsId2Pitr7: + return true + default: + return false + } +} + // Defines values for ListProjectAddonsResponseAvailableAddonsVariantsId3. const ( ListProjectAddonsResponseAvailableAddonsVariantsId3Ipv4Default ListProjectAddonsResponseAvailableAddonsVariantsId3 = "ipv4_default" ) +// Valid indicates whether the value is a known member of the ListProjectAddonsResponseAvailableAddonsVariantsId3 enum. +func (e ListProjectAddonsResponseAvailableAddonsVariantsId3) Valid() bool { + switch e { + case ListProjectAddonsResponseAvailableAddonsVariantsId3Ipv4Default: + return true + default: + return false + } +} + // Defines values for ListProjectAddonsResponseAvailableAddonsVariantsId4. const ( ListProjectAddonsResponseAvailableAddonsVariantsId4AuthMfaPhoneDefault ListProjectAddonsResponseAvailableAddonsVariantsId4 = "auth_mfa_phone_default" ) +// Valid indicates whether the value is a known member of the ListProjectAddonsResponseAvailableAddonsVariantsId4 enum. +func (e ListProjectAddonsResponseAvailableAddonsVariantsId4) Valid() bool { + switch e { + case ListProjectAddonsResponseAvailableAddonsVariantsId4AuthMfaPhoneDefault: + return true + default: + return false + } +} + // Defines values for ListProjectAddonsResponseAvailableAddonsVariantsId5. const ( ListProjectAddonsResponseAvailableAddonsVariantsId5AuthMfaWebAuthnDefault ListProjectAddonsResponseAvailableAddonsVariantsId5 = "auth_mfa_web_authn_default" ) +// Valid indicates whether the value is a known member of the ListProjectAddonsResponseAvailableAddonsVariantsId5 enum. +func (e ListProjectAddonsResponseAvailableAddonsVariantsId5) Valid() bool { + switch e { + case ListProjectAddonsResponseAvailableAddonsVariantsId5AuthMfaWebAuthnDefault: + return true + default: + return false + } +} + // Defines values for ListProjectAddonsResponseAvailableAddonsVariantsId6. const ( ListProjectAddonsResponseAvailableAddonsVariantsId6LogDrainDefault ListProjectAddonsResponseAvailableAddonsVariantsId6 = "log_drain_default" ) +// Valid indicates whether the value is a known member of the ListProjectAddonsResponseAvailableAddonsVariantsId6 enum. +func (e ListProjectAddonsResponseAvailableAddonsVariantsId6) Valid() bool { + switch e { + case ListProjectAddonsResponseAvailableAddonsVariantsId6LogDrainDefault: + return true + default: + return false + } +} + // Defines values for ListProjectAddonsResponseAvailableAddonsVariantsId7. const ( ListProjectAddonsResponseAvailableAddonsVariantsId7EtlPipelineDefault ListProjectAddonsResponseAvailableAddonsVariantsId7 = "etl_pipeline_default" ) +// Valid indicates whether the value is a known member of the ListProjectAddonsResponseAvailableAddonsVariantsId7 enum. +func (e ListProjectAddonsResponseAvailableAddonsVariantsId7) Valid() bool { + switch e { + case ListProjectAddonsResponseAvailableAddonsVariantsId7EtlPipelineDefault: + return true + default: + return false + } +} + // Defines values for ListProjectAddonsResponseAvailableAddonsVariantsPriceInterval. const ( ListProjectAddonsResponseAvailableAddonsVariantsPriceIntervalHourly ListProjectAddonsResponseAvailableAddonsVariantsPriceInterval = "hourly" ListProjectAddonsResponseAvailableAddonsVariantsPriceIntervalMonthly ListProjectAddonsResponseAvailableAddonsVariantsPriceInterval = "monthly" ) +// Valid indicates whether the value is a known member of the ListProjectAddonsResponseAvailableAddonsVariantsPriceInterval enum. +func (e ListProjectAddonsResponseAvailableAddonsVariantsPriceInterval) Valid() bool { + switch e { + case ListProjectAddonsResponseAvailableAddonsVariantsPriceIntervalHourly: + return true + case ListProjectAddonsResponseAvailableAddonsVariantsPriceIntervalMonthly: + return true + default: + return false + } +} + // Defines values for ListProjectAddonsResponseAvailableAddonsVariantsPriceType. const ( ListProjectAddonsResponseAvailableAddonsVariantsPriceTypeFixed ListProjectAddonsResponseAvailableAddonsVariantsPriceType = "fixed" ListProjectAddonsResponseAvailableAddonsVariantsPriceTypeUsage ListProjectAddonsResponseAvailableAddonsVariantsPriceType = "usage" ) +// Valid indicates whether the value is a known member of the ListProjectAddonsResponseAvailableAddonsVariantsPriceType enum. +func (e ListProjectAddonsResponseAvailableAddonsVariantsPriceType) Valid() bool { + switch e { + case ListProjectAddonsResponseAvailableAddonsVariantsPriceTypeFixed: + return true + case ListProjectAddonsResponseAvailableAddonsVariantsPriceTypeUsage: + return true + default: + return false + } +} + // Defines values for ListProjectAddonsResponseSelectedAddonsType. const ( ListProjectAddonsResponseSelectedAddonsTypeAuthMfaPhone ListProjectAddonsResponseSelectedAddonsType = "auth_mfa_phone" @@ -632,6 +1832,30 @@ const ( ListProjectAddonsResponseSelectedAddonsTypePitr ListProjectAddonsResponseSelectedAddonsType = "pitr" ) +// Valid indicates whether the value is a known member of the ListProjectAddonsResponseSelectedAddonsType enum. +func (e ListProjectAddonsResponseSelectedAddonsType) Valid() bool { + switch e { + case ListProjectAddonsResponseSelectedAddonsTypeAuthMfaPhone: + return true + case ListProjectAddonsResponseSelectedAddonsTypeAuthMfaWebAuthn: + return true + case ListProjectAddonsResponseSelectedAddonsTypeComputeInstance: + return true + case ListProjectAddonsResponseSelectedAddonsTypeCustomDomain: + return true + case ListProjectAddonsResponseSelectedAddonsTypeEtlPipeline: + return true + case ListProjectAddonsResponseSelectedAddonsTypeIpv4: + return true + case ListProjectAddonsResponseSelectedAddonsTypeLogDrain: + return true + case ListProjectAddonsResponseSelectedAddonsTypePitr: + return true + default: + return false + } +} + // Defines values for ListProjectAddonsResponseSelectedAddonsVariantId0. const ( ListProjectAddonsResponseSelectedAddonsVariantId0Ci12xlarge ListProjectAddonsResponseSelectedAddonsVariantId0 = "ci_12xlarge" @@ -654,11 +1878,65 @@ const ( ListProjectAddonsResponseSelectedAddonsVariantId0CiXlarge ListProjectAddonsResponseSelectedAddonsVariantId0 = "ci_xlarge" ) +// Valid indicates whether the value is a known member of the ListProjectAddonsResponseSelectedAddonsVariantId0 enum. +func (e ListProjectAddonsResponseSelectedAddonsVariantId0) Valid() bool { + switch e { + case ListProjectAddonsResponseSelectedAddonsVariantId0Ci12xlarge: + return true + case ListProjectAddonsResponseSelectedAddonsVariantId0Ci16xlarge: + return true + case ListProjectAddonsResponseSelectedAddonsVariantId0Ci24xlarge: + return true + case ListProjectAddonsResponseSelectedAddonsVariantId0Ci24xlargeHighMemory: + return true + case ListProjectAddonsResponseSelectedAddonsVariantId0Ci24xlargeOptimizedCpu: + return true + case ListProjectAddonsResponseSelectedAddonsVariantId0Ci24xlargeOptimizedMemory: + return true + case ListProjectAddonsResponseSelectedAddonsVariantId0Ci2xlarge: + return true + case ListProjectAddonsResponseSelectedAddonsVariantId0Ci48xlarge: + return true + case ListProjectAddonsResponseSelectedAddonsVariantId0Ci48xlargeHighMemory: + return true + case ListProjectAddonsResponseSelectedAddonsVariantId0Ci48xlargeOptimizedCpu: + return true + case ListProjectAddonsResponseSelectedAddonsVariantId0Ci48xlargeOptimizedMemory: + return true + case ListProjectAddonsResponseSelectedAddonsVariantId0Ci4xlarge: + return true + case ListProjectAddonsResponseSelectedAddonsVariantId0Ci8xlarge: + return true + case ListProjectAddonsResponseSelectedAddonsVariantId0CiLarge: + return true + case ListProjectAddonsResponseSelectedAddonsVariantId0CiMedium: + return true + case ListProjectAddonsResponseSelectedAddonsVariantId0CiMicro: + return true + case ListProjectAddonsResponseSelectedAddonsVariantId0CiSmall: + return true + case ListProjectAddonsResponseSelectedAddonsVariantId0CiXlarge: + return true + default: + return false + } +} + // Defines values for ListProjectAddonsResponseSelectedAddonsVariantId1. const ( ListProjectAddonsResponseSelectedAddonsVariantId1CdDefault ListProjectAddonsResponseSelectedAddonsVariantId1 = "cd_default" ) +// Valid indicates whether the value is a known member of the ListProjectAddonsResponseSelectedAddonsVariantId1 enum. +func (e ListProjectAddonsResponseSelectedAddonsVariantId1) Valid() bool { + switch e { + case ListProjectAddonsResponseSelectedAddonsVariantId1CdDefault: + return true + default: + return false + } +} + // Defines values for ListProjectAddonsResponseSelectedAddonsVariantId2. const ( ListProjectAddonsResponseSelectedAddonsVariantId2Pitr14 ListProjectAddonsResponseSelectedAddonsVariantId2 = "pitr_14" @@ -666,79 +1944,239 @@ const ( ListProjectAddonsResponseSelectedAddonsVariantId2Pitr7 ListProjectAddonsResponseSelectedAddonsVariantId2 = "pitr_7" ) +// Valid indicates whether the value is a known member of the ListProjectAddonsResponseSelectedAddonsVariantId2 enum. +func (e ListProjectAddonsResponseSelectedAddonsVariantId2) Valid() bool { + switch e { + case ListProjectAddonsResponseSelectedAddonsVariantId2Pitr14: + return true + case ListProjectAddonsResponseSelectedAddonsVariantId2Pitr28: + return true + case ListProjectAddonsResponseSelectedAddonsVariantId2Pitr7: + return true + default: + return false + } +} + // Defines values for ListProjectAddonsResponseSelectedAddonsVariantId3. const ( ListProjectAddonsResponseSelectedAddonsVariantId3Ipv4Default ListProjectAddonsResponseSelectedAddonsVariantId3 = "ipv4_default" ) +// Valid indicates whether the value is a known member of the ListProjectAddonsResponseSelectedAddonsVariantId3 enum. +func (e ListProjectAddonsResponseSelectedAddonsVariantId3) Valid() bool { + switch e { + case ListProjectAddonsResponseSelectedAddonsVariantId3Ipv4Default: + return true + default: + return false + } +} + // Defines values for ListProjectAddonsResponseSelectedAddonsVariantId4. const ( ListProjectAddonsResponseSelectedAddonsVariantId4AuthMfaPhoneDefault ListProjectAddonsResponseSelectedAddonsVariantId4 = "auth_mfa_phone_default" ) +// Valid indicates whether the value is a known member of the ListProjectAddonsResponseSelectedAddonsVariantId4 enum. +func (e ListProjectAddonsResponseSelectedAddonsVariantId4) Valid() bool { + switch e { + case ListProjectAddonsResponseSelectedAddonsVariantId4AuthMfaPhoneDefault: + return true + default: + return false + } +} + // Defines values for ListProjectAddonsResponseSelectedAddonsVariantId5. const ( ListProjectAddonsResponseSelectedAddonsVariantId5AuthMfaWebAuthnDefault ListProjectAddonsResponseSelectedAddonsVariantId5 = "auth_mfa_web_authn_default" ) +// Valid indicates whether the value is a known member of the ListProjectAddonsResponseSelectedAddonsVariantId5 enum. +func (e ListProjectAddonsResponseSelectedAddonsVariantId5) Valid() bool { + switch e { + case ListProjectAddonsResponseSelectedAddonsVariantId5AuthMfaWebAuthnDefault: + return true + default: + return false + } +} + // Defines values for ListProjectAddonsResponseSelectedAddonsVariantId6. const ( ListProjectAddonsResponseSelectedAddonsVariantId6LogDrainDefault ListProjectAddonsResponseSelectedAddonsVariantId6 = "log_drain_default" ) +// Valid indicates whether the value is a known member of the ListProjectAddonsResponseSelectedAddonsVariantId6 enum. +func (e ListProjectAddonsResponseSelectedAddonsVariantId6) Valid() bool { + switch e { + case ListProjectAddonsResponseSelectedAddonsVariantId6LogDrainDefault: + return true + default: + return false + } +} + // Defines values for ListProjectAddonsResponseSelectedAddonsVariantId7. const ( ListProjectAddonsResponseSelectedAddonsVariantId7EtlPipelineDefault ListProjectAddonsResponseSelectedAddonsVariantId7 = "etl_pipeline_default" ) +// Valid indicates whether the value is a known member of the ListProjectAddonsResponseSelectedAddonsVariantId7 enum. +func (e ListProjectAddonsResponseSelectedAddonsVariantId7) Valid() bool { + switch e { + case ListProjectAddonsResponseSelectedAddonsVariantId7EtlPipelineDefault: + return true + default: + return false + } +} + // Defines values for ListProjectAddonsResponseSelectedAddonsVariantPriceInterval. const ( ListProjectAddonsResponseSelectedAddonsVariantPriceIntervalHourly ListProjectAddonsResponseSelectedAddonsVariantPriceInterval = "hourly" ListProjectAddonsResponseSelectedAddonsVariantPriceIntervalMonthly ListProjectAddonsResponseSelectedAddonsVariantPriceInterval = "monthly" ) +// Valid indicates whether the value is a known member of the ListProjectAddonsResponseSelectedAddonsVariantPriceInterval enum. +func (e ListProjectAddonsResponseSelectedAddonsVariantPriceInterval) Valid() bool { + switch e { + case ListProjectAddonsResponseSelectedAddonsVariantPriceIntervalHourly: + return true + case ListProjectAddonsResponseSelectedAddonsVariantPriceIntervalMonthly: + return true + default: + return false + } +} + // Defines values for ListProjectAddonsResponseSelectedAddonsVariantPriceType. const ( ListProjectAddonsResponseSelectedAddonsVariantPriceTypeFixed ListProjectAddonsResponseSelectedAddonsVariantPriceType = "fixed" ListProjectAddonsResponseSelectedAddonsVariantPriceTypeUsage ListProjectAddonsResponseSelectedAddonsVariantPriceType = "usage" ) +// Valid indicates whether the value is a known member of the ListProjectAddonsResponseSelectedAddonsVariantPriceType enum. +func (e ListProjectAddonsResponseSelectedAddonsVariantPriceType) Valid() bool { + switch e { + case ListProjectAddonsResponseSelectedAddonsVariantPriceTypeFixed: + return true + case ListProjectAddonsResponseSelectedAddonsVariantPriceTypeUsage: + return true + default: + return false + } +} + // Defines values for NetworkRestrictionsResponseEntitlement. const ( NetworkRestrictionsResponseEntitlementAllowed NetworkRestrictionsResponseEntitlement = "allowed" NetworkRestrictionsResponseEntitlementDisallowed NetworkRestrictionsResponseEntitlement = "disallowed" ) +// Valid indicates whether the value is a known member of the NetworkRestrictionsResponseEntitlement enum. +func (e NetworkRestrictionsResponseEntitlement) Valid() bool { + switch e { + case NetworkRestrictionsResponseEntitlementAllowed: + return true + case NetworkRestrictionsResponseEntitlementDisallowed: + return true + default: + return false + } +} + // Defines values for NetworkRestrictionsResponseStatus. const ( NetworkRestrictionsResponseStatusApplied NetworkRestrictionsResponseStatus = "applied" NetworkRestrictionsResponseStatusStored NetworkRestrictionsResponseStatus = "stored" ) +// Valid indicates whether the value is a known member of the NetworkRestrictionsResponseStatus enum. +func (e NetworkRestrictionsResponseStatus) Valid() bool { + switch e { + case NetworkRestrictionsResponseStatusApplied: + return true + case NetworkRestrictionsResponseStatusStored: + return true + default: + return false + } +} + // Defines values for NetworkRestrictionsV2ResponseConfigDbAllowedCidrsType. const ( NetworkRestrictionsV2ResponseConfigDbAllowedCidrsTypeV4 NetworkRestrictionsV2ResponseConfigDbAllowedCidrsType = "v4" NetworkRestrictionsV2ResponseConfigDbAllowedCidrsTypeV6 NetworkRestrictionsV2ResponseConfigDbAllowedCidrsType = "v6" ) +// Valid indicates whether the value is a known member of the NetworkRestrictionsV2ResponseConfigDbAllowedCidrsType enum. +func (e NetworkRestrictionsV2ResponseConfigDbAllowedCidrsType) Valid() bool { + switch e { + case NetworkRestrictionsV2ResponseConfigDbAllowedCidrsTypeV4: + return true + case NetworkRestrictionsV2ResponseConfigDbAllowedCidrsTypeV6: + return true + default: + return false + } +} + // Defines values for NetworkRestrictionsV2ResponseEntitlement. const ( NetworkRestrictionsV2ResponseEntitlementAllowed NetworkRestrictionsV2ResponseEntitlement = "allowed" NetworkRestrictionsV2ResponseEntitlementDisallowed NetworkRestrictionsV2ResponseEntitlement = "disallowed" ) +// Valid indicates whether the value is a known member of the NetworkRestrictionsV2ResponseEntitlement enum. +func (e NetworkRestrictionsV2ResponseEntitlement) Valid() bool { + switch e { + case NetworkRestrictionsV2ResponseEntitlementAllowed: + return true + case NetworkRestrictionsV2ResponseEntitlementDisallowed: + return true + default: + return false + } +} + // Defines values for NetworkRestrictionsV2ResponseOldConfigDbAllowedCidrsType. const ( NetworkRestrictionsV2ResponseOldConfigDbAllowedCidrsTypeV4 NetworkRestrictionsV2ResponseOldConfigDbAllowedCidrsType = "v4" NetworkRestrictionsV2ResponseOldConfigDbAllowedCidrsTypeV6 NetworkRestrictionsV2ResponseOldConfigDbAllowedCidrsType = "v6" ) +// Valid indicates whether the value is a known member of the NetworkRestrictionsV2ResponseOldConfigDbAllowedCidrsType enum. +func (e NetworkRestrictionsV2ResponseOldConfigDbAllowedCidrsType) Valid() bool { + switch e { + case NetworkRestrictionsV2ResponseOldConfigDbAllowedCidrsTypeV4: + return true + case NetworkRestrictionsV2ResponseOldConfigDbAllowedCidrsTypeV6: + return true + default: + return false + } +} + // Defines values for NetworkRestrictionsV2ResponseStatus. const ( NetworkRestrictionsV2ResponseStatusApplied NetworkRestrictionsV2ResponseStatus = "applied" NetworkRestrictionsV2ResponseStatusStored NetworkRestrictionsV2ResponseStatus = "stored" ) +// Valid indicates whether the value is a known member of the NetworkRestrictionsV2ResponseStatus enum. +func (e NetworkRestrictionsV2ResponseStatus) Valid() bool { + switch e { + case NetworkRestrictionsV2ResponseStatusApplied: + return true + case NetworkRestrictionsV2ResponseStatusStored: + return true + default: + return false + } +} + // Defines values for OAuthTokenBodyGrantType. const ( AuthorizationCode OAuthTokenBodyGrantType = "authorization_code" @@ -746,11 +2184,35 @@ const ( UrnIetfParamsOauthGrantTypeJwtBearer OAuthTokenBodyGrantType = "urn:ietf:params:oauth:grant-type:jwt-bearer" ) +// Valid indicates whether the value is a known member of the OAuthTokenBodyGrantType enum. +func (e OAuthTokenBodyGrantType) Valid() bool { + switch e { + case AuthorizationCode: + return true + case RefreshToken: + return true + case UrnIetfParamsOauthGrantTypeJwtBearer: + return true + default: + return false + } +} + // Defines values for OAuthTokenResponseTokenType. const ( Bearer OAuthTokenResponseTokenType = "Bearer" ) +// Valid indicates whether the value is a known member of the OAuthTokenResponseTokenType enum. +func (e OAuthTokenResponseTokenType) Valid() bool { + switch e { + case Bearer: + return true + default: + return false + } +} + // Defines values for OrganizationProjectClaimResponsePreviewSourceSubscriptionPlan. const ( OrganizationProjectClaimResponsePreviewSourceSubscriptionPlanEnterprise OrganizationProjectClaimResponsePreviewSourceSubscriptionPlan = "enterprise" @@ -760,6 +2222,24 @@ const ( OrganizationProjectClaimResponsePreviewSourceSubscriptionPlanTeam OrganizationProjectClaimResponsePreviewSourceSubscriptionPlan = "team" ) +// Valid indicates whether the value is a known member of the OrganizationProjectClaimResponsePreviewSourceSubscriptionPlan enum. +func (e OrganizationProjectClaimResponsePreviewSourceSubscriptionPlan) Valid() bool { + switch e { + case OrganizationProjectClaimResponsePreviewSourceSubscriptionPlanEnterprise: + return true + case OrganizationProjectClaimResponsePreviewSourceSubscriptionPlanFree: + return true + case OrganizationProjectClaimResponsePreviewSourceSubscriptionPlanPlatform: + return true + case OrganizationProjectClaimResponsePreviewSourceSubscriptionPlanPro: + return true + case OrganizationProjectClaimResponsePreviewSourceSubscriptionPlanTeam: + return true + default: + return false + } +} + // Defines values for OrganizationProjectClaimResponsePreviewTargetSubscriptionPlan. const ( OrganizationProjectClaimResponsePreviewTargetSubscriptionPlanEnterprise OrganizationProjectClaimResponsePreviewTargetSubscriptionPlan = "enterprise" @@ -769,12 +2249,42 @@ const ( OrganizationProjectClaimResponsePreviewTargetSubscriptionPlanTeam OrganizationProjectClaimResponsePreviewTargetSubscriptionPlan = "team" ) +// Valid indicates whether the value is a known member of the OrganizationProjectClaimResponsePreviewTargetSubscriptionPlan enum. +func (e OrganizationProjectClaimResponsePreviewTargetSubscriptionPlan) Valid() bool { + switch e { + case OrganizationProjectClaimResponsePreviewTargetSubscriptionPlanEnterprise: + return true + case OrganizationProjectClaimResponsePreviewTargetSubscriptionPlanFree: + return true + case OrganizationProjectClaimResponsePreviewTargetSubscriptionPlanPlatform: + return true + case OrganizationProjectClaimResponsePreviewTargetSubscriptionPlanPro: + return true + case OrganizationProjectClaimResponsePreviewTargetSubscriptionPlanTeam: + return true + default: + return false + } +} + // Defines values for OrganizationProjectsResponseProjectsDatabasesDiskType. const ( Gp3 OrganizationProjectsResponseProjectsDatabasesDiskType = "gp3" Io2 OrganizationProjectsResponseProjectsDatabasesDiskType = "io2" ) +// Valid indicates whether the value is a known member of the OrganizationProjectsResponseProjectsDatabasesDiskType enum. +func (e OrganizationProjectsResponseProjectsDatabasesDiskType) Valid() bool { + switch e { + case Gp3: + return true + case Io2: + return true + default: + return false + } +} + // Defines values for OrganizationProjectsResponseProjectsDatabasesInfraComputeSize. const ( OrganizationProjectsResponseProjectsDatabasesInfraComputeSizeLarge OrganizationProjectsResponseProjectsDatabasesInfraComputeSize = "large" @@ -799,6 +2309,54 @@ const ( OrganizationProjectsResponseProjectsDatabasesInfraComputeSizeXlarge OrganizationProjectsResponseProjectsDatabasesInfraComputeSize = "xlarge" ) +// Valid indicates whether the value is a known member of the OrganizationProjectsResponseProjectsDatabasesInfraComputeSize enum. +func (e OrganizationProjectsResponseProjectsDatabasesInfraComputeSize) Valid() bool { + switch e { + case OrganizationProjectsResponseProjectsDatabasesInfraComputeSizeLarge: + return true + case OrganizationProjectsResponseProjectsDatabasesInfraComputeSizeMedium: + return true + case OrganizationProjectsResponseProjectsDatabasesInfraComputeSizeMicro: + return true + case OrganizationProjectsResponseProjectsDatabasesInfraComputeSizeN12xlarge: + return true + case OrganizationProjectsResponseProjectsDatabasesInfraComputeSizeN16xlarge: + return true + case OrganizationProjectsResponseProjectsDatabasesInfraComputeSizeN24xlarge: + return true + case OrganizationProjectsResponseProjectsDatabasesInfraComputeSizeN24xlargeHighMemory: + return true + case OrganizationProjectsResponseProjectsDatabasesInfraComputeSizeN24xlargeOptimizedCpu: + return true + case OrganizationProjectsResponseProjectsDatabasesInfraComputeSizeN24xlargeOptimizedMemory: + return true + case OrganizationProjectsResponseProjectsDatabasesInfraComputeSizeN2xlarge: + return true + case OrganizationProjectsResponseProjectsDatabasesInfraComputeSizeN48xlarge: + return true + case OrganizationProjectsResponseProjectsDatabasesInfraComputeSizeN48xlargeHighMemory: + return true + case OrganizationProjectsResponseProjectsDatabasesInfraComputeSizeN48xlargeOptimizedCpu: + return true + case OrganizationProjectsResponseProjectsDatabasesInfraComputeSizeN48xlargeOptimizedMemory: + return true + case OrganizationProjectsResponseProjectsDatabasesInfraComputeSizeN4xlarge: + return true + case OrganizationProjectsResponseProjectsDatabasesInfraComputeSizeN8xlarge: + return true + case OrganizationProjectsResponseProjectsDatabasesInfraComputeSizeNano: + return true + case OrganizationProjectsResponseProjectsDatabasesInfraComputeSizePico: + return true + case OrganizationProjectsResponseProjectsDatabasesInfraComputeSizeSmall: + return true + case OrganizationProjectsResponseProjectsDatabasesInfraComputeSizeXlarge: + return true + default: + return false + } +} + // Defines values for OrganizationProjectsResponseProjectsDatabasesStatus. const ( OrganizationProjectsResponseProjectsDatabasesStatusACTIVEHEALTHY OrganizationProjectsResponseProjectsDatabasesStatus = "ACTIVE_HEALTHY" @@ -815,12 +2373,56 @@ const ( OrganizationProjectsResponseProjectsDatabasesStatusUNKNOWN OrganizationProjectsResponseProjectsDatabasesStatus = "UNKNOWN" ) +// Valid indicates whether the value is a known member of the OrganizationProjectsResponseProjectsDatabasesStatus enum. +func (e OrganizationProjectsResponseProjectsDatabasesStatus) Valid() bool { + switch e { + case OrganizationProjectsResponseProjectsDatabasesStatusACTIVEHEALTHY: + return true + case OrganizationProjectsResponseProjectsDatabasesStatusACTIVEUNHEALTHY: + return true + case OrganizationProjectsResponseProjectsDatabasesStatusCOMINGUP: + return true + case OrganizationProjectsResponseProjectsDatabasesStatusGOINGDOWN: + return true + case OrganizationProjectsResponseProjectsDatabasesStatusINITFAILED: + return true + case OrganizationProjectsResponseProjectsDatabasesStatusINITREADREPLICA: + return true + case OrganizationProjectsResponseProjectsDatabasesStatusINITREADREPLICAFAILED: + return true + case OrganizationProjectsResponseProjectsDatabasesStatusREMOVED: + return true + case OrganizationProjectsResponseProjectsDatabasesStatusRESIZING: + return true + case OrganizationProjectsResponseProjectsDatabasesStatusRESTARTING: + return true + case OrganizationProjectsResponseProjectsDatabasesStatusRESTORING: + return true + case OrganizationProjectsResponseProjectsDatabasesStatusUNKNOWN: + return true + default: + return false + } +} + // Defines values for OrganizationProjectsResponseProjectsDatabasesType. const ( OrganizationProjectsResponseProjectsDatabasesTypePRIMARY OrganizationProjectsResponseProjectsDatabasesType = "PRIMARY" OrganizationProjectsResponseProjectsDatabasesTypeREADREPLICA OrganizationProjectsResponseProjectsDatabasesType = "READ_REPLICA" ) +// Valid indicates whether the value is a known member of the OrganizationProjectsResponseProjectsDatabasesType enum. +func (e OrganizationProjectsResponseProjectsDatabasesType) Valid() bool { + switch e { + case OrganizationProjectsResponseProjectsDatabasesTypePRIMARY: + return true + case OrganizationProjectsResponseProjectsDatabasesTypeREADREPLICA: + return true + default: + return false + } +} + // Defines values for OrganizationProjectsResponseProjectsStatus. const ( OrganizationProjectsResponseProjectsStatusACTIVEHEALTHY OrganizationProjectsResponseProjectsStatus = "ACTIVE_HEALTHY" @@ -840,11 +2442,59 @@ const ( OrganizationProjectsResponseProjectsStatusUPGRADING OrganizationProjectsResponseProjectsStatus = "UPGRADING" ) +// Valid indicates whether the value is a known member of the OrganizationProjectsResponseProjectsStatus enum. +func (e OrganizationProjectsResponseProjectsStatus) Valid() bool { + switch e { + case OrganizationProjectsResponseProjectsStatusACTIVEHEALTHY: + return true + case OrganizationProjectsResponseProjectsStatusACTIVEUNHEALTHY: + return true + case OrganizationProjectsResponseProjectsStatusCOMINGUP: + return true + case OrganizationProjectsResponseProjectsStatusGOINGDOWN: + return true + case OrganizationProjectsResponseProjectsStatusINACTIVE: + return true + case OrganizationProjectsResponseProjectsStatusINITFAILED: + return true + case OrganizationProjectsResponseProjectsStatusPAUSEFAILED: + return true + case OrganizationProjectsResponseProjectsStatusPAUSING: + return true + case OrganizationProjectsResponseProjectsStatusREMOVED: + return true + case OrganizationProjectsResponseProjectsStatusRESIZING: + return true + case OrganizationProjectsResponseProjectsStatusRESTARTING: + return true + case OrganizationProjectsResponseProjectsStatusRESTOREFAILED: + return true + case OrganizationProjectsResponseProjectsStatusRESTORING: + return true + case OrganizationProjectsResponseProjectsStatusUNKNOWN: + return true + case OrganizationProjectsResponseProjectsStatusUPGRADING: + return true + default: + return false + } +} + // Defines values for PlanGateErrorBodyErrorCode. const ( EntitlementRequired PlanGateErrorBodyErrorCode = "entitlement_required" ) +// Valid indicates whether the value is a known member of the PlanGateErrorBodyErrorCode enum. +func (e PlanGateErrorBodyErrorCode) Valid() bool { + switch e { + case EntitlementRequired: + return true + default: + return false + } +} + // Defines values for PostgresConfigResponseSessionReplicationRole. const ( PostgresConfigResponseSessionReplicationRoleLocal PostgresConfigResponseSessionReplicationRole = "local" @@ -852,6 +2502,20 @@ const ( PostgresConfigResponseSessionReplicationRoleReplica PostgresConfigResponseSessionReplicationRole = "replica" ) +// Valid indicates whether the value is a known member of the PostgresConfigResponseSessionReplicationRole enum. +func (e PostgresConfigResponseSessionReplicationRole) Valid() bool { + switch e { + case PostgresConfigResponseSessionReplicationRoleLocal: + return true + case PostgresConfigResponseSessionReplicationRoleOrigin: + return true + case PostgresConfigResponseSessionReplicationRoleReplica: + return true + default: + return false + } +} + // Defines values for ProjectUpgradeEligibilityResponseCurrentAppVersionReleaseChannel. const ( ProjectUpgradeEligibilityResponseCurrentAppVersionReleaseChannelAlpha ProjectUpgradeEligibilityResponseCurrentAppVersionReleaseChannel = "alpha" @@ -862,6 +2526,26 @@ const ( ProjectUpgradeEligibilityResponseCurrentAppVersionReleaseChannelWithdrawn ProjectUpgradeEligibilityResponseCurrentAppVersionReleaseChannel = "withdrawn" ) +// Valid indicates whether the value is a known member of the ProjectUpgradeEligibilityResponseCurrentAppVersionReleaseChannel enum. +func (e ProjectUpgradeEligibilityResponseCurrentAppVersionReleaseChannel) Valid() bool { + switch e { + case ProjectUpgradeEligibilityResponseCurrentAppVersionReleaseChannelAlpha: + return true + case ProjectUpgradeEligibilityResponseCurrentAppVersionReleaseChannelBeta: + return true + case ProjectUpgradeEligibilityResponseCurrentAppVersionReleaseChannelGa: + return true + case ProjectUpgradeEligibilityResponseCurrentAppVersionReleaseChannelInternal: + return true + case ProjectUpgradeEligibilityResponseCurrentAppVersionReleaseChannelPreview: + return true + case ProjectUpgradeEligibilityResponseCurrentAppVersionReleaseChannelWithdrawn: + return true + default: + return false + } +} + // Defines values for ProjectUpgradeEligibilityResponseTargetUpgradeVersionsPostgresVersion. const ( N13 ProjectUpgradeEligibilityResponseTargetUpgradeVersionsPostgresVersion = "13" @@ -871,6 +2555,24 @@ const ( N17Oriole ProjectUpgradeEligibilityResponseTargetUpgradeVersionsPostgresVersion = "17-oriole" ) +// Valid indicates whether the value is a known member of the ProjectUpgradeEligibilityResponseTargetUpgradeVersionsPostgresVersion enum. +func (e ProjectUpgradeEligibilityResponseTargetUpgradeVersionsPostgresVersion) Valid() bool { + switch e { + case N13: + return true + case N14: + return true + case N15: + return true + case N17: + return true + case N17Oriole: + return true + default: + return false + } +} + // Defines values for ProjectUpgradeEligibilityResponseTargetUpgradeVersionsReleaseChannel. const ( ProjectUpgradeEligibilityResponseTargetUpgradeVersionsReleaseChannelAlpha ProjectUpgradeEligibilityResponseTargetUpgradeVersionsReleaseChannel = "alpha" @@ -881,77 +2583,239 @@ const ( ProjectUpgradeEligibilityResponseTargetUpgradeVersionsReleaseChannelWithdrawn ProjectUpgradeEligibilityResponseTargetUpgradeVersionsReleaseChannel = "withdrawn" ) +// Valid indicates whether the value is a known member of the ProjectUpgradeEligibilityResponseTargetUpgradeVersionsReleaseChannel enum. +func (e ProjectUpgradeEligibilityResponseTargetUpgradeVersionsReleaseChannel) Valid() bool { + switch e { + case ProjectUpgradeEligibilityResponseTargetUpgradeVersionsReleaseChannelAlpha: + return true + case ProjectUpgradeEligibilityResponseTargetUpgradeVersionsReleaseChannelBeta: + return true + case ProjectUpgradeEligibilityResponseTargetUpgradeVersionsReleaseChannelGa: + return true + case ProjectUpgradeEligibilityResponseTargetUpgradeVersionsReleaseChannelInternal: + return true + case ProjectUpgradeEligibilityResponseTargetUpgradeVersionsReleaseChannelPreview: + return true + case ProjectUpgradeEligibilityResponseTargetUpgradeVersionsReleaseChannelWithdrawn: + return true + default: + return false + } +} + // Defines values for ProjectUpgradeEligibilityResponseValidationErrors0Type. const ( ObjectsDependingOnPgCron ProjectUpgradeEligibilityResponseValidationErrors0Type = "objects_depending_on_pg_cron" ) +// Valid indicates whether the value is a known member of the ProjectUpgradeEligibilityResponseValidationErrors0Type enum. +func (e ProjectUpgradeEligibilityResponseValidationErrors0Type) Valid() bool { + switch e { + case ObjectsDependingOnPgCron: + return true + default: + return false + } +} + // Defines values for ProjectUpgradeEligibilityResponseValidationErrors1Type. const ( IndexesReferencingLlToEarth ProjectUpgradeEligibilityResponseValidationErrors1Type = "indexes_referencing_ll_to_earth" ) +// Valid indicates whether the value is a known member of the ProjectUpgradeEligibilityResponseValidationErrors1Type enum. +func (e ProjectUpgradeEligibilityResponseValidationErrors1Type) Valid() bool { + switch e { + case IndexesReferencingLlToEarth: + return true + default: + return false + } +} + // Defines values for ProjectUpgradeEligibilityResponseValidationErrors2Type. const ( FunctionUsingObsoleteLang ProjectUpgradeEligibilityResponseValidationErrors2Type = "function_using_obsolete_lang" ) +// Valid indicates whether the value is a known member of the ProjectUpgradeEligibilityResponseValidationErrors2Type enum. +func (e ProjectUpgradeEligibilityResponseValidationErrors2Type) Valid() bool { + switch e { + case FunctionUsingObsoleteLang: + return true + default: + return false + } +} + // Defines values for ProjectUpgradeEligibilityResponseValidationErrors3Type. const ( UnsupportedExtension ProjectUpgradeEligibilityResponseValidationErrors3Type = "unsupported_extension" ) +// Valid indicates whether the value is a known member of the ProjectUpgradeEligibilityResponseValidationErrors3Type enum. +func (e ProjectUpgradeEligibilityResponseValidationErrors3Type) Valid() bool { + switch e { + case UnsupportedExtension: + return true + default: + return false + } +} + // Defines values for ProjectUpgradeEligibilityResponseValidationErrors4Type. const ( UnsupportedFdwHandler ProjectUpgradeEligibilityResponseValidationErrors4Type = "unsupported_fdw_handler" ) +// Valid indicates whether the value is a known member of the ProjectUpgradeEligibilityResponseValidationErrors4Type enum. +func (e ProjectUpgradeEligibilityResponseValidationErrors4Type) Valid() bool { + switch e { + case UnsupportedFdwHandler: + return true + default: + return false + } +} + // Defines values for ProjectUpgradeEligibilityResponseValidationErrors5Type. const ( UnloggedTableWithPersistentSequence ProjectUpgradeEligibilityResponseValidationErrors5Type = "unlogged_table_with_persistent_sequence" ) +// Valid indicates whether the value is a known member of the ProjectUpgradeEligibilityResponseValidationErrors5Type enum. +func (e ProjectUpgradeEligibilityResponseValidationErrors5Type) Valid() bool { + switch e { + case UnloggedTableWithPersistentSequence: + return true + default: + return false + } +} + // Defines values for ProjectUpgradeEligibilityResponseValidationErrors6ObjType. const ( ProjectUpgradeEligibilityResponseValidationErrors6ObjTypeFunction ProjectUpgradeEligibilityResponseValidationErrors6ObjType = "function" ProjectUpgradeEligibilityResponseValidationErrors6ObjTypeTable ProjectUpgradeEligibilityResponseValidationErrors6ObjType = "table" ) +// Valid indicates whether the value is a known member of the ProjectUpgradeEligibilityResponseValidationErrors6ObjType enum. +func (e ProjectUpgradeEligibilityResponseValidationErrors6ObjType) Valid() bool { + switch e { + case ProjectUpgradeEligibilityResponseValidationErrors6ObjTypeFunction: + return true + case ProjectUpgradeEligibilityResponseValidationErrors6ObjTypeTable: + return true + default: + return false + } +} + // Defines values for ProjectUpgradeEligibilityResponseValidationErrors6Type. const ( UserDefinedObjectsInInternalSchemas ProjectUpgradeEligibilityResponseValidationErrors6Type = "user_defined_objects_in_internal_schemas" ) +// Valid indicates whether the value is a known member of the ProjectUpgradeEligibilityResponseValidationErrors6Type enum. +func (e ProjectUpgradeEligibilityResponseValidationErrors6Type) Valid() bool { + switch e { + case UserDefinedObjectsInInternalSchemas: + return true + default: + return false + } +} + // Defines values for ProjectUpgradeEligibilityResponseValidationErrors7Type. const ( ActiveReplicationSlot ProjectUpgradeEligibilityResponseValidationErrors7Type = "active_replication_slot" ) +// Valid indicates whether the value is a known member of the ProjectUpgradeEligibilityResponseValidationErrors7Type enum. +func (e ProjectUpgradeEligibilityResponseValidationErrors7Type) Valid() bool { + switch e { + case ActiveReplicationSlot: + return true + default: + return false + } +} + // Defines values for ProjectUpgradeEligibilityResponseValidationErrors8Type. const ( X86Architecture ProjectUpgradeEligibilityResponseValidationErrors8Type = "x86_architecture" ) +// Valid indicates whether the value is a known member of the ProjectUpgradeEligibilityResponseValidationErrors8Type enum. +func (e ProjectUpgradeEligibilityResponseValidationErrors8Type) Valid() bool { + switch e { + case X86Architecture: + return true + default: + return false + } +} + // Defines values for ProjectUpgradeEligibilityResponseValidationErrors9Type. const ( ProjectHibernating ProjectUpgradeEligibilityResponseValidationErrors9Type = "project_hibernating" ) +// Valid indicates whether the value is a known member of the ProjectUpgradeEligibilityResponseValidationErrors9Type enum. +func (e ProjectUpgradeEligibilityResponseValidationErrors9Type) Valid() bool { + switch e { + case ProjectHibernating: + return true + default: + return false + } +} + // Defines values for ProjectUpgradeEligibilityResponseWarnings0Type. const ( PgGraphqlIntrospectionChange ProjectUpgradeEligibilityResponseWarnings0Type = "pg_graphql_introspection_change" ) +// Valid indicates whether the value is a known member of the ProjectUpgradeEligibilityResponseWarnings0Type enum. +func (e ProjectUpgradeEligibilityResponseWarnings0Type) Valid() bool { + switch e { + case PgGraphqlIntrospectionChange: + return true + default: + return false + } +} + // Defines values for ProjectUpgradeEligibilityResponseWarnings1Type. const ( LtreeReindexRequired ProjectUpgradeEligibilityResponseWarnings1Type = "ltree_reindex_required" ) +// Valid indicates whether the value is a known member of the ProjectUpgradeEligibilityResponseWarnings1Type enum. +func (e ProjectUpgradeEligibilityResponseWarnings1Type) Valid() bool { + switch e { + case LtreeReindexRequired: + return true + default: + return false + } +} + // Defines values for ProjectUpgradeEligibilityResponseWarnings2Type. const ( OperatorEstimatorGate ProjectUpgradeEligibilityResponseWarnings2Type = "operator_estimator_gate" ) +// Valid indicates whether the value is a known member of the ProjectUpgradeEligibilityResponseWarnings2Type enum. +func (e ProjectUpgradeEligibilityResponseWarnings2Type) Valid() bool { + switch e { + case OperatorEstimatorGate: + return true + default: + return false + } +} + // Defines values for RegionsInfoAllSmartGroupCode. const ( RegionsInfoAllSmartGroupCodeAmericas RegionsInfoAllSmartGroupCode = "americas" @@ -959,11 +2823,35 @@ const ( RegionsInfoAllSmartGroupCodeEmea RegionsInfoAllSmartGroupCode = "emea" ) +// Valid indicates whether the value is a known member of the RegionsInfoAllSmartGroupCode enum. +func (e RegionsInfoAllSmartGroupCode) Valid() bool { + switch e { + case RegionsInfoAllSmartGroupCodeAmericas: + return true + case RegionsInfoAllSmartGroupCodeApac: + return true + case RegionsInfoAllSmartGroupCodeEmea: + return true + default: + return false + } +} + // Defines values for RegionsInfoAllSmartGroupType. const ( RegionsInfoAllSmartGroupTypeSmartGroup RegionsInfoAllSmartGroupType = "smartGroup" ) +// Valid indicates whether the value is a known member of the RegionsInfoAllSmartGroupType enum. +func (e RegionsInfoAllSmartGroupType) Valid() bool { + switch e { + case RegionsInfoAllSmartGroupTypeSmartGroup: + return true + default: + return false + } +} + // Defines values for RegionsInfoAllSpecificCode. const ( RegionsInfoAllSpecificCodeApEast1 RegionsInfoAllSpecificCode = "ap-east-1" @@ -986,6 +2874,50 @@ const ( RegionsInfoAllSpecificCodeUsWest2 RegionsInfoAllSpecificCode = "us-west-2" ) +// Valid indicates whether the value is a known member of the RegionsInfoAllSpecificCode enum. +func (e RegionsInfoAllSpecificCode) Valid() bool { + switch e { + case RegionsInfoAllSpecificCodeApEast1: + return true + case RegionsInfoAllSpecificCodeApNortheast1: + return true + case RegionsInfoAllSpecificCodeApNortheast2: + return true + case RegionsInfoAllSpecificCodeApSouth1: + return true + case RegionsInfoAllSpecificCodeApSoutheast1: + return true + case RegionsInfoAllSpecificCodeApSoutheast2: + return true + case RegionsInfoAllSpecificCodeCaCentral1: + return true + case RegionsInfoAllSpecificCodeEuCentral1: + return true + case RegionsInfoAllSpecificCodeEuCentral2: + return true + case RegionsInfoAllSpecificCodeEuNorth1: + return true + case RegionsInfoAllSpecificCodeEuWest1: + return true + case RegionsInfoAllSpecificCodeEuWest2: + return true + case RegionsInfoAllSpecificCodeEuWest3: + return true + case RegionsInfoAllSpecificCodeSaEast1: + return true + case RegionsInfoAllSpecificCodeUsEast1: + return true + case RegionsInfoAllSpecificCodeUsEast2: + return true + case RegionsInfoAllSpecificCodeUsWest1: + return true + case RegionsInfoAllSpecificCodeUsWest2: + return true + default: + return false + } +} + // Defines values for RegionsInfoAllSpecificProvider. const ( RegionsInfoAllSpecificProviderAWS RegionsInfoAllSpecificProvider = "AWS" @@ -994,17 +2926,55 @@ const ( RegionsInfoAllSpecificProviderFLY RegionsInfoAllSpecificProvider = "FLY" ) +// Valid indicates whether the value is a known member of the RegionsInfoAllSpecificProvider enum. +func (e RegionsInfoAllSpecificProvider) Valid() bool { + switch e { + case RegionsInfoAllSpecificProviderAWS: + return true + case RegionsInfoAllSpecificProviderAWSK8S: + return true + case RegionsInfoAllSpecificProviderAWSNIMBUS: + return true + case RegionsInfoAllSpecificProviderFLY: + return true + default: + return false + } +} + // Defines values for RegionsInfoAllSpecificStatus. const ( RegionsInfoAllSpecificStatusCapacity RegionsInfoAllSpecificStatus = "capacity" RegionsInfoAllSpecificStatusOther RegionsInfoAllSpecificStatus = "other" ) +// Valid indicates whether the value is a known member of the RegionsInfoAllSpecificStatus enum. +func (e RegionsInfoAllSpecificStatus) Valid() bool { + switch e { + case RegionsInfoAllSpecificStatusCapacity: + return true + case RegionsInfoAllSpecificStatusOther: + return true + default: + return false + } +} + // Defines values for RegionsInfoAllSpecificType. const ( RegionsInfoAllSpecificTypeSpecific RegionsInfoAllSpecificType = "specific" ) +// Valid indicates whether the value is a known member of the RegionsInfoAllSpecificType enum. +func (e RegionsInfoAllSpecificType) Valid() bool { + switch e { + case RegionsInfoAllSpecificTypeSpecific: + return true + default: + return false + } +} + // Defines values for RegionsInfoRecommendationsSmartGroupCode. const ( RegionsInfoRecommendationsSmartGroupCodeAmericas RegionsInfoRecommendationsSmartGroupCode = "americas" @@ -1012,11 +2982,35 @@ const ( RegionsInfoRecommendationsSmartGroupCodeEmea RegionsInfoRecommendationsSmartGroupCode = "emea" ) +// Valid indicates whether the value is a known member of the RegionsInfoRecommendationsSmartGroupCode enum. +func (e RegionsInfoRecommendationsSmartGroupCode) Valid() bool { + switch e { + case RegionsInfoRecommendationsSmartGroupCodeAmericas: + return true + case RegionsInfoRecommendationsSmartGroupCodeApac: + return true + case RegionsInfoRecommendationsSmartGroupCodeEmea: + return true + default: + return false + } +} + // Defines values for RegionsInfoRecommendationsSmartGroupType. const ( RegionsInfoRecommendationsSmartGroupTypeSmartGroup RegionsInfoRecommendationsSmartGroupType = "smartGroup" ) +// Valid indicates whether the value is a known member of the RegionsInfoRecommendationsSmartGroupType enum. +func (e RegionsInfoRecommendationsSmartGroupType) Valid() bool { + switch e { + case RegionsInfoRecommendationsSmartGroupTypeSmartGroup: + return true + default: + return false + } +} + // Defines values for RegionsInfoRecommendationsSpecificCode. const ( RegionsInfoRecommendationsSpecificCodeApEast1 RegionsInfoRecommendationsSpecificCode = "ap-east-1" @@ -1039,6 +3033,50 @@ const ( RegionsInfoRecommendationsSpecificCodeUsWest2 RegionsInfoRecommendationsSpecificCode = "us-west-2" ) +// Valid indicates whether the value is a known member of the RegionsInfoRecommendationsSpecificCode enum. +func (e RegionsInfoRecommendationsSpecificCode) Valid() bool { + switch e { + case RegionsInfoRecommendationsSpecificCodeApEast1: + return true + case RegionsInfoRecommendationsSpecificCodeApNortheast1: + return true + case RegionsInfoRecommendationsSpecificCodeApNortheast2: + return true + case RegionsInfoRecommendationsSpecificCodeApSouth1: + return true + case RegionsInfoRecommendationsSpecificCodeApSoutheast1: + return true + case RegionsInfoRecommendationsSpecificCodeApSoutheast2: + return true + case RegionsInfoRecommendationsSpecificCodeCaCentral1: + return true + case RegionsInfoRecommendationsSpecificCodeEuCentral1: + return true + case RegionsInfoRecommendationsSpecificCodeEuCentral2: + return true + case RegionsInfoRecommendationsSpecificCodeEuNorth1: + return true + case RegionsInfoRecommendationsSpecificCodeEuWest1: + return true + case RegionsInfoRecommendationsSpecificCodeEuWest2: + return true + case RegionsInfoRecommendationsSpecificCodeEuWest3: + return true + case RegionsInfoRecommendationsSpecificCodeSaEast1: + return true + case RegionsInfoRecommendationsSpecificCodeUsEast1: + return true + case RegionsInfoRecommendationsSpecificCodeUsEast2: + return true + case RegionsInfoRecommendationsSpecificCodeUsWest1: + return true + case RegionsInfoRecommendationsSpecificCodeUsWest2: + return true + default: + return false + } +} + // Defines values for RegionsInfoRecommendationsSpecificProvider. const ( RegionsInfoRecommendationsSpecificProviderAWS RegionsInfoRecommendationsSpecificProvider = "AWS" @@ -1047,17 +3085,55 @@ const ( RegionsInfoRecommendationsSpecificProviderFLY RegionsInfoRecommendationsSpecificProvider = "FLY" ) +// Valid indicates whether the value is a known member of the RegionsInfoRecommendationsSpecificProvider enum. +func (e RegionsInfoRecommendationsSpecificProvider) Valid() bool { + switch e { + case RegionsInfoRecommendationsSpecificProviderAWS: + return true + case RegionsInfoRecommendationsSpecificProviderAWSK8S: + return true + case RegionsInfoRecommendationsSpecificProviderAWSNIMBUS: + return true + case RegionsInfoRecommendationsSpecificProviderFLY: + return true + default: + return false + } +} + // Defines values for RegionsInfoRecommendationsSpecificStatus. const ( RegionsInfoRecommendationsSpecificStatusCapacity RegionsInfoRecommendationsSpecificStatus = "capacity" RegionsInfoRecommendationsSpecificStatusOther RegionsInfoRecommendationsSpecificStatus = "other" ) +// Valid indicates whether the value is a known member of the RegionsInfoRecommendationsSpecificStatus enum. +func (e RegionsInfoRecommendationsSpecificStatus) Valid() bool { + switch e { + case RegionsInfoRecommendationsSpecificStatusCapacity: + return true + case RegionsInfoRecommendationsSpecificStatusOther: + return true + default: + return false + } +} + // Defines values for RegionsInfoRecommendationsSpecificType. const ( RegionsInfoRecommendationsSpecificTypeSpecific RegionsInfoRecommendationsSpecificType = "specific" ) +// Valid indicates whether the value is a known member of the RegionsInfoRecommendationsSpecificType enum. +func (e RegionsInfoRecommendationsSpecificType) Valid() bool { + switch e { + case RegionsInfoRecommendationsSpecificTypeSpecific: + return true + default: + return false + } +} + // Defines values for SetUpReadReplicaBodyReadReplicaRegion. const ( SetUpReadReplicaBodyReadReplicaRegionApEast1 SetUpReadReplicaBodyReadReplicaRegion = "ap-east-1" @@ -1080,6 +3156,50 @@ const ( SetUpReadReplicaBodyReadReplicaRegionUsWest2 SetUpReadReplicaBodyReadReplicaRegion = "us-west-2" ) +// Valid indicates whether the value is a known member of the SetUpReadReplicaBodyReadReplicaRegion enum. +func (e SetUpReadReplicaBodyReadReplicaRegion) Valid() bool { + switch e { + case SetUpReadReplicaBodyReadReplicaRegionApEast1: + return true + case SetUpReadReplicaBodyReadReplicaRegionApNortheast1: + return true + case SetUpReadReplicaBodyReadReplicaRegionApNortheast2: + return true + case SetUpReadReplicaBodyReadReplicaRegionApSouth1: + return true + case SetUpReadReplicaBodyReadReplicaRegionApSoutheast1: + return true + case SetUpReadReplicaBodyReadReplicaRegionApSoutheast2: + return true + case SetUpReadReplicaBodyReadReplicaRegionCaCentral1: + return true + case SetUpReadReplicaBodyReadReplicaRegionEuCentral1: + return true + case SetUpReadReplicaBodyReadReplicaRegionEuCentral2: + return true + case SetUpReadReplicaBodyReadReplicaRegionEuNorth1: + return true + case SetUpReadReplicaBodyReadReplicaRegionEuWest1: + return true + case SetUpReadReplicaBodyReadReplicaRegionEuWest2: + return true + case SetUpReadReplicaBodyReadReplicaRegionEuWest3: + return true + case SetUpReadReplicaBodyReadReplicaRegionSaEast1: + return true + case SetUpReadReplicaBodyReadReplicaRegionUsEast1: + return true + case SetUpReadReplicaBodyReadReplicaRegionUsEast2: + return true + case SetUpReadReplicaBodyReadReplicaRegionUsWest1: + return true + case SetUpReadReplicaBodyReadReplicaRegionUsWest2: + return true + default: + return false + } +} + // Defines values for SigningKeyResponseAlgorithm. const ( SigningKeyResponseAlgorithmES256 SigningKeyResponseAlgorithm = "ES256" @@ -1088,6 +3208,22 @@ const ( SigningKeyResponseAlgorithmRS256 SigningKeyResponseAlgorithm = "RS256" ) +// Valid indicates whether the value is a known member of the SigningKeyResponseAlgorithm enum. +func (e SigningKeyResponseAlgorithm) Valid() bool { + switch e { + case SigningKeyResponseAlgorithmES256: + return true + case SigningKeyResponseAlgorithmEdDSA: + return true + case SigningKeyResponseAlgorithmHS256: + return true + case SigningKeyResponseAlgorithmRS256: + return true + default: + return false + } +} + // Defines values for SigningKeyResponseStatus. const ( SigningKeyResponseStatusInUse SigningKeyResponseStatus = "in_use" @@ -1096,6 +3232,22 @@ const ( SigningKeyResponseStatusStandby SigningKeyResponseStatus = "standby" ) +// Valid indicates whether the value is a known member of the SigningKeyResponseStatus enum. +func (e SigningKeyResponseStatus) Valid() bool { + switch e { + case SigningKeyResponseStatusInUse: + return true + case SigningKeyResponseStatusPreviouslyUsed: + return true + case SigningKeyResponseStatusRevoked: + return true + case SigningKeyResponseStatusStandby: + return true + default: + return false + } +} + // Defines values for SigningKeysResponseKeysAlgorithm. const ( SigningKeysResponseKeysAlgorithmES256 SigningKeysResponseKeysAlgorithm = "ES256" @@ -1104,6 +3256,22 @@ const ( SigningKeysResponseKeysAlgorithmRS256 SigningKeysResponseKeysAlgorithm = "RS256" ) +// Valid indicates whether the value is a known member of the SigningKeysResponseKeysAlgorithm enum. +func (e SigningKeysResponseKeysAlgorithm) Valid() bool { + switch e { + case SigningKeysResponseKeysAlgorithmES256: + return true + case SigningKeysResponseKeysAlgorithmEdDSA: + return true + case SigningKeysResponseKeysAlgorithmHS256: + return true + case SigningKeysResponseKeysAlgorithmRS256: + return true + default: + return false + } +} + // Defines values for SigningKeysResponseKeysStatus. const ( SigningKeysResponseKeysStatusInUse SigningKeysResponseKeysStatus = "in_use" @@ -1112,11 +3280,37 @@ const ( SigningKeysResponseKeysStatusStandby SigningKeysResponseKeysStatus = "standby" ) +// Valid indicates whether the value is a known member of the SigningKeysResponseKeysStatus enum. +func (e SigningKeysResponseKeysStatus) Valid() bool { + switch e { + case SigningKeysResponseKeysStatusInUse: + return true + case SigningKeysResponseKeysStatusPreviouslyUsed: + return true + case SigningKeysResponseKeysStatusRevoked: + return true + case SigningKeysResponseKeysStatusStandby: + return true + default: + return false + } +} + // Defines values for SnippetListDataType. const ( SnippetListDataTypeSql SnippetListDataType = "sql" ) +// Valid indicates whether the value is a known member of the SnippetListDataType enum. +func (e SnippetListDataType) Valid() bool { + switch e { + case SnippetListDataTypeSql: + return true + default: + return false + } +} + // Defines values for SnippetListDataVisibility. const ( SnippetListDataVisibilityOrg SnippetListDataVisibility = "org" @@ -1125,11 +3319,37 @@ const ( SnippetListDataVisibilityUser SnippetListDataVisibility = "user" ) +// Valid indicates whether the value is a known member of the SnippetListDataVisibility enum. +func (e SnippetListDataVisibility) Valid() bool { + switch e { + case SnippetListDataVisibilityOrg: + return true + case SnippetListDataVisibilityProject: + return true + case SnippetListDataVisibilityPublic: + return true + case SnippetListDataVisibilityUser: + return true + default: + return false + } +} + // Defines values for SnippetResponseType. const ( SnippetResponseTypeSql SnippetResponseType = "sql" ) +// Valid indicates whether the value is a known member of the SnippetResponseType enum. +func (e SnippetResponseType) Valid() bool { + switch e { + case SnippetResponseTypeSql: + return true + default: + return false + } +} + // Defines values for SnippetResponseVisibility. const ( SnippetResponseVisibilityOrg SnippetResponseVisibility = "org" @@ -1138,30 +3358,94 @@ const ( SnippetResponseVisibilityUser SnippetResponseVisibility = "user" ) +// Valid indicates whether the value is a known member of the SnippetResponseVisibility enum. +func (e SnippetResponseVisibility) Valid() bool { + switch e { + case SnippetResponseVisibilityOrg: + return true + case SnippetResponseVisibilityProject: + return true + case SnippetResponseVisibilityPublic: + return true + case SnippetResponseVisibilityUser: + return true + default: + return false + } +} + // Defines values for StorageConfigResponseExternalUpstreamTarget. const ( StorageConfigResponseExternalUpstreamTargetCanary StorageConfigResponseExternalUpstreamTarget = "canary" StorageConfigResponseExternalUpstreamTargetMain StorageConfigResponseExternalUpstreamTarget = "main" ) +// Valid indicates whether the value is a known member of the StorageConfigResponseExternalUpstreamTarget enum. +func (e StorageConfigResponseExternalUpstreamTarget) Valid() bool { + switch e { + case StorageConfigResponseExternalUpstreamTargetCanary: + return true + case StorageConfigResponseExternalUpstreamTargetMain: + return true + default: + return false + } +} + // Defines values for SupavisorConfigResponseDatabaseType. const ( SupavisorConfigResponseDatabaseTypePRIMARY SupavisorConfigResponseDatabaseType = "PRIMARY" SupavisorConfigResponseDatabaseTypeREADREPLICA SupavisorConfigResponseDatabaseType = "READ_REPLICA" ) +// Valid indicates whether the value is a known member of the SupavisorConfigResponseDatabaseType enum. +func (e SupavisorConfigResponseDatabaseType) Valid() bool { + switch e { + case SupavisorConfigResponseDatabaseTypePRIMARY: + return true + case SupavisorConfigResponseDatabaseTypeREADREPLICA: + return true + default: + return false + } +} + // Defines values for SupavisorConfigResponsePoolMode. const ( SupavisorConfigResponsePoolModeSession SupavisorConfigResponsePoolMode = "session" SupavisorConfigResponsePoolModeTransaction SupavisorConfigResponsePoolMode = "transaction" ) +// Valid indicates whether the value is a known member of the SupavisorConfigResponsePoolMode enum. +func (e SupavisorConfigResponsePoolMode) Valid() bool { + switch e { + case SupavisorConfigResponsePoolModeSession: + return true + case SupavisorConfigResponsePoolModeTransaction: + return true + default: + return false + } +} + // Defines values for UpdateAuthConfigBodyDbMaxPoolSizeUnit. const ( UpdateAuthConfigBodyDbMaxPoolSizeUnitConnections UpdateAuthConfigBodyDbMaxPoolSizeUnit = "connections" UpdateAuthConfigBodyDbMaxPoolSizeUnitPercent UpdateAuthConfigBodyDbMaxPoolSizeUnit = "percent" ) +// Valid indicates whether the value is a known member of the UpdateAuthConfigBodyDbMaxPoolSizeUnit enum. +func (e UpdateAuthConfigBodyDbMaxPoolSizeUnit) Valid() bool { + switch e { + case UpdateAuthConfigBodyDbMaxPoolSizeUnitConnections: + return true + case UpdateAuthConfigBodyDbMaxPoolSizeUnitPercent: + return true + default: + return false + } +} + // Defines values for UpdateAuthConfigBodyPasswordRequiredCharacters. const ( UpdateAuthConfigBodyPasswordRequiredCharactersAbcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 UpdateAuthConfigBodyPasswordRequiredCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ:0123456789" @@ -1170,12 +3454,40 @@ const ( UpdateAuthConfigBodyPasswordRequiredCharactersEmpty UpdateAuthConfigBodyPasswordRequiredCharacters = "" ) +// Valid indicates whether the value is a known member of the UpdateAuthConfigBodyPasswordRequiredCharacters enum. +func (e UpdateAuthConfigBodyPasswordRequiredCharacters) Valid() bool { + switch e { + case UpdateAuthConfigBodyPasswordRequiredCharactersAbcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789: + return true + case UpdateAuthConfigBodyPasswordRequiredCharactersAbcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567891: + return true + case UpdateAuthConfigBodyPasswordRequiredCharactersAbcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567892: + return true + case UpdateAuthConfigBodyPasswordRequiredCharactersEmpty: + return true + default: + return false + } +} + // Defines values for UpdateAuthConfigBodySecurityCaptchaProvider. const ( UpdateAuthConfigBodySecurityCaptchaProviderHcaptcha UpdateAuthConfigBodySecurityCaptchaProvider = "hcaptcha" UpdateAuthConfigBodySecurityCaptchaProviderTurnstile UpdateAuthConfigBodySecurityCaptchaProvider = "turnstile" ) +// Valid indicates whether the value is a known member of the UpdateAuthConfigBodySecurityCaptchaProvider enum. +func (e UpdateAuthConfigBodySecurityCaptchaProvider) Valid() bool { + switch e { + case UpdateAuthConfigBodySecurityCaptchaProviderHcaptcha: + return true + case UpdateAuthConfigBodySecurityCaptchaProviderTurnstile: + return true + default: + return false + } +} + // Defines values for UpdateAuthConfigBodySmsProvider. const ( UpdateAuthConfigBodySmsProviderMessagebird UpdateAuthConfigBodySmsProvider = "messagebird" @@ -1185,6 +3497,24 @@ const ( UpdateAuthConfigBodySmsProviderVonage UpdateAuthConfigBodySmsProvider = "vonage" ) +// Valid indicates whether the value is a known member of the UpdateAuthConfigBodySmsProvider enum. +func (e UpdateAuthConfigBodySmsProvider) Valid() bool { + switch e { + case UpdateAuthConfigBodySmsProviderMessagebird: + return true + case UpdateAuthConfigBodySmsProviderTextlocal: + return true + case UpdateAuthConfigBodySmsProviderTwilio: + return true + case UpdateAuthConfigBodySmsProviderTwilioVerify: + return true + case UpdateAuthConfigBodySmsProviderVonage: + return true + default: + return false + } +} + // Defines values for UpdateBranchBodyStatus. const ( UpdateBranchBodyStatusCREATINGPROJECT UpdateBranchBodyStatus = "CREATING_PROJECT" @@ -1195,6 +3525,26 @@ const ( UpdateBranchBodyStatusRUNNINGMIGRATIONS UpdateBranchBodyStatus = "RUNNING_MIGRATIONS" ) +// Valid indicates whether the value is a known member of the UpdateBranchBodyStatus enum. +func (e UpdateBranchBodyStatus) Valid() bool { + switch e { + case UpdateBranchBodyStatusCREATINGPROJECT: + return true + case UpdateBranchBodyStatusFUNCTIONSDEPLOYED: + return true + case UpdateBranchBodyStatusFUNCTIONSFAILED: + return true + case UpdateBranchBodyStatusMIGRATIONSFAILED: + return true + case UpdateBranchBodyStatusMIGRATIONSPASSED: + return true + case UpdateBranchBodyStatusRUNNINGMIGRATIONS: + return true + default: + return false + } +} + // Defines values for UpdateCustomHostnameResponseStatus. const ( N1NotStarted UpdateCustomHostnameResponseStatus = "1_not_started" @@ -1204,6 +3554,24 @@ const ( N5ServicesReconfigured UpdateCustomHostnameResponseStatus = "5_services_reconfigured" ) +// Valid indicates whether the value is a known member of the UpdateCustomHostnameResponseStatus enum. +func (e UpdateCustomHostnameResponseStatus) Valid() bool { + switch e { + case N1NotStarted: + return true + case N2Initiated: + return true + case N3ChallengeVerified: + return true + case N4OriginSetupCompleted: + return true + case N5ServicesReconfigured: + return true + default: + return false + } +} + // Defines values for UpdatePostgresConfigBodySessionReplicationRole. const ( UpdatePostgresConfigBodySessionReplicationRoleLocal UpdatePostgresConfigBodySessionReplicationRole = "local" @@ -1211,6 +3579,20 @@ const ( UpdatePostgresConfigBodySessionReplicationRoleReplica UpdatePostgresConfigBodySessionReplicationRole = "replica" ) +// Valid indicates whether the value is a known member of the UpdatePostgresConfigBodySessionReplicationRole enum. +func (e UpdatePostgresConfigBodySessionReplicationRole) Valid() bool { + switch e { + case UpdatePostgresConfigBodySessionReplicationRoleLocal: + return true + case UpdatePostgresConfigBodySessionReplicationRoleOrigin: + return true + case UpdatePostgresConfigBodySessionReplicationRoleReplica: + return true + default: + return false + } +} + // Defines values for UpdateProviderBodyNameIdFormat. const ( UpdateProviderBodyNameIdFormatUrnOasisNamesTcSAML11NameidFormatEmailAddress UpdateProviderBodyNameIdFormat = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" @@ -1219,6 +3601,22 @@ const ( UpdateProviderBodyNameIdFormatUrnOasisNamesTcSAML20NameidFormatTransient UpdateProviderBodyNameIdFormat = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient" ) +// Valid indicates whether the value is a known member of the UpdateProviderBodyNameIdFormat enum. +func (e UpdateProviderBodyNameIdFormat) Valid() bool { + switch e { + case UpdateProviderBodyNameIdFormatUrnOasisNamesTcSAML11NameidFormatEmailAddress: + return true + case UpdateProviderBodyNameIdFormatUrnOasisNamesTcSAML11NameidFormatUnspecified: + return true + case UpdateProviderBodyNameIdFormatUrnOasisNamesTcSAML20NameidFormatPersistent: + return true + case UpdateProviderBodyNameIdFormatUrnOasisNamesTcSAML20NameidFormatTransient: + return true + default: + return false + } +} + // Defines values for UpdateRunStatusBodyClone. const ( UpdateRunStatusBodyCloneCREATED UpdateRunStatusBodyClone = "CREATED" @@ -1230,6 +3628,28 @@ const ( UpdateRunStatusBodyCloneRUNNING UpdateRunStatusBodyClone = "RUNNING" ) +// Valid indicates whether the value is a known member of the UpdateRunStatusBodyClone enum. +func (e UpdateRunStatusBodyClone) Valid() bool { + switch e { + case UpdateRunStatusBodyCloneCREATED: + return true + case UpdateRunStatusBodyCloneDEAD: + return true + case UpdateRunStatusBodyCloneEXITED: + return true + case UpdateRunStatusBodyClonePAUSED: + return true + case UpdateRunStatusBodyCloneREMOVING: + return true + case UpdateRunStatusBodyCloneRESTARTING: + return true + case UpdateRunStatusBodyCloneRUNNING: + return true + default: + return false + } +} + // Defines values for UpdateRunStatusBodyConfigure. const ( UpdateRunStatusBodyConfigureCREATED UpdateRunStatusBodyConfigure = "CREATED" @@ -1241,6 +3661,28 @@ const ( UpdateRunStatusBodyConfigureRUNNING UpdateRunStatusBodyConfigure = "RUNNING" ) +// Valid indicates whether the value is a known member of the UpdateRunStatusBodyConfigure enum. +func (e UpdateRunStatusBodyConfigure) Valid() bool { + switch e { + case UpdateRunStatusBodyConfigureCREATED: + return true + case UpdateRunStatusBodyConfigureDEAD: + return true + case UpdateRunStatusBodyConfigureEXITED: + return true + case UpdateRunStatusBodyConfigurePAUSED: + return true + case UpdateRunStatusBodyConfigureREMOVING: + return true + case UpdateRunStatusBodyConfigureRESTARTING: + return true + case UpdateRunStatusBodyConfigureRUNNING: + return true + default: + return false + } +} + // Defines values for UpdateRunStatusBodyDeploy. const ( UpdateRunStatusBodyDeployCREATED UpdateRunStatusBodyDeploy = "CREATED" @@ -1252,6 +3694,28 @@ const ( UpdateRunStatusBodyDeployRUNNING UpdateRunStatusBodyDeploy = "RUNNING" ) +// Valid indicates whether the value is a known member of the UpdateRunStatusBodyDeploy enum. +func (e UpdateRunStatusBodyDeploy) Valid() bool { + switch e { + case UpdateRunStatusBodyDeployCREATED: + return true + case UpdateRunStatusBodyDeployDEAD: + return true + case UpdateRunStatusBodyDeployEXITED: + return true + case UpdateRunStatusBodyDeployPAUSED: + return true + case UpdateRunStatusBodyDeployREMOVING: + return true + case UpdateRunStatusBodyDeployRESTARTING: + return true + case UpdateRunStatusBodyDeployRUNNING: + return true + default: + return false + } +} + // Defines values for UpdateRunStatusBodyHealth. const ( UpdateRunStatusBodyHealthCREATED UpdateRunStatusBodyHealth = "CREATED" @@ -1263,6 +3727,28 @@ const ( UpdateRunStatusBodyHealthRUNNING UpdateRunStatusBodyHealth = "RUNNING" ) +// Valid indicates whether the value is a known member of the UpdateRunStatusBodyHealth enum. +func (e UpdateRunStatusBodyHealth) Valid() bool { + switch e { + case UpdateRunStatusBodyHealthCREATED: + return true + case UpdateRunStatusBodyHealthDEAD: + return true + case UpdateRunStatusBodyHealthEXITED: + return true + case UpdateRunStatusBodyHealthPAUSED: + return true + case UpdateRunStatusBodyHealthREMOVING: + return true + case UpdateRunStatusBodyHealthRESTARTING: + return true + case UpdateRunStatusBodyHealthRUNNING: + return true + default: + return false + } +} + // Defines values for UpdateRunStatusBodyMigrate. const ( UpdateRunStatusBodyMigrateCREATED UpdateRunStatusBodyMigrate = "CREATED" @@ -1274,6 +3760,28 @@ const ( UpdateRunStatusBodyMigrateRUNNING UpdateRunStatusBodyMigrate = "RUNNING" ) +// Valid indicates whether the value is a known member of the UpdateRunStatusBodyMigrate enum. +func (e UpdateRunStatusBodyMigrate) Valid() bool { + switch e { + case UpdateRunStatusBodyMigrateCREATED: + return true + case UpdateRunStatusBodyMigrateDEAD: + return true + case UpdateRunStatusBodyMigrateEXITED: + return true + case UpdateRunStatusBodyMigratePAUSED: + return true + case UpdateRunStatusBodyMigrateREMOVING: + return true + case UpdateRunStatusBodyMigrateRESTARTING: + return true + case UpdateRunStatusBodyMigrateRUNNING: + return true + default: + return false + } +} + // Defines values for UpdateRunStatusBodyPull. const ( UpdateRunStatusBodyPullCREATED UpdateRunStatusBodyPull = "CREATED" @@ -1285,6 +3793,28 @@ const ( UpdateRunStatusBodyPullRUNNING UpdateRunStatusBodyPull = "RUNNING" ) +// Valid indicates whether the value is a known member of the UpdateRunStatusBodyPull enum. +func (e UpdateRunStatusBodyPull) Valid() bool { + switch e { + case UpdateRunStatusBodyPullCREATED: + return true + case UpdateRunStatusBodyPullDEAD: + return true + case UpdateRunStatusBodyPullEXITED: + return true + case UpdateRunStatusBodyPullPAUSED: + return true + case UpdateRunStatusBodyPullREMOVING: + return true + case UpdateRunStatusBodyPullRESTARTING: + return true + case UpdateRunStatusBodyPullRUNNING: + return true + default: + return false + } +} + // Defines values for UpdateRunStatusBodySeed. const ( UpdateRunStatusBodySeedCREATED UpdateRunStatusBodySeed = "CREATED" @@ -1296,11 +3826,43 @@ const ( UpdateRunStatusBodySeedRUNNING UpdateRunStatusBodySeed = "RUNNING" ) +// Valid indicates whether the value is a known member of the UpdateRunStatusBodySeed enum. +func (e UpdateRunStatusBodySeed) Valid() bool { + switch e { + case UpdateRunStatusBodySeedCREATED: + return true + case UpdateRunStatusBodySeedDEAD: + return true + case UpdateRunStatusBodySeedEXITED: + return true + case UpdateRunStatusBodySeedPAUSED: + return true + case UpdateRunStatusBodySeedREMOVING: + return true + case UpdateRunStatusBodySeedRESTARTING: + return true + case UpdateRunStatusBodySeedRUNNING: + return true + default: + return false + } +} + // Defines values for UpdateRunStatusResponseMessage. const ( UpdateRunStatusResponseMessageOk UpdateRunStatusResponseMessage = "ok" ) +// Valid indicates whether the value is a known member of the UpdateRunStatusResponseMessage enum. +func (e UpdateRunStatusResponseMessage) Valid() bool { + switch e { + case UpdateRunStatusResponseMessageOk: + return true + default: + return false + } +} + // Defines values for UpdateSigningKeyBodyStatus. const ( UpdateSigningKeyBodyStatusInUse UpdateSigningKeyBodyStatus = "in_use" @@ -1309,18 +3871,58 @@ const ( UpdateSigningKeyBodyStatusStandby UpdateSigningKeyBodyStatus = "standby" ) +// Valid indicates whether the value is a known member of the UpdateSigningKeyBodyStatus enum. +func (e UpdateSigningKeyBodyStatus) Valid() bool { + switch e { + case UpdateSigningKeyBodyStatusInUse: + return true + case UpdateSigningKeyBodyStatusPreviouslyUsed: + return true + case UpdateSigningKeyBodyStatusRevoked: + return true + case UpdateSigningKeyBodyStatusStandby: + return true + default: + return false + } +} + // Defines values for UpdateStorageConfigBodyExternalUpstreamTarget. const ( UpdateStorageConfigBodyExternalUpstreamTargetCanary UpdateStorageConfigBodyExternalUpstreamTarget = "canary" UpdateStorageConfigBodyExternalUpstreamTargetMain UpdateStorageConfigBodyExternalUpstreamTarget = "main" ) +// Valid indicates whether the value is a known member of the UpdateStorageConfigBodyExternalUpstreamTarget enum. +func (e UpdateStorageConfigBodyExternalUpstreamTarget) Valid() bool { + switch e { + case UpdateStorageConfigBodyExternalUpstreamTargetCanary: + return true + case UpdateStorageConfigBodyExternalUpstreamTargetMain: + return true + default: + return false + } +} + // Defines values for UpdateSupavisorConfigBodyPoolMode. const ( UpdateSupavisorConfigBodyPoolModeSession UpdateSupavisorConfigBodyPoolMode = "session" UpdateSupavisorConfigBodyPoolModeTransaction UpdateSupavisorConfigBodyPoolMode = "transaction" ) +// Valid indicates whether the value is a known member of the UpdateSupavisorConfigBodyPoolMode enum. +func (e UpdateSupavisorConfigBodyPoolMode) Valid() bool { + switch e { + case UpdateSupavisorConfigBodyPoolModeSession: + return true + case UpdateSupavisorConfigBodyPoolModeTransaction: + return true + default: + return false + } +} + // Defines values for UpgradeDatabaseBodyReleaseChannel. const ( UpgradeDatabaseBodyReleaseChannelAlpha UpgradeDatabaseBodyReleaseChannel = "alpha" @@ -1331,6 +3933,26 @@ const ( UpgradeDatabaseBodyReleaseChannelWithdrawn UpgradeDatabaseBodyReleaseChannel = "withdrawn" ) +// Valid indicates whether the value is a known member of the UpgradeDatabaseBodyReleaseChannel enum. +func (e UpgradeDatabaseBodyReleaseChannel) Valid() bool { + switch e { + case UpgradeDatabaseBodyReleaseChannelAlpha: + return true + case UpgradeDatabaseBodyReleaseChannelBeta: + return true + case UpgradeDatabaseBodyReleaseChannelGa: + return true + case UpgradeDatabaseBodyReleaseChannelInternal: + return true + case UpgradeDatabaseBodyReleaseChannelPreview: + return true + case UpgradeDatabaseBodyReleaseChannelWithdrawn: + return true + default: + return false + } +} + // Defines values for V1BackupsResponseBackupsStatus. const ( V1BackupsResponseBackupsStatusARCHIVED V1BackupsResponseBackupsStatus = "ARCHIVED" @@ -1341,6 +3963,26 @@ const ( V1BackupsResponseBackupsStatusREMOVED V1BackupsResponseBackupsStatus = "REMOVED" ) +// Valid indicates whether the value is a known member of the V1BackupsResponseBackupsStatus enum. +func (e V1BackupsResponseBackupsStatus) Valid() bool { + switch e { + case V1BackupsResponseBackupsStatusARCHIVED: + return true + case V1BackupsResponseBackupsStatusCANCELLED: + return true + case V1BackupsResponseBackupsStatusCOMPLETED: + return true + case V1BackupsResponseBackupsStatusFAILED: + return true + case V1BackupsResponseBackupsStatusPENDING: + return true + case V1BackupsResponseBackupsStatusREMOVED: + return true + default: + return false + } +} + // Defines values for V1CreateProjectBodyDesiredInstanceSize. const ( V1CreateProjectBodyDesiredInstanceSizeLarge V1CreateProjectBodyDesiredInstanceSize = "large" @@ -1364,12 +4006,70 @@ const ( V1CreateProjectBodyDesiredInstanceSizeXlarge V1CreateProjectBodyDesiredInstanceSize = "xlarge" ) +// Valid indicates whether the value is a known member of the V1CreateProjectBodyDesiredInstanceSize enum. +func (e V1CreateProjectBodyDesiredInstanceSize) Valid() bool { + switch e { + case V1CreateProjectBodyDesiredInstanceSizeLarge: + return true + case V1CreateProjectBodyDesiredInstanceSizeMedium: + return true + case V1CreateProjectBodyDesiredInstanceSizeMicro: + return true + case V1CreateProjectBodyDesiredInstanceSizeN12xlarge: + return true + case V1CreateProjectBodyDesiredInstanceSizeN16xlarge: + return true + case V1CreateProjectBodyDesiredInstanceSizeN24xlarge: + return true + case V1CreateProjectBodyDesiredInstanceSizeN24xlargeHighMemory: + return true + case V1CreateProjectBodyDesiredInstanceSizeN24xlargeOptimizedCpu: + return true + case V1CreateProjectBodyDesiredInstanceSizeN24xlargeOptimizedMemory: + return true + case V1CreateProjectBodyDesiredInstanceSizeN2xlarge: + return true + case V1CreateProjectBodyDesiredInstanceSizeN48xlarge: + return true + case V1CreateProjectBodyDesiredInstanceSizeN48xlargeHighMemory: + return true + case V1CreateProjectBodyDesiredInstanceSizeN48xlargeOptimizedCpu: + return true + case V1CreateProjectBodyDesiredInstanceSizeN48xlargeOptimizedMemory: + return true + case V1CreateProjectBodyDesiredInstanceSizeN4xlarge: + return true + case V1CreateProjectBodyDesiredInstanceSizeN8xlarge: + return true + case V1CreateProjectBodyDesiredInstanceSizeNano: + return true + case V1CreateProjectBodyDesiredInstanceSizeSmall: + return true + case V1CreateProjectBodyDesiredInstanceSizeXlarge: + return true + default: + return false + } +} + // Defines values for V1CreateProjectBodyPlan. const ( V1CreateProjectBodyPlanFree V1CreateProjectBodyPlan = "free" V1CreateProjectBodyPlanPro V1CreateProjectBodyPlan = "pro" ) +// Valid indicates whether the value is a known member of the V1CreateProjectBodyPlan enum. +func (e V1CreateProjectBodyPlan) Valid() bool { + switch e { + case V1CreateProjectBodyPlanFree: + return true + case V1CreateProjectBodyPlanPro: + return true + default: + return false + } +} + // Defines values for V1CreateProjectBodyRegion. const ( V1CreateProjectBodyRegionApEast1 V1CreateProjectBodyRegion = "ap-east-1" @@ -1392,6 +4092,50 @@ const ( V1CreateProjectBodyRegionUsWest2 V1CreateProjectBodyRegion = "us-west-2" ) +// Valid indicates whether the value is a known member of the V1CreateProjectBodyRegion enum. +func (e V1CreateProjectBodyRegion) Valid() bool { + switch e { + case V1CreateProjectBodyRegionApEast1: + return true + case V1CreateProjectBodyRegionApNortheast1: + return true + case V1CreateProjectBodyRegionApNortheast2: + return true + case V1CreateProjectBodyRegionApSouth1: + return true + case V1CreateProjectBodyRegionApSoutheast1: + return true + case V1CreateProjectBodyRegionApSoutheast2: + return true + case V1CreateProjectBodyRegionCaCentral1: + return true + case V1CreateProjectBodyRegionEuCentral1: + return true + case V1CreateProjectBodyRegionEuCentral2: + return true + case V1CreateProjectBodyRegionEuNorth1: + return true + case V1CreateProjectBodyRegionEuWest1: + return true + case V1CreateProjectBodyRegionEuWest2: + return true + case V1CreateProjectBodyRegionEuWest3: + return true + case V1CreateProjectBodyRegionSaEast1: + return true + case V1CreateProjectBodyRegionUsEast1: + return true + case V1CreateProjectBodyRegionUsEast2: + return true + case V1CreateProjectBodyRegionUsWest1: + return true + case V1CreateProjectBodyRegionUsWest2: + return true + default: + return false + } +} + // Defines values for V1CreateProjectBodyRegionSelection0Code. const ( ApEast1 V1CreateProjectBodyRegionSelection0Code = "ap-east-1" @@ -1414,11 +4158,65 @@ const ( UsWest2 V1CreateProjectBodyRegionSelection0Code = "us-west-2" ) +// Valid indicates whether the value is a known member of the V1CreateProjectBodyRegionSelection0Code enum. +func (e V1CreateProjectBodyRegionSelection0Code) Valid() bool { + switch e { + case ApEast1: + return true + case ApNortheast1: + return true + case ApNortheast2: + return true + case ApSouth1: + return true + case ApSoutheast1: + return true + case ApSoutheast2: + return true + case CaCentral1: + return true + case EuCentral1: + return true + case EuCentral2: + return true + case EuNorth1: + return true + case EuWest1: + return true + case EuWest2: + return true + case EuWest3: + return true + case SaEast1: + return true + case UsEast1: + return true + case UsEast2: + return true + case UsWest1: + return true + case UsWest2: + return true + default: + return false + } +} + // Defines values for V1CreateProjectBodyRegionSelection0Type. const ( Specific V1CreateProjectBodyRegionSelection0Type = "specific" ) +// Valid indicates whether the value is a known member of the V1CreateProjectBodyRegionSelection0Type enum. +func (e V1CreateProjectBodyRegionSelection0Type) Valid() bool { + switch e { + case Specific: + return true + default: + return false + } +} + // Defines values for V1CreateProjectBodyRegionSelection1Code. const ( Americas V1CreateProjectBodyRegionSelection1Code = "americas" @@ -1426,11 +4224,35 @@ const ( Emea V1CreateProjectBodyRegionSelection1Code = "emea" ) +// Valid indicates whether the value is a known member of the V1CreateProjectBodyRegionSelection1Code enum. +func (e V1CreateProjectBodyRegionSelection1Code) Valid() bool { + switch e { + case Americas: + return true + case Apac: + return true + case Emea: + return true + default: + return false + } +} + // Defines values for V1CreateProjectBodyRegionSelection1Type. const ( SmartGroup V1CreateProjectBodyRegionSelection1Type = "smartGroup" ) +// Valid indicates whether the value is a known member of the V1CreateProjectBodyRegionSelection1Type enum. +func (e V1CreateProjectBodyRegionSelection1Type) Valid() bool { + switch e { + case SmartGroup: + return true + default: + return false + } +} + // Defines values for V1ListEntitlementsResponseEntitlementsFeatureKey. const ( V1ListEntitlementsResponseEntitlementsFeatureKeyApiMembersInvitations V1ListEntitlementsResponseEntitlementsFeatureKey = "api.members.invitations" @@ -1498,6 +4320,140 @@ const ( V1ListEntitlementsResponseEntitlementsFeatureKeyVanitySubdomain V1ListEntitlementsResponseEntitlementsFeatureKey = "vanity_subdomain" ) +// Valid indicates whether the value is a known member of the V1ListEntitlementsResponseEntitlementsFeatureKey enum. +func (e V1ListEntitlementsResponseEntitlementsFeatureKey) Valid() bool { + switch e { + case V1ListEntitlementsResponseEntitlementsFeatureKeyApiMembersInvitations: + return true + case V1ListEntitlementsResponseEntitlementsFeatureKeyApiMembersRoles: + return true + case V1ListEntitlementsResponseEntitlementsFeatureKeyAssistantAdvanceModel: + return true + case V1ListEntitlementsResponseEntitlementsFeatureKeyAuditLogDrains: + return true + case V1ListEntitlementsResponseEntitlementsFeatureKeyAuthAdvancedAuthSettings: + return true + case V1ListEntitlementsResponseEntitlementsFeatureKeyAuthCustomJwtTemplate: + return true + case V1ListEntitlementsResponseEntitlementsFeatureKeyAuthCustomOauthMaxProviders: + return true + case V1ListEntitlementsResponseEntitlementsFeatureKeyAuthHooks: + return true + case V1ListEntitlementsResponseEntitlementsFeatureKeyAuthLeakedPasswordProtection: + return true + case V1ListEntitlementsResponseEntitlementsFeatureKeyAuthMfaEnhancedSecurity: + return true + case V1ListEntitlementsResponseEntitlementsFeatureKeyAuthMfaPhone: + return true + case V1ListEntitlementsResponseEntitlementsFeatureKeyAuthMfaWebAuthn: + return true + case V1ListEntitlementsResponseEntitlementsFeatureKeyAuthPasswordHibp: + return true + case V1ListEntitlementsResponseEntitlementsFeatureKeyAuthPerformanceSettings: + return true + case V1ListEntitlementsResponseEntitlementsFeatureKeyAuthPlatformSso: + return true + case V1ListEntitlementsResponseEntitlementsFeatureKeyAuthSaml2: + return true + case V1ListEntitlementsResponseEntitlementsFeatureKeyAuthUserSessions: + return true + case V1ListEntitlementsResponseEntitlementsFeatureKeyBackupRestoreToNewProject: + return true + case V1ListEntitlementsResponseEntitlementsFeatureKeyBackupRetentionDays: + return true + case V1ListEntitlementsResponseEntitlementsFeatureKeyBackupSchedule: + return true + case V1ListEntitlementsResponseEntitlementsFeatureKeyBranchingLimit: + return true + case V1ListEntitlementsResponseEntitlementsFeatureKeyBranchingPersistent: + return true + case V1ListEntitlementsResponseEntitlementsFeatureKeyCustomDomain: + return true + case V1ListEntitlementsResponseEntitlementsFeatureKeyDedicatedPooler: + return true + case V1ListEntitlementsResponseEntitlementsFeatureKeyFunctionMaxCount: + return true + case V1ListEntitlementsResponseEntitlementsFeatureKeyFunctionSizeLimitMb: + return true + case V1ListEntitlementsResponseEntitlementsFeatureKeyInstancesComputeUpdateAvailableSizes: + return true + case V1ListEntitlementsResponseEntitlementsFeatureKeyInstancesDiskModifications: + return true + case V1ListEntitlementsResponseEntitlementsFeatureKeyInstancesHighAvailability: + return true + case V1ListEntitlementsResponseEntitlementsFeatureKeyInstancesOrioledb: + return true + case V1ListEntitlementsResponseEntitlementsFeatureKeyInstancesReadReplicas: + return true + case V1ListEntitlementsResponseEntitlementsFeatureKeyIntegrationsGithubConnections: + return true + case V1ListEntitlementsResponseEntitlementsFeatureKeyIpv4: + return true + case V1ListEntitlementsResponseEntitlementsFeatureKeyLogDrains: + return true + case V1ListEntitlementsResponseEntitlementsFeatureKeyLogRetentionDays: + return true + case V1ListEntitlementsResponseEntitlementsFeatureKeyObservabilityDashboardAdvancedMetrics: + return true + case V1ListEntitlementsResponseEntitlementsFeatureKeyPitrAvailableVariants: + return true + case V1ListEntitlementsResponseEntitlementsFeatureKeyProjectCloning: + return true + case V1ListEntitlementsResponseEntitlementsFeatureKeyProjectPausing: + return true + case V1ListEntitlementsResponseEntitlementsFeatureKeyProjectRestoreAfterExpiry: + return true + case V1ListEntitlementsResponseEntitlementsFeatureKeyProjectScopedRoles: + return true + case V1ListEntitlementsResponseEntitlementsFeatureKeyRealtimeMaxBytesPerSecond: + return true + case V1ListEntitlementsResponseEntitlementsFeatureKeyRealtimeMaxChannelsPerClient: + return true + case V1ListEntitlementsResponseEntitlementsFeatureKeyRealtimeMaxConcurrentUsers: + return true + case V1ListEntitlementsResponseEntitlementsFeatureKeyRealtimeMaxEventsPerSecond: + return true + case V1ListEntitlementsResponseEntitlementsFeatureKeyRealtimeMaxJoinsPerSecond: + return true + case V1ListEntitlementsResponseEntitlementsFeatureKeyRealtimeMaxPayloadSizeInKb: + return true + case V1ListEntitlementsResponseEntitlementsFeatureKeyRealtimeMaxPresenceEventsPerSecond: + return true + case V1ListEntitlementsResponseEntitlementsFeatureKeyReplicationEtl: + return true + case V1ListEntitlementsResponseEntitlementsFeatureKeySecurityAuditLogsDays: + return true + case V1ListEntitlementsResponseEntitlementsFeatureKeySecurityEnforceMfa: + return true + case V1ListEntitlementsResponseEntitlementsFeatureKeySecurityIso27001Certificate: + return true + case V1ListEntitlementsResponseEntitlementsFeatureKeySecurityMemberRoles: + return true + case V1ListEntitlementsResponseEntitlementsFeatureKeySecurityPrivateLink: + return true + case V1ListEntitlementsResponseEntitlementsFeatureKeySecurityQuestionnaire: + return true + case V1ListEntitlementsResponseEntitlementsFeatureKeySecuritySoc2Report: + return true + case V1ListEntitlementsResponseEntitlementsFeatureKeyStorageIcebergCatalog: + return true + case V1ListEntitlementsResponseEntitlementsFeatureKeyStorageImageTransformations: + return true + case V1ListEntitlementsResponseEntitlementsFeatureKeyStorageMaxFileSize: + return true + case V1ListEntitlementsResponseEntitlementsFeatureKeyStorageMaxFileSizeConfigurable: + return true + case V1ListEntitlementsResponseEntitlementsFeatureKeyStoragePurgeCache: + return true + case V1ListEntitlementsResponseEntitlementsFeatureKeyStorageVectorBuckets: + return true + case V1ListEntitlementsResponseEntitlementsFeatureKeyVanitySubdomain: + return true + default: + return false + } +} + // Defines values for V1ListEntitlementsResponseEntitlementsFeatureType. const ( V1ListEntitlementsResponseEntitlementsFeatureTypeBoolean V1ListEntitlementsResponseEntitlementsFeatureType = "boolean" @@ -1505,6 +4461,20 @@ const ( V1ListEntitlementsResponseEntitlementsFeatureTypeSet V1ListEntitlementsResponseEntitlementsFeatureType = "set" ) +// Valid indicates whether the value is a known member of the V1ListEntitlementsResponseEntitlementsFeatureType enum. +func (e V1ListEntitlementsResponseEntitlementsFeatureType) Valid() bool { + switch e { + case V1ListEntitlementsResponseEntitlementsFeatureTypeBoolean: + return true + case V1ListEntitlementsResponseEntitlementsFeatureTypeNumeric: + return true + case V1ListEntitlementsResponseEntitlementsFeatureTypeSet: + return true + default: + return false + } +} + // Defines values for V1ListEntitlementsResponseEntitlementsType. const ( V1ListEntitlementsResponseEntitlementsTypeBoolean V1ListEntitlementsResponseEntitlementsType = "boolean" @@ -1512,6 +4482,20 @@ const ( V1ListEntitlementsResponseEntitlementsTypeSet V1ListEntitlementsResponseEntitlementsType = "set" ) +// Valid indicates whether the value is a known member of the V1ListEntitlementsResponseEntitlementsType enum. +func (e V1ListEntitlementsResponseEntitlementsType) Valid() bool { + switch e { + case V1ListEntitlementsResponseEntitlementsTypeBoolean: + return true + case V1ListEntitlementsResponseEntitlementsTypeNumeric: + return true + case V1ListEntitlementsResponseEntitlementsTypeSet: + return true + default: + return false + } +} + // Defines values for V1OrganizationSlugResponseAllowedReleaseChannels. const ( V1OrganizationSlugResponseAllowedReleaseChannelsAlpha V1OrganizationSlugResponseAllowedReleaseChannels = "alpha" @@ -1522,6 +4506,26 @@ const ( V1OrganizationSlugResponseAllowedReleaseChannelsWithdrawn V1OrganizationSlugResponseAllowedReleaseChannels = "withdrawn" ) +// Valid indicates whether the value is a known member of the V1OrganizationSlugResponseAllowedReleaseChannels enum. +func (e V1OrganizationSlugResponseAllowedReleaseChannels) Valid() bool { + switch e { + case V1OrganizationSlugResponseAllowedReleaseChannelsAlpha: + return true + case V1OrganizationSlugResponseAllowedReleaseChannelsBeta: + return true + case V1OrganizationSlugResponseAllowedReleaseChannelsGa: + return true + case V1OrganizationSlugResponseAllowedReleaseChannelsInternal: + return true + case V1OrganizationSlugResponseAllowedReleaseChannelsPreview: + return true + case V1OrganizationSlugResponseAllowedReleaseChannelsWithdrawn: + return true + default: + return false + } +} + // Defines values for V1OrganizationSlugResponseOptInTags. const ( AIDATAGENERATOROPTIN V1OrganizationSlugResponseOptInTags = "AI_DATA_GENERATOR_OPT_IN" @@ -1529,6 +4533,20 @@ const ( AISQLGENERATOROPTIN V1OrganizationSlugResponseOptInTags = "AI_SQL_GENERATOR_OPT_IN" ) +// Valid indicates whether the value is a known member of the V1OrganizationSlugResponseOptInTags enum. +func (e V1OrganizationSlugResponseOptInTags) Valid() bool { + switch e { + case AIDATAGENERATOROPTIN: + return true + case AILOGGENERATOROPTIN: + return true + case AISQLGENERATOROPTIN: + return true + default: + return false + } +} + // Defines values for V1OrganizationSlugResponsePlan. const ( V1OrganizationSlugResponsePlanEnterprise V1OrganizationSlugResponsePlan = "enterprise" @@ -1538,6 +4556,24 @@ const ( V1OrganizationSlugResponsePlanTeam V1OrganizationSlugResponsePlan = "team" ) +// Valid indicates whether the value is a known member of the V1OrganizationSlugResponsePlan enum. +func (e V1OrganizationSlugResponsePlan) Valid() bool { + switch e { + case V1OrganizationSlugResponsePlanEnterprise: + return true + case V1OrganizationSlugResponsePlanFree: + return true + case V1OrganizationSlugResponsePlanPlatform: + return true + case V1OrganizationSlugResponsePlanPro: + return true + case V1OrganizationSlugResponsePlanTeam: + return true + default: + return false + } +} + // Defines values for V1PgbouncerConfigResponsePoolMode. const ( Session V1PgbouncerConfigResponsePoolMode = "session" @@ -1545,17 +4581,53 @@ const ( Transaction V1PgbouncerConfigResponsePoolMode = "transaction" ) +// Valid indicates whether the value is a known member of the V1PgbouncerConfigResponsePoolMode enum. +func (e V1PgbouncerConfigResponsePoolMode) Valid() bool { + switch e { + case Session: + return true + case Statement: + return true + case Transaction: + return true + default: + return false + } +} + // Defines values for V1ProjectAdvisorsResponseLintsCategories. const ( PERFORMANCE V1ProjectAdvisorsResponseLintsCategories = "PERFORMANCE" SECURITY V1ProjectAdvisorsResponseLintsCategories = "SECURITY" ) +// Valid indicates whether the value is a known member of the V1ProjectAdvisorsResponseLintsCategories enum. +func (e V1ProjectAdvisorsResponseLintsCategories) Valid() bool { + switch e { + case PERFORMANCE: + return true + case SECURITY: + return true + default: + return false + } +} + // Defines values for V1ProjectAdvisorsResponseLintsFacing. const ( EXTERNAL V1ProjectAdvisorsResponseLintsFacing = "EXTERNAL" ) +// Valid indicates whether the value is a known member of the V1ProjectAdvisorsResponseLintsFacing enum. +func (e V1ProjectAdvisorsResponseLintsFacing) Valid() bool { + switch e { + case EXTERNAL: + return true + default: + return false + } +} + // Defines values for V1ProjectAdvisorsResponseLintsLevel. const ( ERROR V1ProjectAdvisorsResponseLintsLevel = "ERROR" @@ -1563,6 +4635,20 @@ const ( WARN V1ProjectAdvisorsResponseLintsLevel = "WARN" ) +// Valid indicates whether the value is a known member of the V1ProjectAdvisorsResponseLintsLevel enum. +func (e V1ProjectAdvisorsResponseLintsLevel) Valid() bool { + switch e { + case ERROR: + return true + case INFO: + return true + case WARN: + return true + default: + return false + } +} + // Defines values for V1ProjectAdvisorsResponseLintsMetadataType. const ( V1ProjectAdvisorsResponseLintsMetadataTypeAuth V1ProjectAdvisorsResponseLintsMetadataType = "auth" @@ -1573,6 +4659,26 @@ const ( V1ProjectAdvisorsResponseLintsMetadataTypeView V1ProjectAdvisorsResponseLintsMetadataType = "view" ) +// Valid indicates whether the value is a known member of the V1ProjectAdvisorsResponseLintsMetadataType enum. +func (e V1ProjectAdvisorsResponseLintsMetadataType) Valid() bool { + switch e { + case V1ProjectAdvisorsResponseLintsMetadataTypeAuth: + return true + case V1ProjectAdvisorsResponseLintsMetadataTypeCompliance: + return true + case V1ProjectAdvisorsResponseLintsMetadataTypeExtension: + return true + case V1ProjectAdvisorsResponseLintsMetadataTypeFunction: + return true + case V1ProjectAdvisorsResponseLintsMetadataTypeTable: + return true + case V1ProjectAdvisorsResponseLintsMetadataTypeView: + return true + default: + return false + } +} + // Defines values for V1ProjectAdvisorsResponseLintsName. const ( AuthInsufficientMfaOptions V1ProjectAdvisorsResponseLintsName = "auth_insufficient_mfa_options" @@ -1606,6 +4712,72 @@ const ( VulnerablePostgresVersion V1ProjectAdvisorsResponseLintsName = "vulnerable_postgres_version" ) +// Valid indicates whether the value is a known member of the V1ProjectAdvisorsResponseLintsName enum. +func (e V1ProjectAdvisorsResponseLintsName) Valid() bool { + switch e { + case AuthInsufficientMfaOptions: + return true + case AuthLeakedPasswordProtection: + return true + case AuthOtpLongExpiry: + return true + case AuthOtpShortLength: + return true + case AuthPasswordPolicyMissing: + return true + case AuthRlsInitplan: + return true + case AuthUsersExposed: + return true + case DuplicateIndex: + return true + case ExtensionInPublic: + return true + case ForeignTableInApi: + return true + case FunctionSearchPathMutable: + return true + case LeakedServiceKey: + return true + case MaterializedViewInApi: + return true + case MultiplePermissivePolicies: + return true + case NetworkRestrictionsNotSet: + return true + case NoBackupAdmin: + return true + case NoPrimaryKey: + return true + case PasswordRequirementsMinLength: + return true + case PitrNotEnabled: + return true + case PolicyExistsRlsDisabled: + return true + case RlsDisabledInPublic: + return true + case RlsEnabledNoPolicy: + return true + case RlsReferencesUserMetadata: + return true + case SecurityDefinerView: + return true + case SslNotEnforced: + return true + case UnindexedForeignKeys: + return true + case UnsupportedRegTypes: + return true + case UnusedIndex: + return true + case VulnerablePostgresVersion: + return true + default: + return false + } +} + // Defines values for V1ProjectResponseStatus. const ( V1ProjectResponseStatusACTIVEHEALTHY V1ProjectResponseStatus = "ACTIVE_HEALTHY" @@ -1625,6 +4797,44 @@ const ( V1ProjectResponseStatusUPGRADING V1ProjectResponseStatus = "UPGRADING" ) +// Valid indicates whether the value is a known member of the V1ProjectResponseStatus enum. +func (e V1ProjectResponseStatus) Valid() bool { + switch e { + case V1ProjectResponseStatusACTIVEHEALTHY: + return true + case V1ProjectResponseStatusACTIVEUNHEALTHY: + return true + case V1ProjectResponseStatusCOMINGUP: + return true + case V1ProjectResponseStatusGOINGDOWN: + return true + case V1ProjectResponseStatusINACTIVE: + return true + case V1ProjectResponseStatusINITFAILED: + return true + case V1ProjectResponseStatusPAUSEFAILED: + return true + case V1ProjectResponseStatusPAUSING: + return true + case V1ProjectResponseStatusREMOVED: + return true + case V1ProjectResponseStatusRESIZING: + return true + case V1ProjectResponseStatusRESTARTING: + return true + case V1ProjectResponseStatusRESTOREFAILED: + return true + case V1ProjectResponseStatusRESTORING: + return true + case V1ProjectResponseStatusUNKNOWN: + return true + case V1ProjectResponseStatusUPGRADING: + return true + default: + return false + } +} + // Defines values for V1ProjectWithDatabaseResponseStatus. const ( V1ProjectWithDatabaseResponseStatusACTIVEHEALTHY V1ProjectWithDatabaseResponseStatus = "ACTIVE_HEALTHY" @@ -1644,6 +4854,44 @@ const ( V1ProjectWithDatabaseResponseStatusUPGRADING V1ProjectWithDatabaseResponseStatus = "UPGRADING" ) +// Valid indicates whether the value is a known member of the V1ProjectWithDatabaseResponseStatus enum. +func (e V1ProjectWithDatabaseResponseStatus) Valid() bool { + switch e { + case V1ProjectWithDatabaseResponseStatusACTIVEHEALTHY: + return true + case V1ProjectWithDatabaseResponseStatusACTIVEUNHEALTHY: + return true + case V1ProjectWithDatabaseResponseStatusCOMINGUP: + return true + case V1ProjectWithDatabaseResponseStatusGOINGDOWN: + return true + case V1ProjectWithDatabaseResponseStatusINACTIVE: + return true + case V1ProjectWithDatabaseResponseStatusINITFAILED: + return true + case V1ProjectWithDatabaseResponseStatusPAUSEFAILED: + return true + case V1ProjectWithDatabaseResponseStatusPAUSING: + return true + case V1ProjectWithDatabaseResponseStatusREMOVED: + return true + case V1ProjectWithDatabaseResponseStatusRESIZING: + return true + case V1ProjectWithDatabaseResponseStatusRESTARTING: + return true + case V1ProjectWithDatabaseResponseStatusRESTOREFAILED: + return true + case V1ProjectWithDatabaseResponseStatusRESTORING: + return true + case V1ProjectWithDatabaseResponseStatusUNKNOWN: + return true + case V1ProjectWithDatabaseResponseStatusUPGRADING: + return true + default: + return false + } +} + // Defines values for V1RestorePointResponseStatus. const ( V1RestorePointResponseStatusAVAILABLE V1RestorePointResponseStatus = "AVAILABLE" @@ -1652,11 +4900,37 @@ const ( V1RestorePointResponseStatusREMOVED V1RestorePointResponseStatus = "REMOVED" ) +// Valid indicates whether the value is a known member of the V1RestorePointResponseStatus enum. +func (e V1RestorePointResponseStatus) Valid() bool { + switch e { + case V1RestorePointResponseStatusAVAILABLE: + return true + case V1RestorePointResponseStatusFAILED: + return true + case V1RestorePointResponseStatusPENDING: + return true + case V1RestorePointResponseStatusREMOVED: + return true + default: + return false + } +} + // Defines values for V1ServiceHealthResponseInfo0Name. const ( GoTrue V1ServiceHealthResponseInfo0Name = "GoTrue" ) +// Valid indicates whether the value is a known member of the V1ServiceHealthResponseInfo0Name enum. +func (e V1ServiceHealthResponseInfo0Name) Valid() bool { + switch e { + case GoTrue: + return true + default: + return false + } +} + // Defines values for V1ServiceHealthResponseName. const ( V1ServiceHealthResponseNameAuth V1ServiceHealthResponseName = "auth" @@ -1669,6 +4943,30 @@ const ( V1ServiceHealthResponseNameStorage V1ServiceHealthResponseName = "storage" ) +// Valid indicates whether the value is a known member of the V1ServiceHealthResponseName enum. +func (e V1ServiceHealthResponseName) Valid() bool { + switch e { + case V1ServiceHealthResponseNameAuth: + return true + case V1ServiceHealthResponseNameDb: + return true + case V1ServiceHealthResponseNameDbPostgresUser: + return true + case V1ServiceHealthResponseNamePgBouncer: + return true + case V1ServiceHealthResponseNamePooler: + return true + case V1ServiceHealthResponseNameRealtime: + return true + case V1ServiceHealthResponseNameRest: + return true + case V1ServiceHealthResponseNameStorage: + return true + default: + return false + } +} + // Defines values for V1ServiceHealthResponseStatus. const ( ACTIVEHEALTHY V1ServiceHealthResponseStatus = "ACTIVE_HEALTHY" @@ -1676,6 +4974,20 @@ const ( UNHEALTHY V1ServiceHealthResponseStatus = "UNHEALTHY" ) +// Valid indicates whether the value is a known member of the V1ServiceHealthResponseStatus enum. +func (e V1ServiceHealthResponseStatus) Valid() bool { + switch e { + case ACTIVEHEALTHY: + return true + case COMINGUP: + return true + case UNHEALTHY: + return true + default: + return false + } +} + // Defines values for VanitySubdomainConfigResponseStatus. const ( Active VanitySubdomainConfigResponseStatus = "active" @@ -1683,6 +4995,20 @@ const ( NotUsed VanitySubdomainConfigResponseStatus = "not-used" ) +// Valid indicates whether the value is a known member of the VanitySubdomainConfigResponseStatus enum. +func (e VanitySubdomainConfigResponseStatus) Valid() bool { + switch e { + case Active: + return true + case CustomDomainUsed: + return true + case NotUsed: + return true + default: + return false + } +} + // Defines values for V1AuthorizeUserParamsResponseType. const ( V1AuthorizeUserParamsResponseTypeCode V1AuthorizeUserParamsResponseType = "code" @@ -1690,6 +5016,20 @@ const ( V1AuthorizeUserParamsResponseTypeToken V1AuthorizeUserParamsResponseType = "token" ) +// Valid indicates whether the value is a known member of the V1AuthorizeUserParamsResponseType enum. +func (e V1AuthorizeUserParamsResponseType) Valid() bool { + switch e { + case V1AuthorizeUserParamsResponseTypeCode: + return true + case V1AuthorizeUserParamsResponseTypeIdTokenToken: + return true + case V1AuthorizeUserParamsResponseTypeToken: + return true + default: + return false + } +} + // Defines values for V1AuthorizeUserParamsCodeChallengeMethod. const ( V1AuthorizeUserParamsCodeChallengeMethodPlain V1AuthorizeUserParamsCodeChallengeMethod = "plain" @@ -1697,6 +5037,20 @@ const ( V1AuthorizeUserParamsCodeChallengeMethodSha256 V1AuthorizeUserParamsCodeChallengeMethod = "sha256" ) +// Valid indicates whether the value is a known member of the V1AuthorizeUserParamsCodeChallengeMethod enum. +func (e V1AuthorizeUserParamsCodeChallengeMethod) Valid() bool { + switch e { + case V1AuthorizeUserParamsCodeChallengeMethodPlain: + return true + case V1AuthorizeUserParamsCodeChallengeMethodS256: + return true + case V1AuthorizeUserParamsCodeChallengeMethodSha256: + return true + default: + return false + } +} + // Defines values for V1OauthAuthorizeProjectClaimParamsResponseType. const ( V1OauthAuthorizeProjectClaimParamsResponseTypeCode V1OauthAuthorizeProjectClaimParamsResponseType = "code" @@ -1704,6 +5058,20 @@ const ( V1OauthAuthorizeProjectClaimParamsResponseTypeToken V1OauthAuthorizeProjectClaimParamsResponseType = "token" ) +// Valid indicates whether the value is a known member of the V1OauthAuthorizeProjectClaimParamsResponseType enum. +func (e V1OauthAuthorizeProjectClaimParamsResponseType) Valid() bool { + switch e { + case V1OauthAuthorizeProjectClaimParamsResponseTypeCode: + return true + case V1OauthAuthorizeProjectClaimParamsResponseTypeIdTokenToken: + return true + case V1OauthAuthorizeProjectClaimParamsResponseTypeToken: + return true + default: + return false + } +} + // Defines values for V1OauthAuthorizeProjectClaimParamsCodeChallengeMethod. const ( V1OauthAuthorizeProjectClaimParamsCodeChallengeMethodPlain V1OauthAuthorizeProjectClaimParamsCodeChallengeMethod = "plain" @@ -1711,6 +5079,20 @@ const ( V1OauthAuthorizeProjectClaimParamsCodeChallengeMethodSha256 V1OauthAuthorizeProjectClaimParamsCodeChallengeMethod = "sha256" ) +// Valid indicates whether the value is a known member of the V1OauthAuthorizeProjectClaimParamsCodeChallengeMethod enum. +func (e V1OauthAuthorizeProjectClaimParamsCodeChallengeMethod) Valid() bool { + switch e { + case V1OauthAuthorizeProjectClaimParamsCodeChallengeMethodPlain: + return true + case V1OauthAuthorizeProjectClaimParamsCodeChallengeMethodS256: + return true + case V1OauthAuthorizeProjectClaimParamsCodeChallengeMethodSha256: + return true + default: + return false + } +} + // Defines values for V1GetAllProjectsForOrganizationParamsSort. const ( CreatedAsc V1GetAllProjectsForOrganizationParamsSort = "created_asc" @@ -1719,6 +5101,22 @@ const ( NameDesc V1GetAllProjectsForOrganizationParamsSort = "name_desc" ) +// Valid indicates whether the value is a known member of the V1GetAllProjectsForOrganizationParamsSort enum. +func (e V1GetAllProjectsForOrganizationParamsSort) Valid() bool { + switch e { + case CreatedAsc: + return true + case CreatedDesc: + return true + case NameAsc: + return true + case NameDesc: + return true + default: + return false + } +} + // Defines values for V1GetAvailableRegionsParamsContinent. const ( AF V1GetAvailableRegionsParamsContinent = "AF" @@ -1730,6 +5128,28 @@ const ( SA V1GetAvailableRegionsParamsContinent = "SA" ) +// Valid indicates whether the value is a known member of the V1GetAvailableRegionsParamsContinent enum. +func (e V1GetAvailableRegionsParamsContinent) Valid() bool { + switch e { + case AF: + return true + case AN: + return true + case AS: + return true + case EU: + return true + case NA: + return true + case OC: + return true + case SA: + return true + default: + return false + } +} + // Defines values for V1GetAvailableRegionsParamsDesiredInstanceSize. const ( V1GetAvailableRegionsParamsDesiredInstanceSizeLarge V1GetAvailableRegionsParamsDesiredInstanceSize = "large" @@ -1753,11 +5173,67 @@ const ( V1GetAvailableRegionsParamsDesiredInstanceSizeXlarge V1GetAvailableRegionsParamsDesiredInstanceSize = "xlarge" ) +// Valid indicates whether the value is a known member of the V1GetAvailableRegionsParamsDesiredInstanceSize enum. +func (e V1GetAvailableRegionsParamsDesiredInstanceSize) Valid() bool { + switch e { + case V1GetAvailableRegionsParamsDesiredInstanceSizeLarge: + return true + case V1GetAvailableRegionsParamsDesiredInstanceSizeMedium: + return true + case V1GetAvailableRegionsParamsDesiredInstanceSizeMicro: + return true + case V1GetAvailableRegionsParamsDesiredInstanceSizeN12xlarge: + return true + case V1GetAvailableRegionsParamsDesiredInstanceSizeN16xlarge: + return true + case V1GetAvailableRegionsParamsDesiredInstanceSizeN24xlarge: + return true + case V1GetAvailableRegionsParamsDesiredInstanceSizeN24xlargeHighMemory: + return true + case V1GetAvailableRegionsParamsDesiredInstanceSizeN24xlargeOptimizedCpu: + return true + case V1GetAvailableRegionsParamsDesiredInstanceSizeN24xlargeOptimizedMemory: + return true + case V1GetAvailableRegionsParamsDesiredInstanceSizeN2xlarge: + return true + case V1GetAvailableRegionsParamsDesiredInstanceSizeN48xlarge: + return true + case V1GetAvailableRegionsParamsDesiredInstanceSizeN48xlargeHighMemory: + return true + case V1GetAvailableRegionsParamsDesiredInstanceSizeN48xlargeOptimizedCpu: + return true + case V1GetAvailableRegionsParamsDesiredInstanceSizeN48xlargeOptimizedMemory: + return true + case V1GetAvailableRegionsParamsDesiredInstanceSizeN4xlarge: + return true + case V1GetAvailableRegionsParamsDesiredInstanceSizeN8xlarge: + return true + case V1GetAvailableRegionsParamsDesiredInstanceSizeNano: + return true + case V1GetAvailableRegionsParamsDesiredInstanceSizeSmall: + return true + case V1GetAvailableRegionsParamsDesiredInstanceSizeXlarge: + return true + default: + return false + } +} + // Defines values for V1GetSecurityAdvisorsParamsLintType. const ( Sql V1GetSecurityAdvisorsParamsLintType = "sql" ) +// Valid indicates whether the value is a known member of the V1GetSecurityAdvisorsParamsLintType enum. +func (e V1GetSecurityAdvisorsParamsLintType) Valid() bool { + switch e { + case Sql: + return true + default: + return false + } +} + // Defines values for V1GetProjectFunctionCombinedStatsParamsInterval. const ( V1GetProjectFunctionCombinedStatsParamsIntervalN15min V1GetProjectFunctionCombinedStatsParamsInterval = "15min" @@ -1766,6 +5242,22 @@ const ( V1GetProjectFunctionCombinedStatsParamsIntervalN3hr V1GetProjectFunctionCombinedStatsParamsInterval = "3hr" ) +// Valid indicates whether the value is a known member of the V1GetProjectFunctionCombinedStatsParamsInterval enum. +func (e V1GetProjectFunctionCombinedStatsParamsInterval) Valid() bool { + switch e { + case V1GetProjectFunctionCombinedStatsParamsIntervalN15min: + return true + case V1GetProjectFunctionCombinedStatsParamsIntervalN1day: + return true + case V1GetProjectFunctionCombinedStatsParamsIntervalN1hr: + return true + case V1GetProjectFunctionCombinedStatsParamsIntervalN3hr: + return true + default: + return false + } +} + // Defines values for V1GetProjectUsageApiCountParamsInterval. const ( V1GetProjectUsageApiCountParamsIntervalN15min V1GetProjectUsageApiCountParamsInterval = "15min" @@ -1777,6 +5269,28 @@ const ( V1GetProjectUsageApiCountParamsIntervalN7day V1GetProjectUsageApiCountParamsInterval = "7day" ) +// Valid indicates whether the value is a known member of the V1GetProjectUsageApiCountParamsInterval enum. +func (e V1GetProjectUsageApiCountParamsInterval) Valid() bool { + switch e { + case V1GetProjectUsageApiCountParamsIntervalN15min: + return true + case V1GetProjectUsageApiCountParamsIntervalN1day: + return true + case V1GetProjectUsageApiCountParamsIntervalN1hr: + return true + case V1GetProjectUsageApiCountParamsIntervalN30min: + return true + case V1GetProjectUsageApiCountParamsIntervalN3day: + return true + case V1GetProjectUsageApiCountParamsIntervalN3hr: + return true + case V1GetProjectUsageApiCountParamsIntervalN7day: + return true + default: + return false + } +} + // Defines values for V1RemoveProjectAddonParamsAddonVariant0. const ( V1RemoveProjectAddonParamsAddonVariant0Ci12xlarge V1RemoveProjectAddonParamsAddonVariant0 = "ci_12xlarge" @@ -1799,11 +5313,65 @@ const ( V1RemoveProjectAddonParamsAddonVariant0CiXlarge V1RemoveProjectAddonParamsAddonVariant0 = "ci_xlarge" ) +// Valid indicates whether the value is a known member of the V1RemoveProjectAddonParamsAddonVariant0 enum. +func (e V1RemoveProjectAddonParamsAddonVariant0) Valid() bool { + switch e { + case V1RemoveProjectAddonParamsAddonVariant0Ci12xlarge: + return true + case V1RemoveProjectAddonParamsAddonVariant0Ci16xlarge: + return true + case V1RemoveProjectAddonParamsAddonVariant0Ci24xlarge: + return true + case V1RemoveProjectAddonParamsAddonVariant0Ci24xlargeHighMemory: + return true + case V1RemoveProjectAddonParamsAddonVariant0Ci24xlargeOptimizedCpu: + return true + case V1RemoveProjectAddonParamsAddonVariant0Ci24xlargeOptimizedMemory: + return true + case V1RemoveProjectAddonParamsAddonVariant0Ci2xlarge: + return true + case V1RemoveProjectAddonParamsAddonVariant0Ci48xlarge: + return true + case V1RemoveProjectAddonParamsAddonVariant0Ci48xlargeHighMemory: + return true + case V1RemoveProjectAddonParamsAddonVariant0Ci48xlargeOptimizedCpu: + return true + case V1RemoveProjectAddonParamsAddonVariant0Ci48xlargeOptimizedMemory: + return true + case V1RemoveProjectAddonParamsAddonVariant0Ci4xlarge: + return true + case V1RemoveProjectAddonParamsAddonVariant0Ci8xlarge: + return true + case V1RemoveProjectAddonParamsAddonVariant0CiLarge: + return true + case V1RemoveProjectAddonParamsAddonVariant0CiMedium: + return true + case V1RemoveProjectAddonParamsAddonVariant0CiMicro: + return true + case V1RemoveProjectAddonParamsAddonVariant0CiSmall: + return true + case V1RemoveProjectAddonParamsAddonVariant0CiXlarge: + return true + default: + return false + } +} + // Defines values for V1RemoveProjectAddonParamsAddonVariant1. const ( V1RemoveProjectAddonParamsAddonVariant1CdDefault V1RemoveProjectAddonParamsAddonVariant1 = "cd_default" ) +// Valid indicates whether the value is a known member of the V1RemoveProjectAddonParamsAddonVariant1 enum. +func (e V1RemoveProjectAddonParamsAddonVariant1) Valid() bool { + switch e { + case V1RemoveProjectAddonParamsAddonVariant1CdDefault: + return true + default: + return false + } +} + // Defines values for V1RemoveProjectAddonParamsAddonVariant2. const ( V1RemoveProjectAddonParamsAddonVariant2Pitr14 V1RemoveProjectAddonParamsAddonVariant2 = "pitr_14" @@ -1811,11 +5379,35 @@ const ( V1RemoveProjectAddonParamsAddonVariant2Pitr7 V1RemoveProjectAddonParamsAddonVariant2 = "pitr_7" ) +// Valid indicates whether the value is a known member of the V1RemoveProjectAddonParamsAddonVariant2 enum. +func (e V1RemoveProjectAddonParamsAddonVariant2) Valid() bool { + switch e { + case V1RemoveProjectAddonParamsAddonVariant2Pitr14: + return true + case V1RemoveProjectAddonParamsAddonVariant2Pitr28: + return true + case V1RemoveProjectAddonParamsAddonVariant2Pitr7: + return true + default: + return false + } +} + // Defines values for V1RemoveProjectAddonParamsAddonVariant3. const ( V1RemoveProjectAddonParamsAddonVariant3Ipv4Default V1RemoveProjectAddonParamsAddonVariant3 = "ipv4_default" ) +// Valid indicates whether the value is a known member of the V1RemoveProjectAddonParamsAddonVariant3 enum. +func (e V1RemoveProjectAddonParamsAddonVariant3) Valid() bool { + switch e { + case V1RemoveProjectAddonParamsAddonVariant3Ipv4Default: + return true + default: + return false + } +} + // Defines values for V1GetServicesHealthParamsServices. const ( V1GetServicesHealthParamsServicesAuth V1GetServicesHealthParamsServices = "auth" @@ -1828,18 +5420,66 @@ const ( V1GetServicesHealthParamsServicesStorage V1GetServicesHealthParamsServices = "storage" ) +// Valid indicates whether the value is a known member of the V1GetServicesHealthParamsServices enum. +func (e V1GetServicesHealthParamsServices) Valid() bool { + switch e { + case V1GetServicesHealthParamsServicesAuth: + return true + case V1GetServicesHealthParamsServicesDb: + return true + case V1GetServicesHealthParamsServicesDbPostgresUser: + return true + case V1GetServicesHealthParamsServicesPgBouncer: + return true + case V1GetServicesHealthParamsServicesPooler: + return true + case V1GetServicesHealthParamsServicesRealtime: + return true + case V1GetServicesHealthParamsServicesRest: + return true + case V1GetServicesHealthParamsServicesStorage: + return true + default: + return false + } +} + // Defines values for V1ListAllSnippetsParamsSortBy. const ( InsertedAt V1ListAllSnippetsParamsSortBy = "inserted_at" Name V1ListAllSnippetsParamsSortBy = "name" ) +// Valid indicates whether the value is a known member of the V1ListAllSnippetsParamsSortBy enum. +func (e V1ListAllSnippetsParamsSortBy) Valid() bool { + switch e { + case InsertedAt: + return true + case Name: + return true + default: + return false + } +} + // Defines values for V1ListAllSnippetsParamsSortOrder. const ( Asc V1ListAllSnippetsParamsSortOrder = "asc" Desc V1ListAllSnippetsParamsSortOrder = "desc" ) +// Valid indicates whether the value is a known member of the V1ListAllSnippetsParamsSortOrder enum. +func (e V1ListAllSnippetsParamsSortOrder) Valid() bool { + switch e { + case Asc: + return true + case Desc: + return true + default: + return false + } +} + // AcceptInviteExternalUserJitAccessBody defines model for AcceptInviteExternalUserJitAccessBody. type AcceptInviteExternalUserJitAccessBody struct { Email openapi_types.Email `json:"email"` @@ -2243,7 +5883,7 @@ type BranchResponse struct { IsDefault bool `json:"is_default"` // LatestCheckRunId This field is deprecated and will not be populated. - // Deprecated: + // Deprecated: this property has been marked as deprecated upstream, but no `x-deprecated-reason` was set LatestCheckRunId *float32 `json:"latest_check_run_id,omitempty"` Name string `json:"name"` NotifyUrl *string `json:"notify_url,omitempty"` @@ -2255,7 +5895,7 @@ type BranchResponse struct { ReviewRequestedAt *time.Time `json:"review_requested_at,omitempty"` // Status This field is deprecated. List action runs to get branch status instead. - // Deprecated: + // Deprecated: this property has been marked as deprecated upstream, but no `x-deprecated-reason` was set Status BranchResponseStatus `json:"status"` UpdatedAt time.Time `json:"updated_at"` WithData bool `json:"with_data"` @@ -2382,10 +6022,10 @@ type CreateProjectClaimTokenResponse struct { type CreateProviderBody struct { AttributeMapping *struct { Keys map[string]struct { - Array *bool `json:"array,omitempty"` - Default *interface{} `json:"default,omitempty"` - Name *string `json:"name,omitempty"` - Names *[]string `json:"names,omitempty"` + Array *bool `json:"array,omitempty"` + Default interface{} `json:"default,omitempty"` + Name *string `json:"name,omitempty"` + Names *[]string `json:"names,omitempty"` } `json:"keys"` } `json:"attribute_mapping,omitempty"` Domains *[]string `json:"domains,omitempty"` @@ -2409,21 +6049,19 @@ type CreateProviderResponse struct { Domains *[]struct { CreatedAt *string `json:"created_at,omitempty"` Domain *string `json:"domain,omitempty"` - Id string `json:"id"` UpdatedAt *string `json:"updated_at,omitempty"` } `json:"domains,omitempty"` Id string `json:"id"` Saml *struct { AttributeMapping *struct { Keys map[string]struct { - Array *bool `json:"array,omitempty"` - Default *interface{} `json:"default,omitempty"` - Name *string `json:"name,omitempty"` - Names *[]string `json:"names,omitempty"` + Array *bool `json:"array,omitempty"` + Default interface{} `json:"default,omitempty"` + Name *string `json:"name,omitempty"` + Names *[]string `json:"names,omitempty"` } `json:"keys"` } `json:"attribute_mapping,omitempty"` EntityId string `json:"entity_id"` - Id string `json:"id"` MetadataUrl *string `json:"metadata_url,omitempty"` MetadataXml *string `json:"metadata_xml,omitempty"` NameIdFormat *string `json:"name_id_format,omitempty"` @@ -2595,9 +6233,9 @@ type CreateSigningKeyBodyStatus string // CreateThirdPartyAuthBody defines model for CreateThirdPartyAuthBody. type CreateThirdPartyAuthBody struct { - CustomJwks *interface{} `json:"custom_jwks,omitempty"` - JwksUrl *string `json:"jwks_url,omitempty"` - OidcIssuerUrl *string `json:"oidc_issuer_url,omitempty"` + CustomJwks interface{} `json:"custom_jwks,omitempty"` + JwksUrl *string `json:"jwks_url,omitempty"` + OidcIssuerUrl *string `json:"oidc_issuer_url,omitempty"` } // DatabaseUpgradeStatusResponse defines model for DatabaseUpgradeStatusResponse. @@ -2624,21 +6262,19 @@ type DeleteProviderResponse struct { Domains *[]struct { CreatedAt *string `json:"created_at,omitempty"` Domain *string `json:"domain,omitempty"` - Id string `json:"id"` UpdatedAt *string `json:"updated_at,omitempty"` } `json:"domains,omitempty"` Id string `json:"id"` Saml *struct { AttributeMapping *struct { Keys map[string]struct { - Array *bool `json:"array,omitempty"` - Default *interface{} `json:"default,omitempty"` - Name *string `json:"name,omitempty"` - Names *[]string `json:"names,omitempty"` + Array *bool `json:"array,omitempty"` + Default interface{} `json:"default,omitempty"` + Name *string `json:"name,omitempty"` + Names *[]string `json:"names,omitempty"` } `json:"keys"` } `json:"attribute_mapping,omitempty"` EntityId string `json:"entity_id"` - Id string `json:"id"` MetadataUrl *string `json:"metadata_url,omitempty"` MetadataXml *string `json:"metadata_xml,omitempty"` NameIdFormat *string `json:"name_id_format,omitempty"` @@ -2850,21 +6486,19 @@ type GetProviderResponse struct { Domains *[]struct { CreatedAt *string `json:"created_at,omitempty"` Domain *string `json:"domain,omitempty"` - Id string `json:"id"` UpdatedAt *string `json:"updated_at,omitempty"` } `json:"domains,omitempty"` Id string `json:"id"` Saml *struct { AttributeMapping *struct { Keys map[string]struct { - Array *bool `json:"array,omitempty"` - Default *interface{} `json:"default,omitempty"` - Name *string `json:"name,omitempty"` - Names *[]string `json:"names,omitempty"` + Array *bool `json:"array,omitempty"` + Default interface{} `json:"default,omitempty"` + Name *string `json:"name,omitempty"` + Names *[]string `json:"names,omitempty"` } `json:"keys"` } `json:"attribute_mapping,omitempty"` EntityId string `json:"entity_id"` - Id string `json:"id"` MetadataUrl *string `json:"metadata_url,omitempty"` MetadataXml *string `json:"metadata_xml,omitempty"` NameIdFormat *string `json:"name_id_format,omitempty"` @@ -3068,8 +6702,8 @@ type ListProjectAddonsResponse struct { Id ListProjectAddonsResponse_AvailableAddons_Variants_Id `json:"id"` // Meta Any JSON-serializable value - Meta *interface{} `json:"meta,omitempty"` - Name string `json:"name"` + Meta interface{} `json:"meta,omitempty"` + Name string `json:"name"` Price struct { Amount float32 `json:"amount"` Description string `json:"description"` @@ -3084,8 +6718,8 @@ type ListProjectAddonsResponse struct { Id ListProjectAddonsResponse_SelectedAddons_Variant_Id `json:"id"` // Meta Any JSON-serializable value - Meta *interface{} `json:"meta,omitempty"` - Name string `json:"name"` + Meta interface{} `json:"meta,omitempty"` + Name string `json:"name"` Price struct { Amount float32 `json:"amount"` Description string `json:"description"` @@ -3179,21 +6813,19 @@ type ListProvidersResponse struct { Domains *[]struct { CreatedAt *string `json:"created_at,omitempty"` Domain *string `json:"domain,omitempty"` - Id string `json:"id"` UpdatedAt *string `json:"updated_at,omitempty"` } `json:"domains,omitempty"` Id string `json:"id"` Saml *struct { AttributeMapping *struct { Keys map[string]struct { - Array *bool `json:"array,omitempty"` - Default *interface{} `json:"default,omitempty"` - Name *string `json:"name,omitempty"` - Names *[]string `json:"names,omitempty"` + Array *bool `json:"array,omitempty"` + Default interface{} `json:"default,omitempty"` + Name *string `json:"name,omitempty"` + Names *[]string `json:"names,omitempty"` } `json:"keys"` } `json:"attribute_mapping,omitempty"` EntityId string `json:"entity_id"` - Id string `json:"id"` MetadataUrl *string `json:"metadata_url,omitempty"` MetadataXml *string `json:"metadata_xml,omitempty"` NameIdFormat *string `json:"name_id_format,omitempty"` @@ -3427,7 +7059,7 @@ type OrganizationProjectsResponseProjectsStatus string // OrganizationResponseV1 defines model for OrganizationResponseV1. type OrganizationResponseV1 struct { // Id Deprecated: Use `slug` instead. - // Deprecated: + // Deprecated: this property has been marked as deprecated upstream, but no `x-deprecated-reason` was set Id string `json:"id"` Name string `json:"name"` @@ -3548,7 +7180,7 @@ type ProjectUpgradeEligibilityResponse struct { LegacyAuthCustomRoles []string `json:"legacy_auth_custom_roles"` // ObjectsToBeDropped Use validation_errors instead. - // Deprecated: + // Deprecated: this property has been marked as deprecated upstream, but no `x-deprecated-reason` was set ObjectsToBeDropped []string `json:"objects_to_be_dropped"` TargetUpgradeVersions []struct { AppVersion string `json:"app_version"` @@ -3557,11 +7189,11 @@ type ProjectUpgradeEligibilityResponse struct { } `json:"target_upgrade_versions"` // UnsupportedExtensions Use validation_errors instead. - // Deprecated: + // Deprecated: this property has been marked as deprecated upstream, but no `x-deprecated-reason` was set UnsupportedExtensions []string `json:"unsupported_extensions"` // UserDefinedObjectsInInternalSchemas Use validation_errors instead. - // Deprecated: + // Deprecated: this property has been marked as deprecated upstream, but no `x-deprecated-reason` was set UserDefinedObjectsInInternalSchemas []string `json:"user_defined_objects_in_internal_schemas"` ValidationErrors []ProjectUpgradeEligibilityResponse_ValidationErrors_Item `json:"validation_errors"` Warnings []ProjectUpgradeEligibilityResponse_Warnings_Item `json:"warnings"` @@ -3929,7 +7561,7 @@ type SnippetListDataVisibility string type SnippetResponse struct { Content struct { // Favorite Deprecated: Rely on root-level favorite property instead. - // Deprecated: + // Deprecated: this property has been marked as deprecated upstream, but no `x-deprecated-reason` was set Favorite *bool `json:"favorite,omitempty"` SchemaVersion string `json:"schema_version"` Sql string `json:"sql"` @@ -4331,7 +7963,7 @@ type UpdateBranchBody struct { RequestReview *bool `json:"request_review,omitempty"` // ResetOnPush This field is deprecated and will be ignored. Use v1-reset-a-branch endpoint directly instead. - // Deprecated: + // Deprecated: this property has been marked as deprecated upstream, but no `x-deprecated-reason` was set ResetOnPush *bool `json:"reset_on_push,omitempty"` Status *UpdateBranchBodyStatus `json:"status,omitempty"` } @@ -4463,10 +8095,10 @@ type UpdatePostgresConfigBodySessionReplicationRole string type UpdateProviderBody struct { AttributeMapping *struct { Keys map[string]struct { - Array *bool `json:"array,omitempty"` - Default *interface{} `json:"default,omitempty"` - Name *string `json:"name,omitempty"` - Names *[]string `json:"names,omitempty"` + Array *bool `json:"array,omitempty"` + Default interface{} `json:"default,omitempty"` + Name *string `json:"name,omitempty"` + Names *[]string `json:"names,omitempty"` } `json:"keys"` } `json:"attribute_mapping,omitempty"` Domains *[]string `json:"domains,omitempty"` @@ -4484,21 +8116,19 @@ type UpdateProviderResponse struct { Domains *[]struct { CreatedAt *string `json:"created_at,omitempty"` Domain *string `json:"domain,omitempty"` - Id string `json:"id"` UpdatedAt *string `json:"updated_at,omitempty"` } `json:"domains,omitempty"` Id string `json:"id"` Saml *struct { AttributeMapping *struct { Keys map[string]struct { - Array *bool `json:"array,omitempty"` - Default *interface{} `json:"default,omitempty"` - Name *string `json:"name,omitempty"` - Names *[]string `json:"names,omitempty"` + Array *bool `json:"array,omitempty"` + Default interface{} `json:"default,omitempty"` + Name *string `json:"name,omitempty"` + Names *[]string `json:"names,omitempty"` } `json:"keys"` } `json:"attribute_mapping,omitempty"` EntityId string `json:"entity_id"` - Id string `json:"id"` MetadataUrl *string `json:"metadata_url,omitempty"` MetadataXml *string `json:"metadata_xml,omitempty"` NameIdFormat *string `json:"name_id_format,omitempty"` @@ -4705,25 +8335,25 @@ type V1CreateProjectBody struct { HighAvailability *bool `json:"high_availability,omitempty"` // KpsEnabled This field is deprecated and is ignored in this request - // Deprecated: + // Deprecated: this property has been marked as deprecated upstream, but no `x-deprecated-reason` was set KpsEnabled *bool `json:"kps_enabled,omitempty"` // Name Name of your project Name string `json:"name"` // OrganizationId Deprecated: Use `organization_slug` instead. - // Deprecated: + // Deprecated: this property has been marked as deprecated upstream, but no `x-deprecated-reason` was set OrganizationId *string `json:"organization_id,omitempty"` // OrganizationSlug Organization slug OrganizationSlug string `json:"organization_slug"` // Plan Subscription Plan is now set on organization level and is ignored in this request - // Deprecated: + // Deprecated: this property has been marked as deprecated upstream, but no `x-deprecated-reason` was set Plan *V1CreateProjectBodyPlan `json:"plan,omitempty"` // Region Region you want your server to reside in. Use region_selection instead. - // Deprecated: + // Deprecated: this property has been marked as deprecated upstream, but no `x-deprecated-reason` was set Region *V1CreateProjectBodyRegion `json:"region,omitempty"` // RegionSelection Region selection. Only one of region or region_selection can be specified. @@ -5020,14 +8650,14 @@ type V1ProjectResponse struct { CreatedAt string `json:"created_at"` // Id Deprecated: Use `ref` instead. - // Deprecated: + // Deprecated: this property has been marked as deprecated upstream, but no `x-deprecated-reason` was set Id string `json:"id"` // Name Name of your project Name string `json:"name"` // OrganizationId Deprecated: Use `organization_slug` instead. - // Deprecated: + // Deprecated: this property has been marked as deprecated upstream, but no `x-deprecated-reason` was set OrganizationId string `json:"organization_id"` // OrganizationSlug Organization slug @@ -5063,14 +8693,14 @@ type V1ProjectWithDatabaseResponse struct { } `json:"database"` // Id Deprecated: Use `ref` instead. - // Deprecated: + // Deprecated: this property has been marked as deprecated upstream, but no `x-deprecated-reason` was set Id string `json:"id"` // Name Name of your project Name string `json:"name"` // OrganizationId Deprecated: Use `organization_slug` instead. - // Deprecated: + // Deprecated: this property has been marked as deprecated upstream, but no `x-deprecated-reason` was set OrganizationId string `json:"organization_id"` // OrganizationSlug Organization slug @@ -5130,7 +8760,7 @@ type V1ServiceHealthResponse struct { Error *string `json:"error,omitempty"` // Healthy Deprecated. Use `status` instead. - // Deprecated: + // Deprecated: this property has been marked as deprecated upstream, but no `x-deprecated-reason` was set Healthy bool `json:"healthy"` Info *V1ServiceHealthResponse_Info `json:"info,omitempty"` Name V1ServiceHealthResponseName `json:"name"` @@ -5153,7 +8783,7 @@ type V1ServiceHealthResponseInfo1 struct { DbConnected bool `json:"db_connected"` // Healthy Deprecated. Use `status` instead. - // Deprecated: + // Deprecated: this property has been marked as deprecated upstream, but no `x-deprecated-reason` was set Healthy bool `json:"healthy"` ReplicationConnected bool `json:"replication_connected"` } @@ -5247,6 +8877,9 @@ type VanitySubdomainConfigResponse struct { // VanitySubdomainConfigResponseStatus defines model for VanitySubdomainConfigResponse.Status. type VanitySubdomainConfigResponseStatus string +// bearerContextKey is the context key for bearer security scheme +type bearerContextKey string + // V1DeleteABranchParams defines parameters for V1DeleteABranch. type V1DeleteABranchParams struct { // Force If set to false, schedule deletion with 1-hour grace period (only when soft deletion is enabled). @@ -5864,9 +9497,11 @@ func (a GetProjectDbMetadataResponse_Databases_Item) MarshalJSON() ([]byte, erro return nil, fmt.Errorf("error marshaling 'name': %w", err) } - object["schemas"], err = json.Marshal(a.Schemas) - if err != nil { - return nil, fmt.Errorf("error marshaling 'schemas': %w", err) + if a.Schemas != nil { + object["schemas"], err = json.Marshal(a.Schemas) + if err != nil { + return nil, fmt.Errorf("error marshaling 'schemas': %w", err) + } } for fieldName, field := range a.AdditionalProperties { From 3fa5a9d9f7106311c8dd06ed9de7cf519856ac88 Mon Sep 17 00:00:00 2001 From: Pamela Chia Date: Tue, 21 Jul 2026 21:59:32 +0800 Subject: [PATCH 68/79] fix(cli): stop telemetry hangs and errors on blocked networks (#5880) On networks that block or blackhole `eu.posthog.com` (corporate proxies, CI and Docker egress policies, air-gapped machines), telemetry breaks the CLI itself: stable v2.109.1 pays a 5s trailing hang and exits 1 with an UnknownError payload after every command, long-running commands print raw SDK stack traces to stderr, and the Go binary's shutdown wait is unbounded. The only workaround was `supabase telemetry disable`, which permanently removes those users from CLI analytics. This PR makes telemetry strictly fire-and-forget: a failed delivery never changes a command's output, exit code, or runtime beyond a 2s cap. **Changed:** - **Exit 1 after successful commands**: the TS shutdown finalizer was `Effect.promise(...).pipe(Effect.ignore)`, but `Effect.promise` turns the shutdown-deadline rejection into a defect that `Effect.ignore` cannot swallow, so a telemetry timeout failed the whole command. The rejection is now caught on the promise itself. - **Unbounded and stacking exit delays**: one shared 2s budget (`EXIT_DELAY_CAP_MS`) bounds each request and the entire `_shutdown` drain on the TS side, and the Go client gets the same 2s `ShutdownTimeout` (the SDK default waits indefinitely, including retries). The previous 5s shutdown deadline left room for two sequential 2s request timeouts (~4s exits). - **Work outliving the deadline** (review round 3): the SDK's shutdown deadline only stops the wait, it does not cancel work. The in-flight request kept running past scope release and the drain then started the queued request, keeping the runtime alive ~4s instead of 2s. A lifecycle `AbortController` now composes into every request's signal and fires when the deadline hits, so no request survives or starts after the scope closes. Covered by a unit regression asserting zero active requests one tick after release (red on the previous code) and an e2e test that runs the compiled binary against a TCP blackhole and asserts wall-clock process exit, empty stderr, and that telemetry actually reached the wire. - **Stderr noise**: Go routes SDK logs to the `--debug` logger; TS reports delivery failures as delivered inside the wrapped fetch, because posthog-node has no logger hook, only hardcoded `console.error` calls, so an infallible transport is the one supported way to get no retries, no output, and no stall. **Note:** the exit bounds are caps, not floors. On healthy networks both shells return as soon as the flush completes (`flushAt: 1` keeps the send overlapping command runtime), and fully detaching the send instead would race process exit and silently drop events on healthy networks. The accepted tradeoff is that deliveries slower than 2s are dropped; telemetry is best-effort, observable under `--debug` on the Go side only. ## Linear - fixes GROWTH-1001 --------- Co-authored-by: Andrew Valleteau --- apps/cli-go/internal/telemetry/client.go | 15 ++- apps/cli-go/internal/telemetry/client_test.go | 37 ++++++ .../telemetry/legacy-analytics.layer.ts | 11 +- .../src/shared/telemetry/analytics.layer.ts | 12 +- .../telemetry/posthog-client.e2e.test.ts | 63 +++++++++++ .../src/shared/telemetry/posthog-client.ts | 56 +++++++++ .../telemetry/posthog-client.unit.test.ts | 107 ++++++++++++++++++ 7 files changed, 283 insertions(+), 18 deletions(-) create mode 100644 apps/cli/src/shared/telemetry/posthog-client.e2e.test.ts create mode 100644 apps/cli/src/shared/telemetry/posthog-client.ts create mode 100644 apps/cli/src/shared/telemetry/posthog-client.unit.test.ts diff --git a/apps/cli-go/internal/telemetry/client.go b/apps/cli-go/internal/telemetry/client.go index 34b529a3b3..717e4af181 100644 --- a/apps/cli-go/internal/telemetry/client.go +++ b/apps/cli-go/internal/telemetry/client.go @@ -1,11 +1,14 @@ package telemetry import ( + "log" "net/http" "strings" + "time" "github.com/go-errors/errors" "github.com/posthog/posthog-go" + "github.com/supabase/cli/internal/utils" ) type Analytics interface { @@ -24,6 +27,8 @@ type queueClient interface { type constructor func(apiKey string, config posthog.Config) (queueClient, error) +const shutdownTimeout = 2 * time.Second + type Client struct { client queueClient baseProperties posthog.Properties @@ -38,7 +43,15 @@ func NewClient(apiKey string, endpoint string, baseProperties map[string]any, fa return posthog.NewWithConfig(apiKey, config) } } - config := posthog.Config{} + // Without a positive ShutdownTimeout, Close blocks indefinitely when the + // endpoint is unreachable, hanging every command on networks that block + // PostHog; keep it tight because blackholed networks pay the whole timeout + // at exit. The default logger prints delivery failures to stderr even for + // commands that succeeded, so route them to the --debug logger instead. + config := posthog.Config{ + ShutdownTimeout: shutdownTimeout, + Logger: posthog.StdLogger(log.New(utils.GetDebugLogger(), "posthog ", log.LstdFlags), true), + } if endpoint != "" { config.Endpoint = endpoint } diff --git a/apps/cli-go/internal/telemetry/client_test.go b/apps/cli-go/internal/telemetry/client_test.go index 6ee8ccb27a..bcc8db7d24 100644 --- a/apps/cli-go/internal/telemetry/client_test.go +++ b/apps/cli-go/internal/telemetry/client_test.go @@ -1,10 +1,14 @@ package telemetry import ( + "io" "net/http" + "os" "testing" + "time" "github.com/posthog/posthog-go" + "github.com/spf13/viper" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/supabase/cli/internal/debug" @@ -40,6 +44,8 @@ func TestNewClient(t *testing.T) { assert.True(t, client.Enabled()) assert.Equal(t, "phc_test", gotKey) assert.Equal(t, "https://eu.i.posthog.com", gotConfig.Endpoint) + assert.Equal(t, 2*time.Second, gotConfig.ShutdownTimeout) + assert.NotNil(t, gotConfig.Logger) }) t.Run("becomes a no-op when key is empty", func(t *testing.T) { @@ -53,6 +59,37 @@ func TestNewClient(t *testing.T) { assert.NoError(t, client.Capture("device-1", EventCommandExecuted, map[string]any{"command": "login"}, nil)) assert.NoError(t, client.Close()) }) + t.Run("routes posthog logs to stderr only in debug mode", func(t *testing.T) { + originalStderr := os.Stderr + originalDebug := viper.GetBool("DEBUG") + reader, writer, err := os.Pipe() + require.NoError(t, err) + os.Stderr = writer + t.Cleanup(func() { + os.Stderr = originalStderr + viper.Set("DEBUG", originalDebug) + }) + + configFor := func(debugEnabled bool) posthog.Config { + viper.Set("DEBUG", debugEnabled) + var gotConfig posthog.Config + _, err := NewClient("phc_test", "", nil, func(apiKey string, config posthog.Config) (queueClient, error) { + gotConfig = config + return &fakeQueue{}, nil + }) + require.NoError(t, err) + return gotConfig + } + + configFor(false).Logger.Errorf("dropped %d messages", 1) + configFor(true).Logger.Errorf("dropped %d messages", 2) + + require.NoError(t, writer.Close()) + output, err := io.ReadAll(reader) + require.NoError(t, err) + assert.NotContains(t, string(output), "dropped 1 messages") + assert.Contains(t, string(output), "dropped 2 messages") + }) t.Run("works when debug wraps the default transport", func(t *testing.T) { original := http.DefaultTransport http.DefaultTransport = debug.NewTransport() diff --git a/apps/cli/src/legacy/telemetry/legacy-analytics.layer.ts b/apps/cli/src/legacy/telemetry/legacy-analytics.layer.ts index c09fc962b1..1a9ccabeaf 100644 --- a/apps/cli/src/legacy/telemetry/legacy-analytics.layer.ts +++ b/apps/cli/src/legacy/telemetry/legacy-analytics.layer.ts @@ -1,5 +1,4 @@ import { Effect, FileSystem, Layer, Option, Path } from "effect"; -import { PostHog } from "posthog-node"; import { aiToolLayer } from "../../shared/telemetry/ai-tool.layer.ts"; import { AiTool } from "../../shared/telemetry/ai-tool.service.ts"; import { @@ -26,6 +25,7 @@ import { PropSchemaVersion, PropSessionId, } from "../../shared/telemetry/event-catalog.ts"; +import { scopedPosthogClient } from "../../shared/telemetry/posthog-client.ts"; import { resolvePosthogConfig } from "../../shared/telemetry/posthog-config.ts"; import { telemetryRuntimeLayer } from "../../shared/telemetry/runtime.layer.ts"; import { TelemetryRuntime } from "../../shared/telemetry/runtime.service.ts"; @@ -158,14 +158,7 @@ export const legacyAnalyticsLayer = Layer.effect( }); } - const client = new PostHog(posthogConfig.key.value, { - host: posthogConfig.host, - flushAt: 1, - flushInterval: 0, - }); - yield* Effect.addFinalizer(() => - Effect.promise(() => client._shutdown(5_000)).pipe(Effect.ignore), - ); + const client = yield* scopedPosthogClient(posthogConfig.key.value, posthogConfig.host); const loadLinkedProject = makeLoadLinkedProject(fs, path); diff --git a/apps/cli/src/shared/telemetry/analytics.layer.ts b/apps/cli/src/shared/telemetry/analytics.layer.ts index 66a3a8e685..e25f983d7f 100644 --- a/apps/cli/src/shared/telemetry/analytics.layer.ts +++ b/apps/cli/src/shared/telemetry/analytics.layer.ts @@ -1,4 +1,3 @@ -import { PostHog } from "posthog-node"; import { Effect, Layer, Option } from "effect"; import type { ProjectLinkStateValue } from "../../next/config/project-link-state.service.ts"; import { ProjectLinkState } from "../../next/config/project-link-state.service.ts"; @@ -7,6 +6,7 @@ import { aiToolLayer } from "./ai-tool.layer.ts"; import { CurrentAnalyticsContext, type AnalyticsContext } from "./analytics-context.ts"; import { Analytics } from "./analytics.service.ts"; import { AiTool } from "./ai-tool.service.ts"; +import { scopedPosthogClient } from "./posthog-client.ts"; import { telemetryRuntimeLayer } from "./runtime.layer.ts"; import { TelemetryRuntime } from "./runtime.service.ts"; @@ -59,13 +59,9 @@ export const analyticsLayer = Layer.effect( }); } - const client = new PostHog(cliConfig.telemetryPosthogKey.value, { - host: cliConfig.telemetryPosthogHost, - flushAt: 1, - flushInterval: 0, - }); - yield* Effect.addFinalizer(() => - Effect.promise(() => client._shutdown(5_000)).pipe(Effect.ignore), + const client = yield* scopedPosthogClient( + cliConfig.telemetryPosthogKey.value, + cliConfig.telemetryPosthogHost, ); const baseProperties = stripUndefined({ diff --git a/apps/cli/src/shared/telemetry/posthog-client.e2e.test.ts b/apps/cli/src/shared/telemetry/posthog-client.e2e.test.ts new file mode 100644 index 0000000000..a8a80cea61 --- /dev/null +++ b/apps/cli/src/shared/telemetry/posthog-client.e2e.test.ts @@ -0,0 +1,63 @@ +import { createServer, type Server, type Socket } from "node:net"; +import { afterAll, beforeAll, describe, expect, test } from "vitest"; +import { runSupabase } from "../../../tests/helpers/cli.ts"; + +// A TCP blackhole: accepts connections and never responds, so telemetry +// requests connect and then hang until aborted. Asserting on the spawned +// process's wall-clock exit (not scope or shutdown internals) is deliberate: +// pending sockets keep the runtime alive, so only actual process exit proves +// the telemetry exit cap holds end to end. +describe("telemetry against a blackholed PostHog endpoint", () => { + let server: Server; + let host: string; + let connections = 0; + const sockets = new Set(); + + beforeAll(async () => { + server = createServer((socket) => { + connections += 1; + sockets.add(socket); + // Aborted requests reset the connection; without a listener the + // server-side ECONNRESET becomes an uncaught exception. + socket.on("error", () => {}); + socket.on("close", () => sockets.delete(socket)); + }); + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + const address = server.address(); + if (address === null || typeof address === "string") { + throw new Error("Failed to allocate a blackhole port"); + } + host = `http://127.0.0.1:${address.port}`; + }); + + afterAll(async () => { + for (const socket of sockets) socket.destroy(); + await new Promise((resolve) => server.close(() => resolve())); + }); + + test("commands exit promptly, cleanly, and quietly", async () => { + const startedAt = performance.now(); + const { stdout, stderr, exitCode } = await runSupabase(["telemetry", "status"], { + entrypoint: "legacy", + env: { + // spawnSupabase disables telemetry for every test by default; this + // test exists to exercise it, so turn it back on explicitly. + SUPABASE_TELEMETRY_DISABLED: "0", + DO_NOT_TRACK: "0", + SUPABASE_TELEMETRY_POSTHOG_KEY: "phc_e2e_blackhole_test", + SUPABASE_TELEMETRY_POSTHOG_HOST: host, + }, + }); + const elapsedMs = performance.now() - startedAt; + + expect(exitCode).toBe(0); + expect(stdout).toContain("Telemetry is enabled."); + expect(stderr).toBe(""); + // Telemetry must have actually reached the blackhole, otherwise the + // timing assertion below passes vacuously with telemetry off. + expect(connections).toBeGreaterThanOrEqual(1); + // Healthy runs measure ~2.5s (2s drain cap + spawn overhead); the nearest + // real failure signature is the SDK's 5s default deadline plus startup. + expect(elapsedMs).toBeLessThan(4_500); + }); +}); diff --git a/apps/cli/src/shared/telemetry/posthog-client.ts b/apps/cli/src/shared/telemetry/posthog-client.ts new file mode 100644 index 0000000000..e44f5d2c63 --- /dev/null +++ b/apps/cli/src/shared/telemetry/posthog-client.ts @@ -0,0 +1,56 @@ +import { Effect } from "effect"; +import { PostHog, type PostHogOptions } from "posthog-node"; + +const EXIT_DELAY_CAP_MS = 2_000; + +const delivered = { + status: 200, + text: () => Promise.resolve(""), + json: () => Promise.resolve({}), +}; + +// posthog-node has no logger hook: delivery failures hit hardcoded +// console.error calls and multi-second retries, so report them as delivered. +export const fireAndForgetFetch: NonNullable = async (url, options) => { + try { + const response = await globalThis.fetch(url, options); + return response.status >= 400 ? delivered : response; + } catch { + return delivered; + } +}; + +export const scopedPosthogClient = (apiKey: string, host: string) => + Effect.acquireRelease( + Effect.sync(() => { + const shutdown = new AbortController(); + const client = new PostHog(apiKey, { + host, + flushAt: 1, + flushInterval: 0, + requestTimeout: EXIT_DELAY_CAP_MS, + fetch: (url, options) => + fireAndForgetFetch(url, { + ...options, + signal: options.signal + ? AbortSignal.any([options.signal, shutdown.signal]) + : shutdown.signal, + }), + }); + return { client, shutdown }; + }), + ({ client, shutdown }) => + Effect.promise(async () => { + try { + await client._shutdown(EXIT_DELAY_CAP_MS); + } catch { + // The deadline rejection must be swallowed: Effect.promise turns + // rejections into defects, which would fail the command. + } finally { + // The shutdown deadline only stops the wait; the SDK's drain keeps + // in-flight requests running and starts queued ones after release. + // Aborting cancels them so nothing outlives the scope. + shutdown.abort(); + } + }), + ).pipe(Effect.map(({ client }) => client)); diff --git a/apps/cli/src/shared/telemetry/posthog-client.unit.test.ts b/apps/cli/src/shared/telemetry/posthog-client.unit.test.ts new file mode 100644 index 0000000000..0f43315014 --- /dev/null +++ b/apps/cli/src/shared/telemetry/posthog-client.unit.test.ts @@ -0,0 +1,107 @@ +import { describe, expect, it } from "@effect/vitest"; +import { afterEach, vi } from "vitest"; +import { Effect } from "effect"; +import { PostHog } from "posthog-node"; +import { fireAndForgetFetch, scopedPosthogClient } from "./posthog-client.ts"; + +const BATCH_URL = "https://eu.i.posthog.com/batch/"; +const BATCH_OPTIONS = { method: "POST" as const, headers: {}, body: "{}" }; + +describe("fireAndForgetFetch", () => { + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it("passes successful responses through untouched", async () => { + vi.stubGlobal("fetch", async () => new Response(`{"status":1}`, { status: 200 })); + + const response = await fireAndForgetFetch(BATCH_URL, BATCH_OPTIONS); + + expect(response.status).toBe(200); + expect(await response.text()).toBe(`{"status":1}`); + }); + + it("reports success when the network is unreachable", async () => { + vi.stubGlobal("fetch", async () => { + throw new Error("connect ECONNREFUSED"); + }); + + const response = await fireAndForgetFetch(BATCH_URL, BATCH_OPTIONS); + + expect(response.status).toBe(200); + expect(await response.text()).toBe(""); + expect(await response.json()).toEqual({}); + }); + + it("reports success on error responses so the SDK never retries or logs", async () => { + vi.stubGlobal( + "fetch", + async () => new Response("Proxy Authentication Required", { status: 407 }), + ); + + const response = await fireAndForgetFetch(BATCH_URL, BATCH_OPTIONS); + + expect(response.status).toBe(200); + expect(await response.text()).toBe(""); + }); +}); + +describe("scopedPosthogClient", () => { + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it.live("captures and shuts down cleanly against an unreachable host", () => + Effect.gen(function* () { + const client = yield* scopedPosthogClient("phc_test", "http://127.0.0.1:9"); + expect(client).toBeInstanceOf(PostHog); + client.capture({ event: "verify_event", distinctId: "device-1" }); + }).pipe(Effect.scoped), + ); + + it.live( + "bounds the whole shutdown when a request is in flight and another event is queued", + () => + Effect.gen(function* () { + let requestStarted = () => {}; + const firstRequestInFlight = new Promise((resolve) => { + requestStarted = resolve; + }); + let activeRequests = 0; + vi.stubGlobal( + "fetch", + (_url: string, options: { signal?: AbortSignal }) => + new Promise((_resolve, reject) => { + activeRequests += 1; + requestStarted(); + const abort = () => { + activeRequests -= 1; + reject(new DOMException("The operation was aborted.", "AbortError")); + }; + if (options.signal?.aborted) { + abort(); + return; + } + options.signal?.addEventListener("abort", abort); + }), + ); + + const startedAt = performance.now(); + yield* Effect.gen(function* () { + const client = yield* scopedPosthogClient("phc_test", "https://blackhole.invalid"); + client.capture({ event: "first_event", distinctId: "device-1" }); + yield* Effect.promise(() => firstRequestInFlight); + client.capture({ event: "second_event", distinctId: "device-1" }); + }).pipe(Effect.scoped); + + expect(performance.now() - startedAt).toBeLessThan(3_000); + + // The SDK's drain keeps running past the shutdown deadline; without + // cancellation it starts the queued request AFTER scope release and + // keeps the process alive for that request's own timeout. + yield* Effect.promise(() => new Promise((resolve) => setTimeout(resolve, 50))); + expect(activeRequests).toBe(0); + }), + 10_000, + ); +}); From 07edb5199307269e547a946e754c8f622756c17a Mon Sep 17 00:00:00 2001 From: Andrew Valleteau Date: Tue, 21 Jul 2026 16:54:01 +0200 Subject: [PATCH 69/79] chore(deps): align pnpm minimumReleaseAge with dependency firewall (#5919) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary The release pipeline's `pnpm install --frozen-lockfile` was failing with `ERR_PNPM_FETCH_451`. The Dependency Firewall (`firewall.depthfirst.com`) quarantines newly published package versions for **7 days** and returns HTTP 451 "Unavailable For Legal Reasons" for them. With `minimumReleaseAge: 0`, pnpm applied no cooldown of its own, so Dependabot bumps pinned the lockfile to versions younger than the firewall's window (e.g. `knip@6.27.0`, `lucide-react@1.25.0`, `@swc-node/register@1.12.0`, `@posthog/core@1.43.1`), which the firewall then refused to serve — breaking the install. This sets `minimumReleaseAge` to **10200 minutes** (7 days + a 2-hour buffer; the setting is in minutes) so pnpm only ever resolves versions old enough for the firewall to serve, keeping pnpm's floor in lockstep with the firewall. The lockfile is re-resolved onto firewall-approved versions (`knip@6.26.0`, `lucide-react@1.24.0`, `@swc-node/register@1.11.1`, `@posthog/core@1.40.2`, …). The buffer was kept at exactly 2 hours rather than a larger margin on purpose: a value *stricter* than the firewall would reject exact-pinned bleeding-edge previews (e.g. `next@16.3.0-preview.6` in `apps/docs`) that the firewall itself would still serve. ## Linked issue Closes # - [ ] The linked issue is **open** and carries the `open-for-contribution` label (or I'm a Supabase maintainer). ## Checklist - [ ] The PR title follows [Conventional Commits](https://www.conventionalcommits.org/) (e.g. `fix(cli): …`). - [ ] Tests added or updated for the change. - [ ] `pnpm check:all` and `pnpm test` pass for the workspace(s) I touched. https://claude.ai/code/session_019Qp5ihFoD67CTcAAEr5js7 Co-authored-by: Claude --- pnpm-lock.yaml | 1408 ++++++++++++++++++++----------------------- pnpm-workspace.yaml | 2 +- 2 files changed, 656 insertions(+), 754 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c92c7b3ed2..c2db0ba728 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,7 +26,7 @@ catalogs: version: 23.1.0 '@swc-node/register': specifier: ^1.10.9 - version: 1.12.0 + version: 1.11.1 '@swc/core': specifier: ^1.15.43 version: 1.15.43 @@ -47,7 +47,7 @@ catalogs: version: 4.0.0-beta.97 knip: specifier: ^6.26.0 - version: 6.27.0 + version: 6.26.0 nx: specifier: ^23.0.2 version: 23.1.0 @@ -56,7 +56,7 @@ catalogs: version: 0.58.0 oxlint: specifier: ^1.72.0 - version: 1.73.0 + version: 1.74.0 oxlint-tsgolint: specifier: ^0.24.0 version: 0.24.0 @@ -76,7 +76,7 @@ importers: devDependencies: '@swc-node/register': specifier: 'catalog:' - version: 1.12.0(@swc/core@1.15.43)(@swc/types@0.1.27)(@typescript/typescript6@6.0.2) + version: 1.11.1(@swc/core@1.15.43)(@swc/types@0.1.27)(@typescript/typescript6@6.0.2) '@swc/core': specifier: 'catalog:' version: 1.15.43 @@ -85,7 +85,7 @@ importers: version: typescript@7.0.2 nx: specifier: 'catalog:' - version: 23.1.0(@swc-node/register@1.12.0(@swc/core@1.15.43)(@swc/types@0.1.27)(@typescript/typescript6@6.0.2))(@swc/core@1.15.43) + version: 23.1.0(@swc-node/register@1.11.1(@swc/core@1.15.43)(@swc/types@0.1.27)(@typescript/typescript6@6.0.2))(@swc/core@1.15.43) pkg-pr-new: specifier: 0.0.75 version: 0.0.75 @@ -94,7 +94,7 @@ importers: version: '@typescript/typescript6@6.0.2' verdaccio: specifier: ^6.7.4 - version: 6.7.4(typanion@3.14.0) + version: 6.8.0(typanion@3.14.0) apps/cli: dependencies: @@ -107,7 +107,7 @@ importers: devDependencies: '@anthropic-ai/claude-agent-sdk': specifier: ^0.3.207 - version: 0.3.207(@anthropic-ai/sdk@0.111.0(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(zod@4.4.3) + version: 0.3.209(@anthropic-ai/sdk@0.111.0(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(zod@4.4.3) '@anthropic-ai/sdk': specifier: ^0.111.0 version: 0.111.0(zod@4.4.3) @@ -188,13 +188,13 @@ importers: version: 5.0.0(ink@7.1.0(@types/react@19.2.17)(react-devtools-core@7.0.1)(react@19.2.7))(react@19.2.7) knip: specifier: 'catalog:' - version: 6.27.0 + version: 6.26.0 oxfmt: specifier: 'catalog:' version: 0.58.0 oxlint: specifier: 'catalog:' - version: 1.73.0(oxlint-tsgolint@0.24.0) + version: 1.74.0(oxlint-tsgolint@0.24.0) oxlint-tsgolint: specifier: 'catalog:' version: 0.24.0 @@ -215,7 +215,7 @@ importers: version: 7.0.1 semantic-release: specifier: ^25.0.6 - version: 25.0.6(typescript@7.0.2) + version: 25.0.7(typescript@7.0.2) smol-toml: specifier: ^1.7.0 version: 1.7.0 @@ -224,7 +224,7 @@ importers: version: 7.4.8 vitest: specifier: 'catalog:' - version: 4.1.10(@types/node@26.1.1)(@vitest/coverage-istanbul@4.1.10)(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) + version: 4.1.10(@types/node@26.1.1)(@vitest/coverage-istanbul@4.1.10)(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) yaml: specifier: ^2.9.0 version: 2.9.0 @@ -274,31 +274,31 @@ importers: version: 4.1.10(vitest@4.1.10) knip: specifier: 'catalog:' - version: 6.27.0 + version: 6.26.0 oxfmt: specifier: 'catalog:' version: 0.58.0 oxlint: specifier: 'catalog:' - version: 1.73.0(oxlint-tsgolint@0.24.0) + version: 1.74.0(oxlint-tsgolint@0.24.0) oxlint-tsgolint: specifier: 'catalog:' version: 0.24.0 vitest: specifier: 'catalog:' - version: 4.1.10(@types/node@26.1.1)(@vitest/coverage-istanbul@4.1.10)(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) + version: 4.1.10(@types/node@26.1.1)(@vitest/coverage-istanbul@4.1.10)(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) apps/docs: dependencies: fumadocs-core: specifier: ^16.11.3 - version: 16.11.3(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.25.0(react@19.2.7))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3) + version: 16.11.4(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.24.0(react@19.2.7))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3) fumadocs-mdx: specifier: ^15.1.0 - version: 15.1.0(@types/mdast@4.0.4)(@types/mdx@2.0.14)(@types/react@19.2.17)(fumadocs-core@16.11.3(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.25.0(react@19.2.7))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(rolldown@1.0.2)(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) + version: 15.1.1(@types/mdast@4.0.4)(@types/mdx@2.0.14)(@types/react@19.2.17)(fumadocs-core@16.11.4(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.24.0(react@19.2.7))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(rolldown@1.1.5)(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) fumadocs-ui: specifier: ^16.11.3 - version: 16.11.3(@types/mdx@2.0.14)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(fumadocs-core@16.11.3(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.25.0(react@19.2.7))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 16.11.4(@types/mdx@2.0.14)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(fumadocs-core@16.11.4(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.24.0(react@19.2.7))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) next: specifier: 16.3.0-preview.6 version: 16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -332,7 +332,7 @@ importers: version: 4.0.0-beta.97(effect@4.0.0-beta.97) '@effect/platform-node': specifier: 'catalog:' - version: 4.0.0-beta.97(effect@4.0.0-beta.97)(ioredis@5.11.0) + version: 4.0.0-beta.97(effect@4.0.0-beta.97)(ioredis@5.11.1) effect: specifier: 'catalog:' version: 4.0.0-beta.97 @@ -354,19 +354,19 @@ importers: version: 4.1.10(vitest@4.1.10) knip: specifier: 'catalog:' - version: 6.27.0 + version: 6.26.0 oxfmt: specifier: 'catalog:' version: 0.58.0 oxlint: specifier: 'catalog:' - version: 1.73.0(oxlint-tsgolint@0.24.0) + version: 1.74.0(oxlint-tsgolint@0.24.0) oxlint-tsgolint: specifier: 'catalog:' version: 0.24.0 vitest: specifier: 'catalog:' - version: 4.1.10(@types/node@26.1.1)(@vitest/coverage-istanbul@4.1.10)(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) + version: 4.1.10(@types/node@26.1.1)(@vitest/coverage-istanbul@4.1.10)(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) packages/cli-darwin-arm64: {} @@ -396,19 +396,19 @@ importers: version: 4.1.10(vitest@4.1.10) knip: specifier: 'catalog:' - version: 6.27.0 + version: 6.26.0 oxfmt: specifier: 'catalog:' version: 0.58.0 oxlint: specifier: 'catalog:' - version: 1.73.0(oxlint-tsgolint@0.24.0) + version: 1.74.0(oxlint-tsgolint@0.24.0) oxlint-tsgolint: specifier: 'catalog:' version: 0.24.0 vitest: specifier: 'catalog:' - version: 4.1.10(@types/node@26.1.1)(@vitest/coverage-istanbul@4.1.10)(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) + version: 4.1.10(@types/node@26.1.1)(@vitest/coverage-istanbul@4.1.10)(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) packages/cli-windows-arm64: {} @@ -421,7 +421,7 @@ importers: version: 4.0.0-beta.97(effect@4.0.0-beta.97) '@effect/platform-node': specifier: 'catalog:' - version: 4.0.0-beta.97(effect@4.0.0-beta.97)(ioredis@5.11.0) + version: 4.0.0-beta.97(effect@4.0.0-beta.97)(ioredis@5.11.1) dedent: specifier: ^1.7.2 version: 1.7.2 @@ -446,19 +446,19 @@ importers: version: 4.1.10(vitest@4.1.10) knip: specifier: 'catalog:' - version: 6.27.0 + version: 6.26.0 oxfmt: specifier: 'catalog:' version: 0.58.0 oxlint: specifier: 'catalog:' - version: 1.73.0(oxlint-tsgolint@0.24.0) + version: 1.74.0(oxlint-tsgolint@0.24.0) oxlint-tsgolint: specifier: 'catalog:' version: 0.24.0 vitest: specifier: 'catalog:' - version: 4.1.10(@types/node@26.1.1)(@vitest/coverage-istanbul@4.1.10)(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) + version: 4.1.10(@types/node@26.1.1)(@vitest/coverage-istanbul@4.1.10)(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) packages/process-compose: dependencies: @@ -486,19 +486,19 @@ importers: version: 4.1.10(vitest@4.1.10) knip: specifier: 'catalog:' - version: 6.27.0 + version: 6.26.0 oxfmt: specifier: 'catalog:' version: 0.58.0 oxlint: specifier: 'catalog:' - version: 1.73.0(oxlint-tsgolint@0.24.0) + version: 1.74.0(oxlint-tsgolint@0.24.0) oxlint-tsgolint: specifier: 'catalog:' version: 0.24.0 vitest: specifier: 'catalog:' - version: 4.1.10(@types/node@26.1.1)(@vitest/coverage-istanbul@4.1.10)(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) + version: 4.1.10(@types/node@26.1.1)(@vitest/coverage-istanbul@4.1.10)(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) packages/stack: dependencies: @@ -507,7 +507,7 @@ importers: version: 4.0.0-beta.97(effect@4.0.0-beta.97) '@effect/platform-node': specifier: 'catalog:' - version: 4.0.0-beta.97(effect@4.0.0-beta.97)(ioredis@5.11.0) + version: 4.0.0-beta.97(effect@4.0.0-beta.97)(ioredis@5.11.1) '@supabase/config': specifier: workspace:* version: link:../config @@ -523,7 +523,7 @@ importers: version: 4.0.0-beta.97(effect@4.0.0-beta.97)(vitest@4.1.10) '@supabase/supabase-js': specifier: ^2.110.2 - version: 2.110.2 + version: 2.110.5 '@tsconfig/bun': specifier: 'catalog:' version: 1.0.10 @@ -538,31 +538,31 @@ importers: version: 4.1.10(vitest@4.1.10) knip: specifier: 'catalog:' - version: 6.27.0 + version: 6.26.0 oxfmt: specifier: 'catalog:' version: 0.58.0 oxlint: specifier: 'catalog:' - version: 1.73.0(oxlint-tsgolint@0.24.0) + version: 1.74.0(oxlint-tsgolint@0.24.0) oxlint-tsgolint: specifier: 'catalog:' version: 0.24.0 vitest: specifier: 'catalog:' - version: 4.1.10(@types/node@26.1.1)(@vitest/coverage-istanbul@4.1.10)(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) + version: 4.1.10(@types/node@26.1.1)(@vitest/coverage-istanbul@4.1.10)(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) tools/nx-plugins: dependencies: '@nx/devkit': specifier: 'catalog:' - version: 23.1.0(nx@23.1.0(@swc-node/register@1.12.0(@swc/core@1.15.43)(@swc/types@0.1.27)(@typescript/typescript6@6.0.2))(@swc/core@1.15.43)) + version: 23.1.0(nx@23.1.0(@swc-node/register@1.11.1(@swc/core@1.15.43)(@swc/types@0.1.27)(@typescript/typescript6@6.0.2))(@swc/core@1.15.43)) typescript: specifier: npm:@typescript/typescript6@^6.0.2 version: '@typescript/typescript6@6.0.2' vitest: specifier: 'catalog:' - version: 4.1.10(@types/node@26.1.1)(@vitest/coverage-istanbul@4.1.10)(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) + version: 4.1.10(@types/node@26.1.1)(@vitest/coverage-istanbul@4.1.10)(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) packages: @@ -582,52 +582,52 @@ packages: resolution: {integrity: sha512-p+CMKJ93HFmLkjXKlXiVGlMQEuRb6H0MokBSwUsX+S6BRX8eV5naFZpQJFfJHjRZY0Hmnqy1/r6UWl3x+19zYA==} engines: {node: '>=18'} - '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.207': - resolution: {integrity: sha512-08xSo1FDx8h0aLhL5tvcRxa2SMmcUV3aDWeZiEJVTclyiDAs61BgTjAxCg+SZcu1CndjJO8cfO0yM5dhamxz3g==} + '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.209': + resolution: {integrity: sha512-08qu5ZImKxAsfyxQdXw2fHpIvTS39kAN/T6iWdRPY/1yicnqAxIl8R9PQ63TJaDnWTo7wWu6O26GKbEX9d7xDw==} cpu: [arm64] os: [darwin] - '@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.207': - resolution: {integrity: sha512-1o7K4EYqyCixZ/oeOZSh7AzSy6TM86xoOuf4VuORjPSS31hBnoqY0NGZd27+2VDs9LGtsdksmsTqcNGx9xd1hA==} + '@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.209': + resolution: {integrity: sha512-p3egSQ75amhjOO0P21pNfs94SueEAbOc0s7IFrGy8CZKXcFMIrOzUXKoi2iTWtR8P9v4ayibf38AJRa2Rz7KPA==} cpu: [x64] os: [darwin] - '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.207': - resolution: {integrity: sha512-oPj+g2DslhH4Y9nCTs7al4t9wZv78FZwLFQwOCg99BXuz1o0ZOpKmxyvR7J9eBR+GPszeMMS8gYplQTiZC9o2w==} + '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.209': + resolution: {integrity: sha512-jtoia0l10xgrsBWjA12n6Q0WxOyr/8qf1eUMOGSO7zdNau/7wABYBXS+VGF8utBEsF4NJjfCEEj2yhEB8uHjLA==} cpu: [arm64] os: [linux] libc: [musl] - '@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.207': - resolution: {integrity: sha512-X4uezYOifDiNTTmmugfRCdg3nNamrr1LFRY9hg30vWYTShL+bbN+nfC3KaFfSYCl4GTtsEEUbYdOTC2F3bBpcA==} + '@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.209': + resolution: {integrity: sha512-eTgga/TuganOj9VAWUrjtt6BSqapeJau4ioCrQRw0RbDN/d/oLih/hdKzPeCn3yBw4Wdwwlip2z1cHKPNEggtQ==} cpu: [arm64] os: [linux] libc: [glibc] - '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.207': - resolution: {integrity: sha512-uRv+D5oG/7EYr41FAJ9IPo2pZYBe2ZMaA6nSHCeizsgPxCSMtl5bNppmU21+jZJvo4hivObEgkGFERAhdGqygg==} + '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.209': + resolution: {integrity: sha512-GjxSlqk6yYCxb9BApVZvJ/Aq3OoI5mDRhpBpsZM23idNHg5/019sBBx4IGmkBjmfxTX5zzvu+m0fU18vl3W2Mg==} cpu: [x64] os: [linux] libc: [musl] - '@anthropic-ai/claude-agent-sdk-linux-x64@0.3.207': - resolution: {integrity: sha512-Kg6BPH8Ee0ny/oEUWJmvT1jCRBne4jVpRSOMsJcYp1Fav1rMEgpU219oJJs+LWwx4ifuuLtNWedqJNnVw7mnKg==} + '@anthropic-ai/claude-agent-sdk-linux-x64@0.3.209': + resolution: {integrity: sha512-XGhc23qmqk1yUwG633iZUxPM91iJjrX/a+DVy+Pz5X5J64aXFAsR9R+kNkJ1X5FDcm+MSYywUtQm9AmAZaiG2Q==} cpu: [x64] os: [linux] libc: [glibc] - '@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.207': - resolution: {integrity: sha512-9fWpUzfkXlPAg2tf8JpQe7w9avFaomAUbfAwyAmykQgSIf66LwaJjvI5hNqhNqczRKyfsXPn3ei2S5HKlmFP+Q==} + '@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.209': + resolution: {integrity: sha512-LIMVnToVRmcTeN/dDAdFP/U60/aM0/qbKOWza+0BabhrGwF/3nPIIoGrMrkLsKPzOsfNWrl+kepexP1zWb1x5w==} cpu: [arm64] os: [win32] - '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.207': - resolution: {integrity: sha512-YPjVT0q6aXEM2MgN4CI6/9fqiTXwETji+4NoPOzCYuqAkhXZqp30Jsk7/NHqYGNNSfURKrsuAoliKB0rsbpbjg==} + '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.209': + resolution: {integrity: sha512-tY5/+lmhqm7JQhvxLVTkhe2/UAw46gu8RnFrTwhoM0gEXDTmQhNlTM+m4F9LGeSfgKb2TElHpcQ237XNG1Ok6Q==} cpu: [x64] os: [win32] - '@anthropic-ai/claude-agent-sdk@0.3.207': - resolution: {integrity: sha512-y0PkQRmQBi96MHiN5Xzfq+GaddxCZCqI/cXEQBLYBLXGa4i1nDSlulQqkMBj2RorrrSGQJ6Wdw+uhu6OfHNPzA==} + '@anthropic-ai/claude-agent-sdk@0.3.209': + resolution: {integrity: sha512-HdxLmpnIOd4CSYxdlD/4ShKFvHAuWefP1MTf/B+Mj0Oq8Tb9qzmhLuHCZMd5Pf21Knrt7jke/t4coYAtokFcuw==} engines: {node: '>=18.0.0'} peerDependencies: '@anthropic-ai/sdk': '>=0.93.0' @@ -772,9 +772,6 @@ packages: effect: ^4.0.0-beta.97 vitest: ^3.0.0 || ^4.0.0 - '@emnapi/core@1.10.0': - resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} - '@emnapi/core@1.11.0': resolution: {integrity: sha512-l9Oo58x0HOP5znGzVhYW9U3e5wVuA4LAZU2AGezTmkhO1CgQRFDhDg4nneHsu/t3WniXg9QrG2nIXL/ZS8ln8Q==} @@ -787,9 +784,6 @@ packages: '@emnapi/core@1.4.5': resolution: {integrity: sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==} - '@emnapi/runtime@1.10.0': - resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} - '@emnapi/runtime@1.11.0': resolution: {integrity: sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg==} @@ -805,9 +799,6 @@ packages: '@emnapi/wasi-threads@1.0.4': resolution: {integrity: sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==} - '@emnapi/wasi-threads@1.2.1': - resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} - '@emnapi/wasi-threads@1.2.2': resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} @@ -1310,9 +1301,6 @@ packages: resolution: {integrity: sha512-WrOw/bcXm0f9qHkumlT1QlArXSTWqaY9sunsDpOk+yCCorCKMxvWT/a3xko4EYHVdeZoh00yI2TydXn6eyICDA==} engines: {node: '>= 10'} - '@napi-rs/wasm-runtime@0.2.12': - resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} - '@napi-rs/wasm-runtime@0.2.4': resolution: {integrity: sha512-9zESzOO5aDByvhIAsOy9TbpZ0Ur2AJbUI7UT73kcUTS2mxAMHOBaa1st/jAymNoCtvrit99kkzT1FZuXVcgfIQ==} @@ -1389,97 +1377,6 @@ packages: resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} engines: {node: ^14.21.3 || >=16} - '@node-rs/xxhash-android-arm-eabi@1.7.6': - resolution: {integrity: sha512-ptmfpFZ8SgTef58Us+0HsZ9BKhyX/gZYbhLkuzPt7qUoMqMSJK85NC7LEgzDgjUiG+S5GahEEQ9/tfh9BVvKhw==} - engines: {node: '>= 12'} - cpu: [arm] - os: [android] - - '@node-rs/xxhash-android-arm64@1.7.6': - resolution: {integrity: sha512-n4MyZvqifuoARfBvrZ2IBqmsGzwlVI3kb2mB0gVvoHtMsPbl/q94zoDBZ7WgeP3t4Wtli+QS3zgeTCOWUbqqUQ==} - engines: {node: '>= 12'} - cpu: [arm64] - os: [android] - - '@node-rs/xxhash-darwin-arm64@1.7.6': - resolution: {integrity: sha512-6xGuE07CiCIry/KT3IiwQd/kykTOmjKzO/ZnHlE5ibGMx64NFE0qDuwJbxQ4rGyUzgJ0KuN9ZdOhUDJmepnpcw==} - engines: {node: '>= 12'} - cpu: [arm64] - os: [darwin] - - '@node-rs/xxhash-darwin-x64@1.7.6': - resolution: {integrity: sha512-Z4oNnhyznDvHhxv+s0ka+5KG8mdfLVucZMZMejj9BL+CPmamClygPiHIRiifRcPAoX9uPZykaCsULngIfLeF3Q==} - engines: {node: '>= 12'} - cpu: [x64] - os: [darwin] - - '@node-rs/xxhash-freebsd-x64@1.7.6': - resolution: {integrity: sha512-arCDOf3xZ5NfBL5fk5J52sNPjXL2cVWN6nXNB3nrtRFFdPBLsr6YXtshAc6wMVxnIW4VGaEv/5K6IpTA8AFyWw==} - engines: {node: '>= 12'} - cpu: [x64] - os: [freebsd] - - '@node-rs/xxhash-linux-arm-gnueabihf@1.7.6': - resolution: {integrity: sha512-ndLLEW+MwLH3lFS0ahlHCcmkf2ykOv/pbP8OBBeAOlz/Xc3jKztg5IJ9HpkjKOkHk470yYxgHVaw1QMoMzU00A==} - engines: {node: '>= 12'} - cpu: [arm] - os: [linux] - - '@node-rs/xxhash-linux-arm64-gnu@1.7.6': - resolution: {integrity: sha512-VX7VkTG87mAdrF2vw4aroiRpFIIN8Lj6NgtGHF+IUVbzQxPudl4kG+FPEjsNH8y04yQxRbPE7naQNgHcTKMrNw==} - engines: {node: '>= 12'} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@node-rs/xxhash-linux-arm64-musl@1.7.6': - resolution: {integrity: sha512-AB5m6crGYSllM9F/xZNOQSPImotR5lOa9e4arW99Bv82S+gcpphI8fGMDOVTTCXY/RLRhvvhwzLDxmLB2O8VDg==} - engines: {node: '>= 12'} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@node-rs/xxhash-linux-x64-gnu@1.7.6': - resolution: {integrity: sha512-a2A6M+5tc0PVlJlE/nl0XsLEzMpKkwg7Y1lR5urFUbW9uVQnKjJYQDrUojhlXk0Uv3VnYQPa6ThmwlacZA5mvQ==} - engines: {node: '>= 12'} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@node-rs/xxhash-linux-x64-musl@1.7.6': - resolution: {integrity: sha512-WioGJSC1GoxQpmdQrG5l/uddSBAS4XCWczHNwXe895J5xadGQzyvmr0r17BNfihvbBUDH1H9jwouNYzDDeA6+A==} - engines: {node: '>= 12'} - cpu: [x64] - os: [linux] - libc: [musl] - - '@node-rs/xxhash-wasm32-wasi@1.7.6': - resolution: {integrity: sha512-WDXXKMMFMrez+esm2DzMPHFNPFYf+wQUtaXrXwtxXeQMFEzleOLwEaqV0+bbXGJTwhPouL3zY1Qo2xmIH4kkTg==} - engines: {node: '>=14.0.0'} - cpu: [wasm32] - - '@node-rs/xxhash-win32-arm64-msvc@1.7.6': - resolution: {integrity: sha512-qjDFUZJT/Zq0yFS+0TApkD86p0NBdPXlOoHur9yNeO9YX2/9/b1sC2P7N27PgOu13h61TUOvTUC00e/82jAZRQ==} - engines: {node: '>= 12'} - cpu: [arm64] - os: [win32] - - '@node-rs/xxhash-win32-ia32-msvc@1.7.6': - resolution: {integrity: sha512-s7a+mQWOTnU4NiiypRq/vbNGot/il0HheXuy9oxJ0SW2q/e4BJ8j0pnP6UBlAjsk+005A76vOwsEj01qbQw8+A==} - engines: {node: '>= 12'} - cpu: [ia32] - os: [win32] - - '@node-rs/xxhash-win32-x64-msvc@1.7.6': - resolution: {integrity: sha512-zHOHm2UaIahRhgRPJll+4Xy4Z18aAT/7KNeQW+QJupGvFz+GzOFXMGs3R/3B1Ktob/F5ui3i1MrW9GEob3CWTg==} - engines: {node: '>= 12'} - cpu: [x64] - os: [win32] - - '@node-rs/xxhash@1.7.6': - resolution: {integrity: sha512-XMisO+aQHsVpxRp/85EszTtOQTOlhPbd149P/Xa9F55wafA6UM3h2UhOgOs7aAzItnHU/Aw1WQ1FVTEg7WB43Q==} - engines: {node: '>= 12'} - '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1730,115 +1627,218 @@ packages: cpu: [x64] os: [win32] - '@oxc-project/types@0.132.0': - resolution: {integrity: sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==} - '@oxc-project/types@0.137.0': resolution: {integrity: sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==} + '@oxc-project/types@0.139.0': + resolution: {integrity: sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==} + '@oxc-resolver/binding-android-arm-eabi@11.21.3': resolution: {integrity: sha512-eNU11A2WNizh04v3uyaJCootrHIaS0B9aHYXvAvVnPNk4xYSjMUjHnhQ6dewPN2MRYDskV85d1N0Aw0WNWhcyg==} cpu: [arm] os: [android] + '@oxc-resolver/binding-android-arm-eabi@11.24.2': + resolution: {integrity: sha512-y09e0L0SRI2OA2tUIrjBgoV3eH5hvUKXNkJqXmNo5V2WxIjyC7I7aJfRLMEVpA8yi95f90gFDvO0VMgrDw+vwA==} + cpu: [arm] + os: [android] + '@oxc-resolver/binding-android-arm64@11.21.3': resolution: {integrity: sha512-8Q+ZjTLvn2dIcWsrmhdrEihm7q+ag/k+mkry7Z+t0QbbHaVxXQfvH9AewyVMh/WrpEKhQ3DDgx9fYbqeCpeOEw==} cpu: [arm64] os: [android] + '@oxc-resolver/binding-android-arm64@11.24.2': + resolution: {integrity: sha512-cl4icWaZFnLdg8m6qtnh5rBMuGbxc/ptStFHLeCNwr+2cZjkjNwQu/jYRS0CHlnPecOJMpuS5M6/BH+0J/YkEg==} + cpu: [arm64] + os: [android] + '@oxc-resolver/binding-darwin-arm64@11.21.3': resolution: {integrity: sha512-wkh0qKZGHXVUDxFw3oA1TXnU2BDYY/r775oJflGeIr8uDPPoN2pk8gijQIzYRT6hoql/lg3+Tx/SaTn9e2/aGg==} cpu: [arm64] os: [darwin] + '@oxc-resolver/binding-darwin-arm64@11.24.2': + resolution: {integrity: sha512-At29QEMF6HajbQvgY8K6OXnHD1x9rad74xBEfmCB6ZqCGsdq75aK7tOYcTbOanMy8qdIBrfL3SMr3p/lfSlb9w==} + cpu: [arm64] + os: [darwin] + '@oxc-resolver/binding-darwin-x64@11.21.3': resolution: {integrity: sha512-HbNc23FAQYbuyDV2vBWMez4u4mrsm5RAkniGZAWqr6lYZ3N4beeqIb776jzwRl8qL2zRhHVXpUj97X0QgogVzg==} cpu: [x64] os: [darwin] + '@oxc-resolver/binding-darwin-x64@11.24.2': + resolution: {integrity: sha512-A5Kqr1EUj4oIL5CF4WRssq/o5P0Y11cwoFouMRmQ7YnC/A8V93nv1nb7aSU8HwcgmXropjLNkVTl4MN87cu28Q==} + cpu: [x64] + os: [darwin] + '@oxc-resolver/binding-freebsd-x64@11.21.3': resolution: {integrity: sha512-K6xNsTUPEUdfrn0+kbMq5nOUB5w1C5pavPQngt4TM2FpN91lP0PBe2srSpamb4d69O7h86oAi/qWX/kZNRSjkw==} cpu: [x64] os: [freebsd] + '@oxc-resolver/binding-freebsd-x64@11.24.2': + resolution: {integrity: sha512-R5xkRBRRz7ceH/P5Jrc6G7FmdUdgpLYyESFAUDVTNQ9K0sGPxcp4ljiwEwEqsvNcQ4sYbMRrWcHHBCu7ksAJVw==} + cpu: [x64] + os: [freebsd] + '@oxc-resolver/binding-linux-arm-gnueabihf@11.21.3': resolution: {integrity: sha512-VcFmOpcpWX1zoEy8M58tR2M9YxM+Z9RuQhqAx5q0CTmrruaP7Gveejg75hzd/5sg5nk9G3aLALEa3hE2FsmmTQ==} cpu: [arm] os: [linux] + '@oxc-resolver/binding-linux-arm-gnueabihf@11.24.2': + resolution: {integrity: sha512-k/RuYL4L/R58IBn3wT5ma3Wh4k62bp1eYCFRWCmMsasUOqL+H6sW0VGFadEzKWXFFlz+2uIMoeMk9ySSZJHgbg==} + cpu: [arm] + os: [linux] + '@oxc-resolver/binding-linux-arm-musleabihf@11.21.3': resolution: {integrity: sha512-quVoxFLBy43hWaQbbDtQNRwAX5vX76mv7n64icAtQcJ3eNgVeblqmkupF/hAneNthdqSlnd1sTjb3aQSaDPaCQ==} cpu: [arm] os: [linux] + '@oxc-resolver/binding-linux-arm-musleabihf@11.24.2': + resolution: {integrity: sha512-bnHAak3ujYfH5pKk4NieFNbvYvernfoQDgwLddbZ3OtMYrem87/qjlA+u+aKG0oZcqSLGCful/6/CEA+aeAgaA==} + cpu: [arm] + os: [linux] + '@oxc-resolver/binding-linux-arm64-gnu@11.21.3': resolution: {integrity: sha512-X0AqNZgcD07Q4V3RDK18/vYOj/HQT/FnmEFGYS2jTWqY7JO13ryE3TEs3eAIgUJhBnNkpEaiXqz3VK8M7qQhWQ==} cpu: [arm64] os: [linux] libc: [glibc] + '@oxc-resolver/binding-linux-arm64-gnu@11.24.2': + resolution: {integrity: sha512-vDT3KHgzYp47gmtNOqL2VNhCyl5Zv643eyxm//A68J8DeUGXrvD1pZFiaT4jSfe+RInfnn1R2yVHye4enx6RnA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + '@oxc-resolver/binding-linux-arm64-musl@11.21.3': resolution: {integrity: sha512-YkaQnaKYdbuaXvRt5Qd0GpbihzVnyfR6z1SpYfIUC6RTu4NF7lDKPjVkYb+jRI2gedVO2rVpN35Y6akG6ud4Lw==} cpu: [arm64] os: [linux] libc: [musl] + '@oxc-resolver/binding-linux-arm64-musl@11.24.2': + resolution: {integrity: sha512-+kMlQvbzfyEYtu5FcjE4p+ttBLpKW4d/AsAsuE69BxV6V4twZJeIQZFfD8gh/wqglY0MkPSezWXQH0jBV13MUw==} + cpu: [arm64] + os: [linux] + libc: [musl] + '@oxc-resolver/binding-linux-ppc64-gnu@11.21.3': resolution: {integrity: sha512-gB9HwhrPiFqUzDeEq+y/CgAijz1YdI6BnXz5GaH2Pa9cWdutchlkGFAiAuGb/PjVQpiK6NFKzFuztxrweoit7A==} cpu: [ppc64] os: [linux] libc: [glibc] + '@oxc-resolver/binding-linux-ppc64-gnu@11.24.2': + resolution: {integrity: sha512-shjfMhmZ3gq9fv/w7bi3PnZlgOPG+2QAOFf0BJF0EgBSIGZ6PMLN2zbGEblTUYB/NKVDRyYhE2ff3dJ1QqNPkA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + '@oxc-resolver/binding-linux-riscv64-gnu@11.21.3': resolution: {integrity: sha512-zjDWBlYk8QGv0H8dsPUWqkfjYIIjG2TvspGkzXL0eImbgxtZorA/klKeHyolevoT3Kvbi+1iMr9Lhrh7jf54Og==} cpu: [riscv64] os: [linux] libc: [glibc] + '@oxc-resolver/binding-linux-riscv64-gnu@11.24.2': + resolution: {integrity: sha512-zGelwFR5oRo+b69k8Lrzun86DyUHzfKN6cnjbR9l7Z7NIRznOE/2ZvPa1IUKqAL2PzAXOdwkfVqNvO1H2RlpAw==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + '@oxc-resolver/binding-linux-riscv64-musl@11.21.3': resolution: {integrity: sha512-4UfsQvacV388y1zpXL7C1x1FNYaV52JtuNRiuzrfQA2z1z6ElVrsidkGsrvQ5EgeSq1Pj7kaKqrgGkvFuxJ/tw==} cpu: [riscv64] os: [linux] libc: [musl] + '@oxc-resolver/binding-linux-riscv64-musl@11.24.2': + resolution: {integrity: sha512-qxZ1SWCXJY0eyhAlP6Lmo9F2Nrtx7EkYj9oCgL8apDPCwXwCEDA2U697bbT81JIc2IrVjxO4KX6WU2N+oN9Z4w==} + cpu: [riscv64] + os: [linux] + libc: [musl] + '@oxc-resolver/binding-linux-s390x-gnu@11.21.3': resolution: {integrity: sha512-b5uH+HKH0MP5mNBYaK75SKsJbw52URqrx2LavYdq6wb0l3ExAG5niYRP9DWUNHdKilpaBVM2bXk9HNWrH3ew7Q==} cpu: [s390x] os: [linux] libc: [glibc] + '@oxc-resolver/binding-linux-s390x-gnu@11.24.2': + resolution: {integrity: sha512-sGCecF3cx2DFlH4t/z7ApnOnXqN48p5p5mlHDEnHTAukQa2P+qMVE4CwyWE9W+q/m3QJ7kKfGrIjax31f44oFQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + '@oxc-resolver/binding-linux-x64-gnu@11.21.3': resolution: {integrity: sha512-PjYlmilBpNRh2ntXNYAK3Am5w/nPfEpnU/96iNx7CI8EzAn12J4JRiec63wHJTH31nLoCNxBg/829pN+3CfG3Q==} cpu: [x64] os: [linux] libc: [glibc] + '@oxc-resolver/binding-linux-x64-gnu@11.24.2': + resolution: {integrity: sha512-k/VlMMcSzMlahb3/fENM4rTlsJ0s3fFROA0KXPBmKggqmTSaE383sl8F3KCOXPLmVsYfW6hCitMhXCEtNeZxxg==} + cpu: [x64] + os: [linux] + libc: [glibc] + '@oxc-resolver/binding-linux-x64-musl@11.21.3': resolution: {integrity: sha512-QTBAb7JuHlZ7JUEyM8UiQi2f7m/L4swBhP2TNpYIDc9Wp/wRw1G/8sl6i13aIzQAXH7LKIm294LeOHd0lQR8zA==} cpu: [x64] os: [linux] libc: [musl] + '@oxc-resolver/binding-linux-x64-musl@11.24.2': + resolution: {integrity: sha512-8hbnZyNi97b/8wapYaIF9+t9GmZKBW2vunaOc3h9HGJptH7b7XpvZqOTBSm/MpTjr7H497BlgOaSfLUdhmy2bw==} + cpu: [x64] + os: [linux] + libc: [musl] + '@oxc-resolver/binding-openharmony-arm64@11.21.3': resolution: {integrity: sha512-4j1DFwjwv36ec9kds0jU/ucQ5Ha4ERO/H95BxR5JFf0kqUUAJ1kwII7XhTc1vZrkdJkvLGC9Q2MbpObpum8RBg==} cpu: [arm64] os: [openharmony] + '@oxc-resolver/binding-openharmony-arm64@11.24.2': + resolution: {integrity: sha512-MvyGik3a6pVgZ0t/kWlbmFxFLmXQJwgLsY2eYFHLpy0wGwRbfzeIGgDwQ3kXqE30z+kSXennRkCrT7TUvkptNg==} + cpu: [arm64] + os: [openharmony] + '@oxc-resolver/binding-wasm32-wasi@11.21.3': resolution: {integrity: sha512-i8oluoel5kru/j1WNrjmQSiA3GQ7wvIYVR1IwIoZtKogAhya2iub+ZKIeSIkcJOrnzQ18Tzl/F+kL3fYOxZLvA==} engines: {node: '>=14.0.0'} cpu: [wasm32] + '@oxc-resolver/binding-wasm32-wasi@11.24.2': + resolution: {integrity: sha512-vHcssMPwO08RTvj/c0iOBz90attxyG3wQJ0dTcyEQK43LRpcdLWZlV5feBhv6Isn6ahbQIzHbCgfa81+RiML0Q==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + '@oxc-resolver/binding-win32-arm64-msvc@11.21.3': resolution: {integrity: sha512-M/8dw8dD6aOs+NlPJax401CZB9I7Aut84isQLgALGGwke4Afvw+/7yYhZb94yXf6t2sPLhQLmSmtSV+2FhsOWg==} cpu: [arm64] os: [win32] + '@oxc-resolver/binding-win32-arm64-msvc@11.24.2': + resolution: {integrity: sha512-uokJqro2iBqkFvJdKQLP7d8/BUmFwESQFVmIJUQKj1Xn1a/LysJoe1vmeECLF5b3jsV8CAL5sEMJXX6SdK9Nhg==} + cpu: [arm64] + os: [win32] + '@oxc-resolver/binding-win32-x64-msvc@11.21.3': resolution: {integrity: sha512-H7BCt/VnS9hnmMp42eGhZ99izSCRvlnWwy/N71K1/J8QoExwY4262Z8QiEkMDtduRJrztayDxETTckmUuAVL9Q==} cpu: [x64] os: [win32] + '@oxc-resolver/binding-win32-x64-msvc@11.24.2': + resolution: {integrity: sha512-UqGPmo56KDfLlfXFAFIrNflHT8tFxWGEivWg3Zeyp4Uy2NlKN1FGPr6/BxcLGG3+kZ6Wp14g5Uj+n71boqZfiw==} + cpu: [x64] + os: [win32] + '@oxfmt/binding-android-arm-eabi@0.58.0': resolution: {integrity: sha512-Uz62sHduGGPftXtILGyxdSW4PX82rUg+rfdNqhsgxe881g4rIoXlIqmZQ6HVKcF4f+F8qMhdD03Bx5u7gmeTdg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1991,124 +1991,124 @@ packages: cpu: [x64] os: [win32] - '@oxlint/binding-android-arm-eabi@1.73.0': - resolution: {integrity: sha512-HZQRN/UMBu+Ut+/9MiAChkbP4qZqrNOWBcNI45vOT40GVhbGR0JgHB87L48D4iAqFQIdVmeQYtV9RF89AjTKkg==} + '@oxlint/binding-android-arm-eabi@1.74.0': + resolution: {integrity: sha512-+gHd12muVI9ZLBaWLPkHt3Fj7jihFjgQ1MGtBaRL8vWrWrI0P7dLUty/cHrHS0oqPYIRgQUJsPu2CExQuMcwNw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@oxlint/binding-android-arm64@1.73.0': - resolution: {integrity: sha512-Gp+KJRylv2aW7thRpG5p1KTxZq4ZJFbWowrKzufNq9d3ssl3r3JviYV45/+p+7CN1Nv0zDd1e8Ex0b/HUDq4TQ==} + '@oxlint/binding-android-arm64@1.74.0': + resolution: {integrity: sha512-xjKdoMB+H+RCOByv/7l7nfIGW9mlOisqYdcyC75UqYuQecLpReAeEYUf2CNeDEI3KtmUgxpRw/+c63y4AeF/Bw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxlint/binding-darwin-arm64@1.73.0': - resolution: {integrity: sha512-3de96NdtXhxERMjIz7wsp2HYMY6pMQycGxFWac2mFecAx6VeARF/IqFb1QIaqiCRIdfzBwzTed+pCTCoiS+CYA==} + '@oxlint/binding-darwin-arm64@1.74.0': + resolution: {integrity: sha512-iUK7wvc6sejMKsC+Pt67mntoF5weFcyEunhZfLJceU6gL419mexz5wBkSx/EnkFBExMLNtOi9fnDSc5xfK0IzQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxlint/binding-darwin-x64@1.73.0': - resolution: {integrity: sha512-5zx/uPW32TiaOeVY1dQ/H5iOf0K1HOdFKOJhLqGl4o63+i1fpzoqqu/mKtd7OFgFjNCdhlyTGgjVkQTZm1ELcg==} + '@oxlint/binding-darwin-x64@1.74.0': + resolution: {integrity: sha512-ggKc/tn5SJ1u2yG2izC6VKODfYKV8MQ2AicJlNzOjuyrC29udvOef6/JzK2r32xqCnBDLFouR1VCkjzEI0/N9Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxlint/binding-freebsd-x64@1.73.0': - resolution: {integrity: sha512-qNe4gKHaGnLuZJ8toUg90JAa0S2vTVvDw+0bRi3q1avXZXDT4u5mMeECf3nD4HYrbdn1O7dXqWut4onY/yx/Xg==} + '@oxlint/binding-freebsd-x64@1.74.0': + resolution: {integrity: sha512-u++dH/43jy9hTLbneaWlS0gla/Bp1JdwJ2zgevCl8nDFUh6qRCGMxcL0f0lb7By3A9p/LfFr+7cG4HU1hG856g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxlint/binding-linux-arm-gnueabihf@1.73.0': - resolution: {integrity: sha512-cCehYh5hTbfShm/fxTD6wwrGUWIpvX+N5OxmAMhFhDeTGXvw+BeNj889tpxsFQ9ZLatQ6wImuY8tsKLZ+FMz7w==} + '@oxlint/binding-linux-arm-gnueabihf@1.74.0': + resolution: {integrity: sha512-Sj1zmtFDVTPeIbIz4ZfcXAbFHqCmKCXdCUlAJzvTF7I20NTH1RDpoF2PhkqNODutJzVhJYmm3oz0GwgY+tvE2g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxlint/binding-linux-arm-musleabihf@1.73.0': - resolution: {integrity: sha512-d5j5GDU/2dMgjVhw7TQT9ITrsIr1Y02KEXKyVGIXUkD+KiaxE9TP65FS2ZdgTBemQvoRL+gSBdbrIm3cQIeacg==} + '@oxlint/binding-linux-arm-musleabihf@1.74.0': + resolution: {integrity: sha512-//PKyQb/tQXcHArx2f7z+oVI/eMS2Jpv+edNuAtOrgIhWdGcpHxogveAxzmF2rpH1AIHp4Hq04RF/rgJdiICnQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxlint/binding-linux-arm64-gnu@1.73.0': - resolution: {integrity: sha512-Eyf1SrP3+yR1DI3OJgOY2Pvrr9dWP9TK37xPaDYycwTtlGlI45erJAVIfH5/m/xosDt6BupJYEFi47bvbTuuyw==} + '@oxlint/binding-linux-arm64-gnu@1.74.0': + resolution: {integrity: sha512-/k1Me+aX2tjuH10K62mLS0y8cLkJBHX6Ce0xPK+eWeel4bSdEGZ8dv4+hYMzg0GrSmjwy4yAYsDPeEeKBft/2w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-arm64-musl@1.73.0': - resolution: {integrity: sha512-IlT/OJApEDKaMmCooHuncgJZbbCe7T5QIWmTZBEtYscWvzPQuuEinVcid6kwQRVQOUdb7PUCz4jQHnaYXdfJXw==} + '@oxlint/binding-linux-arm64-musl@1.74.0': + resolution: {integrity: sha512-3tFSjBxc5D8/zvjEuLvOqcA8ZXKD0+6NuaVO/edeamNc49MoAsbfaC9s1UiwODwgF6slGaF8yJA2TPkukd77tg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@oxlint/binding-linux-ppc64-gnu@1.73.0': - resolution: {integrity: sha512-L+JYcb/vdg5fmcH08V6o0YYLU28cTH1SPNulwJdvK9NK49aXSkYy6oNpKBmddArVOXYqNepriDGiZ04G54kh1Q==} + '@oxlint/binding-linux-ppc64-gnu@1.74.0': + resolution: {integrity: sha512-9QggtPkSPXOCTu8Szis7auOK/sC7KdQaN+/TujP7YVVhzCAOhgdRfgv8uEz0r2tk5xdgus5rLYUrCDoZNtiRUw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-riscv64-gnu@1.73.0': - resolution: {integrity: sha512-Qtk0g3bKV6OwWjIm7R8kQN1uOZRKQt/MODK2a8QfkwhTpXBD53ozx5XLVWLGDQAVyp2otLW4D2wB98XfAfMPGA==} + '@oxlint/binding-linux-riscv64-gnu@1.74.0': + resolution: {integrity: sha512-VM5VPUJ4DJIWiK+AZn8FScUqMr6OFrCAYybMYjEEi7W13ParI64MByiXTkKMqZpBmvQ9zxl9Ebq2VUOiZRJYUg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-riscv64-musl@1.73.0': - resolution: {integrity: sha512-wX0NQKZVxltkAOVmzFcpOaMpdaUvsq1Eqpx9tkAfl71UdkTlSo1R4AdAnGccR1Fm2+TzFgZ22CyyGuZ41RDr/A==} + '@oxlint/binding-linux-riscv64-musl@1.74.0': + resolution: {integrity: sha512-SaDY1gh9rOA592J54g+gu5hkOFFQBZsMmIYHs+NRHG+Uq0OxtuuCXMWQ3vu1830Eugv5uMXyjG+bv2Z9y4IXjw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [musl] - '@oxlint/binding-linux-s390x-gnu@1.73.0': - resolution: {integrity: sha512-vPe7UGBMWyiLTtnqS4xxgMQFSFGmtQwhwCxuiw6lXygaO6bVt0D8dFVg8Xv05eaiN3ybC0HXXHUAohFMFvqoCQ==} + '@oxlint/binding-linux-s390x-gnu@1.74.0': + resolution: {integrity: sha512-ZATQeHZCyr6MbDveg0obD5sxLHFOghtOdC5jwVwYlvFWqtFOxctgFEG6Ef/64hYvZrWyhyCckB10AelqLopeDA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@oxlint/binding-linux-x64-gnu@1.73.0': - resolution: {integrity: sha512-2CwIWr9cemFC/CbRBWZvuk5mffz6ObmfFkfcC/9rTQ7f+icNhYr2kOjf9Rt8lLvugvkdGDOmkoVoFFHh6ClCTw==} + '@oxlint/binding-linux-x64-gnu@1.74.0': + resolution: {integrity: sha512-+aIvJyrdeD7LwCQ2WYLMUWNmnbeDRSPb40aBYtPjD9+PTqUwgJnk+HK5yLfSMeqXrMrDhE9uTmtt2y50tvjhHw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-x64-musl@1.73.0': - resolution: {integrity: sha512-nDadfJgg7NBBxG0N560wOe7LLX5QiYp6qBaI7viuk5EUORFBktU/NfV0MbTqU3gTqQDCh4VyxKdo5VADxk9w8Q==} + '@oxlint/binding-linux-x64-musl@1.74.0': + resolution: {integrity: sha512-XyktaR8lhK2qWiCK0Tk8oYD+/cgn+oHA6ddRnxSSXUKkkojkV78CmShZUxQF+yrBFs0SuW+JBOPG6hecyc/iZg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@oxlint/binding-openharmony-arm64@1.73.0': - resolution: {integrity: sha512-wGjJC+NLH9xP+IKGn9RDW94ojJR/wPbg5WCnQjj/oReaOtCQthr8ws1zICe77JFmo4ouUdeTHHZL/ESGiF6Pmw==} + '@oxlint/binding-openharmony-arm64@1.74.0': + resolution: {integrity: sha512-mzbjrPl4neaVUiJ1fUiEUxTGaSZBoiKtaoB6jmIpz9S+VOA2vDYmJpihQ82w6178V5jxziclTg8Cgj5yF6tTDg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@oxlint/binding-win32-arm64-msvc@1.73.0': - resolution: {integrity: sha512-I7X47GPGljw225YUQ5SbC/rb1Kkdrd0yQf0x+hYxeKS6DpfjMbo9ccQPQ6LNY6BoJQ1sHhgDUGuMn5Vg5gHT6w==} + '@oxlint/binding-win32-arm64-msvc@1.74.0': + resolution: {integrity: sha512-vUAe9okpS2Oa5+lX67lqHMuNUvfkleRKwrUDJ/WJBsgmddvZ1mrsh2HVmuFDRzqFELhaJhFaCNOuR6a7L3rtIA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxlint/binding-win32-ia32-msvc@1.73.0': - resolution: {integrity: sha512-5lWj+3h+74Fm1jYOO9qkJA4xkAlZA099DkXppuXsk7UpnpZLttsefrZU469vChGaG6hcSqrkKXQOvMTZtbjeNg==} + '@oxlint/binding-win32-ia32-msvc@1.74.0': + resolution: {integrity: sha512-yyXXJyYYSXL4I8K8jAWjJs+J3fa9gH2JmEbo4f5adm+1tNC9itseicBNuwK7BDHvqQ5J534s+yDULu89vYL2ZQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@oxlint/binding-win32-x64-msvc@1.73.0': - resolution: {integrity: sha512-WaNRvh4f6zY9CvUQk2YoA1O90ieWrIklI84+HXFr9Isjz9CSESrdqo/RtIYt4Dll/cAchqGDMehfaZd0vqEFZw==} + '@oxlint/binding-win32-x64-msvc@1.74.0': + resolution: {integrity: sha512-VTC9IYTIMrVUk/i6Ms1ohzzDKZFkWn0KU2OBbPBzgmVZ2V30165T/zK4LztTr0Xgp9fZ1qQZ1rsZAu/rEmySlA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -2216,11 +2216,11 @@ packages: resolution: {integrity: sha512-//0sR/cow/s4ICQaYoAobOl4aU8cjU6x/V24V7XkKotb9+O+3zySIYp146vpaobYHnxa4pZX8NkV54Z5AwbDKA==} engines: {node: '>=12'} - '@posthog/core@1.43.1': - resolution: {integrity: sha512-hGM8f5sp3we6Em/RQHXbmyYm554hUx9+9jhf92ZQgDS4/xW72KHyZiO9wcFye+qAx2cJAOhOCDIznmO1FV8IbA==} + '@posthog/core@1.40.2': + resolution: {integrity: sha512-H12j7O9iHGvpK9t2ko8W4pvfbV1pBDxrsWC1LA6yp2RhzwvC4T3sWhu+AekDQJSRSrJEWlB0t/Ueq9QhPSq7FQ==} - '@posthog/types@1.397.0': - resolution: {integrity: sha512-Pa7FtsBo3V0XrhY4y8Xquwp8U07syuZ2IGQdlsRyxhz0yejQLceFjI58UssCXE5zDCDj3km5l7H3ufD6rHChCg==} + '@posthog/types@1.393.0': + resolution: {integrity: sha512-vzWeEJZ7ERQhFRoQYaP5jzN1JvIu46UJyHXsuv+dTGW2r3sMgREOhNxXLZjmFHwZ8/FOHQoyqqQmXTCXZSfMSg==} '@radix-ui/number@1.1.2': resolution: {integrity: sha512-ceTwaxc4I5IOi97DgCotl3pqiyRGvffcc0oOsE2dQYaJOFIDsDt4VWG6xEbg1QePv9QWausCEIppud/tJ1wNig==} @@ -2578,97 +2578,97 @@ packages: '@radix-ui/rect@1.1.2': resolution: {integrity: sha512-xnXE7wG13PI+cxieVssYXlQJuYVRhH9NBoxt3KNwzghDIA69GMm7d4wXRouHIYjE+KvS6U/MsMO73NdS2MH9ZA==} - '@rolldown/binding-android-arm64@1.0.2': - resolution: {integrity: sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==} + '@rolldown/binding-android-arm64@1.1.5': + resolution: {integrity: sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.0.2': - resolution: {integrity: sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==} + '@rolldown/binding-darwin-arm64@1.1.5': + resolution: {integrity: sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.2': - resolution: {integrity: sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==} + '@rolldown/binding-darwin-x64@1.1.5': + resolution: {integrity: sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.0.2': - resolution: {integrity: sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==} + '@rolldown/binding-freebsd-x64@1.1.5': + resolution: {integrity: sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.0.2': - resolution: {integrity: sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==} + '@rolldown/binding-linux-arm-gnueabihf@1.1.5': + resolution: {integrity: sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.0.2': - resolution: {integrity: sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==} + '@rolldown/binding-linux-arm64-gnu@1.1.5': + resolution: {integrity: sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.0.2': - resolution: {integrity: sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==} + '@rolldown/binding-linux-arm64-musl@1.1.5': + resolution: {integrity: sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.0.2': - resolution: {integrity: sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==} + '@rolldown/binding-linux-ppc64-gnu@1.1.5': + resolution: {integrity: sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.0.2': - resolution: {integrity: sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==} + '@rolldown/binding-linux-s390x-gnu@1.1.5': + resolution: {integrity: sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.0.2': - resolution: {integrity: sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==} + '@rolldown/binding-linux-x64-gnu@1.1.5': + resolution: {integrity: sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.0.2': - resolution: {integrity: sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==} + '@rolldown/binding-linux-x64-musl@1.1.5': + resolution: {integrity: sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rolldown/binding-openharmony-arm64@1.0.2': - resolution: {integrity: sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==} + '@rolldown/binding-openharmony-arm64@1.1.5': + resolution: {integrity: sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.0.2': - resolution: {integrity: sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==} + '@rolldown/binding-wasm32-wasi@1.1.5': + resolution: {integrity: sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [wasm32] - '@rolldown/binding-win32-arm64-msvc@1.0.2': - resolution: {integrity: sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A==} + '@rolldown/binding-win32-arm64-msvc@1.1.5': + resolution: {integrity: sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.2': - resolution: {integrity: sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==} + '@rolldown/binding-win32-x64-msvc@1.1.5': + resolution: {integrity: sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -2756,45 +2756,45 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@supabase/auth-js@2.110.2': - resolution: {integrity: sha512-Qj7a6EDP+AMMQFWqGv+qFa8r6re//dk+qQI5bA0KK+PZmnI3JPu97TDeNt6SMiQ2FkklP79hP2yDFYSnA989OA==} + '@supabase/auth-js@2.110.5': + resolution: {integrity: sha512-QSlI5CNeEefHP95/GbeMhNgr8aHEHiXFh8c1IWthYyI9ZzBwEigYzJFCw4Ff+GkL8OK9tArBmGGv6rpg5zLXSw==} engines: {node: '>=22.0.0'} - '@supabase/functions-js@2.110.2': - resolution: {integrity: sha512-ZjjqrXpxM9/rE+eAtZxiK45EWy9EBoJQ322Q5Y75LccYQNh212neHTgXP/o4MIzmH0LNXT8UzvTZtQOfOzyoeQ==} + '@supabase/functions-js@2.110.5': + resolution: {integrity: sha512-nuQuoIoEGT8ukrwr6THlY5v50bSMJ2rqti1FFsGyDaC98POLmcFQVFFh23PVPhRsKWUxrFc0MpmEoHKa7CY4mg==} engines: {node: '>=22.0.0'} '@supabase/phoenix@0.4.4': resolution: {integrity: sha512-Gt0pqoXuIqX/8dvG0OKp/wMCobXNH3klNbUPBNyOfN0YA1IswrM3HyWFMOPk1Jy+BRaIyDPcFx4jLBwHNmlyfQ==} - '@supabase/postgrest-js@2.110.2': - resolution: {integrity: sha512-++LBmcIMwCtgO4tISQUmo9+2xkRwHQqS8ZKMCnhXLe9P8k8YQRXuMoh/RiSzQSoev8gqet0W7yOboW0cUxnt0Q==} + '@supabase/postgrest-js@2.110.5': + resolution: {integrity: sha512-eObfgBjxLPzFakwMpUmsv8nuNPOhoDhzzN8cwvEvGkxkmuRIZcOCYH9+DRbpKnxh7tgsBZW+KWtaZI2j98b2/g==} engines: {node: '>=22.0.0'} - '@supabase/realtime-js@2.110.2': - resolution: {integrity: sha512-z3jTOTPgyn6E3r6dVOOQ10He4yAMB2czjFw7xVdX3s16MHElna5rY1gVaePs0NIo6xvtMYbtmOXlFaFt/ePLpg==} + '@supabase/realtime-js@2.110.5': + resolution: {integrity: sha512-VtOZxw5jfrc37KgIfFdp9SOFJjtvJ0qU+gT8CPQjL7cqy4DJlaTXacw5aBBogLZksqAI0YN67qhlDaMKFwfOuw==} engines: {node: '>=22.0.0'} - '@supabase/storage-js@2.110.2': - resolution: {integrity: sha512-EhsRSwSnmQefKJsAxoRUZ0hvHr92ECM8DDGAKR5z0HdoJx4heI60PjHUTruVNZxKX6XeobLGDyLud020Bw1iwg==} + '@supabase/storage-js@2.110.5': + resolution: {integrity: sha512-7GkOZlrYknVGVPyijoDbu5OXGvQ2oQ6dkU0juAkIMPZ9QgtmJ8IPrxe76zGSbmzbaC1GdKw7pqeXEi2HT67sbA==} engines: {node: '>=22.0.0'} - '@supabase/supabase-js@2.110.2': - resolution: {integrity: sha512-r9q9w4ZQ6mOjh36aqUNFSisBF611vzpO8JphBESr2Q1SWvmGFQeI7Jq7Y+PaNMZ6Zszz+S2yTlJStCpnaMSnQg==} + '@supabase/supabase-js@2.110.5': + resolution: {integrity: sha512-cAO1Nm+CCogRNVXN93bBkh0vjOdLM5e6J9gB/cHV9Lqni/gEmN2HJFrnn4NI33GYIfmlh4Wbm6siH+XXRgpexA==} engines: {node: '>=22.0.0'} - '@swc-node/core@1.15.0': - resolution: {integrity: sha512-WAerrrl087WgenB92XG4Th2t0NQFfMNLYSe0sW2cEMMqM/LQmP4rozsDJl0vuzTrbewjpKQryxFXI+aYig/dBg==} + '@swc-node/core@1.14.1': + resolution: {integrity: sha512-jrt5GUaZUU6cmMS+WTJEvGvaB6j1YNKPHPzC2PUi2BjaFbtxURHj6641Az6xN7b665hNniAIdvjxWcRml5yCnw==} engines: {node: '>= 10'} peerDependencies: '@swc/core': '>= 1.13.3' '@swc/types': '>= 0.1' - '@swc-node/register@1.12.0': - resolution: {integrity: sha512-RnkF4NOHqv3JAKJS9YguLeFH81NPv8VQKTXt5lMJfOujB7qvZXJRei5LJeulihF0ZMD1UIruIA0gm9it/bH37g==} + '@swc-node/register@1.11.1': + resolution: {integrity: sha512-VQ0hJ5jX31TVv/fhZx4xJRzd8pwn6VvzYd2tGOHHr2TfXGCBixZoqdPDXTiEoJLCTS2MmvBf6zyQZZ0M8aGQCQ==} peerDependencies: '@swc/core': '>= 1.4.13' - typescript: '>= 4.3 < 7' + typescript: '>= 4.3' '@swc-node/sourcemap-support@0.6.1': resolution: {integrity: sha512-ovltDVH5QpdHXZkW138vG4+dgcNsxfwxHVoV6BtmTbz2KKl1A8ZSlbdtxzzfNjCjbpayda8Us9eMtcHobm38dA==} @@ -2938,9 +2938,6 @@ packages: '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} - '@types/node@25.9.3': - resolution: {integrity: sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg==} - '@types/node@26.1.1': resolution: {integrity: sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==} @@ -3167,8 +3164,8 @@ packages: resolution: {integrity: sha512-SZ9uxnQKppiM+/67xTaaBP4AZ/Q+weUB22ci1gF861icYWhWFu3njA3ofYig/4yNqkYRVEKVAIwnH97BaBEYbg==} engines: {node: '>=18'} - '@verdaccio/hooks@8.0.3': - resolution: {integrity: sha512-5e71ArLhvcc/CFkDZW/gS3GWFkIAgIr0SCmEUXsTYHoqfyLV6VlXIpb6baGCnQ1EoQQ5ZkHtaWNFxSzM96ZlXA==} + '@verdaccio/hooks@8.0.4': + resolution: {integrity: sha512-FJRCe7pH8c1pCqf7BVwKwCtvN63HMQhM6991qQwBS8CgZf86a1TY9TMxW1ICSHqD+jPidtLq+6W2s+bfaiHwJA==} engines: {node: '>=18'} '@verdaccio/loaders@8.0.3': @@ -3215,8 +3212,8 @@ packages: resolution: {integrity: sha512-BKdksIRaqhrptP6JmVW71TeUTuA1hHWcGeEabSDzYgi1PXgLS9l8On3HWLs+TZ93PRtTjZiZJGCJPbqoy5itvg==} engines: {node: '>=18'} - '@verdaccio/ui-theme@9.0.0-next-9.20': - resolution: {integrity: sha512-eo4xqFzzUU382zKozJxipJGLp3HUNQvMZ81oTBa0SZ+Q/Bk2Fum82qAiE5ww4hTaVDV0Rjb7fqC4qJeBuLcGww==} + '@verdaccio/ui-theme@9.0.0-next-9.21': + resolution: {integrity: sha512-w423IDgBOTmUW94CF84bXosW9Kg6khNhbGxCAPEOtE7yZUyASMxoZP14Ro3N2F492pd0Nd/MV3a//opI+SNPQA==} '@verdaccio/url@13.0.3': resolution: {integrity: sha512-BGH19Qc0pwoefyafJ100izQkgqKuuSF0EVdNjgipG8154yIluELxyliL8cqvTTDVZLVwz/CBuBq8IpUU9lhv9g==} @@ -3316,6 +3313,9 @@ packages: ajv@8.18.0: resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} @@ -3438,8 +3438,8 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - baseline-browser-mapping@2.10.42: - resolution: {integrity: sha512-c/jurFrDLyui7o1J86yLkRu4LMsTYcBohveus7/I2Hzdn9KIP2bdJPTue/lR1KH46enoPbD77GKeSYNdyPoD3Q==} + baseline-browser-mapping@2.10.43: + resolution: {integrity: sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ==} engines: {node: '>=6.0.0'} hasBin: true @@ -3455,12 +3455,12 @@ packages: bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} - body-parser@1.20.5: - resolution: {integrity: sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==} + body-parser@1.20.6: + resolution: {integrity: sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - body-parser@2.2.2: - resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + body-parser@2.3.0: + resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==} engines: {node: '>=18'} bottleneck@2.19.5: @@ -3480,8 +3480,8 @@ packages: browserify-zlib@0.1.4: resolution: {integrity: sha512-19OEpq7vWgsH6WkvkBJQDFvJS1uPcbFOQ4v9CU839dO+ZZXUZO6XpE6hNCqvlIIj+4fZvRiJ6DsAQ382GwiyTQ==} - browserslist@4.28.2: - resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + browserslist@4.28.6: + resolution: {integrity: sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -3516,10 +3516,6 @@ packages: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} - call-bind@1.0.9: - resolution: {integrity: sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==} - engines: {node: '>= 0.4'} - call-bound@1.0.4: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} @@ -3528,8 +3524,8 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} - caniuse-lite@1.0.30001803: - resolution: {integrity: sha512-g/uHREV2ZpK9qMalCsWaxmA6ol+DX8GYhuf3T40RKoP+oL7vhRJh8LNt73PCjpnR6l14FzfPrB5Yux4PKm2meg==} + caniuse-lite@1.0.30001805: + resolution: {integrity: sha512-52noaS3DubycKSXaU30TwPGIp+POyQSUVa5jBEq3vkRkY0kjyb3LQgvhU6WGyCcyXqVLWO0Cw0Q6BSdD0kUfVA==} caseless@0.12.0: resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} @@ -3609,8 +3605,8 @@ packages: resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} engines: {node: 10.* || >= 12.*} - cli-truncate@6.1.0: - resolution: {integrity: sha512-ofWtXdvPO1WepoE9Gn4dPdZw+ADud1Yz9K+xm9FjK5vvrs/D60vrlKIQeSw1wJX4cphW2bnWi8GZSKvf9T6shw==} + cli-truncate@6.1.1: + resolution: {integrity: sha512-06p9vyLahLa4zkGcgsGxU6iEkSOiuI4fhCH6Emhe2lPAcoUv73n72DnODsnHA+5wwXGnV0n9M9/qOQJSjYhFhw==} engines: {node: '>=22'} client-only@0.0.1: @@ -3834,10 +3830,6 @@ packages: resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} engines: {node: '>=10'} - define-data-property@1.1.4: - resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} - engines: {node: '>= 0.4'} - define-lazy-prop@2.0.0: resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} engines: {node: '>=8'} @@ -3923,8 +3915,8 @@ packages: engines: {node: '>=0.12.18'} hasBin: true - electron-to-chromium@1.5.363: - resolution: {integrity: sha512-VjUKPyWzGnT1fujlkEGC/BvN70Hh70KXtAqcmniXviYlJC/ivcT+BWGPyxWVbJZLfvtKR6dqg1L7T7pgAMBtWA==} + electron-to-chromium@1.5.389: + resolution: {integrity: sha512-cEto7aeOqBfU1D+c5py5pE+ooscKE75JifxLBdFUZsqAxRS6y7kebtxAZvICszSl05gPjYHDTjY+lXpyGvpJbg==} emoji-regex@10.6.0: resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} @@ -3978,8 +3970,8 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} - es-module-lexer@2.1.0: - resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} + es-module-lexer@2.3.1: + resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==} es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} @@ -4061,8 +4053,8 @@ packages: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} - eventsource-parser@3.0.8: - resolution: {integrity: sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==} + eventsource-parser@3.1.0: + resolution: {integrity: sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==} engines: {node: '>=18.0.0'} eventsource@3.0.7: @@ -4077,8 +4069,8 @@ packages: resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==} engines: {node: ^18.19.0 || >=20.5.0} - expect-type@1.3.0: - resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} engines: {node: '>=12.0.0'} express-rate-limit@5.5.1: @@ -4132,8 +4124,8 @@ packages: fast-string-width@3.0.2: resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} - fast-uri@3.1.2: - resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} + fast-uri@3.1.3: + resolution: {integrity: sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==} fast-wrap-ansi@0.2.2: resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==} @@ -4258,8 +4250,8 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] - fumadocs-core@16.11.3: - resolution: {integrity: sha512-AHhOYY2YA98vEbgzLBxqZ9yyfW6VmOTIIjK803xCOWUmqyiVU8ONckI6IuIEIWTwyvk4s/PAC49p01J0Cjio+w==} + fumadocs-core@16.11.4: + resolution: {integrity: sha512-PckGsjeXoQ7UkOn6XuJSNMoZYgYHAtEV5izxCHbjry1A9CM1m/u2dLM6Fefi9PS3SDVGQ8BKiYxtx1i4FF7T7g==} peerDependencies: '@mdx-js/mdx': '*' '@mixedbread/sdk': 0.x.x @@ -4317,8 +4309,8 @@ packages: zod: optional: true - fumadocs-mdx@15.1.0: - resolution: {integrity: sha512-2nDusSlYFuNVcyB51jgY3tA3r01ALTwoURrMDNoc7cbJKZ2sac/PW+CDq6SHTArkgRMmFiKYQGfspJdjgTtPTg==} + fumadocs-mdx@15.1.1: + resolution: {integrity: sha512-qJ5WstRONnCkidmxdKcu9WgYfmM8TbAZRsprWXWnSZk+RIa+QQCnn0FXa1c9Pm8qesYSFVm6HmgKGSI2uPpQ6w==} hasBin: true peerDependencies: '@fumadocs/satteri': 0.x.x @@ -4354,12 +4346,12 @@ packages: vite: optional: true - fumadocs-ui@16.11.3: - resolution: {integrity: sha512-JM7wf10FxL2N0PL9RSX2ZOtPwRzH5FCkCkAxmqBhOxeTTL8FfPhhOt/MJCXArv0QrTBUIOAnb4M7oVQJN5DTmw==} + fumadocs-ui@16.11.4: + resolution: {integrity: sha512-ZLvyfPQjqa3m0+R6P9vkYtFW0HDC34b9lWiA6UPIJil7BwC8OUE9tpebKfQD69/DZF1KAC9lxz6929vLTraylQ==} peerDependencies: '@types/mdx': '*' '@types/react': '*' - fumadocs-core: 16.11.3 + fumadocs-core: 16.11.4 next: 16.x.x react: ^19.2.0 react-dom: ^19.2.0 @@ -4476,9 +4468,6 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} - has-property-descriptors@1.0.2: - resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} - has-symbols@1.1.0: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} @@ -4521,8 +4510,8 @@ packages: highlight.js@10.7.3: resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} - hono@4.12.21: - resolution: {integrity: sha512-uV63apnb0kyPtAUwoWgaGh9HyIFcv8lgmzPZSiTBQAFOFGIzka5EZ1dZocmGnn0XdX0+XTqJ6Tqv7selMuGLRQ==} + hono@4.12.30: + resolution: {integrity: sha512-emn+JoJjrN9YTpRDS5it/UI2SO9BAE37T6I3d963RxcZ81G9A4pr2SZTEiiaiKbzx+NKRg5BZ89fCL7gCJCUog==} engines: {node: '>=16.9.0'} hook-std@4.0.0: @@ -4589,8 +4578,8 @@ packages: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} - iconv-lite@0.7.2: - resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + iconv-lite@0.7.3: + resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} engines: {node: '>=0.10.0'} ieee754@1.2.1: @@ -4656,8 +4645,8 @@ packages: inline-style-parser@0.2.7: resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} - ioredis@5.11.0: - resolution: {integrity: sha512-EZBErytyVovD8f6pDfG3Kb37N6Y3lmDA9NNj+4+IP13CzzHGeX+OyeRM2Um13khRzoBSzzL+5lVnCX8V2RLeMg==} + ioredis@5.11.1: + resolution: {integrity: sha512-ehuGcf94bQXhfagULNXrJdfnWO38v070jxSx/qE87Kjzmu2fU7ro5EFAb+OPituLqgfyuQaym5DlrNydW2sJ9A==} engines: {node: '>=12.22.0'} ip-address@10.2.0: @@ -4764,9 +4753,6 @@ packages: isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} - isarray@2.0.5: - resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} - isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -4811,10 +4797,6 @@ packages: resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==} hasBin: true - js-yaml@5.2.1: - resolution: {integrity: sha512-zfLtNfQqxVqq3uaTqSkh4x4hZw3KHobGUA0fJUj4wawW8bsQLTVqpHdXSIzidh7o+4lEW36tANuAGdaFx6Zgnw==} - hasBin: true - jsbn@0.1.1: resolution: {integrity: sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==} @@ -4845,10 +4827,6 @@ packages: json-schema@0.4.0: resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} - json-stable-stringify@1.3.0: - resolution: {integrity: sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==} - engines: {node: '>= 0.4'} - json-stringify-safe@5.0.1: resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} @@ -4866,9 +4844,6 @@ packages: jsonfile@6.2.1: resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==} - jsonify@0.0.1: - resolution: {integrity: sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==} - jsonparse@1.3.1: resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} engines: {'0': node >= 0.2.0} @@ -4890,8 +4865,8 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} - knip@6.27.0: - resolution: {integrity: sha512-CngYEYrD0n20N06FXA8n3u/0Wnnugoa+B9k14OP+iKIgkCHuzvIdsP3nfwjhByoc1WfogpxfiriMboAXFETDUw==} + knip@6.26.0: + resolution: {integrity: sha512-e9eELEEpBpGTd4H4HB7818/DYj9dMzMyUqAddfYwUN/EbSkgIjOuWEF96W/xHsmV0SDrsdXjIM+oZ2xpPzPsBA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -5055,8 +5030,8 @@ packages: resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} engines: {node: '>=12'} - lucide-react@1.25.0: - resolution: {integrity: sha512-/mdJTRbiwcLOQ1NZZK1amZF9rIZyvO18D6r9TngE6TG1NmqHgFuT4eE7Xrkm9UsXMbBJD1NlfwHVltCDWHrOTw==} + lucide-react@1.24.0: + resolution: {integrity: sha512-YT6mBD8lGKkg4nM39enlm94/sfJIiW0YKUT60fBy4YK8tai31ylg1VhGNWxkpSKHo9UagfnZqwIff3HTDQwXeA==} peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -5455,8 +5430,8 @@ packages: resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==} hasBin: true - node-releases@2.0.46: - resolution: {integrity: sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==} + node-releases@2.0.51: + resolution: {integrity: sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==} engines: {node: '>=18'} normalize-package-data@6.0.2: @@ -5578,15 +5553,12 @@ packages: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} - object-keys@1.1.1: - resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} - engines: {node: '>= 0.4'} - obuf@1.1.2: resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} - obug@2.1.1: - resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + obug@2.1.3: + resolution: {integrity: sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==} + engines: {node: '>=12.20.0'} on-exit-leak-free@2.1.2: resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} @@ -5632,6 +5604,9 @@ packages: oxc-resolver@11.21.3: resolution: {integrity: sha512-2Mx3fKQz7+xgrBONjsxOgCGtMHOn38/HxMzW1I5efwXB5a4lRN0Vp40gYUJFBWJslcrvwoofTrqoTnLbwTd3pA==} + oxc-resolver@11.24.2: + resolution: {integrity: sha512-FY91FiDBj7ls5MsFS9jN3tjz2o0/zsdSsymlakySaBwVJZorHhkWyICLZMKxlu1R9vYo+sd3z1jwb4J8x7bNDw==} + oxfmt@0.58.0: resolution: {integrity: sha512-8feG/7NVEHDVwc1OUpP6Pks+TnaDFUw2jLLFIMi5bcmmwxAX2wBQvjSzj62RRTYBf2Op1Wt8xbkmagmPTR5ETg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -5649,8 +5624,8 @@ packages: resolution: {integrity: sha512-giCk5sEvG02d5tzPmFMX3hem8ndzEEu1xvGYS5OwNfO2WGl6ZVxt5LjE0yiMDoz94INI7XkXwgFAQiydPvVHDw==} hasBin: true - oxlint@1.73.0: - resolution: {integrity: sha512-u91G9TJzU6yqKWNZUYprQB07W7YvntZXaRxQ6CkoytepYhLWUXWsr1M8zUJ34VatNPuUAr3Z8GH+O2A331CluQ==} + oxlint@1.74.0: + resolution: {integrity: sha512-odGl2s2x5IOJoj3A0v1k0PGBXVFBZeZ2+AK/+K2MJur7Ghi3bkyX5NuLUWHKqa4js1wjep3hJeuTQJOlr+4+dA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -5806,9 +5781,6 @@ packages: peerDependencies: pg: '>=8.0' - pg-protocol@1.14.0: - resolution: {integrity: sha512-n5taZ1kO3s9ngDTVxsEznOqCyToTgz0FLuPq0B33COy5pPpuWJpY3/2oRBVETuOgzdqRXfWpM9HIhp2LBBT1BA==} - pg-protocol@1.15.0: resolution: {integrity: sha512-cq9sECI5s0+uPUXjbz8ioyPJni6RzsRib0US67i5IoTZKw8fNeYlVE7u8F4dG7vEJJtc5wdD1K189lCCUwqWTQ==} @@ -5839,10 +5811,6 @@ packages: resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} engines: {node: '>=8.6'} - picomatch@4.0.4: - resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} - engines: {node: '>=12'} - picomatch@4.0.5: resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} engines: {node: '>=12'} @@ -5981,8 +5949,8 @@ packages: pumpify@1.5.1: resolution: {integrity: sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==} - pure-rand@8.4.0: - resolution: {integrity: sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A==} + pure-rand@8.4.2: + resolution: {integrity: sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng==} qs@6.14.2: resolution: {integrity: sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==} @@ -6006,6 +5974,10 @@ packages: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} + range-parser@1.3.0: + resolution: {integrity: sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==} + engines: {node: '>= 0.6'} + raw-body@2.5.3: resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==} engines: {node: '>= 0.8'} @@ -6201,8 +6173,8 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rolldown@1.0.2: - resolution: {integrity: sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g==} + rolldown@1.1.5: + resolution: {integrity: sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -6235,8 +6207,8 @@ packages: scroll-into-view-if-needed@3.1.0: resolution: {integrity: sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==} - semantic-release@25.0.6: - resolution: {integrity: sha512-EGS5PWCDavYD4jAE4vKQ+VKcwXFpxLsKg05UcrUQwqxUCM1KtNRx9ZdMyYJL3z2aTyAH9zDgQSNFjRvbsIuKHQ==} + semantic-release@25.0.7: + resolution: {integrity: sha512-fFiUD7LNFI0TOGkc49WDHUX4GYKrOMDKct5ReSB1EJCZiPsPdbIPIJGys1xb2ILpK1v9JMsRkjJQgTRpbr7DgQ==} engines: {node: ^22.14.0 || >= 24.10.0} hasBin: true @@ -6253,11 +6225,6 @@ packages: engines: {node: '>=10'} hasBin: true - semver@7.8.2: - resolution: {integrity: sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==} - engines: {node: '>=10'} - hasBin: true - semver@7.8.4: resolution: {integrity: sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==} engines: {node: '>=10'} @@ -6284,10 +6251,6 @@ packages: resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} engines: {node: '>= 18'} - set-function-length@1.2.2: - resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} - engines: {node: '>= 0.4'} - setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} @@ -6303,8 +6266,8 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - shell-quote@1.8.4: - resolution: {integrity: sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==} + shell-quote@1.10.0: + resolution: {integrity: sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==} engines: {node: '>= 0.4'} shiki@4.3.1: @@ -6432,8 +6395,8 @@ packages: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} - std-env@4.1.0: - resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} + std-env@4.2.0: + resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} steno@0.4.4: resolution: {integrity: sha512-EEHMVYHNXFHfGtgjNITnka0aHhiAlo93F7z2/Pwd+g0teG9CnM3JIINM7hVVB5/rhw9voufD7Wukwgtw2uqh6w==} @@ -6455,8 +6418,8 @@ packages: resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} engines: {node: '>=18'} - string-width@8.2.1: - resolution: {integrity: sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==} + string-width@8.2.2: + resolution: {integrity: sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==} engines: {node: '>=20'} string_decoder@1.1.1: @@ -6681,10 +6644,6 @@ packages: resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} engines: {node: '>=16'} - type-fest@5.7.0: - resolution: {integrity: sha512-1URUxUqfHFM1c+zfSPsa3gnkO7Aq21qyH75SIduNYz4SzY964rn1X2vCMQaHSHhktiw+0kPa2iyb6PUpXqB6Vg==} - engines: {node: '>=20'} - type-fest@5.8.0: resolution: {integrity: sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA==} engines: {node: '>=20'} @@ -6716,9 +6675,6 @@ packages: resolution: {integrity: sha512-8gwNZ29+0/3zmXw7ToIHZtg6wK37xnniRUdBt7B27xZxaxfgR5tGMaGHT0t0dLtBV9fXE7zurh0s6Z1DHVjfWg==} engines: {node: '>=14'} - undici-types@7.24.6: - resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} - undici-types@8.3.0: resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} @@ -6860,8 +6816,8 @@ packages: resolution: {integrity: sha512-xh0VO4zRfcbIR2bpH2SwW4kLHzCEKFYg9lykpuEENJ9OIcoc12mGXH5DTuOuJ9po8Y0mGTzFh3JUZIwXJlmOSw==} engines: {node: '>=18'} - verdaccio@6.7.4: - resolution: {integrity: sha512-C59DdKYtc1vayM3HiRFVRCRJMd4Hq4QCQnjTMM6CVanJvMpaqHlZ+DHE31/L8ScXkzUl1oS0HulizpxmMXW1LA==} + verdaccio@6.8.0: + resolution: {integrity: sha512-fGKQZnFQVuLiRYbTDnyOaQDtAs+SgnoK2gQSztM3MIMXMa5MfiRQ+Ub/+8OMFygoroZU21d05d+JJJr4f2TnEQ==} engines: {node: '>=20'} hasBin: true @@ -6878,13 +6834,13 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} - vite@8.0.14: - resolution: {integrity: sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw==} + vite@8.1.4: + resolution: {integrity: sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.1.18 + '@vitejs/devtools': ^0.3.0 esbuild: ^0.27.0 || ^0.28.0 jiti: '>=1.21.0' less: ^4.0.0 @@ -7123,44 +7079,44 @@ snapshots: ansi-styles: 6.2.3 is-fullwidth-code-point: 5.1.0 - '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.207': + '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.209': optional: true - '@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.207': + '@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.209': optional: true - '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.207': + '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.209': optional: true - '@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.207': + '@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.209': optional: true - '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.207': + '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.209': optional: true - '@anthropic-ai/claude-agent-sdk-linux-x64@0.3.207': + '@anthropic-ai/claude-agent-sdk-linux-x64@0.3.209': optional: true - '@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.207': + '@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.209': optional: true - '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.207': + '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.209': optional: true - '@anthropic-ai/claude-agent-sdk@0.3.207(@anthropic-ai/sdk@0.111.0(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(zod@4.4.3)': + '@anthropic-ai/claude-agent-sdk@0.3.209(@anthropic-ai/sdk@0.111.0(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(zod@4.4.3)': dependencies: '@anthropic-ai/sdk': 0.111.0(zod@4.4.3) '@modelcontextprotocol/sdk': 1.29.0(zod@4.4.3) zod: 4.4.3 optionalDependencies: - '@anthropic-ai/claude-agent-sdk-darwin-arm64': 0.3.207 - '@anthropic-ai/claude-agent-sdk-darwin-x64': 0.3.207 - '@anthropic-ai/claude-agent-sdk-linux-arm64': 0.3.207 - '@anthropic-ai/claude-agent-sdk-linux-arm64-musl': 0.3.207 - '@anthropic-ai/claude-agent-sdk-linux-x64': 0.3.207 - '@anthropic-ai/claude-agent-sdk-linux-x64-musl': 0.3.207 - '@anthropic-ai/claude-agent-sdk-win32-arm64': 0.3.207 - '@anthropic-ai/claude-agent-sdk-win32-x64': 0.3.207 + '@anthropic-ai/claude-agent-sdk-darwin-arm64': 0.3.209 + '@anthropic-ai/claude-agent-sdk-darwin-x64': 0.3.209 + '@anthropic-ai/claude-agent-sdk-linux-arm64': 0.3.209 + '@anthropic-ai/claude-agent-sdk-linux-arm64-musl': 0.3.209 + '@anthropic-ai/claude-agent-sdk-linux-x64': 0.3.209 + '@anthropic-ai/claude-agent-sdk-linux-x64-musl': 0.3.209 + '@anthropic-ai/claude-agent-sdk-win32-arm64': 0.3.209 + '@anthropic-ai/claude-agent-sdk-win32-x64': 0.3.209 '@anthropic-ai/sdk@0.111.0(zod@4.4.3)': dependencies: @@ -7209,7 +7165,7 @@ snapshots: dependencies: '@babel/compat-data': 7.29.7 '@babel/helper-validator-option': 7.29.7 - browserslist: 4.28.2 + browserslist: 4.28.6 lru-cache: 5.1.1 semver: 6.3.1 @@ -7334,11 +7290,11 @@ snapshots: - bufferutil - utf-8-validate - '@effect/platform-node@4.0.0-beta.97(effect@4.0.0-beta.97)(ioredis@5.11.0)': + '@effect/platform-node@4.0.0-beta.97(effect@4.0.0-beta.97)(ioredis@5.11.1)': dependencies: '@effect/platform-node-shared': 4.0.0-beta.97(effect@4.0.0-beta.97) effect: 4.0.0-beta.97 - ioredis: 5.11.0 + ioredis: 5.11.1 mime: 4.1.0 undici: 8.7.0 transitivePeerDependencies: @@ -7359,13 +7315,7 @@ snapshots: '@effect/vitest@4.0.0-beta.97(effect@4.0.0-beta.97)(vitest@4.1.10)': dependencies: effect: 4.0.0-beta.97 - vitest: 4.1.10(@types/node@26.1.1)(@vitest/coverage-istanbul@4.1.10)(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) - - '@emnapi/core@1.10.0': - dependencies: - '@emnapi/wasi-threads': 1.2.1 - tslib: 2.8.1 - optional: true + vitest: 4.1.10(@types/node@26.1.1)(@vitest/coverage-istanbul@4.1.10)(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) '@emnapi/core@1.11.0': dependencies: @@ -7390,11 +7340,6 @@ snapshots: '@emnapi/wasi-threads': 1.0.4 tslib: 2.8.1 - '@emnapi/runtime@1.10.0': - dependencies: - tslib: 2.8.1 - optional: true - '@emnapi/runtime@1.11.0': dependencies: tslib: 2.8.1 @@ -7418,11 +7363,6 @@ snapshots: dependencies: tslib: 2.8.1 - '@emnapi/wasi-threads@1.2.1': - dependencies: - tslib: 2.8.1 - optional: true - '@emnapi/wasi-threads@1.2.2': dependencies: tslib: 2.8.1 @@ -7532,9 +7472,9 @@ snapshots: '@fumadocs/tailwind@0.1.1': {} - '@hono/node-server@1.19.14(hono@4.12.21)': + '@hono/node-server@1.19.14(hono@4.12.30)': dependencies: - hono: 4.12.21 + hono: 4.12.30 '@img/colour@1.1.0': optional: true @@ -7690,17 +7630,17 @@ snapshots: '@modelcontextprotocol/sdk@1.29.0(zod@4.4.3)': dependencies: - '@hono/node-server': 1.19.14(hono@4.12.21) - ajv: 8.18.0 - ajv-formats: 3.0.1(ajv@8.18.0) + '@hono/node-server': 1.19.14(hono@4.12.30) + ajv: 8.20.0 + ajv-formats: 3.0.1(ajv@8.20.0) content-type: 1.0.5 cors: 2.8.6 cross-spawn: 7.0.6 eventsource: 3.0.7 - eventsource-parser: 3.0.8 + eventsource-parser: 3.1.0 express: 5.2.1 express-rate-limit: 8.5.2(express@5.2.1) - hono: 4.12.21 + hono: 4.12.30 jose: 6.2.3 json-schema-typed: 8.0.2 pkce-challenge: 5.0.1 @@ -7779,26 +7719,12 @@ snapshots: '@napi-rs/keyring-win32-ia32-msvc': 1.3.0 '@napi-rs/keyring-win32-x64-msvc': 1.3.0 - '@napi-rs/wasm-runtime@0.2.12': - dependencies: - '@emnapi/core': 1.11.2 - '@emnapi/runtime': 1.11.2 - '@tybys/wasm-util': 0.10.3 - optional: true - '@napi-rs/wasm-runtime@0.2.4': dependencies: '@emnapi/core': 1.4.5 '@emnapi/runtime': 1.4.5 '@tybys/wasm-util': 0.9.0 - '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': - dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@tybys/wasm-util': 0.10.3 - optional: true - '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.0)(@emnapi/runtime@1.11.0)': dependencies: '@emnapi/core': 1.11.0 @@ -7813,6 +7739,13 @@ snapshots: '@tybys/wasm-util': 0.10.3 optional: true + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)': + dependencies: + '@emnapi/core': 1.11.2 + '@emnapi/runtime': 1.11.2 + '@tybys/wasm-util': 0.10.3 + optional: true + '@next/env@16.3.0-preview.6': {} '@next/swc-darwin-arm64@16.3.0-preview.6': @@ -7847,67 +7780,6 @@ snapshots: '@noble/hashes@1.8.0': {} - '@node-rs/xxhash-android-arm-eabi@1.7.6': - optional: true - - '@node-rs/xxhash-android-arm64@1.7.6': - optional: true - - '@node-rs/xxhash-darwin-arm64@1.7.6': - optional: true - - '@node-rs/xxhash-darwin-x64@1.7.6': - optional: true - - '@node-rs/xxhash-freebsd-x64@1.7.6': - optional: true - - '@node-rs/xxhash-linux-arm-gnueabihf@1.7.6': - optional: true - - '@node-rs/xxhash-linux-arm64-gnu@1.7.6': - optional: true - - '@node-rs/xxhash-linux-arm64-musl@1.7.6': - optional: true - - '@node-rs/xxhash-linux-x64-gnu@1.7.6': - optional: true - - '@node-rs/xxhash-linux-x64-musl@1.7.6': - optional: true - - '@node-rs/xxhash-wasm32-wasi@1.7.6': - dependencies: - '@napi-rs/wasm-runtime': 0.2.12 - optional: true - - '@node-rs/xxhash-win32-arm64-msvc@1.7.6': - optional: true - - '@node-rs/xxhash-win32-ia32-msvc@1.7.6': - optional: true - - '@node-rs/xxhash-win32-x64-msvc@1.7.6': - optional: true - - '@node-rs/xxhash@1.7.6': - optionalDependencies: - '@node-rs/xxhash-android-arm-eabi': 1.7.6 - '@node-rs/xxhash-android-arm64': 1.7.6 - '@node-rs/xxhash-darwin-arm64': 1.7.6 - '@node-rs/xxhash-darwin-x64': 1.7.6 - '@node-rs/xxhash-freebsd-x64': 1.7.6 - '@node-rs/xxhash-linux-arm-gnueabihf': 1.7.6 - '@node-rs/xxhash-linux-arm64-gnu': 1.7.6 - '@node-rs/xxhash-linux-arm64-musl': 1.7.6 - '@node-rs/xxhash-linux-x64-gnu': 1.7.6 - '@node-rs/xxhash-linux-x64-musl': 1.7.6 - '@node-rs/xxhash-wasm32-wasi': 1.7.6 - '@node-rs/xxhash-win32-arm64-msvc': 1.7.6 - '@node-rs/xxhash-win32-ia32-msvc': 1.7.6 - '@node-rs/xxhash-win32-x64-msvc': 1.7.6 - '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -7920,13 +7792,13 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 - '@nx/devkit@23.1.0(nx@23.1.0(@swc-node/register@1.12.0(@swc/core@1.15.43)(@swc/types@0.1.27)(@typescript/typescript6@6.0.2))(@swc/core@1.15.43))': + '@nx/devkit@23.1.0(nx@23.1.0(@swc-node/register@1.11.1(@swc/core@1.15.43)(@swc/types@0.1.27)(@typescript/typescript6@6.0.2))(@swc/core@1.15.43))': dependencies: '@zkochan/js-yaml': 0.0.7 ejs: 5.0.1 enquirer: 2.3.6 minimatch: 10.2.5 - nx: 23.1.0(@swc-node/register@1.12.0(@swc/core@1.15.43)(@swc/types@0.1.27)(@typescript/typescript6@6.0.2))(@swc/core@1.15.43) + nx: 23.1.0(@swc-node/register@1.11.1(@swc/core@1.15.43)(@swc/types@0.1.27)(@typescript/typescript6@6.0.2))(@swc/core@1.15.43) semver: 7.8.5 tslib: 2.8.1 yaml: 2.9.0 @@ -8088,58 +7960,106 @@ snapshots: '@oxc-parser/binding-win32-x64-msvc@0.137.0': optional: true - '@oxc-project/types@0.132.0': {} - '@oxc-project/types@0.137.0': {} + '@oxc-project/types@0.139.0': {} + '@oxc-resolver/binding-android-arm-eabi@11.21.3': optional: true + '@oxc-resolver/binding-android-arm-eabi@11.24.2': + optional: true + '@oxc-resolver/binding-android-arm64@11.21.3': optional: true + '@oxc-resolver/binding-android-arm64@11.24.2': + optional: true + '@oxc-resolver/binding-darwin-arm64@11.21.3': optional: true + '@oxc-resolver/binding-darwin-arm64@11.24.2': + optional: true + '@oxc-resolver/binding-darwin-x64@11.21.3': optional: true + '@oxc-resolver/binding-darwin-x64@11.24.2': + optional: true + '@oxc-resolver/binding-freebsd-x64@11.21.3': optional: true + '@oxc-resolver/binding-freebsd-x64@11.24.2': + optional: true + '@oxc-resolver/binding-linux-arm-gnueabihf@11.21.3': optional: true + '@oxc-resolver/binding-linux-arm-gnueabihf@11.24.2': + optional: true + '@oxc-resolver/binding-linux-arm-musleabihf@11.21.3': optional: true + '@oxc-resolver/binding-linux-arm-musleabihf@11.24.2': + optional: true + '@oxc-resolver/binding-linux-arm64-gnu@11.21.3': optional: true + '@oxc-resolver/binding-linux-arm64-gnu@11.24.2': + optional: true + '@oxc-resolver/binding-linux-arm64-musl@11.21.3': optional: true + '@oxc-resolver/binding-linux-arm64-musl@11.24.2': + optional: true + '@oxc-resolver/binding-linux-ppc64-gnu@11.21.3': optional: true + '@oxc-resolver/binding-linux-ppc64-gnu@11.24.2': + optional: true + '@oxc-resolver/binding-linux-riscv64-gnu@11.21.3': optional: true + '@oxc-resolver/binding-linux-riscv64-gnu@11.24.2': + optional: true + '@oxc-resolver/binding-linux-riscv64-musl@11.21.3': optional: true + '@oxc-resolver/binding-linux-riscv64-musl@11.24.2': + optional: true + '@oxc-resolver/binding-linux-s390x-gnu@11.21.3': optional: true + '@oxc-resolver/binding-linux-s390x-gnu@11.24.2': + optional: true + '@oxc-resolver/binding-linux-x64-gnu@11.21.3': optional: true + '@oxc-resolver/binding-linux-x64-gnu@11.24.2': + optional: true + '@oxc-resolver/binding-linux-x64-musl@11.21.3': optional: true + '@oxc-resolver/binding-linux-x64-musl@11.24.2': + optional: true + '@oxc-resolver/binding-openharmony-arm64@11.21.3': optional: true + '@oxc-resolver/binding-openharmony-arm64@11.24.2': + optional: true + '@oxc-resolver/binding-wasm32-wasi@11.21.3': dependencies: '@emnapi/core': 1.11.0 @@ -8147,12 +8067,25 @@ snapshots: '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.0)(@emnapi/runtime@1.11.0) optional: true + '@oxc-resolver/binding-wasm32-wasi@11.24.2': + dependencies: + '@emnapi/core': 1.11.2 + '@emnapi/runtime': 1.11.2 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2) + optional: true + '@oxc-resolver/binding-win32-arm64-msvc@11.21.3': optional: true + '@oxc-resolver/binding-win32-arm64-msvc@11.24.2': + optional: true + '@oxc-resolver/binding-win32-x64-msvc@11.21.3': optional: true + '@oxc-resolver/binding-win32-x64-msvc@11.24.2': + optional: true + '@oxfmt/binding-android-arm-eabi@0.58.0': optional: true @@ -8228,61 +8161,61 @@ snapshots: '@oxlint-tsgolint/win32-x64@0.24.0': optional: true - '@oxlint/binding-android-arm-eabi@1.73.0': + '@oxlint/binding-android-arm-eabi@1.74.0': optional: true - '@oxlint/binding-android-arm64@1.73.0': + '@oxlint/binding-android-arm64@1.74.0': optional: true - '@oxlint/binding-darwin-arm64@1.73.0': + '@oxlint/binding-darwin-arm64@1.74.0': optional: true - '@oxlint/binding-darwin-x64@1.73.0': + '@oxlint/binding-darwin-x64@1.74.0': optional: true - '@oxlint/binding-freebsd-x64@1.73.0': + '@oxlint/binding-freebsd-x64@1.74.0': optional: true - '@oxlint/binding-linux-arm-gnueabihf@1.73.0': + '@oxlint/binding-linux-arm-gnueabihf@1.74.0': optional: true - '@oxlint/binding-linux-arm-musleabihf@1.73.0': + '@oxlint/binding-linux-arm-musleabihf@1.74.0': optional: true - '@oxlint/binding-linux-arm64-gnu@1.73.0': + '@oxlint/binding-linux-arm64-gnu@1.74.0': optional: true - '@oxlint/binding-linux-arm64-musl@1.73.0': + '@oxlint/binding-linux-arm64-musl@1.74.0': optional: true - '@oxlint/binding-linux-ppc64-gnu@1.73.0': + '@oxlint/binding-linux-ppc64-gnu@1.74.0': optional: true - '@oxlint/binding-linux-riscv64-gnu@1.73.0': + '@oxlint/binding-linux-riscv64-gnu@1.74.0': optional: true - '@oxlint/binding-linux-riscv64-musl@1.73.0': + '@oxlint/binding-linux-riscv64-musl@1.74.0': optional: true - '@oxlint/binding-linux-s390x-gnu@1.73.0': + '@oxlint/binding-linux-s390x-gnu@1.74.0': optional: true - '@oxlint/binding-linux-x64-gnu@1.73.0': + '@oxlint/binding-linux-x64-gnu@1.74.0': optional: true - '@oxlint/binding-linux-x64-musl@1.73.0': + '@oxlint/binding-linux-x64-musl@1.74.0': optional: true - '@oxlint/binding-openharmony-arm64@1.73.0': + '@oxlint/binding-openharmony-arm64@1.74.0': optional: true - '@oxlint/binding-win32-arm64-msvc@1.73.0': + '@oxlint/binding-win32-arm64-msvc@1.74.0': optional: true - '@oxlint/binding-win32-ia32-msvc@1.73.0': + '@oxlint/binding-win32-ia32-msvc@1.74.0': optional: true - '@oxlint/binding-win32-x64-msvc@1.73.0': + '@oxlint/binding-win32-x64-msvc@1.74.0': optional: true '@parcel/watcher-android-arm64@2.5.6': @@ -8329,7 +8262,7 @@ snapshots: detect-libc: 2.1.2 is-glob: 4.0.3 node-addon-api: 7.1.1 - picomatch: 4.0.4 + picomatch: 4.0.5 optionalDependencies: '@parcel/watcher-android-arm64': 2.5.6 '@parcel/watcher-darwin-arm64': 2.5.6 @@ -8359,11 +8292,11 @@ snapshots: '@pnpm/network.ca-file': 1.0.2 config-chain: 1.1.13 - '@posthog/core@1.43.1': + '@posthog/core@1.40.2': dependencies: - '@posthog/types': 1.397.0 + '@posthog/types': 1.393.0 - '@posthog/types@1.397.0': {} + '@posthog/types@1.393.0': {} '@radix-ui/number@1.1.2': {} @@ -8714,60 +8647,60 @@ snapshots: '@radix-ui/rect@1.1.2': {} - '@rolldown/binding-android-arm64@1.0.2': + '@rolldown/binding-android-arm64@1.1.5': optional: true - '@rolldown/binding-darwin-arm64@1.0.2': + '@rolldown/binding-darwin-arm64@1.1.5': optional: true - '@rolldown/binding-darwin-x64@1.0.2': + '@rolldown/binding-darwin-x64@1.1.5': optional: true - '@rolldown/binding-freebsd-x64@1.0.2': + '@rolldown/binding-freebsd-x64@1.1.5': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.2': + '@rolldown/binding-linux-arm-gnueabihf@1.1.5': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.2': + '@rolldown/binding-linux-arm64-gnu@1.1.5': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.2': + '@rolldown/binding-linux-arm64-musl@1.1.5': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.0.2': + '@rolldown/binding-linux-ppc64-gnu@1.1.5': optional: true - '@rolldown/binding-linux-s390x-gnu@1.0.2': + '@rolldown/binding-linux-s390x-gnu@1.1.5': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.2': + '@rolldown/binding-linux-x64-gnu@1.1.5': optional: true - '@rolldown/binding-linux-x64-musl@1.0.2': + '@rolldown/binding-linux-x64-musl@1.1.5': optional: true - '@rolldown/binding-openharmony-arm64@1.0.2': + '@rolldown/binding-openharmony-arm64@1.1.5': optional: true - '@rolldown/binding-wasm32-wasi@1.0.2': + '@rolldown/binding-wasm32-wasi@1.1.5': dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.2': + '@rolldown/binding-win32-arm64-msvc@1.1.5': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.2': + '@rolldown/binding-win32-x64-msvc@1.1.5': optional: true '@rolldown/pluginutils@1.0.1': {} '@sec-ant/readable-stream@0.4.1': {} - '@semantic-release/commit-analyzer@13.0.1(semantic-release@25.0.6(typescript@7.0.2))': + '@semantic-release/commit-analyzer@13.0.1(semantic-release@25.0.7(typescript@7.0.2))': dependencies: conventional-changelog-angular: 8.3.1 conventional-changelog-writer: 8.4.0 @@ -8777,13 +8710,13 @@ snapshots: import-from-esm: 2.0.0 lodash-es: 4.18.1 micromatch: 4.0.8 - semantic-release: 25.0.6(typescript@7.0.2) + semantic-release: 25.0.7(typescript@7.0.2) transitivePeerDependencies: - supports-color '@semantic-release/error@4.0.0': {} - '@semantic-release/github@12.0.9(semantic-release@25.0.6(typescript@7.0.2))': + '@semantic-release/github@12.0.9(semantic-release@25.0.7(typescript@7.0.2))': dependencies: '@octokit/core': 7.0.6 '@octokit/plugin-paginate-rest': 14.0.0(@octokit/core@7.0.6) @@ -8799,7 +8732,7 @@ snapshots: lodash-es: 4.18.1 mime: 4.1.0 p-filter: 4.1.0 - semantic-release: 25.0.6(typescript@7.0.2) + semantic-release: 25.0.7(typescript@7.0.2) tinyglobby: 0.2.17 undici: 7.28.0 url-join: 5.0.0 @@ -8807,7 +8740,7 @@ snapshots: - kerberos - supports-color - '@semantic-release/npm@13.1.5(semantic-release@25.0.6(typescript@7.0.2))': + '@semantic-release/npm@13.1.5(semantic-release@25.0.7(typescript@7.0.2))': dependencies: '@actions/core': 3.0.1 '@semantic-release/error': 4.0.0 @@ -8822,11 +8755,11 @@ snapshots: rc: 1.2.8 read-pkg: 10.1.0 registry-auth-token: 5.1.1 - semantic-release: 25.0.6(typescript@7.0.2) + semantic-release: 25.0.7(typescript@7.0.2) semver: 7.8.5 tempy: 3.2.0 - '@semantic-release/release-notes-generator@14.1.1(semantic-release@25.0.6(typescript@7.0.2))': + '@semantic-release/release-notes-generator@14.1.1(semantic-release@25.0.7(typescript@7.0.2))': dependencies: conventional-changelog-angular: 8.3.1 conventional-changelog-writer: 8.4.0 @@ -8836,7 +8769,7 @@ snapshots: import-from-esm: 2.0.0 lodash-es: 4.18.1 read-package-up: 11.0.0 - semantic-release: 25.0.6(typescript@7.0.2) + semantic-release: 25.0.7(typescript@7.0.2) transitivePeerDependencies: - supports-color @@ -8890,53 +8823,51 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@supabase/auth-js@2.110.2': + '@supabase/auth-js@2.110.5': dependencies: tslib: 2.8.1 - '@supabase/functions-js@2.110.2': + '@supabase/functions-js@2.110.5': dependencies: tslib: 2.8.1 '@supabase/phoenix@0.4.4': {} - '@supabase/postgrest-js@2.110.2': + '@supabase/postgrest-js@2.110.5': dependencies: tslib: 2.8.1 - '@supabase/realtime-js@2.110.2': + '@supabase/realtime-js@2.110.5': dependencies: '@supabase/phoenix': 0.4.4 tslib: 2.8.1 - '@supabase/storage-js@2.110.2': + '@supabase/storage-js@2.110.5': dependencies: iceberg-js: 0.8.1 tslib: 2.8.1 - '@supabase/supabase-js@2.110.2': + '@supabase/supabase-js@2.110.5': dependencies: - '@supabase/auth-js': 2.110.2 - '@supabase/functions-js': 2.110.2 - '@supabase/postgrest-js': 2.110.2 - '@supabase/realtime-js': 2.110.2 - '@supabase/storage-js': 2.110.2 + '@supabase/auth-js': 2.110.5 + '@supabase/functions-js': 2.110.5 + '@supabase/postgrest-js': 2.110.5 + '@supabase/realtime-js': 2.110.5 + '@supabase/storage-js': 2.110.5 - '@swc-node/core@1.15.0(@swc/core@1.15.43)(@swc/types@0.1.27)': + '@swc-node/core@1.14.1(@swc/core@1.15.43)(@swc/types@0.1.27)': dependencies: '@swc/core': 1.15.43 '@swc/types': 0.1.27 - '@swc-node/register@1.12.0(@swc/core@1.15.43)(@swc/types@0.1.27)(@typescript/typescript6@6.0.2)': + '@swc-node/register@1.11.1(@swc/core@1.15.43)(@swc/types@0.1.27)(@typescript/typescript6@6.0.2)': dependencies: - '@node-rs/xxhash': 1.7.6 - '@swc-node/core': 1.15.0(@swc/core@1.15.43)(@swc/types@0.1.27) + '@swc-node/core': 1.14.1(@swc/core@1.15.43)(@swc/types@0.1.27) '@swc-node/sourcemap-support': 0.6.1 '@swc/core': 1.15.43 colorette: 2.0.20 debug: 4.4.3(supports-color@7.2.0) - json-stable-stringify: 1.3.0 - oxc-resolver: 11.21.3 + oxc-resolver: 11.24.2 pirates: 4.0.7 tslib: 2.8.1 typescript: '@typescript/typescript6@6.0.2' @@ -9061,10 +8992,6 @@ snapshots: '@types/ms@2.1.0': {} - '@types/node@25.9.3': - dependencies: - undici-types: 7.24.6 - '@types/node@26.1.1': dependencies: undici-types: 8.3.0 @@ -9073,13 +9000,13 @@ snapshots: '@types/pg-copy-streams@1.2.5': dependencies: - '@types/node': 25.9.3 + '@types/node': 26.1.1 '@types/pg': 8.20.0 '@types/pg@8.20.0': dependencies: - '@types/node': 25.9.3 - pg-protocol: 1.14.0 + '@types/node': 26.1.1 + pg-protocol: 1.15.0 pg-types: 2.2.0 '@types/react-dom@19.2.3(@types/react@19.2.17)': @@ -9235,7 +9162,7 @@ snapshots: dependencies: lockfile: 1.0.4 - '@verdaccio/hooks@8.0.3': + '@verdaccio/hooks@8.0.4': dependencies: '@verdaccio/core': 8.1.2 '@verdaccio/logger': 8.0.3 @@ -9343,7 +9270,7 @@ snapshots: - react-native-b4a - supports-color - '@verdaccio/ui-theme@9.0.0-next-9.20': + '@verdaccio/ui-theme@9.0.0-next-9.21': dependencies: debug: 4.4.3(supports-color@7.2.0) transitivePeerDependencies: @@ -9373,9 +9300,9 @@ snapshots: istanbul-lib-report: 3.0.1 istanbul-reports: 3.2.0 magicast: 0.5.3 - obug: 2.1.1 + obug: 2.1.3 tinyrainbow: 3.1.0 - vitest: 4.1.10(@types/node@26.1.1)(@vitest/coverage-istanbul@4.1.10)(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) + vitest: 4.1.10(@types/node@26.1.1)(@vitest/coverage-istanbul@4.1.10)(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) transitivePeerDependencies: - supports-color @@ -9388,13 +9315,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.10(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0))': + '@vitest/mocker@4.1.10(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0))': dependencies: '@vitest/spy': 4.1.10 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0) + vite: 8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0) '@vitest/pretty-format@4.1.10': dependencies: @@ -9464,14 +9391,21 @@ snapshots: clean-stack: 5.3.0 indent-string: 5.0.0 - ajv-formats@3.0.1(ajv@8.18.0): + ajv-formats@3.0.1(ajv@8.20.0): optionalDependencies: - ajv: 8.18.0 + ajv: 8.20.0 ajv@8.18.0: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 3.1.2 + fast-uri: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.3 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 @@ -9557,7 +9491,7 @@ snapshots: base64-js@1.5.1: {} - baseline-browser-mapping@2.10.42: {} + baseline-browser-mapping@2.10.43: {} bcrypt-pbkdf@1.0.2: dependencies: @@ -9573,7 +9507,7 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 - body-parser@1.20.5: + body-parser@1.20.6: dependencies: bytes: 3.1.2 content-type: 1.0.5 @@ -9590,15 +9524,15 @@ snapshots: transitivePeerDependencies: - supports-color - body-parser@2.2.2: + body-parser@2.3.0: dependencies: bytes: 3.1.2 - content-type: 1.0.5 + content-type: 2.0.0 debug: 4.4.3(supports-color@7.2.0) http-errors: 2.0.1 - iconv-lite: 0.7.2 + iconv-lite: 0.7.3 on-finished: 2.4.1 - qs: 6.14.2 + qs: 6.15.3 raw-body: 3.0.2 type-is: 2.1.0 transitivePeerDependencies: @@ -9622,13 +9556,13 @@ snapshots: dependencies: pako: 0.2.9 - browserslist@4.28.2: + browserslist@4.28.6: dependencies: - baseline-browser-mapping: 2.10.42 - caniuse-lite: 1.0.30001803 - electron-to-chromium: 1.5.363 - node-releases: 2.0.46 - update-browserslist-db: 1.2.3(browserslist@4.28.2) + baseline-browser-mapping: 2.10.43 + caniuse-lite: 1.0.30001805 + electron-to-chromium: 1.5.389 + node-releases: 2.0.51 + update-browserslist-db: 1.2.3(browserslist@4.28.6) buffer-equal-constant-time@1.0.1: {} @@ -9667,13 +9601,6 @@ snapshots: es-errors: 1.3.0 function-bind: 1.1.2 - call-bind@1.0.9: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-define-property: 1.0.1 - get-intrinsic: 1.3.0 - set-function-length: 1.2.2 - call-bound@1.0.4: dependencies: call-bind-apply-helpers: 1.0.2 @@ -9681,7 +9608,7 @@ snapshots: callsites@3.1.0: {} - caniuse-lite@1.0.30001803: {} + caniuse-lite@1.0.30001805: {} caseless@0.12.0: {} @@ -9753,10 +9680,10 @@ snapshots: optionalDependencies: '@colors/colors': 1.5.0 - cli-truncate@6.1.0: + cli-truncate@6.1.1: dependencies: slice-ansi: 9.0.0 - string-width: 8.2.1 + string-width: 8.2.2 client-only@0.0.1: {} @@ -9953,12 +9880,6 @@ snapshots: defer-to-connect@2.0.1: {} - define-data-property@1.1.4: - dependencies: - es-define-property: 1.0.1 - es-errors: 1.3.0 - gopd: 1.2.0 - define-lazy-prop@2.0.0: {} delayed-stream@1.0.0: {} @@ -10045,7 +9966,7 @@ snapshots: ejs@5.0.1: {} - electron-to-chromium@1.5.363: {} + electron-to-chromium@1.5.389: {} emoji-regex@10.6.0: {} @@ -10084,7 +10005,7 @@ snapshots: es-errors@1.3.0: {} - es-module-lexer@2.1.0: {} + es-module-lexer@2.3.1: {} es-object-atoms@1.1.1: dependencies: @@ -10201,11 +10122,11 @@ snapshots: events@3.3.0: {} - eventsource-parser@3.0.8: {} + eventsource-parser@3.1.0: {} eventsource@3.0.7: dependencies: - eventsource-parser: 3.0.8 + eventsource-parser: 3.1.0 execa@8.0.1: dependencies: @@ -10234,7 +10155,7 @@ snapshots: strip-final-newline: 4.0.0 yoctocolors: 2.1.2 - expect-type@1.3.0: {} + expect-type@1.4.0: {} express-rate-limit@5.5.1: {} @@ -10247,7 +10168,7 @@ snapshots: dependencies: accepts: 1.3.8 array-flatten: 1.1.1 - body-parser: 1.20.5 + body-parser: 1.20.6 content-disposition: 0.5.4 content-type: 1.0.5 cookie: 0.7.2 @@ -10283,7 +10204,7 @@ snapshots: dependencies: accepts: 1.3.8 array-flatten: 1.1.1 - body-parser: 1.20.5 + body-parser: 1.20.6 content-disposition: 0.5.4 content-type: 1.0.5 cookie: 0.7.2 @@ -10318,7 +10239,7 @@ snapshots: express@5.2.1: dependencies: accepts: 2.0.0 - body-parser: 2.2.2 + body-parser: 2.3.0 content-disposition: 1.1.0 content-type: 1.0.5 cookie: 0.7.2 @@ -10337,8 +10258,8 @@ snapshots: once: 1.4.0 parseurl: 1.3.3 proxy-addr: 2.0.7 - qs: 6.14.2 - range-parser: 1.2.1 + qs: 6.15.3 + range-parser: 1.3.0 router: 2.2.0 send: 1.2.1 serve-static: 2.2.1 @@ -10354,7 +10275,7 @@ snapshots: fast-check@4.9.0: dependencies: - pure-rand: 8.4.0 + pure-rand: 8.4.2 fast-deep-equal@3.1.3: {} @@ -10376,7 +10297,7 @@ snapshots: dependencies: fast-string-truncated-width: 3.0.3 - fast-uri@3.1.2: {} + fast-uri@3.1.3: {} fast-wrap-ansi@0.2.2: dependencies: @@ -10494,14 +10415,13 @@ snapshots: fsevents@2.3.3: optional: true - fumadocs-core@16.11.3(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.25.0(react@19.2.7))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3): + fumadocs-core@16.11.4(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.24.0(react@19.2.7))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3): dependencies: '@orama/orama': 3.1.18 estree-util-value-to-estree: 3.5.0 github-slugger: 2.0.0 hast-util-to-estree: 3.1.3 hast-util-to-jsx-runtime: 2.3.6 - js-yaml: 5.2.1 mdast-util-mdx: 3.0.0 mdast-util-to-markdown: 2.1.2 remark: 15.0.1 @@ -10513,13 +10433,14 @@ snapshots: unified: 11.0.5 unist-util-visit: 5.1.0 vfile: 6.0.3 + yaml: 2.9.0 optionalDependencies: '@mdx-js/mdx': 3.1.1 '@types/estree-jsx': 1.0.5 '@types/hast': 3.0.5 '@types/mdast': 4.0.4 '@types/react': 19.2.17 - lucide-react: 1.25.0(react@19.2.7) + lucide-react: 1.24.0(react@19.2.7) next: 16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) @@ -10527,16 +10448,15 @@ snapshots: transitivePeerDependencies: - supports-color - fumadocs-mdx@15.1.0(@types/mdast@4.0.4)(@types/mdx@2.0.14)(@types/react@19.2.17)(fumadocs-core@16.11.3(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.25.0(react@19.2.7))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(rolldown@1.0.2)(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)): + fumadocs-mdx@15.1.1(@types/mdast@4.0.4)(@types/mdx@2.0.14)(@types/react@19.2.17)(fumadocs-core@16.11.4(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.24.0(react@19.2.7))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(rolldown@1.1.5)(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)): dependencies: '@mdx-js/mdx': 3.1.1 '@standard-schema/spec': 1.1.0 chokidar: 5.0.0 esbuild: 0.28.1 estree-util-value-to-estree: 3.5.0 - fumadocs-core: 16.11.3(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.25.0(react@19.2.7))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3) + fumadocs-core: 16.11.4(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.24.0(react@19.2.7))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3) github-slugger: 2.0.0 - js-yaml: 5.2.1 mdast-util-mdx: 3.0.0 picocolors: 1.1.1 picomatch: 4.0.5 @@ -10546,6 +10466,7 @@ snapshots: unist-util-remove-position: 5.0.0 unist-util-visit: 5.1.0 vfile: 6.0.3 + yaml: 2.9.0 zod: 4.4.3 optionalDependencies: '@types/mdast': 4.0.4 @@ -10553,12 +10474,12 @@ snapshots: '@types/react': 19.2.17 next: 16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react: 19.2.7 - rolldown: 1.0.2 - vite: 8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0) + rolldown: 1.1.5 + vite: 8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0) transitivePeerDependencies: - supports-color - fumadocs-ui@16.11.3(@types/mdx@2.0.14)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(fumadocs-core@16.11.3(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.25.0(react@19.2.7))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + fumadocs-ui@16.11.4(@types/mdx@2.0.14)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(fumadocs-core@16.11.4(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.24.0(react@19.2.7))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7): dependencies: '@fuma-translate/react': 1.0.2(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@fumadocs/tailwind': 0.1.1 @@ -10574,8 +10495,8 @@ snapshots: '@radix-ui/react-tabs': 1.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) class-variance-authority: 0.7.1 cnfast: 0.0.8 - fumadocs-core: 16.11.3(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.25.0(react@19.2.7))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3) - lucide-react: 1.25.0(react@19.2.7) + fumadocs-core: 16.11.4(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.24.0(react@19.2.7))(next@16.3.0-preview.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3) + lucide-react: 1.24.0(react@19.2.7) motion: 12.42.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) next-themes: 0.4.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react: 19.2.7 @@ -10714,10 +10635,6 @@ snapshots: has-flag@4.0.0: {} - has-property-descriptors@1.0.2: - dependencies: - es-define-property: 1.0.1 - has-symbols@1.1.0: {} has-tostringtag@1.0.2: @@ -10838,7 +10755,7 @@ snapshots: highlight.js@10.7.3: {} - hono@4.12.21: {} + hono@4.12.30: {} hook-std@4.0.0: {} @@ -10912,7 +10829,7 @@ snapshots: dependencies: safer-buffer: 2.1.2 - iconv-lite@0.7.2: + iconv-lite@0.7.3: dependencies: safer-buffer: 2.1.2 @@ -10961,7 +10878,7 @@ snapshots: chalk: 5.6.2 cli-boxes: 4.0.1 cli-cursor: 4.0.0 - cli-truncate: 6.1.0 + cli-truncate: 6.1.1 code-excerpt: 4.0.0 es-toolkit: 1.49.0 indent-string: 5.0.0 @@ -10973,9 +10890,9 @@ snapshots: signal-exit: 3.0.7 slice-ansi: 9.0.0 stack-utils: 2.0.6 - string-width: 8.2.1 + string-width: 8.2.2 terminal-size: 4.0.1 - type-fest: 5.7.0 + type-fest: 5.8.0 widest-line: 6.0.0 wrap-ansi: 10.0.0 ws: 8.21.0 @@ -10989,7 +10906,7 @@ snapshots: inline-style-parser@0.2.7: {} - ioredis@5.11.0: + ioredis@5.11.1: dependencies: '@ioredis/commands': 1.10.0 cluster-key-slot: 1.1.1 @@ -11066,8 +10983,6 @@ snapshots: isarray@1.0.0: {} - isarray@2.0.5: {} - isexe@2.0.0: {} isstream@0.1.2: {} @@ -11109,10 +11024,6 @@ snapshots: dependencies: argparse: 2.0.1 - js-yaml@5.2.1: - dependencies: - argparse: 2.0.1 - jsbn@0.1.1: {} jsesc@3.1.0: {} @@ -11134,14 +11045,6 @@ snapshots: json-schema@0.4.0: {} - json-stable-stringify@1.3.0: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - isarray: 2.0.5 - jsonify: 0.0.1 - object-keys: 1.1.1 - json-stringify-safe@5.0.1: {} json-with-bigint@3.5.10: {} @@ -11156,8 +11059,6 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 - jsonify@0.0.1: {} - jsonparse@1.3.1: {} jsonwebtoken@9.0.3: @@ -11171,7 +11072,7 @@ snapshots: lodash.isstring: 4.0.1 lodash.once: 4.1.1 ms: 2.1.3 - semver: 7.8.2 + semver: 7.8.5 jsprim@2.0.2: dependencies: @@ -11195,7 +11096,7 @@ snapshots: dependencies: json-buffer: 3.0.1 - knip@6.27.0: + knip@6.26.0: dependencies: fdir: 6.5.0(picomatch@4.0.5) formatly: 0.3.0 @@ -11333,7 +11234,7 @@ snapshots: lru-cache@7.18.3: {} - lucide-react@1.25.0(react@19.2.7): + lucide-react@1.24.0(react@19.2.7): dependencies: react: 19.2.7 @@ -11925,8 +11826,8 @@ snapshots: dependencies: '@next/env': 16.3.0-preview.6 '@swc/helpers': 0.5.15 - baseline-browser-mapping: 2.10.42 - caniuse-lite: 1.0.30001803 + baseline-browser-mapping: 2.10.43 + caniuse-lite: 1.0.30001805 postcss: 8.5.10 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) @@ -11963,7 +11864,7 @@ snapshots: detect-libc: 2.1.2 optional: true - node-releases@2.0.46: {} + node-releases@2.0.51: {} normalize-package-data@6.0.2: dependencies: @@ -11996,7 +11897,7 @@ snapshots: npm@11.18.0: {} - nx@23.1.0(@swc-node/register@1.12.0(@swc/core@1.15.43)(@swc/types@0.1.27)(@typescript/typescript6@6.0.2))(@swc/core@1.15.43): + nx@23.1.0(@swc-node/register@1.11.1(@swc/core@1.15.43)(@swc/types@0.1.27)(@typescript/typescript6@6.0.2))(@swc/core@1.15.43): dependencies: '@emnapi/core': 1.4.5 '@emnapi/runtime': 1.4.5 @@ -12124,18 +12025,16 @@ snapshots: '@nx/nx-linux-x64-musl': 23.1.0 '@nx/nx-win32-arm64-msvc': 23.1.0 '@nx/nx-win32-x64-msvc': 23.1.0 - '@swc-node/register': 1.12.0(@swc/core@1.15.43)(@swc/types@0.1.27)(@typescript/typescript6@6.0.2) + '@swc-node/register': 1.11.1(@swc/core@1.15.43)(@swc/types@0.1.27)(@typescript/typescript6@6.0.2) '@swc/core': 1.15.43 object-assign@4.1.1: {} object-inspect@1.13.4: {} - object-keys@1.1.1: {} - obuf@1.1.2: {} - obug@2.1.1: {} + obug@2.1.3: {} on-exit-leak-free@2.1.2: {} @@ -12230,6 +12129,28 @@ snapshots: '@oxc-resolver/binding-win32-arm64-msvc': 11.21.3 '@oxc-resolver/binding-win32-x64-msvc': 11.21.3 + oxc-resolver@11.24.2: + optionalDependencies: + '@oxc-resolver/binding-android-arm-eabi': 11.24.2 + '@oxc-resolver/binding-android-arm64': 11.24.2 + '@oxc-resolver/binding-darwin-arm64': 11.24.2 + '@oxc-resolver/binding-darwin-x64': 11.24.2 + '@oxc-resolver/binding-freebsd-x64': 11.24.2 + '@oxc-resolver/binding-linux-arm-gnueabihf': 11.24.2 + '@oxc-resolver/binding-linux-arm-musleabihf': 11.24.2 + '@oxc-resolver/binding-linux-arm64-gnu': 11.24.2 + '@oxc-resolver/binding-linux-arm64-musl': 11.24.2 + '@oxc-resolver/binding-linux-ppc64-gnu': 11.24.2 + '@oxc-resolver/binding-linux-riscv64-gnu': 11.24.2 + '@oxc-resolver/binding-linux-riscv64-musl': 11.24.2 + '@oxc-resolver/binding-linux-s390x-gnu': 11.24.2 + '@oxc-resolver/binding-linux-x64-gnu': 11.24.2 + '@oxc-resolver/binding-linux-x64-musl': 11.24.2 + '@oxc-resolver/binding-openharmony-arm64': 11.24.2 + '@oxc-resolver/binding-wasm32-wasi': 11.24.2 + '@oxc-resolver/binding-win32-arm64-msvc': 11.24.2 + '@oxc-resolver/binding-win32-x64-msvc': 11.24.2 + oxfmt@0.58.0: dependencies: tinypool: 2.1.0 @@ -12263,27 +12184,27 @@ snapshots: '@oxlint-tsgolint/win32-arm64': 0.24.0 '@oxlint-tsgolint/win32-x64': 0.24.0 - oxlint@1.73.0(oxlint-tsgolint@0.24.0): + oxlint@1.74.0(oxlint-tsgolint@0.24.0): optionalDependencies: - '@oxlint/binding-android-arm-eabi': 1.73.0 - '@oxlint/binding-android-arm64': 1.73.0 - '@oxlint/binding-darwin-arm64': 1.73.0 - '@oxlint/binding-darwin-x64': 1.73.0 - '@oxlint/binding-freebsd-x64': 1.73.0 - '@oxlint/binding-linux-arm-gnueabihf': 1.73.0 - '@oxlint/binding-linux-arm-musleabihf': 1.73.0 - '@oxlint/binding-linux-arm64-gnu': 1.73.0 - '@oxlint/binding-linux-arm64-musl': 1.73.0 - '@oxlint/binding-linux-ppc64-gnu': 1.73.0 - '@oxlint/binding-linux-riscv64-gnu': 1.73.0 - '@oxlint/binding-linux-riscv64-musl': 1.73.0 - '@oxlint/binding-linux-s390x-gnu': 1.73.0 - '@oxlint/binding-linux-x64-gnu': 1.73.0 - '@oxlint/binding-linux-x64-musl': 1.73.0 - '@oxlint/binding-openharmony-arm64': 1.73.0 - '@oxlint/binding-win32-arm64-msvc': 1.73.0 - '@oxlint/binding-win32-ia32-msvc': 1.73.0 - '@oxlint/binding-win32-x64-msvc': 1.73.0 + '@oxlint/binding-android-arm-eabi': 1.74.0 + '@oxlint/binding-android-arm64': 1.74.0 + '@oxlint/binding-darwin-arm64': 1.74.0 + '@oxlint/binding-darwin-x64': 1.74.0 + '@oxlint/binding-freebsd-x64': 1.74.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.74.0 + '@oxlint/binding-linux-arm-musleabihf': 1.74.0 + '@oxlint/binding-linux-arm64-gnu': 1.74.0 + '@oxlint/binding-linux-arm64-musl': 1.74.0 + '@oxlint/binding-linux-ppc64-gnu': 1.74.0 + '@oxlint/binding-linux-riscv64-gnu': 1.74.0 + '@oxlint/binding-linux-riscv64-musl': 1.74.0 + '@oxlint/binding-linux-s390x-gnu': 1.74.0 + '@oxlint/binding-linux-x64-gnu': 1.74.0 + '@oxlint/binding-linux-x64-musl': 1.74.0 + '@oxlint/binding-openharmony-arm64': 1.74.0 + '@oxlint/binding-win32-arm64-msvc': 1.74.0 + '@oxlint/binding-win32-ia32-msvc': 1.74.0 + '@oxlint/binding-win32-x64-msvc': 1.74.0 oxlint-tsgolint: 0.24.0 p-cancelable@2.1.1: {} @@ -12407,8 +12328,6 @@ snapshots: dependencies: pg: 8.22.0 - pg-protocol@1.14.0: {} - pg-protocol@1.15.0: {} pg-types@2.2.0: @@ -12447,8 +12366,6 @@ snapshots: picomatch@2.3.2: {} - picomatch@4.0.4: {} - picomatch@4.0.5: {} pify@3.0.0: {} @@ -12525,7 +12442,7 @@ snapshots: posthog-node@5.41.0: dependencies: - '@posthog/core': 1.43.1 + '@posthog/core': 1.40.2 pretty-ms@9.3.0: dependencies: @@ -12568,7 +12485,7 @@ snapshots: inherits: 2.0.4 pump: 2.0.1 - pure-rand@8.4.0: {} + pure-rand@8.4.2: {} qs@6.14.2: dependencies: @@ -12587,6 +12504,8 @@ snapshots: range-parser@1.2.1: {} + range-parser@1.3.0: {} + raw-body@2.5.3: dependencies: bytes: 3.1.2 @@ -12598,7 +12517,7 @@ snapshots: dependencies: bytes: 3.1.2 http-errors: 2.0.1 - iconv-lite: 0.7.2 + iconv-lite: 0.7.3 unpipe: 1.0.0 rc@1.2.8: @@ -12610,7 +12529,7 @@ snapshots: react-devtools-core@7.0.1: dependencies: - shell-quote: 1.8.4 + shell-quote: 1.10.0 ws: 7.5.11 transitivePeerDependencies: - bufferutil @@ -12854,26 +12773,26 @@ snapshots: reusify@1.1.0: {} - rolldown@1.0.2: + rolldown@1.1.5: dependencies: - '@oxc-project/types': 0.132.0 + '@oxc-project/types': 0.139.0 '@rolldown/pluginutils': 1.0.1 optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.2 - '@rolldown/binding-darwin-arm64': 1.0.2 - '@rolldown/binding-darwin-x64': 1.0.2 - '@rolldown/binding-freebsd-x64': 1.0.2 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.2 - '@rolldown/binding-linux-arm64-gnu': 1.0.2 - '@rolldown/binding-linux-arm64-musl': 1.0.2 - '@rolldown/binding-linux-ppc64-gnu': 1.0.2 - '@rolldown/binding-linux-s390x-gnu': 1.0.2 - '@rolldown/binding-linux-x64-gnu': 1.0.2 - '@rolldown/binding-linux-x64-musl': 1.0.2 - '@rolldown/binding-openharmony-arm64': 1.0.2 - '@rolldown/binding-wasm32-wasi': 1.0.2 - '@rolldown/binding-win32-arm64-msvc': 1.0.2 - '@rolldown/binding-win32-x64-msvc': 1.0.2 + '@rolldown/binding-android-arm64': 1.1.5 + '@rolldown/binding-darwin-arm64': 1.1.5 + '@rolldown/binding-darwin-x64': 1.1.5 + '@rolldown/binding-freebsd-x64': 1.1.5 + '@rolldown/binding-linux-arm-gnueabihf': 1.1.5 + '@rolldown/binding-linux-arm64-gnu': 1.1.5 + '@rolldown/binding-linux-arm64-musl': 1.1.5 + '@rolldown/binding-linux-ppc64-gnu': 1.1.5 + '@rolldown/binding-linux-s390x-gnu': 1.1.5 + '@rolldown/binding-linux-x64-gnu': 1.1.5 + '@rolldown/binding-linux-x64-musl': 1.1.5 + '@rolldown/binding-openharmony-arm64': 1.1.5 + '@rolldown/binding-wasm32-wasi': 1.1.5 + '@rolldown/binding-win32-arm64-msvc': 1.1.5 + '@rolldown/binding-win32-x64-msvc': 1.1.5 router@2.2.0: dependencies: @@ -12907,13 +12826,13 @@ snapshots: dependencies: compute-scroll-into-view: 3.1.1 - semantic-release@25.0.6(typescript@7.0.2): + semantic-release@25.0.7(typescript@7.0.2): dependencies: - '@semantic-release/commit-analyzer': 13.0.1(semantic-release@25.0.6(typescript@7.0.2)) + '@semantic-release/commit-analyzer': 13.0.1(semantic-release@25.0.7(typescript@7.0.2)) '@semantic-release/error': 4.0.0 - '@semantic-release/github': 12.0.9(semantic-release@25.0.6(typescript@7.0.2)) - '@semantic-release/npm': 13.1.5(semantic-release@25.0.6(typescript@7.0.2)) - '@semantic-release/release-notes-generator': 14.1.1(semantic-release@25.0.6(typescript@7.0.2)) + '@semantic-release/github': 12.0.9(semantic-release@25.0.7(typescript@7.0.2)) + '@semantic-release/npm': 13.1.5(semantic-release@25.0.7(typescript@7.0.2)) + '@semantic-release/release-notes-generator': 14.1.1(semantic-release@25.0.7(typescript@7.0.2)) aggregate-error: 5.0.0 cosmiconfig: 9.0.2(typescript@7.0.2) debug: 4.4.3(supports-color@7.2.0) @@ -12948,8 +12867,6 @@ snapshots: semver@7.7.4: {} - semver@7.8.2: {} - semver@7.8.4: {} semver@7.8.5: {} @@ -12983,7 +12900,7 @@ snapshots: mime-types: 3.0.2 ms: 2.1.3 on-finished: 2.4.1 - range-parser: 1.2.1 + range-parser: 1.3.0 statuses: 2.0.2 transitivePeerDependencies: - supports-color @@ -13006,15 +12923,6 @@ snapshots: transitivePeerDependencies: - supports-color - set-function-length@1.2.2: - dependencies: - define-data-property: 1.1.4 - es-errors: 1.3.0 - function-bind: 1.1.2 - get-intrinsic: 1.3.0 - gopd: 1.2.0 - has-property-descriptors: 1.0.2 - setprototypeof@1.2.0: {} sharp@0.34.5: @@ -13055,7 +12963,7 @@ snapshots: shebang-regex@3.0.0: {} - shell-quote@1.8.4: {} + shell-quote@1.10.0: {} shiki@4.3.1: dependencies: @@ -13195,7 +13103,7 @@ snapshots: statuses@2.0.2: {} - std-env@4.1.0: {} + std-env@4.2.0: {} steno@0.4.4: dependencies: @@ -13229,7 +13137,7 @@ snapshots: get-east-asian-width: 1.6.0 strip-ansi: 7.2.0 - string-width@8.2.1: + string-width@8.2.2: dependencies: get-east-asian-width: 1.6.0 strip-ansi: 7.2.0 @@ -13433,10 +13341,6 @@ snapshots: type-fest@4.41.0: {} - type-fest@5.7.0: - dependencies: - tagged-tag: 1.0.0 - type-fest@5.8.0: dependencies: tagged-tag: 1.0.0 @@ -13482,8 +13386,6 @@ snapshots: unbash@4.0.2: {} - undici-types@7.24.6: {} - undici-types@8.3.0: {} undici@6.27.0: {} @@ -13554,9 +13456,9 @@ snapshots: unpipe@1.0.0: {} - update-browserslist-db@1.2.3(browserslist@4.28.2): + update-browserslist-db@1.2.3(browserslist@4.28.6): dependencies: - browserslist: 4.28.2 + browserslist: 4.28.6 escalade: 3.2.0 picocolors: 1.1.1 @@ -13619,13 +13521,13 @@ snapshots: transitivePeerDependencies: - supports-color - verdaccio@6.7.4(typanion@3.14.0): + verdaccio@6.8.0(typanion@3.14.0): dependencies: '@cypress/request': 3.0.10 '@verdaccio/auth': 8.0.4 '@verdaccio/config': 8.1.2 '@verdaccio/core': 8.1.2 - '@verdaccio/hooks': 8.0.3 + '@verdaccio/hooks': 8.0.4 '@verdaccio/loaders': 8.0.3 '@verdaccio/local-storage-legacy': 11.3.4 '@verdaccio/logger': 8.0.3 @@ -13635,7 +13537,7 @@ snapshots: '@verdaccio/signature': 8.0.3 '@verdaccio/streams': 10.2.5 '@verdaccio/tarball': 13.0.3 - '@verdaccio/ui-theme': 9.0.0-next-9.20 + '@verdaccio/ui-theme': 9.0.0-next-9.21 '@verdaccio/url': 13.0.3 '@verdaccio/utils': 8.1.3 JSONStream: 1.3.5 @@ -13649,7 +13551,7 @@ snapshots: lodash: 4.18.1 lru-cache: 7.18.3 mime: 3.0.0 - semver: 7.8.2 + semver: 7.8.5 verdaccio-audit: 13.0.3 verdaccio-htpasswd: 13.0.3 transitivePeerDependencies: @@ -13680,12 +13582,12 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0): + vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0): dependencies: lightningcss: 1.32.0 picomatch: 4.0.5 postcss: 8.5.19 - rolldown: 1.0.2 + rolldown: 1.1.5 tinyglobby: 0.2.17 optionalDependencies: '@types/node': 26.1.1 @@ -13694,27 +13596,27 @@ snapshots: jiti: 2.7.0 yaml: 2.9.0 - vitest@4.1.10(@types/node@26.1.1)(@vitest/coverage-istanbul@4.1.10)(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)): + vitest@4.1.10(@types/node@26.1.1)(@vitest/coverage-istanbul@4.1.10)(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.10 - '@vitest/mocker': 4.1.10(vite@8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) + '@vitest/mocker': 4.1.10(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) '@vitest/pretty-format': 4.1.10 '@vitest/runner': 4.1.10 '@vitest/snapshot': 4.1.10 '@vitest/spy': 4.1.10 '@vitest/utils': 4.1.10 - es-module-lexer: 2.1.0 - expect-type: 1.3.0 + es-module-lexer: 2.3.1 + expect-type: 1.4.0 magic-string: 0.30.21 - obug: 2.1.1 + obug: 2.1.3 pathe: 2.0.3 picomatch: 4.0.5 - std-env: 4.1.0 + std-env: 4.2.0 tinybench: 2.9.0 tinyexec: 1.2.4 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: 8.0.14(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0) + vite: 8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 26.1.1 @@ -13754,14 +13656,14 @@ snapshots: widest-line@6.0.0: dependencies: - string-width: 8.2.1 + string-width: 8.2.2 wordwrap@1.0.0: {} wrap-ansi@10.0.0: dependencies: ansi-styles: 6.2.3 - string-width: 8.2.1 + string-width: 8.2.2 strip-ansi: 7.2.0 wrap-ansi@7.0.0: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 12064aa39a..42ff83fbac 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -38,7 +38,7 @@ blockExoticSubdeps: true overrides: "@effect/platform-node-shared": "4.0.0-beta.97" -minimumReleaseAge: 0 +minimumReleaseAge: 10200 supportedArchitectures: cpu: From 1d4175d715917f0d772fd5c40b3056c86d3a5ba1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Jul 2026 00:12:24 +0000 Subject: [PATCH 70/79] chore(deps): bump the go-minor group across 2 directories with 2 updates (#5923) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the go-minor group with 1 update in the /apps/cli-go directory: [github.com/getsentry/sentry-go](https://github.com/getsentry/sentry-go). Bumps the go-minor group with 1 update in the /apps/cli-go/pkg directory: [github.com/oapi-codegen/runtime](https://github.com/oapi-codegen/runtime). Updates `github.com/getsentry/sentry-go` from 0.47.0 to 0.48.0

Release notes

Sourced from github.com/getsentry/sentry-go's releases.

0.48.0

Breaking Changes 🛠

New Features ✨

  • Add ClientOptions.DataCollection for granular control over data collected by automatic instrumentation, replacing the broad SendDefaultPII switch. DataCollection can independently configure automatic user.* population, cookies, request/response headers, HTTP bodies, and query parameters. When configured, it is the source of truth and SendDefaultPII is ignored. by @​giortzisg in #1339
    • For backwards compatibility, clients that do not configure DataCollection keep a best-effort mapping of the previous SendDefaultPII behavior. To opt in to the new defaults, pass an empty DataCollection and then restrict individual categories as needed.
    sentry.Init(sentry.ClientOptions{
        Dsn: "https://public@example.com/1",
    
    // Opt in to the new data collection defaults. Omitted fields
    use their
    // defaults: user info, cookies, headers, query params, and supported
    HTTP
    // bodies are collected, with sensitive values filtered.
    DataCollection: &amp;sentry.DataCollection{},
    

    })

    • To opt in while disabling automatic user info and HTTP bodies, configure those fields explicitly:
    sentry.Init(sentry.ClientOptions{
        Dsn: "https://public@example.com/1",
        DataCollection: &sentry.DataCollection{
            UserInfo:   sentry.Set(false),
            HTTPBodies: []sentry.BodyType{},
        },
    })
    
  • PushScope shorthand now returns the new scope reference by @​DoctorJohn in #1335

Bug Fixes 🐛

Internal Changes 🔧

Deps

... (truncated)

Changelog

Sourced from github.com/getsentry/sentry-go's changelog.

0.48.0

Breaking Changes 🛠

New Features ✨

  • Add ClientOptions.DataCollection for granular control over data collected by automatic instrumentation, replacing the broad SendDefaultPII switch. DataCollection can independently configure automatic user.* population, cookies, request/response headers, HTTP bodies, and query parameters. When configured, it is the source of truth and SendDefaultPII is ignored. by @​giortzisg in #1339
    • For backwards compatibility, clients that do not configure DataCollection keep a best-effort mapping of the previous SendDefaultPII behavior. To opt in to the new defaults, pass an empty DataCollection and then restrict individual categories as needed.
    sentry.Init(sentry.ClientOptions{
        Dsn: "https://public@example.com/1",
    
    // Opt in to the new data collection defaults. Omitted fields
    use their
    // defaults: user info, cookies, headers, query params, and supported
    HTTP
    // bodies are collected, with sensitive values filtered.
    DataCollection: &amp;sentry.DataCollection{},
    

    })

    • To opt in while disabling automatic user info and HTTP bodies, configure those fields explicitly:
    sentry.Init(sentry.ClientOptions{
        Dsn: "https://public@example.com/1",
        DataCollection: &sentry.DataCollection{
            UserInfo:   sentry.Set(false),
            HTTPBodies: []sentry.BodyType{},
        },
    })
    
  • PushScope shorthand now returns the new scope reference by @​DoctorJohn in #1335

Bug Fixes 🐛

Internal Changes 🔧

Deps

... (truncated)

Commits
  • d02f9ba release: 0.48.0
  • 99c424f feat!: remove issue creation from logging integrations (#1340)
  • d1c1d3f feat: parse request body for outgoing http (#1339)
  • 9b5cab2 fix: fix fiber route name when using middlewares (#1363)
  • 07a7975 fix: omit empty event id for standalone client reports (#1362)
  • c255382 build(deps): bump fiber/v2 to 2.52.14 (#1359)
  • 06c58dc ci: remove changelog-preview and codecov actions (#1357)
  • a99e044 fix: preserve '%' literal in log messages (#1358)
  • 8aaf1d4 feat: apply sensitive data filters to grpc & sql (#1333)
  • 4347773 feat: apply sensitive data filters to http (#1332)
  • Additional commits viewable in compare view

Updates `github.com/oapi-codegen/runtime` from 1.4.2 to 1.5.0
Release notes

Sourced from github.com/oapi-codegen/runtime's releases.

v1.5.0: RFC3339 durations, and bug fixes

This is mainly a bugfix release, but we're bumping the minor version since we also introduce a new type, Duration into our types/ package, which allows for parsing and emitting RFC3339 durations. Rather than trying to parse a duration string into a time.Duration, which requires assumptions that may not be right for everyone, we decided not to make those decisions and just store all possible fields as provided. Users can convert this to Go Duration as they see fit.

🚀 New features and improvements

🐛 Bug fixes

📝 Documentation updates

📦 Dependency updates

  • fix(deps): update module github.com/labstack/echo/v5 to v5.3.0 (#142) @renovate[bot]
  • chore(deps): update golang/govulncheck-action action to v1.1.0 (#137) @renovate[bot]
  • chore(deps): update oapi-codegen/actions action to v0.8.0 (#130) @renovate[bot]
  • fix(deps): update module github.com/labstack/echo/v5 to v5.2.1 - autoclosed (#126) @renovate[bot]
  • chore(deps): update release-drafter/release-drafter action to v7.5.1 - autoclosed (#124) @renovate[bot]
  • chore(deps): update github/codeql-action action to v4.37.0 (#123) @renovate[bot]

Sponsors

We would like to thank our sponsors for their support during this release.

Commits
  • 540d34a fix(deps): update module github.com/labstack/echo/v5 to v5.3.0 (#142)
  • e89dbb8 Add types.Duration for the RFC 3339 duration format (#144)
  • 324e57f Let generated code declare whether styled parameter values are escaped (#143)
  • 95c13c0 Explain how to send nested objects when style serialization fails (#141)
  • 7c889f3 Prefer the form struct tag over json for form encoding (#140)
  • d0d5c3a chore(deps): update golang/govulncheck-action action to v1.1.0 (#137)
  • 67e86fd chore(deps): update oapi-codegen/actions action to v0.8.0 (#130)
  • df140cb fix(deps): update module github.com/labstack/echo/v5 to v5.2.1 (#126)
  • a90c79a chore(deps): update release-drafter/release-drafter action to v7.5.1 (#124)
  • a6d718f chore(deps): update github/codeql-action action to v4.37.0 (#123)
  • Additional commits viewable in compare view

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/cli-go/go.mod | 4 ++-- apps/cli-go/go.sum | 8 ++++---- apps/cli-go/pkg/go.mod | 2 +- apps/cli-go/pkg/go.sum | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/apps/cli-go/go.mod b/apps/cli-go/go.mod index 92920337f6..a90f0950e2 100644 --- a/apps/cli-go/go.mod +++ b/apps/cli-go/go.mod @@ -20,7 +20,7 @@ require ( github.com/docker/go-connections v0.7.0 github.com/docker/go-units v0.5.0 github.com/fsnotify/fsnotify v1.10.1 - github.com/getsentry/sentry-go v0.47.0 + github.com/getsentry/sentry-go v0.48.0 github.com/go-errors/errors v1.5.1 github.com/go-git/go-git/v5 v5.19.1 github.com/go-playground/validator/v10 v10.30.3 @@ -351,7 +351,7 @@ require ( github.com/nishanths/predeclared v0.2.2 // indirect github.com/nunnatsa/ginkgolinter v0.23.0 // indirect github.com/oapi-codegen/oapi-codegen/v2 v2.7.1 // indirect - github.com/oapi-codegen/runtime v1.4.2 // indirect + github.com/oapi-codegen/runtime v1.5.0 // indirect github.com/oasdiff/yaml v0.0.9 // indirect github.com/oasdiff/yaml3 v0.0.9 // indirect github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect diff --git a/apps/cli-go/go.sum b/apps/cli-go/go.sum index df61dfd4ad..dc19b3e60c 100644 --- a/apps/cli-go/go.sum +++ b/apps/cli-go/go.sum @@ -373,8 +373,8 @@ github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9 github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/getkin/kin-openapi v0.135.0 h1:751SjYfbiwqukYuVjwYEIKNfrSwS5YpA7DZnKSwQgtg= github.com/getkin/kin-openapi v0.135.0/go.mod h1:6dd5FJl6RdX4usBtFBaQhk9q62Yb2J0Mk5IhUO/QqFI= -github.com/getsentry/sentry-go v0.47.0 h1:AnSMSyrYA5qZCIN/2xpgAAwv63sVULV+vBq37ajouc8= -github.com/getsentry/sentry-go v0.47.0/go.mod h1:h+b4VHpKnK7aUXB5wc+KDnPgp9ZtfliRD4eV85FbiSA= +github.com/getsentry/sentry-go v0.48.0 h1:FRZNr7Uk1C86ev1bSJmYlUkL9oyivQA6YOcdYfaaMmY= +github.com/getsentry/sentry-go v0.48.0/go.mod h1:E5UkA5wp1qR2+MDydNYlVeUiNN2xEdjYMidkgf0Qoss= github.com/ghostiam/protogetter v0.3.20 h1:oW7OPFit2FxZOpmMRPP9FffU4uUpfeE/rEdE1f+MzD0= github.com/ghostiam/protogetter v0.3.20/go.mod h1:FjIu5Yfs6FT391m+Fjp3fbAYJ6rkL/J6ySpZBfnODuI= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= @@ -896,8 +896,8 @@ github.com/oapi-codegen/nullable v1.2.0 h1:VflFkDW980KhBPiFF7nWSyjg+r4Obqj8lXipV github.com/oapi-codegen/nullable v1.2.0/go.mod h1:KUZ3vUzkmEKY90ksAmit2+5juDIhIZhfDl+0PwOQlFY= github.com/oapi-codegen/oapi-codegen/v2 v2.7.1 h1:a7Ab7YlpqkVG5HKrTaeFstm32Z5QOnyjnbsCO0jiMYM= github.com/oapi-codegen/oapi-codegen/v2 v2.7.1/go.mod h1:qzFy6iuobJw/hD1aRILee4G87/ShmhR0xYCwcUtZMCw= -github.com/oapi-codegen/runtime v1.4.2 h1:GMxFVYLzoYLua+/KvzgSphkyK1lLTReQI9Vf4hvATKE= -github.com/oapi-codegen/runtime v1.4.2/go.mod h1:GwV7hC2hviaMzj+ITfHVRESK5J2W/GefVwIND/bMGvU= +github.com/oapi-codegen/runtime v1.5.0 h1:aiil4QnH+eiWYSO60eaYZ4aur7sJH3rz6BvT5EBFnxc= +github.com/oapi-codegen/runtime v1.5.0/go.mod h1:GwV7hC2hviaMzj+ITfHVRESK5J2W/GefVwIND/bMGvU= github.com/oasdiff/yaml v0.0.9 h1:zQOvd2UKoozsSsAknnWoDJlSK4lC0mpmjfDsfqNwX48= github.com/oasdiff/yaml v0.0.9/go.mod h1:8lvhgJG4xiKPj3HN5lDow4jZHPlx1i7dIwzkdAo6oAM= github.com/oasdiff/yaml3 v0.0.9 h1:rWPrKccrdUm8J0F3sGuU+fuh9+1K/RdJlWF7O/9yw2g= diff --git a/apps/cli-go/pkg/go.mod b/apps/cli-go/pkg/go.mod index c2ffb1bc38..b642f8d924 100644 --- a/apps/cli-go/pkg/go.mod +++ b/apps/cli-go/pkg/go.mod @@ -20,7 +20,7 @@ require ( github.com/jackc/pgx/v4 v4.18.3 github.com/joho/godotenv v1.5.1 github.com/oapi-codegen/nullable v1.2.0 - github.com/oapi-codegen/runtime v1.4.2 + github.com/oapi-codegen/runtime v1.5.0 github.com/spf13/afero v1.15.0 github.com/spf13/viper v1.21.0 github.com/stretchr/testify v1.11.1 diff --git a/apps/cli-go/pkg/go.sum b/apps/cli-go/pkg/go.sum index 56f14ba857..796bef2f54 100644 --- a/apps/cli-go/pkg/go.sum +++ b/apps/cli-go/pkg/go.sum @@ -132,8 +132,8 @@ github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= github.com/oapi-codegen/nullable v1.2.0 h1:VflFkDW980KhBPiFF7nWSyjg+r4Obqj8lXipV0UkP5w= github.com/oapi-codegen/nullable v1.2.0/go.mod h1:KUZ3vUzkmEKY90ksAmit2+5juDIhIZhfDl+0PwOQlFY= -github.com/oapi-codegen/runtime v1.4.2 h1:GMxFVYLzoYLua+/KvzgSphkyK1lLTReQI9Vf4hvATKE= -github.com/oapi-codegen/runtime v1.4.2/go.mod h1:GwV7hC2hviaMzj+ITfHVRESK5J2W/GefVwIND/bMGvU= +github.com/oapi-codegen/runtime v1.5.0 h1:aiil4QnH+eiWYSO60eaYZ4aur7sJH3rz6BvT5EBFnxc= +github.com/oapi-codegen/runtime v1.5.0/go.mod h1:GwV7hC2hviaMzj+ITfHVRESK5J2W/GefVwIND/bMGvU= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= From 3ecca621b19bf6673ba324f3e70e4a16be47992a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Jul 2026 00:15:47 +0000 Subject: [PATCH 71/79] chore(ci): bump the actions-major group across 1 directory with 2 updates (#5926) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the actions-major group with 2 updates in the / directory: [actions/setup-go](https://github.com/actions/setup-go) and [softprops/action-gh-release](https://github.com/softprops/action-gh-release). Updates `actions/setup-go` from 6.5.0 to 7.0.0
Release notes

Sourced from actions/setup-go's releases.

v7.0.0

What's Changed

New Contributors

Full Changelog: https://github.com/actions/setup-go/compare/v6...v7.0.0

Commits

Updates `softprops/action-gh-release` from 3.0.1 to 3.0.2
Release notes

Sourced from softprops/action-gh-release's releases.

v3.0.2

3.0.2 is a patch release focused on release reliability and compatibility. It reuses existing draft releases when publishing prereleases, supports replacing release assets on Gitea, hardens streamed asset uploads, and provides clearer release-creation diagnostics. It also includes TypeScript, coverage, and tooling maintenance merged since 3.0.1.

This release fixes #795, #438, and #803. The upload transport hardening covers the historical failure reported in #790, although current hosted Node 24 runners did not reproduce it naturally. The diagnostics work is related to #786 and does not claim a reproducible release-creation fix.

What's Changed

Exciting New Features 🎉

Bug fixes 🐛

Other Changes 🔄

Changelog

Sourced from softprops/action-gh-release's changelog.

3.0.2

3.0.2 is a patch release focused on release reliability and compatibility. It reuses existing draft releases when publishing prereleases, supports replacing release assets on Gitea, hardens streamed asset uploads, and provides clearer release-creation diagnostics. It also includes TypeScript, coverage, and tooling maintenance merged since 3.0.1.

This release fixes #795, #438, and #803. The upload transport hardening covers the historical failure reported in #790, although current hosted Node 24 runners did not reproduce it naturally. The diagnostics work is related to #786 and does not claim a reproducible release-creation fix.

What's Changed

Exciting New Features 🎉

Bug fixes 🐛

Other Changes 🔄

3.0.1

  • maintenance release with updated dependencies

3.0.0

3.0.0 is a major release that moves the action runtime from Node 20 to Node 24. Use v3 on GitHub-hosted runners and self-hosted fleets that already support the Node 24 Actions runtime. v2.6.2 was the final Node 20-compatible release and is no longer maintained or supported.

What's Changed

Other Changes 🔄

  • Move the action runtime and bundle target to Node 24
  • Update @types/node to the Node 24 line and allow future Dependabot updates
  • Keep the floating major tag on v3; freeze v2 at the final v2.6.2 release

... (truncated)

Commits
  • 3d0d988 release 3.0.2 (#818)
  • 7e13ed4 fix: clarify release creation 404 errors (#817)
  • e6c70a5 fix: replace existing release assets on Gitea (#816)
  • f345337 fix: publish existing draft releases as prereleases (#801)
  • d8a89a2 fix: upload small checksum assets reliably (#815)
  • 45ece40 chore(deps): remove unused TypeScript tooling (#814)
  • f6b913c feat: improve release error reporting and test coverage (#813)
  • 15f193d chore(deps): upgrade TypeScript to 7 (#812)
  • cc8268d chore(deps): bump actions/checkout in the github-actions group (#810)
  • fd0ed1e chore(deps): bump the npm group with 3 updates (#811)
  • Additional commits viewable in compare view

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/cli-go-api-sync.yml | 2 +- .github/workflows/cli-go-ci.yml | 8 ++++---- .github/workflows/cli-go-mirror.yml | 2 +- .github/workflows/release-shared.yml | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/cli-go-api-sync.yml b/.github/workflows/cli-go-api-sync.yml index 4c7d3a7fbe..f9d2d88872 100644 --- a/.github/workflows/cli-go-api-sync.yml +++ b/.github/workflows/cli-go-api-sync.yml @@ -18,7 +18,7 @@ jobs: with: persist-credentials: false - - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version-file: apps/cli-go/go.mod cache: true diff --git a/.github/workflows/cli-go-ci.yml b/.github/workflows/cli-go-ci.yml index 0d497ffc99..72234c451a 100644 --- a/.github/workflows/cli-go-ci.yml +++ b/.github/workflows/cli-go-ci.yml @@ -23,7 +23,7 @@ jobs: with: persist-credentials: false - - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version-file: apps/cli-go/go.mod cache: true @@ -100,7 +100,7 @@ jobs: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version-file: apps/cli-go/go.mod cache: true @@ -126,7 +126,7 @@ jobs: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version-file: apps/cli-go/go.mod cache: true @@ -144,7 +144,7 @@ jobs: with: persist-credentials: false - - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version-file: apps/cli-go/go.mod cache: true diff --git a/.github/workflows/cli-go-mirror.yml b/.github/workflows/cli-go-mirror.yml index 1e5d3224e9..27bb7f0cfb 100644 --- a/.github/workflows/cli-go-mirror.yml +++ b/.github/workflows/cli-go-mirror.yml @@ -26,7 +26,7 @@ jobs: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version-file: apps/cli-go/go.mod cache: true diff --git a/.github/workflows/release-shared.yml b/.github/workflows/release-shared.yml index a2e30ccfe0..69731ef307 100644 --- a/.github/workflows/release-shared.yml +++ b/.github/workflows/release-shared.yml @@ -389,7 +389,7 @@ jobs: done - name: Create draft GitHub Release - uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1 + uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2 with: tag_name: v${{ inputs.version }} name: v${{ inputs.version }} From 11f31ddaa95b4253f2f6d1b41237cd18be31398a Mon Sep 17 00:00:00 2001 From: Vaibhav <117663341+7ttp@users.noreply.github.com> Date: Wed, 22 Jul 2026 15:13:26 +0530 Subject: [PATCH 72/79] fix(cli): pg-delta workspace mount (#5929) ## TL;DR: `db push` crashes with `ENOENT` reading the `pg-delta` CA cert on every push against a hosted/TLS target, missing Docker mount ## Prob exportCatalog writes the CA bundle to `/supabase/.temp/pgdelta/` and points `sslrootcert` at `/workspace/... `inside the container, but never mounted `cwd `to /workspace like every other pg-delta call site does. Container looks for a file that was never shared with it. ## Sol: (pragmatic fix) adds the same `cwd:/workspace` bind the sibling functions in `internal/db/diff/pgdelta.go` already has. that's it.. ## Ref: - towards #5921 --- apps/cli-go/internal/db/pgcache/cache.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/cli-go/internal/db/pgcache/cache.go b/apps/cli-go/internal/db/pgcache/cache.go index 52960c9b06..3cc1ccd4b4 100644 --- a/apps/cli-go/internal/db/pgcache/cache.go +++ b/apps/cli-go/internal/db/pgcache/cache.go @@ -6,6 +6,7 @@ import ( "crypto/sha256" "encoding/hex" "fmt" + "os" "path/filepath" "regexp" "sort" @@ -261,6 +262,9 @@ func exportCatalog(ctx context.Context, targetRef string, options ...func(*pgx.C } env := append([]string{"TARGET=" + preparedRef, "ROLE=postgres"}, sslEnv...) binds := []string{utils.EdgeRuntimeId + ":/root/.cache/deno:rw"} + if cwd, err := os.Getwd(); err == nil { + binds = append(binds, cwd+":/workspace") + } var stdout, stderr bytes.Buffer script := config.InterpolatePgDeltaScript(config.Config(&utils.Config), pgDeltaCatalogExportTS) if err := utils.RunEdgeRuntimeScript(ctx, env, script, binds, "error exporting pg-delta catalog", &stdout, &stderr, utils.PgDeltaNpmRegistryOption()); err != nil { From e44a2bd47926f213a043ab2235de6a6dfe3efc30 Mon Sep 17 00:00:00 2001 From: kanad Date: Wed, 22 Jul 2026 02:49:59 -0700 Subject: [PATCH 73/79] chore(release): stop mutating global registry config in local-registry (#5922) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What `pnpm local-registry` (the local Verdaccio helper for release testing) no longer redirects the global `npm` and `pnpm` registry config to `localhost:4873` on startup, and no longer needs to restore it on shutdown. The cleanup handler now just kills Verdaccio, and additionally handles `SIGHUP` so closing the terminal window doesn't orphan the server. `CONTRIBUTING.md` is updated to match, and the troubleshooting table now covers recovering from the stale override left behind by older versions of the script. ## Why The restore path had holes that could leave a developer's machine pointed at a dead localhost registry, making every subsequent `pnpm i` fail: - The Verdaccio-crash path called `process.exit(1)` without restoring the registry. - `SIGHUP` (closing the terminal) wasn't handled, so no cleanup ran at all. - A partial restore failure only printed a warning, right as the terminal session was being torn down. This bit for real: pnpm persists the override in its own global config (`~/Library/Preferences/pnpm/auth.ini` on macOS) rather than `~/.npmrc`, so an `~/.npmrc`-focused manual cleanup makes npm look healthy while pnpm silently keeps resolving from `localhost:4873`. The global redirect was also unnecessary: `local-release.ts` already passes `--registry` explicitly to every publish command and writes a scoped `.npmrc` (with the auth token) into its temp publish dir, and the suggested `npx` / `npm install -g` smoke-test commands carry the flag too. With the redirect gone, there is no cleanup left to get wrong — a crash or hard kill can no longer corrupt the developer's registry config. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 --- CONTRIBUTING.md | 4 +-- tools/release/local-registry.ts | 45 +++++++++------------------------ 2 files changed, 14 insertions(+), 35 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cb8978cf1a..0394f608a2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -270,7 +270,7 @@ Test a real end-to-end publish and install of the CLI against a local npm regist pnpm local-registry ``` -This starts Verdaccio on `http://localhost:4873`, creates a publish user, and redirects the global `npm` and `pnpm` registry config to `localhost`. Press **Ctrl+C** when done — the original registry settings are restored automatically. +This starts Verdaccio on `http://localhost:4873` and creates a publish user. Your global `npm` and `pnpm` registry config is never modified — every command that talks to the local registry passes `--registry` explicitly. Press **Ctrl+C** when done. **Terminal 2 — build and publish:** @@ -310,7 +310,7 @@ supabase --version | `Error: Something is already running on port 4873` | Kill the leftover Verdaccio process (`lsof -ti:4873 \| xargs kill`) and retry | | `go not found in PATH` (legacy only) | Install Go from https://go.dev/dl/ | | `Error: Go CLI source not found` (legacy only) | Run `pnpm repos:install` to clone `apps/cli-go` | -| Registry not restored after crash | Run `npm config set registry https://registry.npmjs.org/` and `pnpm config set registry https://registry.npmjs.org/` | +| `npm` / `pnpm` tries to fetch from `localhost:4873` when no registry is running | Stale global registry override left behind by an older version of `local-registry.ts` (the current script never modifies global config). Run `npm config delete registry` and `pnpm config delete registry`. Note that pnpm stores the override in its own global config (`~/Library/Preferences/pnpm/auth.ini` on macOS, `~/.config/pnpm/` on Linux), not `~/.npmrc` — check there if the delete command fails | | `npx` resolves from npm instead of local | Pass `--registry http://localhost:4873` explicitly to `npx` / `npm install` | ## Using Nx diff --git a/tools/release/local-registry.ts b/tools/release/local-registry.ts index 9f1ed524de..695cdc43fb 100644 --- a/tools/release/local-registry.ts +++ b/tools/release/local-registry.ts @@ -5,11 +5,13 @@ * * - Starts Verdaccio on http://localhost:4873 * - Creates a local publish user and stores the auth token in tmp/verdaccio-token - * - Redirects the global npm and pnpm registry config to the local registry - * - Restores the original registry config and kills Verdaccio on Ctrl+C / SIGTERM + * - Kills Verdaccio on Ctrl+C / SIGTERM / SIGHUP + * + * Global npm/pnpm registry config is never modified — everything that talks to + * the local registry passes --registry explicitly (see local-release.ts), so a + * crash or hard kill cannot leave the developer's machine pointed at localhost. */ -import { $ } from "bun"; import { openSync } from "node:fs"; import { mkdir, rm, writeFile } from "node:fs/promises"; import path from "node:path"; @@ -80,15 +82,6 @@ async function createUser(): Promise { return body.token; } -async function getRegistryConfig(tool: "npm" | "pnpm"): Promise { - try { - const value = (await $`${tool} config get registry`.quiet().text()).trim(); - return value === "undefined" ? "https://registry.npmjs.org/" : value; - } catch { - return "https://registry.npmjs.org/"; - } -} - async function main() { if (await isPortInUse()) { console.error(`\nError: Something is already running on port ${PORT}.`); @@ -134,13 +127,6 @@ async function main() { const token = await createUser(); await writeFile(tokenPath, token, "utf-8"); - // Capture current global registry settings so we can restore them on exit. - const origNpm = await getRegistryConfig("npm"); - const origPnpm = await getRegistryConfig("pnpm"); - - await $`npm config set registry ${REGISTRY}`.quiet(); - await $`pnpm config set registry ${REGISTRY}`.quiet(); - console.log(` Registry : ${REGISTRY} Token : ${tokenPath} @@ -150,28 +136,21 @@ async function main() { pnpm cli-release --next pnpm cli-release --legacy - Press Ctrl+C to stop and restore the original registry settings. + Global npm/pnpm registry config is untouched — pass --registry ${REGISTRY} + to npx / npm install when testing the published package. + + Press Ctrl+C to stop. `); - const cleanup = async () => { - process.stdout.write("\nShutting down local registry..."); - try { - await $`npm config set registry ${origNpm}`.quiet(); - await $`pnpm config set registry ${origPnpm}`.quiet(); - process.stdout.write(" registry restored.\n"); - } catch { - process.stdout.write( - "\nWarning: could not restore registry config — run:\n" + - ` npm config set registry ${origNpm}\n` + - ` pnpm config set registry ${origPnpm}\n`, - ); - } + const cleanup = () => { + process.stdout.write("\nShutting down local registry...\n"); proc.kill(); process.exit(0); }; process.on("SIGINT", cleanup); process.on("SIGTERM", cleanup); + process.on("SIGHUP", cleanup); // Block until a signal is received. await new Promise(() => {}); From 92330c5580adb840120966ce95364dfe6c5b130d Mon Sep 17 00:00:00 2001 From: Vaibhav <117663341+7ttp@users.noreply.github.com> Date: Wed, 22 Jul 2026 15:31:45 +0530 Subject: [PATCH 74/79] fix(cli): preserve auth templates (#5906) ## TL;DR Fixes Kong reloads dropping the custom nginx configuration,
so custom Auth email templates no longer silently fall back to GoTrue defaults adds the configured nginx template to the reload command w focused coverage... ## Ref * closes supabase/cli#5905 --- .../functions/serve/serve.integration.test.ts | 13 +++++++++++++ apps/cli/src/shared/functions/serve.ts | 9 +++++---- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/apps/cli/src/legacy/commands/functions/serve/serve.integration.test.ts b/apps/cli/src/legacy/commands/functions/serve/serve.integration.test.ts index 8e3354fe2d..b9c475b004 100644 --- a/apps/cli/src/legacy/commands/functions/serve/serve.integration.test.ts +++ b/apps/cli/src/legacy/commands/functions/serve/serve.integration.test.ts @@ -508,6 +508,19 @@ describe("legacy functions serve integration", () => { }, }); + expect(deployMockState.runCalls).toContainEqual({ + command: "docker", + args: [ + "exec", + "supabase_kong_test-project", + "kong", + "reload", + "--nginx-conf", + "/home/kong/custom_nginx.template", + ], + options: { stdout: "ignore", stderr: "pipe" }, + }); + expect(childSpawner.spawned).toEqual([ { command: "docker", diff --git a/apps/cli/src/shared/functions/serve.ts b/apps/cli/src/shared/functions/serve.ts index 710afdd972..13a27a8470 100644 --- a/apps/cli/src/shared/functions/serve.ts +++ b/apps/cli/src/shared/functions/serve.ts @@ -1225,10 +1225,11 @@ const bestEffortRemoveContainer = Effect.fnUntraced(function* (containerId: stri const reloadKong = Effect.fnUntraced(function* (projectId: string) { const output = yield* Output; const kongId = localDockerId("kong", projectId); - const result = yield* runChildProcess("docker", ["exec", kongId, "kong", "reload"], { - stdout: "ignore", - stderr: "pipe", - }).pipe(Effect.catch(() => Effect.succeed({ exitCode: 1, stdout: "", stderr: "" }))); + const result = yield* runChildProcess( + "docker", + ["exec", kongId, "kong", "reload", "--nginx-conf", "/home/kong/custom_nginx.template"], + { stdout: "ignore", stderr: "pipe" }, + ).pipe(Effect.catch(() => Effect.succeed({ exitCode: 1, stdout: "", stderr: "" }))); if (result.exitCode !== 0) { const suffix = result.stderr.trim().length > 0 ? ` ${result.stderr.trim()}` : ""; From 82dc21b8840ac5f825069e7f7625267e3d66d772 Mon Sep 17 00:00:00 2001 From: Vaibhav <117663341+7ttp@users.noreply.github.com> Date: Wed, 22 Jul 2026 15:32:25 +0530 Subject: [PATCH 75/79] fix(cli): windows logout credentials (#5913) ## TL;DR Fixes stale Windows credentials surviving logout
Go handles this by never decoding the blob before deleting, but the TS port used a decode based existence check that threw on non-UTF16 bytes and treated present credentials as absent. Now it uses a plain enumeration read with no decode step, so it's back to deleted or not, nothing else
(go parity: matches Go's own delete path exactly) ## ref: * closes supabase/cli#5890 --- .../legacy/auth/legacy-credentials.layer.ts | 122 ++++++++---- .../legacy-credentials.layer.unit.test.ts | 188 +++++++++++++++--- 2 files changed, 240 insertions(+), 70 deletions(-) diff --git a/apps/cli/src/legacy/auth/legacy-credentials.layer.ts b/apps/cli/src/legacy/auth/legacy-credentials.layer.ts index 2d78ea2ec8..ffddf8b017 100644 --- a/apps/cli/src/legacy/auth/legacy-credentials.layer.ts +++ b/apps/cli/src/legacy/auth/legacy-credentials.layer.ts @@ -1,4 +1,4 @@ -import { Effect, FileSystem, Layer, Option, Path, Redacted } from "effect"; +import { Effect, FileSystem, Layer, Option, Path, Redacted, Result } from "effect"; import { RuntimeInfo } from "../../shared/runtime/runtime-info.service.ts"; import { normalizeKeyringToken } from "../../shared/auth/keyring-token.ts"; @@ -92,7 +92,7 @@ const tryKeyringDelete = ( deleted = true; } - if (platform === "win32" && readGoWindowsTarget(module, account)) { + if (platform === "win32" && probeWindowsTarget(module, account) !== "absent") { deleted = deleteGoWindowsTarget(module, account) || deleted; } @@ -123,6 +123,45 @@ function readGoWindowsTarget(module: KeyringModule, account: string): string | n } } +// `Entry.withTarget` is avoided as a probe — its constructor writes an empty +// placeholder. A `findCredentials` throw is ambiguous (an undecodable Go blob or +// a real enumeration failure), so reported as `"unknown"`, never assumed present. +type WindowsTargetProbe = "present" | "absent" | "unknown"; + +function probeWindowsTarget(module: KeyringModule, account: string): WindowsTargetProbe { + try { + const credentials = module.findCredentials(KEYRING_SERVICE, goWindowsCredentialTarget(account)); + // An empty password is an orphaned placeholder, not a real credential. + const credential = credentials.find( + (item) => item.account === account && item.password.length > 0, + ); + return credential ? "present" : "absent"; + } catch { + return "unknown"; + } +} + +// Constructing `withTarget` always leaves something at that target — the real +// credential, or the placeholder — so a delete that follows is never a +// legitimate "nothing to delete": any non-success is surfaced. +const deleteProbedWindowsTarget = ( + module: KeyringModule, + account: string, + onFailure: (cause: unknown) => E, +): Effect.Effect => + Effect.gen(function* () { + const result = yield* Effect.try(() => + module.Entry.withTarget( + goWindowsCredentialTarget(account), + KEYRING_SERVICE, + account, + ).deleteCredential(), + ).pipe(Effect.result); + if (Result.isSuccess(result) && result.success) return true; + const cause = Result.isFailure(result) ? result.failure : "credential was not removed"; + return yield* Effect.fail(onFailure(cause)); + }); + function normalizeGoWindowsPassword(value: string): string { const direct = normalizeKeyringToken(value); if (LEGACY_ACCESS_TOKEN_PATTERN.test(direct)) return direct; @@ -172,11 +211,10 @@ function deleteGoWindowsTarget(module: KeyringModule, account: string): boolean // (backend unavailable) and only surfaces other errors (`unlink.go:36-40`). // // The plain `Entry(service, projectRef)` is the macOS/Linux form and the Windows -// default. On Windows, Go also writes a separate target-shaped credential; it is -// detected via `findCredentials` (a plain `getPassword` does not read the Go -// target reliably) and deleted through the `withTarget` entry. The `withTarget` -// entry is only constructed on Windows — on macOS its first argument is an -// invalid keychain domain and throws. +// default. On Windows, Go also writes a separate target-shaped credential, +// deleted through the `withTarget` entry. The `withTarget` entry is only +// constructed on Windows — on macOS its first argument is an invalid keychain +// domain and throws. // // Each entry is probed before `deleteCredential()`: on macOS deleting an absent // entry blocks on a Keychain authorization prompt, and an absent read means @@ -204,22 +242,16 @@ const deleteKeyringEntryStrict = ( deleted = true; } - if (platform === "win32" && readGoWindowsTarget(module, account)) { - const target = module.Entry.withTarget( - goWindowsCredentialTarget(account), - KEYRING_SERVICE, + if (platform === "win32" && probeWindowsTarget(module, account) !== "absent") { + const removed = yield* deleteProbedWindowsTarget( + module, account, - ); - yield* Effect.try({ - try: () => { - target.deleteCredential(); - }, - catch: (cause) => + (cause) => new LegacyCredentialDeleteError({ message: `failed to delete project credential: ${String(cause)}`, }), - }); - deleted = true; + ); + deleted ||= removed; } return deleted; @@ -256,43 +288,48 @@ const deleteProfileKeyringEntry = ( found = true; } - if (platform === "win32" && readGoWindowsTarget(module, account)) { - const target = module.Entry.withTarget( - goWindowsCredentialTarget(account), - KEYRING_SERVICE, + if (platform === "win32" && probeWindowsTarget(module, account) !== "absent") { + const removed = yield* deleteProbedWindowsTarget( + module, account, - ); - yield* Effect.try({ - try: () => { - target.deleteCredential(); - }, - catch: (cause) => + (cause) => new LegacyDeleteTokenError({ message: `failed to delete access token from keyring: ${String(cause)}`, }), - }); - found = true; + ); + found ||= removed; } return found ? "deleted" : "notFound"; }); -// Best-effort wipe of every entry in the `"Supabase CLI"` keyring namespace — -// the project database-password credentials `link` writes. Mirrors Go's -// `keyring.DeleteAll(namespace)` (`store.go:71`). Never fails: per-entry delete -// errors are swallowed so a single stuck credential can't abort logout. +// Best-effort wipe of the `"Supabase CLI"` keyring namespace, mirroring Go's +// `keyring.DeleteAll` (`store.go:71`); errors are swallowed so one stuck +// credential can't abort logout, matching Go's own handling (`logout.go:35`). // -// On Windows, Go stores credentials under the target-shaped name -// `Supabase CLI:` rather than the plain `Entry(service, account)` form -// (see `writeGoWindowsTarget`). So each discovered account is deleted in BOTH -// forms — the plain entry and, on win32, the Go target entry — mirroring the -// individual deletes in `deleteProfileKeyringEntry`. Without this, a Go-written -// project credential would survive `logout` on Windows. +// Go writes Windows credentials under the target-shaped form +// (`Supabase CLI:`), invisible to the plain enumeration below, so +// it's swept separately. `findCredentials` decodes every matched blob and +// aborts entirely on one undecodable entry, unlike Go's raw-byte `wincred.List`. const deleteAllKeyringEntries = ( module: KeyringModule, platform: RuntimePlatform, ): Effect.Effect => Effect.sync(() => { + if (platform === "win32") { + try { + const entries = module.findCredentials( + KEYRING_SERVICE, + `${goWindowsCredentialTarget("")}*`, + ); + for (const { account } of entries) { + deleteGoWindowsTarget(module, account); + } + } catch { + // best-effort + } + } + let entries: ReadonlyArray<{ account: string }>; try { entries = module.findCredentials(KEYRING_SERVICE); @@ -305,9 +342,6 @@ const deleteAllKeyringEntries = ( } catch { // best-effort per entry } - if (platform === "win32" && readGoWindowsTarget(module, account)) { - deleteGoWindowsTarget(module, account); - } } }); 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 06dad317de..f5320dba28 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 @@ -32,24 +32,29 @@ let throwOnSetPassword = false; let throwOnSetSecret = false; const throwOnGetPasswordAccounts = new Set(); const throwOnDeleteAccounts = new Set(); +const failDeleteAccounts = new Set(); const withTargetCalls: string[] = []; +const opaqueAccounts = new Set(); +let throwOnFindCredentials = false; vi.mock("@napi-rs/keyring", () => ({ - findCredentials: (service: string, target?: string) => - Array.from(passwords.entries()) - .filter(([key]) => - // No target → model the Windows `CredEnumerate("*")` sweep, - // which matches both the plain (`/…`) and the Go target-shaped - // (`:/…`) entries by the leading segment. With a - // target → narrow to that specific Go target (used by readGoWindowsTarget). - target === undefined - ? key.split("/")[0]!.startsWith(service) - : key.startsWith(`${target}/`), - ) - .map(([key, password]) => ({ - account: key.split("/").at(-1)!, - password, - })), + findCredentials: (service: string, target?: string) => { + if (throwOnFindCredentials) throw new Error("CredEnumerate failed"); + const targetName = (key: string) => key.split("/")[0]!; + const matchesFilter = (key: string): boolean => { + if (target === undefined) return targetName(key) === service; + if (target.endsWith("*")) return targetName(key).startsWith(target.slice(0, -1)); + return targetName(key) === target; + }; + const matches = Array.from(passwords.entries()).filter(([key]) => matchesFilter(key)); + if (matches.some(([key]) => opaqueAccounts.has(key))) { + throw new Error("BadEncoding: invalid UTF-8 data in secure storage"); + } + return matches.map(([key, password]) => ({ + account: key.split("/").at(-1)!, + password, + })); + }, Entry: class Entry { service: string; account: string; @@ -61,7 +66,9 @@ vi.mock("@napi-rs/keyring", () => ({ } static withTarget(target: string, service: string, account: string) { withTargetCalls.push(`${target}/${service}/${account}`); - return new this(service, account, target); + const entry = new this(service, account, target); + if (!passwords.has(entry.key())) passwords.set(entry.key(), ""); + return entry; } key(): string { return this.target === undefined @@ -86,6 +93,7 @@ vi.mock("@napi-rs/keyring", () => ({ deleteCredential(): boolean { const key = this.key(); if (throwOnDeleteAccounts.has(key)) throw new Error("Keyring delete failed"); + if (failDeleteAccounts.has(key)) return false; if (!passwords.has(key)) throw new Error("not found"); passwords.delete(key); return true; @@ -140,6 +148,9 @@ beforeEach(() => { throwOnGetPasswordAccounts.clear(); throwOnDeleteAccounts.clear(); withTargetCalls.length = 0; + opaqueAccounts.clear(); + failDeleteAccounts.clear(); + throwOnFindCredentials = false; tempHome = mkdtempSync(join(tmpdir(), "supabase-legacy-creds-")); }); @@ -337,7 +348,7 @@ describe("legacyCredentialsLayer.saveAccessToken", () => { return Effect.gen(function* () { const { saveAccessToken } = yield* LegacyCredentials; yield* saveAccessToken(VALID_TOKEN); - expect(passwords.has(goWindowsKey("supabase"))).toBe(false); + expect(passwords.get(goWindowsKey("supabase")) ?? "").toBe(""); expect(passwords.has("Supabase CLI/supabase")).toBe(false); const content = readFileSync(join(tempHome, ".supabase", "access-token"), "utf-8"); expect(content).toBe(VALID_TOKEN); @@ -423,6 +434,31 @@ describe("legacyCredentialsLayer.deleteAccessToken", () => { }, ); + it.effect("win32: no Go target credential → LegacyNotLoggedInError, nothing fabricated", () => { + return Effect.gen(function* () { + const { deleteAccessToken } = yield* LegacyCredentials; + const exit = yield* Effect.exit(deleteAccessToken); + expect(exit._tag).toBe("Failure"); + if (exit._tag === "Failure") { + expect(JSON.stringify(exit.cause)).toContain("LegacyNotLoggedInError"); + } + expect(passwords.has(goWindowsKey("supabase"))).toBe(false); + expect(withTargetCalls).toEqual([]); + }).pipe(Effect.provide(makeLayer({ platform: "win32" }))); + }); + + it.effect("win32: an empty placeholder Go target → LegacyNotLoggedInError", () => { + passwords.set(goWindowsKey("supabase"), ""); + return Effect.gen(function* () { + const { deleteAccessToken } = yield* LegacyCredentials; + const exit = yield* Effect.exit(deleteAccessToken); + expect(exit._tag).toBe("Failure"); + if (exit._tag === "Failure") { + expect(JSON.stringify(exit.cause)).toContain("LegacyNotLoggedInError"); + } + }).pipe(Effect.provide(makeLayer({ platform: "win32" }))); + }); + it.effect( "keyring unavailable (SUPABASE_NO_KEYRING) with token in file → removes file, still NotLoggedIn", () => { @@ -521,6 +557,61 @@ describe("legacyCredentialsLayer.deleteAccessToken", () => { }).pipe(Effect.provide(makeLayer({ platform: "win32" }))); }); + it.effect( + "win32: deletes a Go target whose blob getPassword/findCredentials can't decode", + () => { + passwords.set(goWindowsKey("supabase"), VALID_TOKEN); + opaqueAccounts.add(goWindowsKey("supabase")); + return Effect.gen(function* () { + const { deleteAccessToken } = yield* LegacyCredentials; + yield* deleteAccessToken; + expect(passwords.has(goWindowsKey("supabase"))).toBe(false); + }).pipe(Effect.provide(makeLayer({ platform: "win32" }))); + }, + ); + + it.effect( + "win32: a confirmed Go target whose delete returns false → LegacyDeleteTokenError", + () => { + passwords.set(goWindowsKey("supabase"), VALID_TOKEN); + failDeleteAccounts.add(goWindowsKey("supabase")); + return Effect.gen(function* () { + const { deleteAccessToken } = yield* LegacyCredentials; + const exit = yield* Effect.exit(deleteAccessToken); + expect(exit._tag).toBe("Failure"); + if (exit._tag === "Failure") { + expect(JSON.stringify(exit.cause)).toContain("LegacyDeleteTokenError"); + } + expect(passwords.has(goWindowsKey("supabase"))).toBe(true); + }).pipe(Effect.provide(makeLayer({ platform: "win32" }))); + }, + ); + + it.effect("win32: an enumeration hiccup alone does not block logout", () => { + throwOnFindCredentials = true; + return Effect.gen(function* () { + const { deleteAccessToken } = yield* LegacyCredentials; + const exit = yield* Effect.exit(deleteAccessToken); + expect(exit._tag).toBe("Success"); + }).pipe(Effect.provide(makeLayer({ platform: "win32" }))); + }); + + it.effect( + "win32: an enumeration failure whose target also fails to delete → LegacyDeleteTokenError", + () => { + throwOnFindCredentials = true; + failDeleteAccounts.add(goWindowsKey("supabase")); + return Effect.gen(function* () { + const { deleteAccessToken } = yield* LegacyCredentials; + const exit = yield* Effect.exit(deleteAccessToken); + expect(exit._tag).toBe("Failure"); + if (exit._tag === "Failure") { + expect(JSON.stringify(exit.cause)).toContain("LegacyDeleteTokenError"); + } + }).pipe(Effect.provide(makeLayer({ platform: "win32" }))); + }, + ); + it.effect("legacy-keyring delete error is swallowed and does not change the outcome", () => { passwords.set("Supabase CLI/supabase", VALID_TOKEN); passwords.set("Supabase CLI/access-token", VALID_OAUTH_TOKEN); @@ -554,26 +645,71 @@ describe("legacyCredentialsLayer.deleteAllProjectCredentials", () => { }).pipe(Effect.provide(makeLayer({ env: { SUPABASE_NO_KEYRING: "1" } }))); }); + it.effect("never fails even when an individual delete throws", () => { + passwords.set("Supabase CLI/abcdefghijklmnopqrs1", "secret-1"); + throwOnDeleteAccounts.add("Supabase CLI/abcdefghijklmnopqrs1"); + return Effect.gen(function* () { + const { deleteAllProjectCredentials } = yield* LegacyCredentials; + const exit = yield* Effect.exit(deleteAllProjectCredentials); + expect(exit._tag).toBe("Success"); + }).pipe(Effect.provide(makeLayer())); + }); + it.effect("win32: deletes Go target-shaped project credentials", () => { - // Go stores Windows project credentials under `Supabase CLI:`, not the - // plain `Supabase CLI/` form. The sweep must delete the target form too. passwords.set(goWindowsKey("abcdefghijklmnopqrs1"), "secret-1"); + passwords.set(goWindowsKey("abcdefghijklmnopqrs2"), "secret-2"); return Effect.gen(function* () { const { deleteAllProjectCredentials } = yield* LegacyCredentials; yield* deleteAllProjectCredentials; expect(passwords.has(goWindowsKey("abcdefghijklmnopqrs1"))).toBe(false); + expect(passwords.has(goWindowsKey("abcdefghijklmnopqrs2"))).toBe(false); }).pipe(Effect.provide(makeLayer({ platform: "win32" }))); }); - it.effect("never fails even when an individual delete throws", () => { - passwords.set("Supabase CLI/abcdefghijklmnopqrs1", "secret-1"); - throwOnDeleteAccounts.add("Supabase CLI/abcdefghijklmnopqrs1"); + it.effect( + "win32: a single undecodable entry aborts the Go target sweep without failing logout", + () => { + passwords.set(goWindowsKey("abcdefghijklmnopqrs1"), "secret-1"); + passwords.set(goWindowsKey("abcdefghijklmnopqrs2"), "secret-2"); + opaqueAccounts.add(goWindowsKey("abcdefghijklmnopqrs1")); + return Effect.gen(function* () { + const { deleteAllProjectCredentials } = yield* LegacyCredentials; + const exit = yield* Effect.exit(deleteAllProjectCredentials); + expect(exit._tag).toBe("Success"); + // One undecodable entry aborts the whole findCredentials call. + expect(passwords.has(goWindowsKey("abcdefghijklmnopqrs1"))).toBe(true); + expect(passwords.has(goWindowsKey("abcdefghijklmnopqrs2"))).toBe(true); + }).pipe(Effect.provide(makeLayer({ platform: "win32" }))); + }, + ); +}); + +describe("legacyCredentialsLayer.deleteProjectCredential", () => { + it.effect("win32: deletes a Go target-shaped project credential", () => { + passwords.set(goWindowsKey("abcdefghijklmnopqrs1"), "db-password"); return Effect.gen(function* () { - const { deleteAllProjectCredentials } = yield* LegacyCredentials; - const exit = yield* Effect.exit(deleteAllProjectCredentials); - expect(exit._tag).toBe("Success"); - }).pipe(Effect.provide(makeLayer())); + const { deleteProjectCredential } = yield* LegacyCredentials; + const deleted = yield* deleteProjectCredential("abcdefghijklmnopqrs1"); + expect(deleted).toBe(true); + expect(passwords.has(goWindowsKey("abcdefghijklmnopqrs1"))).toBe(false); + }).pipe(Effect.provide(makeLayer({ platform: "win32" }))); }); + + it.effect( + "win32: a confirmed project credential whose delete fails → LegacyCredentialDeleteError", + () => { + passwords.set(goWindowsKey("abcdefghijklmnopqrs1"), "db-password"); + throwOnDeleteAccounts.add(goWindowsKey("abcdefghijklmnopqrs1")); + return Effect.gen(function* () { + const { deleteProjectCredential } = yield* LegacyCredentials; + const exit = yield* Effect.exit(deleteProjectCredential("abcdefghijklmnopqrs1")); + expect(exit._tag).toBe("Failure"); + if (exit._tag === "Failure") { + expect(JSON.stringify(exit.cause)).toContain("LegacyCredentialDeleteError"); + } + }).pipe(Effect.provide(makeLayer({ platform: "win32" }))); + }, + ); }); // Suppress unused-import nag — referenced in JSDoc / used in assertions above. From e044333d41e9d09bbfc84a851567530ec567a492 Mon Sep 17 00:00:00 2001 From: Vaibhav <117663341+7ttp@users.noreply.github.com> Date: Wed, 22 Jul 2026 15:32:58 +0530 Subject: [PATCH 76/79] fix(cli): ensure temp directory (#5904) ## TL;DR fixes `functions serve --env-file` failing with a generic `Effect.tryPromise` error on windows when `TEMP` or `TMP` points to a missing directory so basically now it creates the configured temp directory before writing runtime artifacts and preserves underlying filesystem errors, matching the go-cli behavior.... ## ref - closes https://github.com/supabase/cli/issues/5892 --- .../functions/serve/serve.integration.test.ts | 113 ++++++++++++++++++ apps/cli/src/shared/functions/serve.ts | 31 +++-- 2 files changed, 134 insertions(+), 10 deletions(-) diff --git a/apps/cli/src/legacy/commands/functions/serve/serve.integration.test.ts b/apps/cli/src/legacy/commands/functions/serve/serve.integration.test.ts index b9c475b004..c3d5a8edf7 100644 --- a/apps/cli/src/legacy/commands/functions/serve/serve.integration.test.ts +++ b/apps/cli/src/legacy/commands/functions/serve/serve.integration.test.ts @@ -157,6 +157,33 @@ function extractFlagValues(args: ReadonlyArray, flag: string) { return args.flatMap((value, index) => (args[index - 1] === flag ? [value] : [])); } +function withConfiguredTempDir( + directory: string, + effect: Effect.Effect, +): Effect.Effect { + const names = ["TMPDIR", "TMP", "TEMP"] as const; + const previous = names.map((name) => [name, process.env[name]] as const); + + return Effect.sync(() => { + for (const name of names) { + process.env[name] = directory; + } + }).pipe( + Effect.andThen(effect), + Effect.ensuring( + Effect.sync(() => { + for (const [name, value] of previous) { + if (value === undefined) { + delete process.env[name]; + } else { + process.env[name] = value; + } + } + }), + ), + ); +} + async function extractDockerEnvEntries(call: { args: ReadonlyArray; options: unknown }) { const values = extractFlagValues(call.args, "-e"); if (values.some((value) => value.includes("="))) { @@ -650,6 +677,92 @@ describe("legacy functions serve integration", () => { }); }); + it.live("creates a missing configured temp directory for runtime artifacts", () => { + const configuredTempDir = join(tempRoot.current, "missing-temp"); + const multilineValue = "line-1\nline-2"; + + return Effect.gen(function* () { + yield* Effect.promise(() => + writeProjectConfig(['project_id = "test-project"', ""].join("\n")), + ); + yield* Effect.promise(() => + writeFunctionFile("hello", "index.ts", 'Deno.serve(() => new Response("hello"))\n'), + ); + yield* Effect.promise(() => + writeProjectFile( + ".env.local", + [`SINGLE_LINE=value`, `MULTILINE_VALUE="${multilineValue}"`, ""].join("\n"), + ), + ); + + const { layer } = setupServe(); + const error = yield* withConfiguredTempDir( + configuredTempDir, + legacyFunctionsServe( + baseFlags({ + envFile: Option.some(".env.local"), + noVerifyJwt: Option.some(true), + }), + ).pipe(Effect.provide(layer), Effect.flip), + ); + + expect(error).toBeInstanceOf(Error); + expect(existsSync(configuredTempDir)).toBe(true); + + const dockerRun = deployMockState.runCalls.find( + (call) => call.command === "docker" && call.args[0] === "run", + ); + expect(dockerRun).toBeDefined(); + if (dockerRun === undefined) { + throw new Error("expected docker run call"); + } + + const envs = yield* Effect.promise(() => extractDockerEnvEntries(dockerRun)); + expect(envs).toContain("SINGLE_LINE=value"); + + const options = + typeof dockerRun.options === "object" && dockerRun.options !== null + ? dockerRun.options + : undefined; + const multilineEnvFiles = + options !== undefined && "multilineEnvFiles" in options + ? (options.multilineEnvFiles as Record | undefined) + : undefined; + expect(multilineEnvFiles).toEqual({ "env-0": multilineValue }); + }); + }); + + it.live("surfaces filesystem errors for an unusable configured temp path", () => { + const configuredTempPath = join(tempRoot.current, "temp-file"); + + return Effect.gen(function* () { + yield* Effect.promise(() => + writeProjectConfig(['project_id = "test-project"', ""].join("\n")), + ); + yield* Effect.promise(() => + writeFunctionFile("hello", "index.ts", 'Deno.serve(() => new Response("hello"))\n'), + ); + yield* Effect.promise(() => writeFile(configuredTempPath, "not a directory")); + + const { layer } = setupServe(); + const error = yield* withConfiguredTempDir( + configuredTempPath, + legacyFunctionsServe(baseFlags()).pipe(Effect.provide(layer), Effect.flip), + ); + + expect(error).toBeInstanceOf(Error); + if (error instanceof Error) { + expect(error.message).toContain(configuredTempPath); + expect(error.message).not.toContain("An error occurred in Effect.tryPromise"); + } + expect( + deployMockState.runCalls.filter( + (call) => call.command === "docker" && call.args[0] === "run", + ), + ).toHaveLength(0); + }); + }); + it.live("fails before startup when a multiline env name is not a shell identifier", () => { return Effect.gen(function* () { yield* Effect.promise(() => diff --git a/apps/cli/src/shared/functions/serve.ts b/apps/cli/src/shared/functions/serve.ts index 13a27a8470..b0041ee8ec 100644 --- a/apps/cli/src/shared/functions/serve.ts +++ b/apps/cli/src/shared/functions/serve.ts @@ -818,13 +818,19 @@ function splitEnvEntry(entry: string) { : ([entry.slice(0, separatorIndex), entry.slice(separatorIndex + 1)] as const); } +async function createFunctionsServeTempDir(prefix: string) { + const root = tmpdir(); + await mkdir(root, { recursive: true }); + return mkdtemp(join(root, prefix)); +} + async function writeDockerEnvFile(env: Readonly>) { const entries = Object.entries(env); if (entries.length === 0) { return undefined; } - const dir = await mkdtemp(join(tmpdir(), "supabase-functions-serve-env-")); + const dir = await createFunctionsServeTempDir("supabase-functions-serve-env-"); const path = join(dir, "docker.env"); // The file holds the JWT secret, anon/service-role keys, and JWKS, so keep it // owner-only rather than relying on the process umask. @@ -850,7 +856,7 @@ async function writeDockerMultilineEnvScript( return undefined; } - const dir = await mkdtemp(join(tmpdir(), "supabase-functions-serve-multiline-env-")); + const dir = await createFunctionsServeTempDir("supabase-functions-serve-multiline-env-"); const scriptName = "multiline-env.sh"; const path = join(dir, scriptName); const envDir = join(containerDir, "values"); @@ -1253,7 +1259,7 @@ export function buildServeEntrypointCommand( async function writeServeMainTemplateFile(template: string) { // Mount the bundled runtime template instead of embedding it in `sh -c` so // Windows does not hit `uv_spawn` ENAMETOOLONG on path-heavy projects. - const dir = await mkdtemp(join(tmpdir(), "supabase-functions-serve-main-")); + const dir = await createFunctionsServeTempDir("supabase-functions-serve-main-"); const pathname = join(dir, "index.ts"); await writeFile(pathname, template); return { @@ -1413,11 +1419,15 @@ const startEdgeRuntime = Effect.fnUntraced(function* (input: { try: () => validateDockerMultilineEnvNames(multilineDockerEnv), catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))), }); - const dockerEnvFile = yield* Effect.tryPromise(() => writeDockerEnvFile(singleLineDockerEnv)); + const dockerEnvFile = yield* Effect.tryPromise({ + try: () => writeDockerEnvFile(singleLineDockerEnv), + catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))), + }); const multilineEnvDir = "/root/.supabase/multiline-env"; - const dockerMultilineEnvScript = yield* Effect.tryPromise(() => - writeDockerMultilineEnvScript(multilineDockerEnv, multilineEnvDir), - ).pipe(Effect.mapError((cause) => (cause instanceof Error ? cause : new Error(String(cause))))); + const dockerMultilineEnvScript = yield* Effect.tryPromise({ + try: () => writeDockerMultilineEnvScript(multilineDockerEnv, multilineEnvDir), + catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))), + }); const labels = dockerProjectLabels(projectId); const runtimeCommand = [ @@ -1430,9 +1440,10 @@ const startEdgeRuntime = Effect.fnUntraced(function* (input: { ...(input.debug ? ["--verbose"] : []), ]; const serveMainTemplate = yield* Effect.promise(() => getLegacyFunctionsServeMainTemplate()); - const serveMainTemplateFile = yield* Effect.tryPromise(() => - writeServeMainTemplateFile(serveMainTemplate), - ).pipe(Effect.mapError((cause) => (cause instanceof Error ? cause : new Error(String(cause))))); + const serveMainTemplateFile = yield* Effect.tryPromise({ + try: () => writeServeMainTemplateFile(serveMainTemplate), + catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))), + }); const command = [ "run", "-d", From 96c682eb2fc0e54238c770f10e0d0d3d69641d92 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 23 Jul 2026 00:11:06 +0000 Subject: [PATCH 77/79] chore(deps): bump the go-minor group across 2 directories with 2 updates (#5933) Bumps the go-minor group with 2 updates in the /apps/cli-go directory: [github.com/posthog/posthog-go](https://github.com/posthog/posthog-go) and [google.golang.org/grpc](https://github.com/grpc/grpc-go). Bumps the go-minor group with 1 update in the /apps/cli-go/pkg directory: [google.golang.org/grpc](https://github.com/grpc/grpc-go). Updates `github.com/posthog/posthog-go` from 1.17.5 to 1.19.0
Release notes

Sourced from github.com/posthog/posthog-go's releases.

1.19.0

Unreleased

1.18.0

Unreleased

Changelog

Sourced from github.com/posthog/posthog-go's changelog.

1.19.0

Minor Changes

  • 154fd08: Add a $feature_flag_has_experiment boolean property to $feature_flag_called events. The value comes from the has_experiment field the server reports on flag metadata (/flags?v=2) and on local-evaluation flag definitions. The property is only sent when the server explicitly reported has_experiment (true or false); it is omitted when unknown (older deployments, v3 responses, or flags missing from the response).

1.18.0

Minor Changes

  • 39c72dc: Drop the newest event instead of blocking when the in-memory queue is full, and make the queue size configurable.

    • Enqueue now returns ErrQueueFull when the queue is full, dropping the newest message rather than blocking the caller until space frees up. This matches posthog-python and posthog-rs. The drop is reported only through the returned error, not through Callback.Failure, so it stays cheap and off the callback goroutines under sustained overload. Backfill/bulk callers: check the error returned by Enqueue for ErrQueueFull and throttle or retry (or raise MaxQueueSize); otherwise events that overflow the queue are dropped, not delayed.
    • Add Config.MaxQueueSize (default DefaultMaxQueueSize = 10000) to control the in-memory message queue capacity independently of BatchSize. It is clamped up to BatchSize so the queue always holds at least one full batch. This replaces the previous hardcoded BatchSize * 10 sizing.
    • Change the default BatchSize (DefaultBatchSize) from 250 to 100, aligning with posthog-python, posthog-node, and posthog-rs. Callers that set BatchSize explicitly are unaffected.
Commits
  • 901880f chore: release v1.19.0 [version bump] [skip ci]
  • 154fd08 feat(flags): add $feature_flag_has_experiment to $feature_flag_called events ...
  • 9e7c67e chore: release v1.18.0 [version bump] [skip ci]
  • 39c72dc feat: drop newest on full queue, add configurable MaxQueueSize (#258)
  • d9b6e46 feat: add SecretKey config, deprecate PersonalApiKey (#253)
  • 67f00c8 fix(compliance): retry feature flag requests (#257)
  • See full diff in compare view

Updates `google.golang.org/grpc` from 1.82.0 to 1.82.1
Release notes

Sourced from google.golang.org/grpc's releases.

Release 1.82.1

Security

  • server: Stop reading from the connection when flooded by HTTP/2 frames. The default value for this limit is 100 frames, excluding DATA and HEADERS, and may be changed by setting environment variable GRPC_GO_EXPERIMENTAL_CONTROL_BUFFER_THROTTLE_LIMIT.
  • xds/rbac: Support Metadata and RequestedServerName permissions matcher fields. If present in a DENY rule, previously these would be ignored and fail-open.
  • xds/rbac: Fix panic when parsing unsupported fields in NotRule/NotId permissions.
  • xds/rbac: Support the deprecated source_ip principal identifier by treating it as equivalent to direct_remote_ip.
Commits

Updates `google.golang.org/grpc` from 1.82.0 to 1.82.1
Release notes

Sourced from google.golang.org/grpc's releases.

Release 1.82.1

Security

  • server: Stop reading from the connection when flooded by HTTP/2 frames. The default value for this limit is 100 frames, excluding DATA and HEADERS, and may be changed by setting environment variable GRPC_GO_EXPERIMENTAL_CONTROL_BUFFER_THROTTLE_LIMIT.
  • xds/rbac: Support Metadata and RequestedServerName permissions matcher fields. If present in a DENY rule, previously these would be ignored and fail-open.
  • xds/rbac: Fix panic when parsing unsupported fields in NotRule/NotId permissions.
  • xds/rbac: Support the deprecated source_ip principal identifier by treating it as equivalent to direct_remote_ip.
Commits

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/cli-go/go.mod | 4 ++-- apps/cli-go/go.sum | 8 ++++---- apps/cli-go/pkg/go.mod | 2 +- apps/cli-go/pkg/go.sum | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/apps/cli-go/go.mod b/apps/cli-go/go.mod index a90f0950e2..397743b9de 100644 --- a/apps/cli-go/go.mod +++ b/apps/cli-go/go.mod @@ -42,7 +42,7 @@ require ( github.com/multigres/multigres v0.0.0-20260126223308-f5a52171bbc4 github.com/oapi-codegen/nullable v1.2.0 github.com/olekukonko/tablewriter v1.1.4 - github.com/posthog/posthog-go v1.17.5 + github.com/posthog/posthog-go v1.19.0 github.com/spf13/afero v1.15.0 github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 @@ -58,7 +58,7 @@ require ( golang.org/x/net v0.57.0 golang.org/x/oauth2 v0.36.0 golang.org/x/term v0.45.0 - google.golang.org/grpc v1.82.0 + google.golang.org/grpc v1.82.1 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/apps/cli-go/go.sum b/apps/cli-go/go.sum index dc19b3e60c..e08e20462a 100644 --- a/apps/cli-go/go.sum +++ b/apps/cli-go/go.sum @@ -970,8 +970,8 @@ github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/posthog/posthog-go v1.17.5 h1:mrLiAdyiQpl8Yeyg23iShyAztJMaUrYzsBjKeH52Aak= -github.com/posthog/posthog-go v1.17.5/go.mod h1://M430hNH3e8CDv4i8SJesb26816Mpa6GIZaiP4pNQU= +github.com/posthog/posthog-go v1.19.0 h1:GsPilbAfQo8dbIo2yheTZ8pbmE5yYW3G289kXrxZxFA= +github.com/posthog/posthog-go v1.19.0/go.mod h1://M430hNH3e8CDv4i8SJesb26816Mpa6GIZaiP4pNQU= github.com/prometheus/client_golang v0.9.0-pre1.0.20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= @@ -1445,8 +1445,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go. google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk= google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.0.5/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= -google.golang.org/grpc v1.82.0 h1:vguDnZUPjE26w09A63VoxZPnvPjB5Riyc0mkXPFmAIU= -google.golang.org/grpc v1.82.0/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= +google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= +google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= diff --git a/apps/cli-go/pkg/go.mod b/apps/cli-go/pkg/go.mod index b642f8d924..4844bcdbcd 100644 --- a/apps/cli-go/pkg/go.mod +++ b/apps/cli-go/pkg/go.mod @@ -26,7 +26,7 @@ require ( github.com/stretchr/testify v1.11.1 github.com/tidwall/jsonc v0.3.3 golang.org/x/mod v0.38.0 - google.golang.org/grpc v1.82.0 + google.golang.org/grpc v1.82.1 ) require ( diff --git a/apps/cli-go/pkg/go.sum b/apps/cli-go/pkg/go.sum index 796bef2f54..40aeed832e 100644 --- a/apps/cli-go/pkg/go.sum +++ b/apps/cli-go/pkg/go.sum @@ -290,8 +290,8 @@ golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/grpc v1.82.0 h1:vguDnZUPjE26w09A63VoxZPnvPjB5Riyc0mkXPFmAIU= -google.golang.org/grpc v1.82.0/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= +google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= +google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= From f3fb960a22cb122fb6a53ba1111ada33ee812a08 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 23 Jul 2026 00:12:50 +0000 Subject: [PATCH 78/79] chore(ci): bump the actions-major group with 3 updates (#5934) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the actions-major group with 3 updates: [jdx/mise-action](https://github.com/jdx/mise-action), [github/codeql-action/init](https://github.com/github/codeql-action) and [github/codeql-action/analyze](https://github.com/github/codeql-action). Updates `jdx/mise-action` from 4.2.0 to 4.2.1
Release notes

Sourced from jdx/mise-action's releases.

v4.2.1: Signed checksums and PATH export fix

A small patch release with two user-facing fixes: mise downloads are now verified against minisign-signed release checksums by default, and the env input no longer leaks the runner's PATH into subsequent steps.

Fixed

Verify mise downloads with signed checksums (#548) by @​jdx

The action now embeds mise's minisign public key and verifies SHASUMS256.txt.minisig before trusting any release checksums, then checks the downloaded mise binary's SHA256 against the verified list. This applies to both GitHub release archives (verified before extraction) and the default mise.jdx.dev CDN path (verified against the signed checksum for the matching release asset). If a CDN download fails verification, the action warns and falls back to the signed GitHub release asset instead of installing an unverified binary.

  • The existing sha256 input still works as an explicit override.
  • Pinned mise versions older than 2024.12.24 (which predate minisign checksums) get a warning and skip signed verification rather than failing.
  • Because tar installs now extract from a verified file on disk, the previous streaming download | tar fast path is replaced with a download-then-verify-then-extract flow.

Thanks to @​potiuk for the detailed threat-model writeup in #547.

Exclude PATH from environment export (#556) by @​jdx

The env input has always documented that "PATH modifications are not part of this", but since the switch to mise env --json in #252 (needed for redaction support), the action was exporting every string value returned by mise — including the computed PATH — into GITHUB_ENV. That effectively snapshotted the runner's entire PATH into subsequent steps and let [env] _.path entries in mise.toml leak past the action's own PATH management.

exportMiseEnv now skips PATH (case-insensitive) when exporting JSON env vars, restoring the documented behavior. Normal mise env vars are still exported, and PATH continues to be managed by the action's own setup (e.g. add_shims_to_path). Fixes #555.

Full Changelog: https://github.com/jdx/mise-action/compare/v4.2.0...v4.2.1

Changelog

Sourced from jdx/mise-action's changelog.

Changelog


4.2.1 - 2026-07-16

🐛 Bug Fixes

🔍 Other Changes

⚙️ Miscellaneous Tasks


4.2.0 - 2026-06-17

🚀 Features

🐛 Bug Fixes

📚 Documentation

⚙️ Miscellaneous Tasks


4.1.0 - 2026-06-04

🚀 Features

🐛 Bug Fixes

... (truncated)

Commits
  • dad1bfd chore: release v4.2.1 (#530)
  • 1e7bfbb chore(ci): automate weekly releases (#557)
  • b107e20 fix: exclude PATH from environment export (#556)
  • d538618 chore(deps): update dependency prettier to v3.9.4 (#553)
  • 99eca4c chore(deps): update github/codeql-action action to v4.36.3 (#552)
  • 43d01e6 chore(deps): update dependency typescript-eslint to v8.62.1 (#551)
  • d44584d chore(deps): update dependency js-yaml to v5.2.1 (#550)
  • 4b4b9b2 chore(release): skip ai reviews for release prs (#549)
  • 0f03c79 fix: verify mise downloads with signed checksums (#548)
  • 6bd511f chore(deps): update dependency js-yaml to v5.2.0 (#546)
  • Additional commits viewable in compare view

Updates `github/codeql-action/init` from 4.37.0 to 4.37.1
Release notes

Sourced from github/codeql-action/init's releases.

v4.37.1

  • Upcoming breaking change: Add a deprecation warning for customers using CodeQL version 2.20.6 and earlier. These versions of CodeQL were discontinued on 1 July 2026 alongside GitHub Enterprise Server 3.16, and will be unsupported by the next minor release of the CodeQL Action. #3956
  • Update default CodeQL bundle version to 2.26.1. #4019
Changelog

Sourced from github/codeql-action/init's changelog.

CodeQL Action Changelog

See the releases page for the relevant changes to the CodeQL CLI and language packs.

[UNRELEASED]

No user facing changes.

4.37.3 - 22 Jul 2026

No user facing changes.

4.37.2 - 21 Jul 2026

  • The new address format for the config-file input that was introduced in CodeQL Action 4.37.0 is now enabled by default. In addition to the format described there, the remote= prefix can now be used to explicitly indicate that the input refers to a remote file. All previous input formats continue to be accepted as well. #4023
  • The CodeQL Action can now make use of configured private registries in Default Setup to retrieve CodeQL configuration files from remote repositories that require authentication. This will allow customers to store their CodeQL configuration in a single repository that can then be referenced by Default Setup workflows in other repositories. We expect to roll this and other, related changes out to everyone in July. #4007

4.37.1 - 16 Jul 2026

  • Upcoming breaking change: Add a deprecation warning for customers using CodeQL version 2.20.6 and earlier. These versions of CodeQL were discontinued on 1 July 2026 alongside GitHub Enterprise Server 3.16, and will be unsupported by the next minor release of the CodeQL Action. #3956
  • Update default CodeQL bundle version to 2.26.1. #4019

4.37.0 - 08 Jul 2026

  • Update default CodeQL bundle version to 2.26.0. #3995
  • In addition to the existing input format, the config-file input for the codeql-action/init step will soon support a new [owner/]repo[@ref][:path] format. All components except the repository name are optional. If omitted, owner defaults to the same owner as the repository the analysis is running for, ref to main, and path to .github/codeql-action.yaml. Support for this format ships in this version of the CodeQL Action, but will only be enabled over the coming weeks. #3973

4.36.3 - 01 Jul 2026

No user facing changes.

4.36.2 - 04 Jun 2026

  • Cache CodeQL CLI version information across Actions steps. #3943
  • Reduce requests while waiting for analysis processing by using exponential backoff when polling SARIF processing status. #3937
  • Update default CodeQL bundle version to 2.25.6. #3948

4.36.1 - 02 Jun 2026

No user facing changes.

4.36.0 - 22 May 2026

  • Breaking change: Bump the minimum required CodeQL bundle version to 2.19.4. #3894
  • Add support for SHA-256 Git object IDs. #3893
  • Update default CodeQL bundle version to 2.25.5. #3926

4.35.5 - 15 May 2026

  • We have improved how the JavaScript bundles for the CodeQL Action are generated to avoid duplication across bundles and reduce the size of the repository by around 70%. This should have no effect on the runtime behaviour of the CodeQL Action. #3899

... (truncated)

Commits
  • 7188fc3 Merge pull request #4020 from github/update-v4.37.1-9e7c07009
  • c8b5f69 Update changelog for v4.37.1
  • 9e7c070 Merge pull request #4014 from github/mbg/explicit-remote-prefix
  • 3492b7e Change REMOTE_PATH_PREFIX to remote=
  • 3654baa Merge remote-tracking branch 'origin/main' into mbg/explicit-remote-prefix
  • 2d682ac Merge pull request #4017 from github/dependabot/github_actions/dot-github/wor...
  • 23f6a50 Merge pull request #4009 from github/mbg/action-state/additions
  • 1ee3c75 Merge pull request #4018 from github/dependabot/github_actions/dot-github/wor...
  • e053684 Merge pull request #4015 from github/dependabot/npm_and_yarn/npm-minor-fd2e83...
  • 6803c56 Merge pull request #4019 from github/update-bundle/codeql-bundle-v2.26.1
  • Additional commits viewable in compare view

Updates `github/codeql-action/analyze` from 4.37.0 to 4.37.1
Release notes

Sourced from github/codeql-action/analyze's releases.

v4.37.1

  • Upcoming breaking change: Add a deprecation warning for customers using CodeQL version 2.20.6 and earlier. These versions of CodeQL were discontinued on 1 July 2026 alongside GitHub Enterprise Server 3.16, and will be unsupported by the next minor release of the CodeQL Action. #3956
  • Update default CodeQL bundle version to 2.26.1. #4019
Changelog

Sourced from github/codeql-action/analyze's changelog.

CodeQL Action Changelog

See the releases page for the relevant changes to the CodeQL CLI and language packs.

[UNRELEASED]

No user facing changes.

4.37.3 - 22 Jul 2026

No user facing changes.

4.37.2 - 21 Jul 2026

  • The new address format for the config-file input that was introduced in CodeQL Action 4.37.0 is now enabled by default. In addition to the format described there, the remote= prefix can now be used to explicitly indicate that the input refers to a remote file. All previous input formats continue to be accepted as well. #4023
  • The CodeQL Action can now make use of configured private registries in Default Setup to retrieve CodeQL configuration files from remote repositories that require authentication. This will allow customers to store their CodeQL configuration in a single repository that can then be referenced by Default Setup workflows in other repositories. We expect to roll this and other, related changes out to everyone in July. #4007

4.37.1 - 16 Jul 2026

  • Upcoming breaking change: Add a deprecation warning for customers using CodeQL version 2.20.6 and earlier. These versions of CodeQL were discontinued on 1 July 2026 alongside GitHub Enterprise Server 3.16, and will be unsupported by the next minor release of the CodeQL Action. #3956
  • Update default CodeQL bundle version to 2.26.1. #4019

4.37.0 - 08 Jul 2026

  • Update default CodeQL bundle version to 2.26.0. #3995
  • In addition to the existing input format, the config-file input for the codeql-action/init step will soon support a new [owner/]repo[@ref][:path] format. All components except the repository name are optional. If omitted, owner defaults to the same owner as the repository the analysis is running for, ref to main, and path to .github/codeql-action.yaml. Support for this format ships in this version of the CodeQL Action, but will only be enabled over the coming weeks. #3973

4.36.3 - 01 Jul 2026

No user facing changes.

4.36.2 - 04 Jun 2026

  • Cache CodeQL CLI version information across Actions steps. #3943
  • Reduce requests while waiting for analysis processing by using exponential backoff when polling SARIF processing status. #3937
  • Update default CodeQL bundle version to 2.25.6. #3948

4.36.1 - 02 Jun 2026

No user facing changes.

4.36.0 - 22 May 2026

  • Breaking change: Bump the minimum required CodeQL bundle version to 2.19.4. #3894
  • Add support for SHA-256 Git object IDs. #3893
  • Update default CodeQL bundle version to 2.25.5. #3926

4.35.5 - 15 May 2026

  • We have improved how the JavaScript bundles for the CodeQL Action are generated to avoid duplication across bundles and reduce the size of the repository by around 70%. This should have no effect on the runtime behaviour of the CodeQL Action. #3899

... (truncated)

Commits
  • 7188fc3 Merge pull request #4020 from github/update-v4.37.1-9e7c07009
  • c8b5f69 Update changelog for v4.37.1
  • 9e7c070 Merge pull request #4014 from github/mbg/explicit-remote-prefix
  • 3492b7e Change REMOTE_PATH_PREFIX to remote=
  • 3654baa Merge remote-tracking branch 'origin/main' into mbg/explicit-remote-prefix
  • 2d682ac Merge pull request #4017 from github/dependabot/github_actions/dot-github/wor...
  • 23f6a50 Merge pull request #4009 from github/mbg/action-state/additions
  • 1ee3c75 Merge pull request #4018 from github/dependabot/github_actions/dot-github/wor...
  • e053684 Merge pull request #4015 from github/dependabot/npm_and_yarn/npm-minor-fd2e83...
  • 6803c56 Merge pull request #4019 from github/update-bundle/codeql-bundle-v2.26.1
  • Additional commits viewable in compare view

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/cli-go-ci.yml | 2 +- .github/workflows/cli-go-codeql.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cli-go-ci.yml b/.github/workflows/cli-go-ci.yml index 72234c451a..17b3a08d72 100644 --- a/.github/workflows/cli-go-ci.yml +++ b/.github/workflows/cli-go-ci.yml @@ -78,7 +78,7 @@ jobs: with: persist-credentials: false - - uses: jdx/mise-action@e6a8b3978addb5a52f2b4cd9d91eafa7f0ab959d # v4 + - uses: jdx/mise-action@dad1bfd3df957f44999b559dd69dc1671cb4e9ea # v4 with: version: 2026.7.0 install: true diff --git a/.github/workflows/cli-go-codeql.yml b/.github/workflows/cli-go-codeql.yml index e55cfffdfd..6a3ad595f1 100644 --- a/.github/workflows/cli-go-codeql.yml +++ b/.github/workflows/cli-go-codeql.yml @@ -67,7 +67,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 + uses: github/codeql-action/init@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -95,7 +95,7 @@ jobs: exit 1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 + uses: github/codeql-action/analyze@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1 with: category: "/language:${{matrix.language}}" defaults: From 621aaff04140b676a40a5a96815ab32fdaf86775 Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Thu, 23 Jul 2026 13:40:58 +0100 Subject: [PATCH 79/79] fix(cli): skip OS junk files when seeding storage buckets (#5938) ## Summary * Skip `.DS_Store`, `Thumbs.db`, and `desktop.ini` when walking a bucket's `objects_path` during `supabase seed buckets`, logging `Skipping OS metadata file: ` instead of uploading them * Previously these OS-generated files either aborted the whole seed run (415 invalid_mime_type on MIME-restricted buckets) or silently became public objects * TS-only fix (legacy port); `apps/cli-go` is left untouched as it's the frozen Go reference Fixes supabase/cli#5931, closes supabase/cli#5931 --- .../commands/seed/buckets/SIDE_EFFECTS.md | 1 + .../seed/buckets/buckets.integration.test.ts | 89 +++++++++++++++++++ .../src/legacy/shared/legacy-seed-buckets.ts | 16 ++++ 3 files changed, 106 insertions(+) diff --git a/apps/cli/src/legacy/commands/seed/buckets/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/seed/buckets/SIDE_EFFECTS.md index 1f33b689e5..e884f1f397 100644 --- a/apps/cli/src/legacy/commands/seed/buckets/SIDE_EFFECTS.md +++ b/apps/cli/src/legacy/commands/seed/buckets/SIDE_EFFECTS.md @@ -116,6 +116,7 @@ Creating vector bucket: Pruning vector bucket: Uploading: / => / Skipping non-regular file: +Skipping OS metadata file: WARNING: Vector buckets are not available in this project's region yet. Skipping vector bucket seeding. WARNING: Vector buckets are not available in the local storage service. If this project is linked, run `supabase link` to update service versions, then restart the local stack. Skipping vector bucket seeding. ``` diff --git a/apps/cli/src/legacy/commands/seed/buckets/buckets.integration.test.ts b/apps/cli/src/legacy/commands/seed/buckets/buckets.integration.test.ts index 1d3ef890c9..a689e82bfb 100644 --- a/apps/cli/src/legacy/commands/seed/buckets/buckets.integration.test.ts +++ b/apps/cli/src/legacy/commands/seed/buckets/buckets.integration.test.ts @@ -1285,6 +1285,95 @@ describe("legacy seed buckets", () => { }); }); + it.live("skips OS metadata files during the object walk (CLI-1950)", () => { + // .DS_Store (macOS Finder), Thumbs.db and desktop.ini (Windows Explorer) + // must never be uploaded as seeded objects — they are never even attempted, + // covering the "silently becomes a public object" failure mode, not just + // an upload-time abort. + mkdirSync(join(tmp.current, "supabase", "assets"), { recursive: true }); + writeFileSync(join(tmp.current, "supabase", "assets", "a.txt"), "hello"); + writeFileSync(join(tmp.current, "supabase", "assets", ".DS_Store"), "junk"); + writeFileSync(join(tmp.current, "supabase", "assets", "Thumbs.db"), "junk"); + writeFileSync(join(tmp.current, "supabase", "assets", "desktop.ini"), "junk"); + const { layer, out, requests } = setupLegacySeedBuckets(tmp.current, { + toml: '[storage.buckets.images]\npublic = true\nobjects_path = "./assets"\n', + routes: [ + { method: "GET", match: "/storage/v1/bucket", body: [] }, + { method: "POST", match: "/storage/v1/object/", body: {} }, + { method: "POST", match: "/storage/v1/bucket", body: { name: "images" } }, + ], + }); + return Effect.gen(function* () { + const exit = yield* legacySeedBuckets(DEFAULT_FLAGS).pipe(Effect.provide(layer), Effect.exit); + expect(Exit.isSuccess(exit)).toBe(true); + expect(out.stderrText).toContain("Skipping OS metadata file: supabase/assets/.DS_Store"); + expect(out.stderrText).toContain("Skipping OS metadata file: supabase/assets/Thumbs.db"); + expect(out.stderrText).toContain("Skipping OS metadata file: supabase/assets/desktop.ini"); + expect(out.stderrText).toContain("Uploading: supabase/assets/a.txt => images/a.txt"); + const uploads = requests.filter((r) => r.url.includes("/storage/v1/object/")); + expect(uploads).toHaveLength(1); + }); + }); + + it.live( + "skips a .DS_Store file in a MIME-restricted bucket instead of uploading it (CLI-1950)", + () => { + // Reproduces the original bug report shape: a bucket with allowed_mime_types + // restricted to images. The test harness's mock HTTP route doesn't enforce + // allowed_mime_types server-side (that's real Storage-service behavior), so + // this doesn't simulate the 415 abort itself — it asserts the junk file is + // skipped and never uploaded, while the real image file still uploads. + mkdirSync(join(tmp.current, "supabase", "assets"), { recursive: true }); + writeFileSync(join(tmp.current, "supabase", "assets", "logo.png"), "fake-png-bytes"); + writeFileSync(join(tmp.current, "supabase", "assets", ".DS_Store"), "junk"); + const { layer, out, requests } = setupLegacySeedBuckets(tmp.current, { + toml: [ + "[storage.buckets.images]", + "public = true", + 'allowed_mime_types = ["image/png"]', + 'objects_path = "./assets"', + ].join("\n"), + routes: [ + { method: "GET", match: "/storage/v1/bucket", body: [] }, + { method: "POST", match: "/storage/v1/object/", body: {} }, + { method: "POST", match: "/storage/v1/bucket", body: { name: "images" } }, + ], + }); + return Effect.gen(function* () { + const exit = yield* legacySeedBuckets(DEFAULT_FLAGS).pipe( + Effect.provide(layer), + Effect.exit, + ); + expect(Exit.isSuccess(exit)).toBe(true); + expect(out.stderrText).toContain("Skipping OS metadata file: supabase/assets/.DS_Store"); + expect(out.stderrText).toContain("Uploading: supabase/assets/logo.png => images/logo.png"); + const uploads = requests.filter((r) => r.url.includes("/storage/v1/object/")); + expect(uploads).toHaveLength(1); + }); + }, + ); + + it.live("skips a .DS_Store file when objects_path points directly at it (CLI-1950)", () => { + // Covers collectFiles' single-file branch: objects_path resolves directly to + // a junk-named file rather than a directory. + mkdirSync(join(tmp.current, "supabase"), { recursive: true }); + writeFileSync(join(tmp.current, "supabase", ".DS_Store"), "junk"); + const { layer, out, requests } = setupLegacySeedBuckets(tmp.current, { + toml: '[storage.buckets.images]\npublic = true\nobjects_path = "./.DS_Store"\n', + routes: [ + { method: "GET", match: "/storage/v1/bucket", body: [] }, + { method: "POST", match: "/storage/v1/bucket", body: { name: "images" } }, + ], + }); + return Effect.gen(function* () { + const exit = yield* legacySeedBuckets(DEFAULT_FLAGS).pipe(Effect.provide(layer), Effect.exit); + expect(Exit.isSuccess(exit)).toBe(true); + expect(out.stderrText).toContain("Skipping OS metadata file: supabase/.DS_Store"); + const uploads = requests.filter((r) => r.url.includes("/storage/v1/object/")); + expect(uploads).toHaveLength(0); + }); + }); + // Root bypasses POSIX permission bits, so chmod 000 wouldn't block open() there // and the open-vs-stat distinction this test relies on would vanish. const isRoot = typeof process.getuid === "function" && process.getuid() === 0; diff --git a/apps/cli/src/legacy/shared/legacy-seed-buckets.ts b/apps/cli/src/legacy/shared/legacy-seed-buckets.ts index ff7754d5c5..8a34a4b19b 100644 --- a/apps/cli/src/legacy/shared/legacy-seed-buckets.ts +++ b/apps/cli/src/legacy/shared/legacy-seed-buckets.ts @@ -38,6 +38,14 @@ import { legacyBucketObjectKey } from "../commands/seed/buckets/buckets.upload.t const CONFIG_PATH = "supabase/config.toml"; const UPLOAD_CONCURRENCY = 5; +/** + * Well-known OS metadata files (macOS Finder, Windows Explorer) that must + * never be uploaded as seeded objects — see CLI-1950. Go has no equivalent + * skip; this is an intentional TS-only improvement over Go's current (also + * buggy) behavior. + */ +const osJunkFileNames = new Set([".DS_Store", "Thumbs.db", "desktop.ini"]); + /** * Mirrors Go's `ValidateBucketName` regex (`apps/cli-go/pkg/config/config.go:1382`). * Used to validate `[storage.buckets]` names before any Storage API call, matching @@ -537,6 +545,10 @@ const collectFiles = ( return yield* collectDir(fs, path, output, absRoot, displayRoot); } if (info.type === "File") { + if (osJunkFileNames.has(path.basename(displayRoot))) { + yield* output.raw(`Skipping OS metadata file: ${displayRoot}\n`, "stderr"); + return []; + } return [{ absPath: absRoot, displayPath: displayRoot }]; } yield* output.raw(`Skipping non-regular file: ${displayRoot}\n`, "stderr"); @@ -556,6 +568,10 @@ const collectDir = ( for (const name of names) { const absChild = path.join(absDir, name); const displayChild = path.join(displayDir, name); + if (osJunkFileNames.has(name)) { + yield* output.raw(`Skipping OS metadata file: ${displayChild}\n`, "stderr"); + continue; + } // `readLink` succeeds only on a symlink — our no-follow detector (Effect's // `stat` follows symlinks and has no `lstat`). const isSymlink = yield* fs.readLink(absChild).pipe(