Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions apps/cli/docs/go-cli-porting-status.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -316,3 +316,10 @@ Legend:
| `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:

- `projects api-keys` has a TS-only `--reveal` flag (no Go equivalent). It sends
`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.
27 changes: 17 additions & 10 deletions apps/cli/src/legacy/commands/projects/api-keys/SIDE_EFFECTS.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,9 +15,13 @@

## API Routes

| Method | Path | Auth | Request body | Response (used fields) |
| ------ | ----------------------------- | ------------ | ------------ | ------------------------------------------- |
| `GET` | `/v1/projects/{ref}/api-keys` | Bearer token | none | `[{name: string, api_key: string \| null}]` |
| Method | Path | Auth | Request body | Response (used fields) |
| ------ | ------------------------------------------- | ------------ | ------------ | ------------------------------------------- |
| `GET` | `/v1/projects/{ref}/api-keys[?reveal=true]` | Bearer token | none | `[{name: string, api_key: string \| null}]` |

The `reveal=true` query param is sent only when `--reveal` is passed; it instructs the
Management API to return the full secret keys (`sb_secret_...`) in `api_key` instead of
`null`. Without `--reveal` the param is omitted entirely (default request).

## Environment Variables

Expand All@@ -28,9 +32,10 @@

## Flags

| Flag | Type | Required | Description |
| --------------- | ------ | -------- | --------------------------------------------------------------------------- |
| `--project-ref` | string | no | Project ref of the Supabase project (resolved from linked config if absent) |
| Flag | Type | Required | Description |
| --------------- | ------- | -------- | --------------------------------------------------------------------------- |
| `--project-ref` | string | no | Project ref of the Supabase project (resolved from linked config if absent) |
| `--reveal` | boolean | no | Reveal the secret API keys in full (sends `reveal=true`); default redacted |

## Exit Codes

Expand All@@ -44,9 +49,9 @@

## Telemetry Events Fired

| Event | When | Notable properties / groups |
| ---------------------- | ------------------------------------------ | ----------------------------------------------------------------------- |
| `cli_command_executed` | post-run, success or failure (via wrapper) | `exit_code`, `duration_ms`, `flags` (`--project-ref` is telemetry-safe) |
| Event | When | Notable properties / groups |
| ---------------------- | ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- |
| `cli_command_executed` | post-run, success or failure (via wrapper) | `exit_code`, `duration_ms`, `flags` (`--project-ref` is telemetry-safe; `--reveal`'s boolean value is logged but never the key values) |

## Output

Expand DownExpand Up@@ -95,7 +100,9 @@ On failure, an `error` event is emitted instead:
## Notes

- API keys with null values (redacted by the API) render as `******` in text mode and
in the toml/env env map; the json/yaml encodings preserve the raw `null`.
in the toml/env env map; the json/yaml encodings preserve the raw `null`. Passing
`--reveal` makes the API return the secret values, so they print in full across all
formats (issue #4775). This is a TS-only flag with no Go CLI equivalent.
- The `--project-ref` flag is optional when the CLI is linked to a project via `supabase link`.
When omitted, the ref is resolved flag → env → `.temp/project-ref` → prompt on a TTY,
failing with a not-linked error otherwise.
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,6 +10,9 @@ const config = {
Flag.withDescription("Project ref of the Supabase project."),
Flag.optional,
),
reveal: Flag.boolean("reveal").pipe(
Flag.withDescription("Reveal the secret API keys in full (e.g. sb_secret_...)."),
),
};
export type LegacyProjectsApiKeysFlags = CliCommand.Command.Config.Infer<typeof config>;

Expand All@@ -21,9 +24,16 @@ export const legacyProjectsApiKeysCommand = Command.make("api-keys", config).pip
command: "supabase projects api-keys --project-ref abcdefghijklmnopqrst",
description: "List all API keys for a project",
},
{
command: "supabase projects api-keys --reveal --output json",
description: "List API keys with the secret keys revealed in full",
},
]),
Command.withHandler((flags) =>
legacyProjectsApiKeys(flags).pipe(
// `reveal` is intentionally not in `safeFlags`: it is a boolean flag, and
// boolean values are always logged verbatim by the instrumentation. Only
// string flags Go marks with `markFlagTelemetrySafe` belong in `safeFlags`.
withLegacyCommandInstrumentation({ flags, safeFlags: ["project-ref"] }),
withJsonErrorHandling,
),
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,7 +35,7 @@ export const legacyProjectsApiKeys = Effect.fn("legacy.projects.api-keys")(funct
yield* Effect.gen(function* () {
const fetching =
output.format === "text" ? yield* output.task("Fetching API keys...") : undefined;
const keys: ApiKeys = yield* legacyGetProjectApiKeys(ref).pipe(
const keys: ApiKeys = yield* legacyGetProjectApiKeys(ref, flags.reveal).pipe(
Effect.tapError(() => fetching?.fail() ?? Effect.void),
);
yield* fetching?.clear() ?? Effect.void;
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,6 +19,11 @@ const SAMPLE_KEYS: ApiKeys = [
{ name: "service_role", api_key: null },
];

const REVEALED_KEYS: ApiKeys = [
{ name: "anon", api_key: "anon-secret" },
{ name: "service_role", api_key: "sb_secret_revealed" },
];

const FLAG_REF = "qrstuvwxyzabcdefghij";

const tempRoot = useLegacyTempWorkdir("supabase-projects-apikeys-int-");
Expand DownExpand Up@@ -55,7 +60,7 @@ describe("legacy projects api-keys integration", () => {
it.live("lists api keys as a NAME / KEY VALUE table and masks null values", () => {
const { layer, out } = setup();
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(out.stdoutText).toContain("NAME");
expect(out.stdoutText).toContain("KEY VALUE");
expect(out.stdoutText).toContain("anon-secret");
Expand All@@ -66,23 +71,75 @@ describe("legacy projects api-keys integration", () => {
it.live("resolves the ref from --project-ref", () => {
const { layer, api } = setup();
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.some(FLAG_REF) });
yield* legacyProjectsApiKeys({ projectRef: Option.some(FLAG_REF), reveal: false });
expect(api.requests[0]?.url).toContain(`/v1/projects/${FLAG_REF}/api-keys`);
}).pipe(Effect.provide(layer));
});

it.live("resolves the ref from the linked project when --project-ref is omitted", () => {
const { layer, api } = setup();
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(api.requests[0]?.url).toContain(`/v1/projects/${LEGACY_VALID_REF}/api-keys`);
}).pipe(Effect.provide(layer));
});

it.live("omits the reveal query param by default (Go request parity)", () => {
const { layer, api } = setup();
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(api.requests[0]?.urlWithParams).not.toContain("reveal");
}).pipe(Effect.provide(layer));
});

it.live("sends reveal=true when --reveal is passed", () => {
const { layer, api } = setup({ response: REVEALED_KEYS });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: true });
expect(api.requests[0]?.urlWithParams).toContain("reveal=true");
}).pipe(Effect.provide(layer));
});

it.live("renders the revealed secret key in full in the text table", () => {
const { layer, out } = setup({ response: REVEALED_KEYS });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: true });
expect(out.stdoutText).toContain("sb_secret_revealed");
expect(out.stdoutText).not.toContain("******");
}).pipe(Effect.provide(layer));
});

it.live("includes the revealed secret in the env map for --output env --reveal", () => {
const { layer, out } = setup({ goOutput: "env", response: REVEALED_KEYS });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: true });
expect(out.stdoutText).toContain('SUPABASE_SERVICE_ROLE_KEY="sb_secret_revealed"');
}).pipe(Effect.provide(layer));
});

it.live("carries the revealed secret in the { keys } payload for --output-format json", () => {
const { layer, out } = setup({ format: "json", response: REVEALED_KEYS });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: true });
const success = out.messages.find((m) => m.type === "success");
expect(success?.data).toMatchObject({ keys: REVEALED_KEYS });
}).pipe(Effect.provide(layer));
});

it.live("emits the revealed secret in the Go json array for --output json --reveal", () => {
const { layer, out } = setup({ goOutput: "json", response: REVEALED_KEYS });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: true });
expect(out.stdoutText).toContain('"api_key": "sb_secret_revealed"');
}).pipe(Effect.provide(layer));
});

it.live("fails with LegacyProjectNotLinkedError when no ref can be resolved", () => {
const { layer } = setup({ projectId: Option.none() });
return Effect.gen(function* () {
const exit = yield* Effect.exit(legacyProjectsApiKeys({ projectRef: Option.none() }));
const exit = yield* Effect.exit(
legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false }),
);
expect(Exit.isFailure(exit)).toBe(true);
if (Exit.isFailure(exit)) {
expect(JSON.stringify(exit.cause)).toContain("LegacyProjectNotLinkedError");
Expand All@@ -93,7 +150,7 @@ describe("legacy projects api-keys integration", () => {
it.live("emits a success event with { keys } for --output-format json", () => {
const { layer, out } = setup({ format: "json" });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
const success = out.messages.find((m) => m.type === "success");
expect(success?.data).toMatchObject({ keys: SAMPLE_KEYS });
}).pipe(Effect.provide(layer));
Expand All@@ -102,15 +159,15 @@ describe("legacy projects api-keys integration", () => {
it.live("emits a success event for --output-format stream-json", () => {
const { layer, out } = setup({ format: "stream-json" });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(out.messages.find((m) => m.type === "success")).toBeDefined();
}).pipe(Effect.provide(layer));
});

it.live("encodes the SUPABASE_<NAME>_KEY map for --output env", () => {
const { layer, out } = setup({ goOutput: "env" });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(out.stdoutText).toContain('SUPABASE_ANON_KEY="anon-secret"');
expect(out.stdoutText).toContain('SUPABASE_SERVICE_ROLE_KEY="******"');
}).pipe(Effect.provide(layer));
Expand All@@ -119,15 +176,15 @@ describe("legacy projects api-keys integration", () => {
it.live("encodes the SUPABASE_<NAME>_KEY map for --output toml", () => {
const { layer, out } = setup({ goOutput: "toml" });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(out.stdoutText).toContain('SUPABASE_ANON_KEY = "anon-secret"');
}).pipe(Effect.provide(layer));
});

it.live("emits a JSON array of api keys for --output json", () => {
const { layer, out } = setup({ goOutput: "json" });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(out.stdoutText).toContain('"name": "anon"');
expect(out.stdoutText.startsWith("[\n")).toBe(true);
}).pipe(Effect.provide(layer));
Expand All@@ -136,15 +193,17 @@ describe("legacy projects api-keys integration", () => {
it.live("emits a YAML array for --output yaml", () => {
const { layer, out } = setup({ goOutput: "yaml" });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(out.stdoutText).toContain("name: anon");
}).pipe(Effect.provide(layer));
});

it.live("fails with LegacyProjectsApiKeysNetworkError on transport failure", () => {
const { layer } = setup({ network: "fail" });
return Effect.gen(function* () {
const exit = yield* Effect.exit(legacyProjectsApiKeys({ projectRef: Option.none() }));
const exit = yield* Effect.exit(
legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false }),
);
expect(Exit.isFailure(exit)).toBe(true);
if (Exit.isFailure(exit)) {
const json = JSON.stringify(exit.cause);
Expand All@@ -157,7 +216,9 @@ describe("legacy projects api-keys integration", () => {
it.live("maps HTTP 503 to `unexpected get api keys status 503`", () => {
const { layer } = setup({ status: 503, response: [] });
return Effect.gen(function* () {
const exit = yield* Effect.exit(legacyProjectsApiKeys({ projectRef: Option.none() }));
const exit = yield* Effect.exit(
legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false }),
);
expect(Exit.isFailure(exit)).toBe(true);
if (Exit.isFailure(exit)) {
const json = JSON.stringify(exit.cause);
Expand Down
17 changes: 11 additions & 6 deletions apps/cli/src/legacy/shared/legacy-get-api-keys.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,15 +19,20 @@ const mapApiKeysError = mapLegacyHttpError({

/**
* Ports Go's `apiKeys.RunGetApiKeys` (`apps/cli-go/internal/projects/apiKeys/api_keys.go:41-49`):
* `GET /v1/projects/{ref}/api-keys` with no `reveal` param, mapping transport /
* non-200 failures to the same `failed to get api keys` / `unexpected get api keys
* status` errors Go raises. Shared by `projects api-keys` (display) and `bootstrap`
* (which derives the `.env` keys).
* `GET /v1/projects/{ref}/api-keys`, mapping transport / non-200 failures to the same
* `failed to get api keys` / `unexpected get api keys status` errors Go raises. Shared by
* `projects api-keys` (display) and `bootstrap` (which derives the `.env` keys).
*
* When `reveal` is `true`, the `reveal=true` query param is sent so the Management API
* returns the full secret keys (prefix `sb_secret_`) in `api_key` instead of `null`
* (issue #4775). The param is omitted entirely when `reveal` is `false` to keep the
* default request byte-identical to Go's (`bootstrap` only consumes the never-redacted
* anon key, so it stays on the default path).
*/
export const legacyGetProjectApiKeys = Effect.fnUntraced(function* (ref: string) {
export const legacyGetProjectApiKeys = Effect.fnUntraced(function* (ref: string, reveal = false) {
const api = yield* LegacyPlatformApi;
const keys: ApiKeys = yield* api.v1
.getProjectApiKeys({ ref })
.getProjectApiKeys(reveal ? { ref, reveal: true } : { ref })
.pipe(Effect.catch(mapApiKeysError));
return keys;
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
feat(cli): add --reveal flag to projects api-keys by Coly010 · Pull Request #5633 · supabase/cli · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions apps/cli/docs/go-cli-porting-status.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -316,3 +316,10 @@ Legend:
| `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:

- `projects api-keys` has a TS-only `--reveal` flag (no Go equivalent). It sends
`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.
27 changes: 17 additions & 10 deletions apps/cli/src/legacy/commands/projects/api-keys/SIDE_EFFECTS.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,9 +15,13 @@

## API Routes

| Method | Path | Auth | Request body | Response (used fields) |
| ------ | ----------------------------- | ------------ | ------------ | ------------------------------------------- |
| `GET` | `/v1/projects/{ref}/api-keys` | Bearer token | none | `[{name: string, api_key: string \| null}]` |
| Method | Path | Auth | Request body | Response (used fields) |
| ------ | ------------------------------------------- | ------------ | ------------ | ------------------------------------------- |
| `GET` | `/v1/projects/{ref}/api-keys[?reveal=true]` | Bearer token | none | `[{name: string, api_key: string \| null}]` |

The `reveal=true` query param is sent only when `--reveal` is passed; it instructs the
Management API to return the full secret keys (`sb_secret_...`) in `api_key` instead of
`null`. Without `--reveal` the param is omitted entirely (default request).

## Environment Variables

Expand All@@ -28,9 +32,10 @@

## Flags

| Flag | Type | Required | Description |
| --------------- | ------ | -------- | --------------------------------------------------------------------------- |
| `--project-ref` | string | no | Project ref of the Supabase project (resolved from linked config if absent) |
| Flag | Type | Required | Description |
| --------------- | ------- | -------- | --------------------------------------------------------------------------- |
| `--project-ref` | string | no | Project ref of the Supabase project (resolved from linked config if absent) |
| `--reveal` | boolean | no | Reveal the secret API keys in full (sends `reveal=true`); default redacted |

## Exit Codes

Expand All@@ -44,9 +49,9 @@

## Telemetry Events Fired

| Event | When | Notable properties / groups |
| ---------------------- | ------------------------------------------ | ----------------------------------------------------------------------- |
| `cli_command_executed` | post-run, success or failure (via wrapper) | `exit_code`, `duration_ms`, `flags` (`--project-ref` is telemetry-safe) |
| Event | When | Notable properties / groups |
| ---------------------- | ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- |
| `cli_command_executed` | post-run, success or failure (via wrapper) | `exit_code`, `duration_ms`, `flags` (`--project-ref` is telemetry-safe; `--reveal`'s boolean value is logged but never the key values) |

## Output

Expand DownExpand Up@@ -95,7 +100,9 @@ On failure, an `error` event is emitted instead:
## Notes

- API keys with null values (redacted by the API) render as `******` in text mode and
in the toml/env env map; the json/yaml encodings preserve the raw `null`.
in the toml/env env map; the json/yaml encodings preserve the raw `null`. Passing
`--reveal` makes the API return the secret values, so they print in full across all
formats (issue #4775). This is a TS-only flag with no Go CLI equivalent.
- The `--project-ref` flag is optional when the CLI is linked to a project via `supabase link`.
When omitted, the ref is resolved flag → env → `.temp/project-ref` → prompt on a TTY,
failing with a not-linked error otherwise.
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,6 +10,9 @@ const config = {
Flag.withDescription("Project ref of the Supabase project."),
Flag.optional,
),
reveal: Flag.boolean("reveal").pipe(
Flag.withDescription("Reveal the secret API keys in full (e.g. sb_secret_...)."),
),
};
export type LegacyProjectsApiKeysFlags = CliCommand.Command.Config.Infer<typeof config>;

Expand All@@ -21,9 +24,16 @@ export const legacyProjectsApiKeysCommand = Command.make("api-keys", config).pip
command: "supabase projects api-keys --project-ref abcdefghijklmnopqrst",
description: "List all API keys for a project",
},
{
command: "supabase projects api-keys --reveal --output json",
description: "List API keys with the secret keys revealed in full",
},
]),
Command.withHandler((flags) =>
legacyProjectsApiKeys(flags).pipe(
// `reveal` is intentionally not in `safeFlags`: it is a boolean flag, and
// boolean values are always logged verbatim by the instrumentation. Only
// string flags Go marks with `markFlagTelemetrySafe` belong in `safeFlags`.
withLegacyCommandInstrumentation({ flags, safeFlags: ["project-ref"] }),
withJsonErrorHandling,
),
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,7 +35,7 @@ export const legacyProjectsApiKeys = Effect.fn("legacy.projects.api-keys")(funct
yield* Effect.gen(function* () {
const fetching =
output.format === "text" ? yield* output.task("Fetching API keys...") : undefined;
const keys: ApiKeys = yield* legacyGetProjectApiKeys(ref).pipe(
const keys: ApiKeys = yield* legacyGetProjectApiKeys(ref, flags.reveal).pipe(
Effect.tapError(() => fetching?.fail() ?? Effect.void),
);
yield* fetching?.clear() ?? Effect.void;
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,6 +19,11 @@ const SAMPLE_KEYS: ApiKeys = [
{ name: "service_role", api_key: null },
];

const REVEALED_KEYS: ApiKeys = [
{ name: "anon", api_key: "anon-secret" },
{ name: "service_role", api_key: "sb_secret_revealed" },
];

const FLAG_REF = "qrstuvwxyzabcdefghij";

const tempRoot = useLegacyTempWorkdir("supabase-projects-apikeys-int-");
Expand DownExpand Up@@ -55,7 +60,7 @@ describe("legacy projects api-keys integration", () => {
it.live("lists api keys as a NAME / KEY VALUE table and masks null values", () => {
const { layer, out } = setup();
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(out.stdoutText).toContain("NAME");
expect(out.stdoutText).toContain("KEY VALUE");
expect(out.stdoutText).toContain("anon-secret");
Expand All@@ -66,23 +71,75 @@ describe("legacy projects api-keys integration", () => {
it.live("resolves the ref from --project-ref", () => {
const { layer, api } = setup();
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.some(FLAG_REF) });
yield* legacyProjectsApiKeys({ projectRef: Option.some(FLAG_REF), reveal: false });
expect(api.requests[0]?.url).toContain(`/v1/projects/${FLAG_REF}/api-keys`);
}).pipe(Effect.provide(layer));
});

it.live("resolves the ref from the linked project when --project-ref is omitted", () => {
const { layer, api } = setup();
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(api.requests[0]?.url).toContain(`/v1/projects/${LEGACY_VALID_REF}/api-keys`);
}).pipe(Effect.provide(layer));
});

it.live("omits the reveal query param by default (Go request parity)", () => {
const { layer, api } = setup();
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(api.requests[0]?.urlWithParams).not.toContain("reveal");
}).pipe(Effect.provide(layer));
});

it.live("sends reveal=true when --reveal is passed", () => {
const { layer, api } = setup({ response: REVEALED_KEYS });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: true });
expect(api.requests[0]?.urlWithParams).toContain("reveal=true");
}).pipe(Effect.provide(layer));
});

it.live("renders the revealed secret key in full in the text table", () => {
const { layer, out } = setup({ response: REVEALED_KEYS });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: true });
expect(out.stdoutText).toContain("sb_secret_revealed");
expect(out.stdoutText).not.toContain("******");
}).pipe(Effect.provide(layer));
});

it.live("includes the revealed secret in the env map for --output env --reveal", () => {
const { layer, out } = setup({ goOutput: "env", response: REVEALED_KEYS });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: true });
expect(out.stdoutText).toContain('SUPABASE_SERVICE_ROLE_KEY="sb_secret_revealed"');
}).pipe(Effect.provide(layer));
});

it.live("carries the revealed secret in the { keys } payload for --output-format json", () => {
const { layer, out } = setup({ format: "json", response: REVEALED_KEYS });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: true });
const success = out.messages.find((m) => m.type === "success");
expect(success?.data).toMatchObject({ keys: REVEALED_KEYS });
}).pipe(Effect.provide(layer));
});

it.live("emits the revealed secret in the Go json array for --output json --reveal", () => {
const { layer, out } = setup({ goOutput: "json", response: REVEALED_KEYS });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: true });
expect(out.stdoutText).toContain('"api_key": "sb_secret_revealed"');
}).pipe(Effect.provide(layer));
});

it.live("fails with LegacyProjectNotLinkedError when no ref can be resolved", () => {
const { layer } = setup({ projectId: Option.none() });
return Effect.gen(function* () {
const exit = yield* Effect.exit(legacyProjectsApiKeys({ projectRef: Option.none() }));
const exit = yield* Effect.exit(
legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false }),
);
expect(Exit.isFailure(exit)).toBe(true);
if (Exit.isFailure(exit)) {
expect(JSON.stringify(exit.cause)).toContain("LegacyProjectNotLinkedError");
Expand All@@ -93,7 +150,7 @@ describe("legacy projects api-keys integration", () => {
it.live("emits a success event with { keys } for --output-format json", () => {
const { layer, out } = setup({ format: "json" });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
const success = out.messages.find((m) => m.type === "success");
expect(success?.data).toMatchObject({ keys: SAMPLE_KEYS });
}).pipe(Effect.provide(layer));
Expand All@@ -102,15 +159,15 @@ describe("legacy projects api-keys integration", () => {
it.live("emits a success event for --output-format stream-json", () => {
const { layer, out } = setup({ format: "stream-json" });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(out.messages.find((m) => m.type === "success")).toBeDefined();
}).pipe(Effect.provide(layer));
});

it.live("encodes the SUPABASE_<NAME>_KEY map for --output env", () => {
const { layer, out } = setup({ goOutput: "env" });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(out.stdoutText).toContain('SUPABASE_ANON_KEY="anon-secret"');
expect(out.stdoutText).toContain('SUPABASE_SERVICE_ROLE_KEY="******"');
}).pipe(Effect.provide(layer));
Expand All@@ -119,15 +176,15 @@ describe("legacy projects api-keys integration", () => {
it.live("encodes the SUPABASE_<NAME>_KEY map for --output toml", () => {
const { layer, out } = setup({ goOutput: "toml" });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(out.stdoutText).toContain('SUPABASE_ANON_KEY = "anon-secret"');
}).pipe(Effect.provide(layer));
});

it.live("emits a JSON array of api keys for --output json", () => {
const { layer, out } = setup({ goOutput: "json" });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(out.stdoutText).toContain('"name": "anon"');
expect(out.stdoutText.startsWith("[\n")).toBe(true);
}).pipe(Effect.provide(layer));
Expand All@@ -136,15 +193,17 @@ describe("legacy projects api-keys integration", () => {
it.live("emits a YAML array for --output yaml", () => {
const { layer, out } = setup({ goOutput: "yaml" });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(out.stdoutText).toContain("name: anon");
}).pipe(Effect.provide(layer));
});

it.live("fails with LegacyProjectsApiKeysNetworkError on transport failure", () => {
const { layer } = setup({ network: "fail" });
return Effect.gen(function* () {
const exit = yield* Effect.exit(legacyProjectsApiKeys({ projectRef: Option.none() }));
const exit = yield* Effect.exit(
legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false }),
);
expect(Exit.isFailure(exit)).toBe(true);
if (Exit.isFailure(exit)) {
const json = JSON.stringify(exit.cause);
Expand All@@ -157,7 +216,9 @@ describe("legacy projects api-keys integration", () => {
it.live("maps HTTP 503 to `unexpected get api keys status 503`", () => {
const { layer } = setup({ status: 503, response: [] });
return Effect.gen(function* () {
const exit = yield* Effect.exit(legacyProjectsApiKeys({ projectRef: Option.none() }));
const exit = yield* Effect.exit(
legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false }),
);
expect(Exit.isFailure(exit)).toBe(true);
if (Exit.isFailure(exit)) {
const json = JSON.stringify(exit.cause);
Expand Down
17 changes: 11 additions & 6 deletions apps/cli/src/legacy/shared/legacy-get-api-keys.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,15 +19,20 @@ const mapApiKeysError = mapLegacyHttpError({

/**
* Ports Go's `apiKeys.RunGetApiKeys` (`apps/cli-go/internal/projects/apiKeys/api_keys.go:41-49`):
* `GET /v1/projects/{ref}/api-keys` with no `reveal` param, mapping transport /
* non-200 failures to the same `failed to get api keys` / `unexpected get api keys
* status` errors Go raises. Shared by `projects api-keys` (display) and `bootstrap`
* (which derives the `.env` keys).
* `GET /v1/projects/{ref}/api-keys`, mapping transport / non-200 failures to the same
* `failed to get api keys` / `unexpected get api keys status` errors Go raises. Shared by
* `projects api-keys` (display) and `bootstrap` (which derives the `.env` keys).
*
* When `reveal` is `true`, the `reveal=true` query param is sent so the Management API
* returns the full secret keys (prefix `sb_secret_`) in `api_key` instead of `null`
* (issue #4775). The param is omitted entirely when `reveal` is `false` to keep the
* default request byte-identical to Go's (`bootstrap` only consumes the never-redacted
* anon key, so it stays on the default path).
*/
export const legacyGetProjectApiKeys = Effect.fnUntraced(function* (ref: string) {
export const legacyGetProjectApiKeys = Effect.fnUntraced(function* (ref: string, reveal = false) {
const api = yield* LegacyPlatformApi;
const keys: ApiKeys = yield* api.v1
.getProjectApiKeys({ ref })
.getProjectApiKeys(reveal ? { ref, reveal: true } : { ref })
.pipe(Effect.catch(mapApiKeysError));
return keys;
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(cli): add --reveal flag to projects api-keys by Coly010 · Pull Request #5633 · supabase/cli · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions apps/cli/docs/go-cli-porting-status.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -316,3 +316,10 @@ Legend:
| `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:

- `projects api-keys` has a TS-only `--reveal` flag (no Go equivalent). It sends
`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.
27 changes: 17 additions & 10 deletions apps/cli/src/legacy/commands/projects/api-keys/SIDE_EFFECTS.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,9 +15,13 @@

## API Routes

| Method | Path | Auth | Request body | Response (used fields) |
| ------ | ----------------------------- | ------------ | ------------ | ------------------------------------------- |
| `GET` | `/v1/projects/{ref}/api-keys` | Bearer token | none | `[{name: string, api_key: string \| null}]` |
| Method | Path | Auth | Request body | Response (used fields) |
| ------ | ------------------------------------------- | ------------ | ------------ | ------------------------------------------- |
| `GET` | `/v1/projects/{ref}/api-keys[?reveal=true]` | Bearer token | none | `[{name: string, api_key: string \| null}]` |

The `reveal=true` query param is sent only when `--reveal` is passed; it instructs the
Management API to return the full secret keys (`sb_secret_...`) in `api_key` instead of
`null`. Without `--reveal` the param is omitted entirely (default request).

## Environment Variables

Expand All@@ -28,9 +32,10 @@

## Flags

| Flag | Type | Required | Description |
| --------------- | ------ | -------- | --------------------------------------------------------------------------- |
| `--project-ref` | string | no | Project ref of the Supabase project (resolved from linked config if absent) |
| Flag | Type | Required | Description |
| --------------- | ------- | -------- | --------------------------------------------------------------------------- |
| `--project-ref` | string | no | Project ref of the Supabase project (resolved from linked config if absent) |
| `--reveal` | boolean | no | Reveal the secret API keys in full (sends `reveal=true`); default redacted |

## Exit Codes

Expand All@@ -44,9 +49,9 @@

## Telemetry Events Fired

| Event | When | Notable properties / groups |
| ---------------------- | ------------------------------------------ | ----------------------------------------------------------------------- |
| `cli_command_executed` | post-run, success or failure (via wrapper) | `exit_code`, `duration_ms`, `flags` (`--project-ref` is telemetry-safe) |
| Event | When | Notable properties / groups |
| ---------------------- | ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- |
| `cli_command_executed` | post-run, success or failure (via wrapper) | `exit_code`, `duration_ms`, `flags` (`--project-ref` is telemetry-safe; `--reveal`'s boolean value is logged but never the key values) |

## Output

Expand DownExpand Up@@ -95,7 +100,9 @@ On failure, an `error` event is emitted instead:
## Notes

- API keys with null values (redacted by the API) render as `******` in text mode and
in the toml/env env map; the json/yaml encodings preserve the raw `null`.
in the toml/env env map; the json/yaml encodings preserve the raw `null`. Passing
`--reveal` makes the API return the secret values, so they print in full across all
formats (issue #4775). This is a TS-only flag with no Go CLI equivalent.
- The `--project-ref` flag is optional when the CLI is linked to a project via `supabase link`.
When omitted, the ref is resolved flag → env → `.temp/project-ref` → prompt on a TTY,
failing with a not-linked error otherwise.
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,6 +10,9 @@ const config = {
Flag.withDescription("Project ref of the Supabase project."),
Flag.optional,
),
reveal: Flag.boolean("reveal").pipe(
Flag.withDescription("Reveal the secret API keys in full (e.g. sb_secret_...)."),
),
};
export type LegacyProjectsApiKeysFlags = CliCommand.Command.Config.Infer<typeof config>;

Expand All@@ -21,9 +24,16 @@ export const legacyProjectsApiKeysCommand = Command.make("api-keys", config).pip
command: "supabase projects api-keys --project-ref abcdefghijklmnopqrst",
description: "List all API keys for a project",
},
{
command: "supabase projects api-keys --reveal --output json",
description: "List API keys with the secret keys revealed in full",
},
]),
Command.withHandler((flags) =>
legacyProjectsApiKeys(flags).pipe(
// `reveal` is intentionally not in `safeFlags`: it is a boolean flag, and
// boolean values are always logged verbatim by the instrumentation. Only
// string flags Go marks with `markFlagTelemetrySafe` belong in `safeFlags`.
withLegacyCommandInstrumentation({ flags, safeFlags: ["project-ref"] }),
withJsonErrorHandling,
),
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,7 +35,7 @@ export const legacyProjectsApiKeys = Effect.fn("legacy.projects.api-keys")(funct
yield* Effect.gen(function* () {
const fetching =
output.format === "text" ? yield* output.task("Fetching API keys...") : undefined;
const keys: ApiKeys = yield* legacyGetProjectApiKeys(ref).pipe(
const keys: ApiKeys = yield* legacyGetProjectApiKeys(ref, flags.reveal).pipe(
Effect.tapError(() => fetching?.fail() ?? Effect.void),
);
yield* fetching?.clear() ?? Effect.void;
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,6 +19,11 @@ const SAMPLE_KEYS: ApiKeys = [
{ name: "service_role", api_key: null },
];

const REVEALED_KEYS: ApiKeys = [
{ name: "anon", api_key: "anon-secret" },
{ name: "service_role", api_key: "sb_secret_revealed" },
];

const FLAG_REF = "qrstuvwxyzabcdefghij";

const tempRoot = useLegacyTempWorkdir("supabase-projects-apikeys-int-");
Expand DownExpand Up@@ -55,7 +60,7 @@ describe("legacy projects api-keys integration", () => {
it.live("lists api keys as a NAME / KEY VALUE table and masks null values", () => {
const { layer, out } = setup();
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(out.stdoutText).toContain("NAME");
expect(out.stdoutText).toContain("KEY VALUE");
expect(out.stdoutText).toContain("anon-secret");
Expand All@@ -66,23 +71,75 @@ describe("legacy projects api-keys integration", () => {
it.live("resolves the ref from --project-ref", () => {
const { layer, api } = setup();
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.some(FLAG_REF) });
yield* legacyProjectsApiKeys({ projectRef: Option.some(FLAG_REF), reveal: false });
expect(api.requests[0]?.url).toContain(`/v1/projects/${FLAG_REF}/api-keys`);
}).pipe(Effect.provide(layer));
});

it.live("resolves the ref from the linked project when --project-ref is omitted", () => {
const { layer, api } = setup();
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(api.requests[0]?.url).toContain(`/v1/projects/${LEGACY_VALID_REF}/api-keys`);
}).pipe(Effect.provide(layer));
});

it.live("omits the reveal query param by default (Go request parity)", () => {
const { layer, api } = setup();
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(api.requests[0]?.urlWithParams).not.toContain("reveal");
}).pipe(Effect.provide(layer));
});

it.live("sends reveal=true when --reveal is passed", () => {
const { layer, api } = setup({ response: REVEALED_KEYS });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: true });
expect(api.requests[0]?.urlWithParams).toContain("reveal=true");
}).pipe(Effect.provide(layer));
});

it.live("renders the revealed secret key in full in the text table", () => {
const { layer, out } = setup({ response: REVEALED_KEYS });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: true });
expect(out.stdoutText).toContain("sb_secret_revealed");
expect(out.stdoutText).not.toContain("******");
}).pipe(Effect.provide(layer));
});

it.live("includes the revealed secret in the env map for --output env --reveal", () => {
const { layer, out } = setup({ goOutput: "env", response: REVEALED_KEYS });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: true });
expect(out.stdoutText).toContain('SUPABASE_SERVICE_ROLE_KEY="sb_secret_revealed"');
}).pipe(Effect.provide(layer));
});

it.live("carries the revealed secret in the { keys } payload for --output-format json", () => {
const { layer, out } = setup({ format: "json", response: REVEALED_KEYS });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: true });
const success = out.messages.find((m) => m.type === "success");
expect(success?.data).toMatchObject({ keys: REVEALED_KEYS });
}).pipe(Effect.provide(layer));
});

it.live("emits the revealed secret in the Go json array for --output json --reveal", () => {
const { layer, out } = setup({ goOutput: "json", response: REVEALED_KEYS });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: true });
expect(out.stdoutText).toContain('"api_key": "sb_secret_revealed"');
}).pipe(Effect.provide(layer));
});

it.live("fails with LegacyProjectNotLinkedError when no ref can be resolved", () => {
const { layer } = setup({ projectId: Option.none() });
return Effect.gen(function* () {
const exit = yield* Effect.exit(legacyProjectsApiKeys({ projectRef: Option.none() }));
const exit = yield* Effect.exit(
legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false }),
);
expect(Exit.isFailure(exit)).toBe(true);
if (Exit.isFailure(exit)) {
expect(JSON.stringify(exit.cause)).toContain("LegacyProjectNotLinkedError");
Expand All@@ -93,7 +150,7 @@ describe("legacy projects api-keys integration", () => {
it.live("emits a success event with { keys } for --output-format json", () => {
const { layer, out } = setup({ format: "json" });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
const success = out.messages.find((m) => m.type === "success");
expect(success?.data).toMatchObject({ keys: SAMPLE_KEYS });
}).pipe(Effect.provide(layer));
Expand All@@ -102,15 +159,15 @@ describe("legacy projects api-keys integration", () => {
it.live("emits a success event for --output-format stream-json", () => {
const { layer, out } = setup({ format: "stream-json" });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(out.messages.find((m) => m.type === "success")).toBeDefined();
}).pipe(Effect.provide(layer));
});

it.live("encodes the SUPABASE_<NAME>_KEY map for --output env", () => {
const { layer, out } = setup({ goOutput: "env" });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(out.stdoutText).toContain('SUPABASE_ANON_KEY="anon-secret"');
expect(out.stdoutText).toContain('SUPABASE_SERVICE_ROLE_KEY="******"');
}).pipe(Effect.provide(layer));
Expand All@@ -119,15 +176,15 @@ describe("legacy projects api-keys integration", () => {
it.live("encodes the SUPABASE_<NAME>_KEY map for --output toml", () => {
const { layer, out } = setup({ goOutput: "toml" });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(out.stdoutText).toContain('SUPABASE_ANON_KEY = "anon-secret"');
}).pipe(Effect.provide(layer));
});

it.live("emits a JSON array of api keys for --output json", () => {
const { layer, out } = setup({ goOutput: "json" });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(out.stdoutText).toContain('"name": "anon"');
expect(out.stdoutText.startsWith("[\n")).toBe(true);
}).pipe(Effect.provide(layer));
Expand All@@ -136,15 +193,17 @@ describe("legacy projects api-keys integration", () => {
it.live("emits a YAML array for --output yaml", () => {
const { layer, out } = setup({ goOutput: "yaml" });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(out.stdoutText).toContain("name: anon");
}).pipe(Effect.provide(layer));
});

it.live("fails with LegacyProjectsApiKeysNetworkError on transport failure", () => {
const { layer } = setup({ network: "fail" });
return Effect.gen(function* () {
const exit = yield* Effect.exit(legacyProjectsApiKeys({ projectRef: Option.none() }));
const exit = yield* Effect.exit(
legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false }),
);
expect(Exit.isFailure(exit)).toBe(true);
if (Exit.isFailure(exit)) {
const json = JSON.stringify(exit.cause);
Expand All@@ -157,7 +216,9 @@ describe("legacy projects api-keys integration", () => {
it.live("maps HTTP 503 to `unexpected get api keys status 503`", () => {
const { layer } = setup({ status: 503, response: [] });
return Effect.gen(function* () {
const exit = yield* Effect.exit(legacyProjectsApiKeys({ projectRef: Option.none() }));
const exit = yield* Effect.exit(
legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false }),
);
expect(Exit.isFailure(exit)).toBe(true);
if (Exit.isFailure(exit)) {
const json = JSON.stringify(exit.cause);
Expand Down
17 changes: 11 additions & 6 deletions apps/cli/src/legacy/shared/legacy-get-api-keys.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,15 +19,20 @@ const mapApiKeysError = mapLegacyHttpError({

/**
* Ports Go's `apiKeys.RunGetApiKeys` (`apps/cli-go/internal/projects/apiKeys/api_keys.go:41-49`):
* `GET /v1/projects/{ref}/api-keys` with no `reveal` param, mapping transport /
* non-200 failures to the same `failed to get api keys` / `unexpected get api keys
* status` errors Go raises. Shared by `projects api-keys` (display) and `bootstrap`
* (which derives the `.env` keys).
* `GET /v1/projects/{ref}/api-keys`, mapping transport / non-200 failures to the same
* `failed to get api keys` / `unexpected get api keys status` errors Go raises. Shared by
* `projects api-keys` (display) and `bootstrap` (which derives the `.env` keys).
*
* When `reveal` is `true`, the `reveal=true` query param is sent so the Management API
* returns the full secret keys (prefix `sb_secret_`) in `api_key` instead of `null`
* (issue #4775). The param is omitted entirely when `reveal` is `false` to keep the
* default request byte-identical to Go's (`bootstrap` only consumes the never-redacted
* anon key, so it stays on the default path).
*/
export const legacyGetProjectApiKeys = Effect.fnUntraced(function* (ref: string) {
export const legacyGetProjectApiKeys = Effect.fnUntraced(function* (ref: string, reveal = false) {
const api = yield* LegacyPlatformApi;
const keys: ApiKeys = yield* api.v1
.getProjectApiKeys({ ref })
.getProjectApiKeys(reveal ? { ref, reveal: true } : { ref })
.pipe(Effect.catch(mapApiKeysError));
return keys;
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(cli): add --reveal flag to projects api-keys by Coly010 · Pull Request #5633 · supabase/cli · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions apps/cli/docs/go-cli-porting-status.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -316,3 +316,10 @@ Legend:
| `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:

- `projects api-keys` has a TS-only `--reveal` flag (no Go equivalent). It sends
`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.
27 changes: 17 additions & 10 deletions apps/cli/src/legacy/commands/projects/api-keys/SIDE_EFFECTS.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,9 +15,13 @@

## API Routes

| Method | Path | Auth | Request body | Response (used fields) |
| ------ | ----------------------------- | ------------ | ------------ | ------------------------------------------- |
| `GET` | `/v1/projects/{ref}/api-keys` | Bearer token | none | `[{name: string, api_key: string \| null}]` |
| Method | Path | Auth | Request body | Response (used fields) |
| ------ | ------------------------------------------- | ------------ | ------------ | ------------------------------------------- |
| `GET` | `/v1/projects/{ref}/api-keys[?reveal=true]` | Bearer token | none | `[{name: string, api_key: string \| null}]` |

The `reveal=true` query param is sent only when `--reveal` is passed; it instructs the
Management API to return the full secret keys (`sb_secret_...`) in `api_key` instead of
`null`. Without `--reveal` the param is omitted entirely (default request).

## Environment Variables

Expand All@@ -28,9 +32,10 @@

## Flags

| Flag | Type | Required | Description |
| --------------- | ------ | -------- | --------------------------------------------------------------------------- |
| `--project-ref` | string | no | Project ref of the Supabase project (resolved from linked config if absent) |
| Flag | Type | Required | Description |
| --------------- | ------- | -------- | --------------------------------------------------------------------------- |
| `--project-ref` | string | no | Project ref of the Supabase project (resolved from linked config if absent) |
| `--reveal` | boolean | no | Reveal the secret API keys in full (sends `reveal=true`); default redacted |

## Exit Codes

Expand All@@ -44,9 +49,9 @@

## Telemetry Events Fired

| Event | When | Notable properties / groups |
| ---------------------- | ------------------------------------------ | ----------------------------------------------------------------------- |
| `cli_command_executed` | post-run, success or failure (via wrapper) | `exit_code`, `duration_ms`, `flags` (`--project-ref` is telemetry-safe) |
| Event | When | Notable properties / groups |
| ---------------------- | ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- |
| `cli_command_executed` | post-run, success or failure (via wrapper) | `exit_code`, `duration_ms`, `flags` (`--project-ref` is telemetry-safe; `--reveal`'s boolean value is logged but never the key values) |

## Output

Expand DownExpand Up@@ -95,7 +100,9 @@ On failure, an `error` event is emitted instead:
## Notes

- API keys with null values (redacted by the API) render as `******` in text mode and
in the toml/env env map; the json/yaml encodings preserve the raw `null`.
in the toml/env env map; the json/yaml encodings preserve the raw `null`. Passing
`--reveal` makes the API return the secret values, so they print in full across all
formats (issue #4775). This is a TS-only flag with no Go CLI equivalent.
- The `--project-ref` flag is optional when the CLI is linked to a project via `supabase link`.
When omitted, the ref is resolved flag → env → `.temp/project-ref` → prompt on a TTY,
failing with a not-linked error otherwise.
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,6 +10,9 @@ const config = {
Flag.withDescription("Project ref of the Supabase project."),
Flag.optional,
),
reveal: Flag.boolean("reveal").pipe(
Flag.withDescription("Reveal the secret API keys in full (e.g. sb_secret_...)."),
),
};
export type LegacyProjectsApiKeysFlags = CliCommand.Command.Config.Infer<typeof config>;

Expand All@@ -21,9 +24,16 @@ export const legacyProjectsApiKeysCommand = Command.make("api-keys", config).pip
command: "supabase projects api-keys --project-ref abcdefghijklmnopqrst",
description: "List all API keys for a project",
},
{
command: "supabase projects api-keys --reveal --output json",
description: "List API keys with the secret keys revealed in full",
},
]),
Command.withHandler((flags) =>
legacyProjectsApiKeys(flags).pipe(
// `reveal` is intentionally not in `safeFlags`: it is a boolean flag, and
// boolean values are always logged verbatim by the instrumentation. Only
// string flags Go marks with `markFlagTelemetrySafe` belong in `safeFlags`.
withLegacyCommandInstrumentation({ flags, safeFlags: ["project-ref"] }),
withJsonErrorHandling,
),
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,7 +35,7 @@ export const legacyProjectsApiKeys = Effect.fn("legacy.projects.api-keys")(funct
yield* Effect.gen(function* () {
const fetching =
output.format === "text" ? yield* output.task("Fetching API keys...") : undefined;
const keys: ApiKeys = yield* legacyGetProjectApiKeys(ref).pipe(
const keys: ApiKeys = yield* legacyGetProjectApiKeys(ref, flags.reveal).pipe(
Effect.tapError(() => fetching?.fail() ?? Effect.void),
);
yield* fetching?.clear() ?? Effect.void;
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,6 +19,11 @@ const SAMPLE_KEYS: ApiKeys = [
{ name: "service_role", api_key: null },
];

const REVEALED_KEYS: ApiKeys = [
{ name: "anon", api_key: "anon-secret" },
{ name: "service_role", api_key: "sb_secret_revealed" },
];

const FLAG_REF = "qrstuvwxyzabcdefghij";

const tempRoot = useLegacyTempWorkdir("supabase-projects-apikeys-int-");
Expand DownExpand Up@@ -55,7 +60,7 @@ describe("legacy projects api-keys integration", () => {
it.live("lists api keys as a NAME / KEY VALUE table and masks null values", () => {
const { layer, out } = setup();
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(out.stdoutText).toContain("NAME");
expect(out.stdoutText).toContain("KEY VALUE");
expect(out.stdoutText).toContain("anon-secret");
Expand All@@ -66,23 +71,75 @@ describe("legacy projects api-keys integration", () => {
it.live("resolves the ref from --project-ref", () => {
const { layer, api } = setup();
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.some(FLAG_REF) });
yield* legacyProjectsApiKeys({ projectRef: Option.some(FLAG_REF), reveal: false });
expect(api.requests[0]?.url).toContain(`/v1/projects/${FLAG_REF}/api-keys`);
}).pipe(Effect.provide(layer));
});

it.live("resolves the ref from the linked project when --project-ref is omitted", () => {
const { layer, api } = setup();
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(api.requests[0]?.url).toContain(`/v1/projects/${LEGACY_VALID_REF}/api-keys`);
}).pipe(Effect.provide(layer));
});

it.live("omits the reveal query param by default (Go request parity)", () => {
const { layer, api } = setup();
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(api.requests[0]?.urlWithParams).not.toContain("reveal");
}).pipe(Effect.provide(layer));
});

it.live("sends reveal=true when --reveal is passed", () => {
const { layer, api } = setup({ response: REVEALED_KEYS });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: true });
expect(api.requests[0]?.urlWithParams).toContain("reveal=true");
}).pipe(Effect.provide(layer));
});

it.live("renders the revealed secret key in full in the text table", () => {
const { layer, out } = setup({ response: REVEALED_KEYS });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: true });
expect(out.stdoutText).toContain("sb_secret_revealed");
expect(out.stdoutText).not.toContain("******");
}).pipe(Effect.provide(layer));
});

it.live("includes the revealed secret in the env map for --output env --reveal", () => {
const { layer, out } = setup({ goOutput: "env", response: REVEALED_KEYS });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: true });
expect(out.stdoutText).toContain('SUPABASE_SERVICE_ROLE_KEY="sb_secret_revealed"');
}).pipe(Effect.provide(layer));
});

it.live("carries the revealed secret in the { keys } payload for --output-format json", () => {
const { layer, out } = setup({ format: "json", response: REVEALED_KEYS });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: true });
const success = out.messages.find((m) => m.type === "success");
expect(success?.data).toMatchObject({ keys: REVEALED_KEYS });
}).pipe(Effect.provide(layer));
});

it.live("emits the revealed secret in the Go json array for --output json --reveal", () => {
const { layer, out } = setup({ goOutput: "json", response: REVEALED_KEYS });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: true });
expect(out.stdoutText).toContain('"api_key": "sb_secret_revealed"');
}).pipe(Effect.provide(layer));
});

it.live("fails with LegacyProjectNotLinkedError when no ref can be resolved", () => {
const { layer } = setup({ projectId: Option.none() });
return Effect.gen(function* () {
const exit = yield* Effect.exit(legacyProjectsApiKeys({ projectRef: Option.none() }));
const exit = yield* Effect.exit(
legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false }),
);
expect(Exit.isFailure(exit)).toBe(true);
if (Exit.isFailure(exit)) {
expect(JSON.stringify(exit.cause)).toContain("LegacyProjectNotLinkedError");
Expand All@@ -93,7 +150,7 @@ describe("legacy projects api-keys integration", () => {
it.live("emits a success event with { keys } for --output-format json", () => {
const { layer, out } = setup({ format: "json" });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
const success = out.messages.find((m) => m.type === "success");
expect(success?.data).toMatchObject({ keys: SAMPLE_KEYS });
}).pipe(Effect.provide(layer));
Expand All@@ -102,15 +159,15 @@ describe("legacy projects api-keys integration", () => {
it.live("emits a success event for --output-format stream-json", () => {
const { layer, out } = setup({ format: "stream-json" });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(out.messages.find((m) => m.type === "success")).toBeDefined();
}).pipe(Effect.provide(layer));
});

it.live("encodes the SUPABASE_<NAME>_KEY map for --output env", () => {
const { layer, out } = setup({ goOutput: "env" });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(out.stdoutText).toContain('SUPABASE_ANON_KEY="anon-secret"');
expect(out.stdoutText).toContain('SUPABASE_SERVICE_ROLE_KEY="******"');
}).pipe(Effect.provide(layer));
Expand All@@ -119,15 +176,15 @@ describe("legacy projects api-keys integration", () => {
it.live("encodes the SUPABASE_<NAME>_KEY map for --output toml", () => {
const { layer, out } = setup({ goOutput: "toml" });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(out.stdoutText).toContain('SUPABASE_ANON_KEY = "anon-secret"');
}).pipe(Effect.provide(layer));
});

it.live("emits a JSON array of api keys for --output json", () => {
const { layer, out } = setup({ goOutput: "json" });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(out.stdoutText).toContain('"name": "anon"');
expect(out.stdoutText.startsWith("[\n")).toBe(true);
}).pipe(Effect.provide(layer));
Expand All@@ -136,15 +193,17 @@ describe("legacy projects api-keys integration", () => {
it.live("emits a YAML array for --output yaml", () => {
const { layer, out } = setup({ goOutput: "yaml" });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(out.stdoutText).toContain("name: anon");
}).pipe(Effect.provide(layer));
});

it.live("fails with LegacyProjectsApiKeysNetworkError on transport failure", () => {
const { layer } = setup({ network: "fail" });
return Effect.gen(function* () {
const exit = yield* Effect.exit(legacyProjectsApiKeys({ projectRef: Option.none() }));
const exit = yield* Effect.exit(
legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false }),
);
expect(Exit.isFailure(exit)).toBe(true);
if (Exit.isFailure(exit)) {
const json = JSON.stringify(exit.cause);
Expand All@@ -157,7 +216,9 @@ describe("legacy projects api-keys integration", () => {
it.live("maps HTTP 503 to `unexpected get api keys status 503`", () => {
const { layer } = setup({ status: 503, response: [] });
return Effect.gen(function* () {
const exit = yield* Effect.exit(legacyProjectsApiKeys({ projectRef: Option.none() }));
const exit = yield* Effect.exit(
legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false }),
);
expect(Exit.isFailure(exit)).toBe(true);
if (Exit.isFailure(exit)) {
const json = JSON.stringify(exit.cause);
Expand Down
17 changes: 11 additions & 6 deletions apps/cli/src/legacy/shared/legacy-get-api-keys.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,15 +19,20 @@ const mapApiKeysError = mapLegacyHttpError({

/**
* Ports Go's `apiKeys.RunGetApiKeys` (`apps/cli-go/internal/projects/apiKeys/api_keys.go:41-49`):
* `GET /v1/projects/{ref}/api-keys` with no `reveal` param, mapping transport /
* non-200 failures to the same `failed to get api keys` / `unexpected get api keys
* status` errors Go raises. Shared by `projects api-keys` (display) and `bootstrap`
* (which derives the `.env` keys).
* `GET /v1/projects/{ref}/api-keys`, mapping transport / non-200 failures to the same
* `failed to get api keys` / `unexpected get api keys status` errors Go raises. Shared by
* `projects api-keys` (display) and `bootstrap` (which derives the `.env` keys).
*
* When `reveal` is `true`, the `reveal=true` query param is sent so the Management API
* returns the full secret keys (prefix `sb_secret_`) in `api_key` instead of `null`
* (issue #4775). The param is omitted entirely when `reveal` is `false` to keep the
* default request byte-identical to Go's (`bootstrap` only consumes the never-redacted
* anon key, so it stays on the default path).
*/
export const legacyGetProjectApiKeys = Effect.fnUntraced(function* (ref: string) {
export const legacyGetProjectApiKeys = Effect.fnUntraced(function* (ref: string, reveal = false) {
const api = yield* LegacyPlatformApi;
const keys: ApiKeys = yield* api.v1
.getProjectApiKeys({ ref })
.getProjectApiKeys(reveal ? { ref, reveal: true } : { ref })
.pipe(Effect.catch(mapApiKeysError));
return keys;
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' feat(cli): add --reveal flag to projects api-keys by Coly010 · Pull Request #5633 · supabase/cli · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions apps/cli/docs/go-cli-porting-status.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -316,3 +316,10 @@ Legend:
| `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:

- `projects api-keys` has a TS-only `--reveal` flag (no Go equivalent). It sends
`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.
27 changes: 17 additions & 10 deletions apps/cli/src/legacy/commands/projects/api-keys/SIDE_EFFECTS.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,9 +15,13 @@

## API Routes

| Method | Path | Auth | Request body | Response (used fields) |
| ------ | ----------------------------- | ------------ | ------------ | ------------------------------------------- |
| `GET` | `/v1/projects/{ref}/api-keys` | Bearer token | none | `[{name: string, api_key: string \| null}]` |
| Method | Path | Auth | Request body | Response (used fields) |
| ------ | ------------------------------------------- | ------------ | ------------ | ------------------------------------------- |
| `GET` | `/v1/projects/{ref}/api-keys[?reveal=true]` | Bearer token | none | `[{name: string, api_key: string \| null}]` |

The `reveal=true` query param is sent only when `--reveal` is passed; it instructs the
Management API to return the full secret keys (`sb_secret_...`) in `api_key` instead of
`null`. Without `--reveal` the param is omitted entirely (default request).

## Environment Variables

Expand All@@ -28,9 +32,10 @@

## Flags

| Flag | Type | Required | Description |
| --------------- | ------ | -------- | --------------------------------------------------------------------------- |
| `--project-ref` | string | no | Project ref of the Supabase project (resolved from linked config if absent) |
| Flag | Type | Required | Description |
| --------------- | ------- | -------- | --------------------------------------------------------------------------- |
| `--project-ref` | string | no | Project ref of the Supabase project (resolved from linked config if absent) |
| `--reveal` | boolean | no | Reveal the secret API keys in full (sends `reveal=true`); default redacted |

## Exit Codes

Expand All@@ -44,9 +49,9 @@

## Telemetry Events Fired

| Event | When | Notable properties / groups |
| ---------------------- | ------------------------------------------ | ----------------------------------------------------------------------- |
| `cli_command_executed` | post-run, success or failure (via wrapper) | `exit_code`, `duration_ms`, `flags` (`--project-ref` is telemetry-safe) |
| Event | When | Notable properties / groups |
| ---------------------- | ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- |
| `cli_command_executed` | post-run, success or failure (via wrapper) | `exit_code`, `duration_ms`, `flags` (`--project-ref` is telemetry-safe; `--reveal`'s boolean value is logged but never the key values) |

## Output

Expand DownExpand Up@@ -95,7 +100,9 @@ On failure, an `error` event is emitted instead:
## Notes

- API keys with null values (redacted by the API) render as `******` in text mode and
in the toml/env env map; the json/yaml encodings preserve the raw `null`.
in the toml/env env map; the json/yaml encodings preserve the raw `null`. Passing
`--reveal` makes the API return the secret values, so they print in full across all
formats (issue #4775). This is a TS-only flag with no Go CLI equivalent.
- The `--project-ref` flag is optional when the CLI is linked to a project via `supabase link`.
When omitted, the ref is resolved flag → env → `.temp/project-ref` → prompt on a TTY,
failing with a not-linked error otherwise.
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,6 +10,9 @@ const config = {
Flag.withDescription("Project ref of the Supabase project."),
Flag.optional,
),
reveal: Flag.boolean("reveal").pipe(
Flag.withDescription("Reveal the secret API keys in full (e.g. sb_secret_...)."),
),
};
export type LegacyProjectsApiKeysFlags = CliCommand.Command.Config.Infer<typeof config>;

Expand All@@ -21,9 +24,16 @@ export const legacyProjectsApiKeysCommand = Command.make("api-keys", config).pip
command: "supabase projects api-keys --project-ref abcdefghijklmnopqrst",
description: "List all API keys for a project",
},
{
command: "supabase projects api-keys --reveal --output json",
description: "List API keys with the secret keys revealed in full",
},
]),
Command.withHandler((flags) =>
legacyProjectsApiKeys(flags).pipe(
// `reveal` is intentionally not in `safeFlags`: it is a boolean flag, and
// boolean values are always logged verbatim by the instrumentation. Only
// string flags Go marks with `markFlagTelemetrySafe` belong in `safeFlags`.
withLegacyCommandInstrumentation({ flags, safeFlags: ["project-ref"] }),
withJsonErrorHandling,
),
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,7 +35,7 @@ export const legacyProjectsApiKeys = Effect.fn("legacy.projects.api-keys")(funct
yield* Effect.gen(function* () {
const fetching =
output.format === "text" ? yield* output.task("Fetching API keys...") : undefined;
const keys: ApiKeys = yield* legacyGetProjectApiKeys(ref).pipe(
const keys: ApiKeys = yield* legacyGetProjectApiKeys(ref, flags.reveal).pipe(
Effect.tapError(() => fetching?.fail() ?? Effect.void),
);
yield* fetching?.clear() ?? Effect.void;
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,6 +19,11 @@ const SAMPLE_KEYS: ApiKeys = [
{ name: "service_role", api_key: null },
];

const REVEALED_KEYS: ApiKeys = [
{ name: "anon", api_key: "anon-secret" },
{ name: "service_role", api_key: "sb_secret_revealed" },
];

const FLAG_REF = "qrstuvwxyzabcdefghij";

const tempRoot = useLegacyTempWorkdir("supabase-projects-apikeys-int-");
Expand DownExpand Up@@ -55,7 +60,7 @@ describe("legacy projects api-keys integration", () => {
it.live("lists api keys as a NAME / KEY VALUE table and masks null values", () => {
const { layer, out } = setup();
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(out.stdoutText).toContain("NAME");
expect(out.stdoutText).toContain("KEY VALUE");
expect(out.stdoutText).toContain("anon-secret");
Expand All@@ -66,23 +71,75 @@ describe("legacy projects api-keys integration", () => {
it.live("resolves the ref from --project-ref", () => {
const { layer, api } = setup();
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.some(FLAG_REF) });
yield* legacyProjectsApiKeys({ projectRef: Option.some(FLAG_REF), reveal: false });
expect(api.requests[0]?.url).toContain(`/v1/projects/${FLAG_REF}/api-keys`);
}).pipe(Effect.provide(layer));
});

it.live("resolves the ref from the linked project when --project-ref is omitted", () => {
const { layer, api } = setup();
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(api.requests[0]?.url).toContain(`/v1/projects/${LEGACY_VALID_REF}/api-keys`);
}).pipe(Effect.provide(layer));
});

it.live("omits the reveal query param by default (Go request parity)", () => {
const { layer, api } = setup();
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(api.requests[0]?.urlWithParams).not.toContain("reveal");
}).pipe(Effect.provide(layer));
});

it.live("sends reveal=true when --reveal is passed", () => {
const { layer, api } = setup({ response: REVEALED_KEYS });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: true });
expect(api.requests[0]?.urlWithParams).toContain("reveal=true");
}).pipe(Effect.provide(layer));
});

it.live("renders the revealed secret key in full in the text table", () => {
const { layer, out } = setup({ response: REVEALED_KEYS });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: true });
expect(out.stdoutText).toContain("sb_secret_revealed");
expect(out.stdoutText).not.toContain("******");
}).pipe(Effect.provide(layer));
});

it.live("includes the revealed secret in the env map for --output env --reveal", () => {
const { layer, out } = setup({ goOutput: "env", response: REVEALED_KEYS });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: true });
expect(out.stdoutText).toContain('SUPABASE_SERVICE_ROLE_KEY="sb_secret_revealed"');
}).pipe(Effect.provide(layer));
});

it.live("carries the revealed secret in the { keys } payload for --output-format json", () => {
const { layer, out } = setup({ format: "json", response: REVEALED_KEYS });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: true });
const success = out.messages.find((m) => m.type === "success");
expect(success?.data).toMatchObject({ keys: REVEALED_KEYS });
}).pipe(Effect.provide(layer));
});

it.live("emits the revealed secret in the Go json array for --output json --reveal", () => {
const { layer, out } = setup({ goOutput: "json", response: REVEALED_KEYS });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: true });
expect(out.stdoutText).toContain('"api_key": "sb_secret_revealed"');
}).pipe(Effect.provide(layer));
});

it.live("fails with LegacyProjectNotLinkedError when no ref can be resolved", () => {
const { layer } = setup({ projectId: Option.none() });
return Effect.gen(function* () {
const exit = yield* Effect.exit(legacyProjectsApiKeys({ projectRef: Option.none() }));
const exit = yield* Effect.exit(
legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false }),
);
expect(Exit.isFailure(exit)).toBe(true);
if (Exit.isFailure(exit)) {
expect(JSON.stringify(exit.cause)).toContain("LegacyProjectNotLinkedError");
Expand All@@ -93,7 +150,7 @@ describe("legacy projects api-keys integration", () => {
it.live("emits a success event with { keys } for --output-format json", () => {
const { layer, out } = setup({ format: "json" });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
const success = out.messages.find((m) => m.type === "success");
expect(success?.data).toMatchObject({ keys: SAMPLE_KEYS });
}).pipe(Effect.provide(layer));
Expand All@@ -102,15 +159,15 @@ describe("legacy projects api-keys integration", () => {
it.live("emits a success event for --output-format stream-json", () => {
const { layer, out } = setup({ format: "stream-json" });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(out.messages.find((m) => m.type === "success")).toBeDefined();
}).pipe(Effect.provide(layer));
});

it.live("encodes the SUPABASE_<NAME>_KEY map for --output env", () => {
const { layer, out } = setup({ goOutput: "env" });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(out.stdoutText).toContain('SUPABASE_ANON_KEY="anon-secret"');
expect(out.stdoutText).toContain('SUPABASE_SERVICE_ROLE_KEY="******"');
}).pipe(Effect.provide(layer));
Expand All@@ -119,15 +176,15 @@ describe("legacy projects api-keys integration", () => {
it.live("encodes the SUPABASE_<NAME>_KEY map for --output toml", () => {
const { layer, out } = setup({ goOutput: "toml" });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(out.stdoutText).toContain('SUPABASE_ANON_KEY = "anon-secret"');
}).pipe(Effect.provide(layer));
});

it.live("emits a JSON array of api keys for --output json", () => {
const { layer, out } = setup({ goOutput: "json" });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(out.stdoutText).toContain('"name": "anon"');
expect(out.stdoutText.startsWith("[\n")).toBe(true);
}).pipe(Effect.provide(layer));
Expand All@@ -136,15 +193,17 @@ describe("legacy projects api-keys integration", () => {
it.live("emits a YAML array for --output yaml", () => {
const { layer, out } = setup({ goOutput: "yaml" });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(out.stdoutText).toContain("name: anon");
}).pipe(Effect.provide(layer));
});

it.live("fails with LegacyProjectsApiKeysNetworkError on transport failure", () => {
const { layer } = setup({ network: "fail" });
return Effect.gen(function* () {
const exit = yield* Effect.exit(legacyProjectsApiKeys({ projectRef: Option.none() }));
const exit = yield* Effect.exit(
legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false }),
);
expect(Exit.isFailure(exit)).toBe(true);
if (Exit.isFailure(exit)) {
const json = JSON.stringify(exit.cause);
Expand All@@ -157,7 +216,9 @@ describe("legacy projects api-keys integration", () => {
it.live("maps HTTP 503 to `unexpected get api keys status 503`", () => {
const { layer } = setup({ status: 503, response: [] });
return Effect.gen(function* () {
const exit = yield* Effect.exit(legacyProjectsApiKeys({ projectRef: Option.none() }));
const exit = yield* Effect.exit(
legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false }),
);
expect(Exit.isFailure(exit)).toBe(true);
if (Exit.isFailure(exit)) {
const json = JSON.stringify(exit.cause);
Expand Down
17 changes: 11 additions & 6 deletions apps/cli/src/legacy/shared/legacy-get-api-keys.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,15 +19,20 @@ const mapApiKeysError = mapLegacyHttpError({

/**
* Ports Go's `apiKeys.RunGetApiKeys` (`apps/cli-go/internal/projects/apiKeys/api_keys.go:41-49`):
* `GET /v1/projects/{ref}/api-keys` with no `reveal` param, mapping transport /
* non-200 failures to the same `failed to get api keys` / `unexpected get api keys
* status` errors Go raises. Shared by `projects api-keys` (display) and `bootstrap`
* (which derives the `.env` keys).
* `GET /v1/projects/{ref}/api-keys`, mapping transport / non-200 failures to the same
* `failed to get api keys` / `unexpected get api keys status` errors Go raises. Shared by
* `projects api-keys` (display) and `bootstrap` (which derives the `.env` keys).
*
* When `reveal` is `true`, the `reveal=true` query param is sent so the Management API
* returns the full secret keys (prefix `sb_secret_`) in `api_key` instead of `null`
* (issue #4775). The param is omitted entirely when `reveal` is `false` to keep the
* default request byte-identical to Go's (`bootstrap` only consumes the never-redacted
* anon key, so it stays on the default path).
*/
export const legacyGetProjectApiKeys = Effect.fnUntraced(function* (ref: string) {
export const legacyGetProjectApiKeys = Effect.fnUntraced(function* (ref: string, reveal = false) {
const api = yield* LegacyPlatformApi;
const keys: ApiKeys = yield* api.v1
.getProjectApiKeys({ ref })
.getProjectApiKeys(reveal ? { ref, reveal: true } : { ref })
.pipe(Effect.catch(mapApiKeysError));
return keys;
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(cli): add --reveal flag to projects api-keys by Coly010 · Pull Request #5633 · supabase/cli · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions apps/cli/docs/go-cli-porting-status.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -316,3 +316,10 @@ Legend:
| `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:

- `projects api-keys` has a TS-only `--reveal` flag (no Go equivalent). It sends
`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.
27 changes: 17 additions & 10 deletions apps/cli/src/legacy/commands/projects/api-keys/SIDE_EFFECTS.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,9 +15,13 @@

## API Routes

| Method | Path | Auth | Request body | Response (used fields) |
| ------ | ----------------------------- | ------------ | ------------ | ------------------------------------------- |
| `GET` | `/v1/projects/{ref}/api-keys` | Bearer token | none | `[{name: string, api_key: string \| null}]` |
| Method | Path | Auth | Request body | Response (used fields) |
| ------ | ------------------------------------------- | ------------ | ------------ | ------------------------------------------- |
| `GET` | `/v1/projects/{ref}/api-keys[?reveal=true]` | Bearer token | none | `[{name: string, api_key: string \| null}]` |

The `reveal=true` query param is sent only when `--reveal` is passed; it instructs the
Management API to return the full secret keys (`sb_secret_...`) in `api_key` instead of
`null`. Without `--reveal` the param is omitted entirely (default request).

## Environment Variables

Expand All@@ -28,9 +32,10 @@

## Flags

| Flag | Type | Required | Description |
| --------------- | ------ | -------- | --------------------------------------------------------------------------- |
| `--project-ref` | string | no | Project ref of the Supabase project (resolved from linked config if absent) |
| Flag | Type | Required | Description |
| --------------- | ------- | -------- | --------------------------------------------------------------------------- |
| `--project-ref` | string | no | Project ref of the Supabase project (resolved from linked config if absent) |
| `--reveal` | boolean | no | Reveal the secret API keys in full (sends `reveal=true`); default redacted |

## Exit Codes

Expand All@@ -44,9 +49,9 @@

## Telemetry Events Fired

| Event | When | Notable properties / groups |
| ---------------------- | ------------------------------------------ | ----------------------------------------------------------------------- |
| `cli_command_executed` | post-run, success or failure (via wrapper) | `exit_code`, `duration_ms`, `flags` (`--project-ref` is telemetry-safe) |
| Event | When | Notable properties / groups |
| ---------------------- | ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- |
| `cli_command_executed` | post-run, success or failure (via wrapper) | `exit_code`, `duration_ms`, `flags` (`--project-ref` is telemetry-safe; `--reveal`'s boolean value is logged but never the key values) |

## Output

Expand DownExpand Up@@ -95,7 +100,9 @@ On failure, an `error` event is emitted instead:
## Notes

- API keys with null values (redacted by the API) render as `******` in text mode and
in the toml/env env map; the json/yaml encodings preserve the raw `null`.
in the toml/env env map; the json/yaml encodings preserve the raw `null`. Passing
`--reveal` makes the API return the secret values, so they print in full across all
formats (issue #4775). This is a TS-only flag with no Go CLI equivalent.
- The `--project-ref` flag is optional when the CLI is linked to a project via `supabase link`.
When omitted, the ref is resolved flag → env → `.temp/project-ref` → prompt on a TTY,
failing with a not-linked error otherwise.
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,6 +10,9 @@ const config = {
Flag.withDescription("Project ref of the Supabase project."),
Flag.optional,
),
reveal: Flag.boolean("reveal").pipe(
Flag.withDescription("Reveal the secret API keys in full (e.g. sb_secret_...)."),
),
};
export type LegacyProjectsApiKeysFlags = CliCommand.Command.Config.Infer<typeof config>;

Expand All@@ -21,9 +24,16 @@ export const legacyProjectsApiKeysCommand = Command.make("api-keys", config).pip
command: "supabase projects api-keys --project-ref abcdefghijklmnopqrst",
description: "List all API keys for a project",
},
{
command: "supabase projects api-keys --reveal --output json",
description: "List API keys with the secret keys revealed in full",
},
]),
Command.withHandler((flags) =>
legacyProjectsApiKeys(flags).pipe(
// `reveal` is intentionally not in `safeFlags`: it is a boolean flag, and
// boolean values are always logged verbatim by the instrumentation. Only
// string flags Go marks with `markFlagTelemetrySafe` belong in `safeFlags`.
withLegacyCommandInstrumentation({ flags, safeFlags: ["project-ref"] }),
withJsonErrorHandling,
),
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,7 +35,7 @@ export const legacyProjectsApiKeys = Effect.fn("legacy.projects.api-keys")(funct
yield* Effect.gen(function* () {
const fetching =
output.format === "text" ? yield* output.task("Fetching API keys...") : undefined;
const keys: ApiKeys = yield* legacyGetProjectApiKeys(ref).pipe(
const keys: ApiKeys = yield* legacyGetProjectApiKeys(ref, flags.reveal).pipe(
Effect.tapError(() => fetching?.fail() ?? Effect.void),
);
yield* fetching?.clear() ?? Effect.void;
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,6 +19,11 @@ const SAMPLE_KEYS: ApiKeys = [
{ name: "service_role", api_key: null },
];

const REVEALED_KEYS: ApiKeys = [
{ name: "anon", api_key: "anon-secret" },
{ name: "service_role", api_key: "sb_secret_revealed" },
];

const FLAG_REF = "qrstuvwxyzabcdefghij";

const tempRoot = useLegacyTempWorkdir("supabase-projects-apikeys-int-");
Expand DownExpand Up@@ -55,7 +60,7 @@ describe("legacy projects api-keys integration", () => {
it.live("lists api keys as a NAME / KEY VALUE table and masks null values", () => {
const { layer, out } = setup();
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(out.stdoutText).toContain("NAME");
expect(out.stdoutText).toContain("KEY VALUE");
expect(out.stdoutText).toContain("anon-secret");
Expand All@@ -66,23 +71,75 @@ describe("legacy projects api-keys integration", () => {
it.live("resolves the ref from --project-ref", () => {
const { layer, api } = setup();
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.some(FLAG_REF) });
yield* legacyProjectsApiKeys({ projectRef: Option.some(FLAG_REF), reveal: false });
expect(api.requests[0]?.url).toContain(`/v1/projects/${FLAG_REF}/api-keys`);
}).pipe(Effect.provide(layer));
});

it.live("resolves the ref from the linked project when --project-ref is omitted", () => {
const { layer, api } = setup();
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(api.requests[0]?.url).toContain(`/v1/projects/${LEGACY_VALID_REF}/api-keys`);
}).pipe(Effect.provide(layer));
});

it.live("omits the reveal query param by default (Go request parity)", () => {
const { layer, api } = setup();
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(api.requests[0]?.urlWithParams).not.toContain("reveal");
}).pipe(Effect.provide(layer));
});

it.live("sends reveal=true when --reveal is passed", () => {
const { layer, api } = setup({ response: REVEALED_KEYS });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: true });
expect(api.requests[0]?.urlWithParams).toContain("reveal=true");
}).pipe(Effect.provide(layer));
});

it.live("renders the revealed secret key in full in the text table", () => {
const { layer, out } = setup({ response: REVEALED_KEYS });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: true });
expect(out.stdoutText).toContain("sb_secret_revealed");
expect(out.stdoutText).not.toContain("******");
}).pipe(Effect.provide(layer));
});

it.live("includes the revealed secret in the env map for --output env --reveal", () => {
const { layer, out } = setup({ goOutput: "env", response: REVEALED_KEYS });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: true });
expect(out.stdoutText).toContain('SUPABASE_SERVICE_ROLE_KEY="sb_secret_revealed"');
}).pipe(Effect.provide(layer));
});

it.live("carries the revealed secret in the { keys } payload for --output-format json", () => {
const { layer, out } = setup({ format: "json", response: REVEALED_KEYS });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: true });
const success = out.messages.find((m) => m.type === "success");
expect(success?.data).toMatchObject({ keys: REVEALED_KEYS });
}).pipe(Effect.provide(layer));
});

it.live("emits the revealed secret in the Go json array for --output json --reveal", () => {
const { layer, out } = setup({ goOutput: "json", response: REVEALED_KEYS });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: true });
expect(out.stdoutText).toContain('"api_key": "sb_secret_revealed"');
}).pipe(Effect.provide(layer));
});

it.live("fails with LegacyProjectNotLinkedError when no ref can be resolved", () => {
const { layer } = setup({ projectId: Option.none() });
return Effect.gen(function* () {
const exit = yield* Effect.exit(legacyProjectsApiKeys({ projectRef: Option.none() }));
const exit = yield* Effect.exit(
legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false }),
);
expect(Exit.isFailure(exit)).toBe(true);
if (Exit.isFailure(exit)) {
expect(JSON.stringify(exit.cause)).toContain("LegacyProjectNotLinkedError");
Expand All@@ -93,7 +150,7 @@ describe("legacy projects api-keys integration", () => {
it.live("emits a success event with { keys } for --output-format json", () => {
const { layer, out } = setup({ format: "json" });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
const success = out.messages.find((m) => m.type === "success");
expect(success?.data).toMatchObject({ keys: SAMPLE_KEYS });
}).pipe(Effect.provide(layer));
Expand All@@ -102,15 +159,15 @@ describe("legacy projects api-keys integration", () => {
it.live("emits a success event for --output-format stream-json", () => {
const { layer, out } = setup({ format: "stream-json" });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(out.messages.find((m) => m.type === "success")).toBeDefined();
}).pipe(Effect.provide(layer));
});

it.live("encodes the SUPABASE_<NAME>_KEY map for --output env", () => {
const { layer, out } = setup({ goOutput: "env" });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(out.stdoutText).toContain('SUPABASE_ANON_KEY="anon-secret"');
expect(out.stdoutText).toContain('SUPABASE_SERVICE_ROLE_KEY="******"');
}).pipe(Effect.provide(layer));
Expand All@@ -119,15 +176,15 @@ describe("legacy projects api-keys integration", () => {
it.live("encodes the SUPABASE_<NAME>_KEY map for --output toml", () => {
const { layer, out } = setup({ goOutput: "toml" });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(out.stdoutText).toContain('SUPABASE_ANON_KEY = "anon-secret"');
}).pipe(Effect.provide(layer));
});

it.live("emits a JSON array of api keys for --output json", () => {
const { layer, out } = setup({ goOutput: "json" });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(out.stdoutText).toContain('"name": "anon"');
expect(out.stdoutText.startsWith("[\n")).toBe(true);
}).pipe(Effect.provide(layer));
Expand All@@ -136,15 +193,17 @@ describe("legacy projects api-keys integration", () => {
it.live("emits a YAML array for --output yaml", () => {
const { layer, out } = setup({ goOutput: "yaml" });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(out.stdoutText).toContain("name: anon");
}).pipe(Effect.provide(layer));
});

it.live("fails with LegacyProjectsApiKeysNetworkError on transport failure", () => {
const { layer } = setup({ network: "fail" });
return Effect.gen(function* () {
const exit = yield* Effect.exit(legacyProjectsApiKeys({ projectRef: Option.none() }));
const exit = yield* Effect.exit(
legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false }),
);
expect(Exit.isFailure(exit)).toBe(true);
if (Exit.isFailure(exit)) {
const json = JSON.stringify(exit.cause);
Expand All@@ -157,7 +216,9 @@ describe("legacy projects api-keys integration", () => {
it.live("maps HTTP 503 to `unexpected get api keys status 503`", () => {
const { layer } = setup({ status: 503, response: [] });
return Effect.gen(function* () {
const exit = yield* Effect.exit(legacyProjectsApiKeys({ projectRef: Option.none() }));
const exit = yield* Effect.exit(
legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false }),
);
expect(Exit.isFailure(exit)).toBe(true);
if (Exit.isFailure(exit)) {
const json = JSON.stringify(exit.cause);
Expand Down
17 changes: 11 additions & 6 deletions apps/cli/src/legacy/shared/legacy-get-api-keys.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,15 +19,20 @@ const mapApiKeysError = mapLegacyHttpError({

/**
* Ports Go's `apiKeys.RunGetApiKeys` (`apps/cli-go/internal/projects/apiKeys/api_keys.go:41-49`):
* `GET /v1/projects/{ref}/api-keys` with no `reveal` param, mapping transport /
* non-200 failures to the same `failed to get api keys` / `unexpected get api keys
* status` errors Go raises. Shared by `projects api-keys` (display) and `bootstrap`
* (which derives the `.env` keys).
* `GET /v1/projects/{ref}/api-keys`, mapping transport / non-200 failures to the same
* `failed to get api keys` / `unexpected get api keys status` errors Go raises. Shared by
* `projects api-keys` (display) and `bootstrap` (which derives the `.env` keys).
*
* When `reveal` is `true`, the `reveal=true` query param is sent so the Management API
* returns the full secret keys (prefix `sb_secret_`) in `api_key` instead of `null`
* (issue #4775). The param is omitted entirely when `reveal` is `false` to keep the
* default request byte-identical to Go's (`bootstrap` only consumes the never-redacted
* anon key, so it stays on the default path).
*/
export const legacyGetProjectApiKeys = Effect.fnUntraced(function* (ref: string) {
export const legacyGetProjectApiKeys = Effect.fnUntraced(function* (ref: string, reveal = false) {
const api = yield* LegacyPlatformApi;
const keys: ApiKeys = yield* api.v1
.getProjectApiKeys({ ref })
.getProjectApiKeys(reveal ? { ref, reveal: true } : { ref })
.pipe(Effect.catch(mapApiKeysError));
return keys;
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(cli): add --reveal flag to projects api-keys by Coly010 · Pull Request #5633 · supabase/cli · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions apps/cli/docs/go-cli-porting-status.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -316,3 +316,10 @@ Legend:
| `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:

- `projects api-keys` has a TS-only `--reveal` flag (no Go equivalent). It sends
`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.
27 changes: 17 additions & 10 deletions apps/cli/src/legacy/commands/projects/api-keys/SIDE_EFFECTS.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,9 +15,13 @@

## API Routes

| Method | Path | Auth | Request body | Response (used fields) |
| ------ | ----------------------------- | ------------ | ------------ | ------------------------------------------- |
| `GET` | `/v1/projects/{ref}/api-keys` | Bearer token | none | `[{name: string, api_key: string \| null}]` |
| Method | Path | Auth | Request body | Response (used fields) |
| ------ | ------------------------------------------- | ------------ | ------------ | ------------------------------------------- |
| `GET` | `/v1/projects/{ref}/api-keys[?reveal=true]` | Bearer token | none | `[{name: string, api_key: string \| null}]` |

The `reveal=true` query param is sent only when `--reveal` is passed; it instructs the
Management API to return the full secret keys (`sb_secret_...`) in `api_key` instead of
`null`. Without `--reveal` the param is omitted entirely (default request).

## Environment Variables

Expand All@@ -28,9 +32,10 @@

## Flags

| Flag | Type | Required | Description |
| --------------- | ------ | -------- | --------------------------------------------------------------------------- |
| `--project-ref` | string | no | Project ref of the Supabase project (resolved from linked config if absent) |
| Flag | Type | Required | Description |
| --------------- | ------- | -------- | --------------------------------------------------------------------------- |
| `--project-ref` | string | no | Project ref of the Supabase project (resolved from linked config if absent) |
| `--reveal` | boolean | no | Reveal the secret API keys in full (sends `reveal=true`); default redacted |

## Exit Codes

Expand All@@ -44,9 +49,9 @@

## Telemetry Events Fired

| Event | When | Notable properties / groups |
| ---------------------- | ------------------------------------------ | ----------------------------------------------------------------------- |
| `cli_command_executed` | post-run, success or failure (via wrapper) | `exit_code`, `duration_ms`, `flags` (`--project-ref` is telemetry-safe) |
| Event | When | Notable properties / groups |
| ---------------------- | ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- |
| `cli_command_executed` | post-run, success or failure (via wrapper) | `exit_code`, `duration_ms`, `flags` (`--project-ref` is telemetry-safe; `--reveal`'s boolean value is logged but never the key values) |

## Output

Expand DownExpand Up@@ -95,7 +100,9 @@ On failure, an `error` event is emitted instead:
## Notes

- API keys with null values (redacted by the API) render as `******` in text mode and
in the toml/env env map; the json/yaml encodings preserve the raw `null`.
in the toml/env env map; the json/yaml encodings preserve the raw `null`. Passing
`--reveal` makes the API return the secret values, so they print in full across all
formats (issue #4775). This is a TS-only flag with no Go CLI equivalent.
- The `--project-ref` flag is optional when the CLI is linked to a project via `supabase link`.
When omitted, the ref is resolved flag → env → `.temp/project-ref` → prompt on a TTY,
failing with a not-linked error otherwise.
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,6 +10,9 @@ const config = {
Flag.withDescription("Project ref of the Supabase project."),
Flag.optional,
),
reveal: Flag.boolean("reveal").pipe(
Flag.withDescription("Reveal the secret API keys in full (e.g. sb_secret_...)."),
),
};
export type LegacyProjectsApiKeysFlags = CliCommand.Command.Config.Infer<typeof config>;

Expand All@@ -21,9 +24,16 @@ export const legacyProjectsApiKeysCommand = Command.make("api-keys", config).pip
command: "supabase projects api-keys --project-ref abcdefghijklmnopqrst",
description: "List all API keys for a project",
},
{
command: "supabase projects api-keys --reveal --output json",
description: "List API keys with the secret keys revealed in full",
},
]),
Command.withHandler((flags) =>
legacyProjectsApiKeys(flags).pipe(
// `reveal` is intentionally not in `safeFlags`: it is a boolean flag, and
// boolean values are always logged verbatim by the instrumentation. Only
// string flags Go marks with `markFlagTelemetrySafe` belong in `safeFlags`.
withLegacyCommandInstrumentation({ flags, safeFlags: ["project-ref"] }),
withJsonErrorHandling,
),
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,7 +35,7 @@ export const legacyProjectsApiKeys = Effect.fn("legacy.projects.api-keys")(funct
yield* Effect.gen(function* () {
const fetching =
output.format === "text" ? yield* output.task("Fetching API keys...") : undefined;
const keys: ApiKeys = yield* legacyGetProjectApiKeys(ref).pipe(
const keys: ApiKeys = yield* legacyGetProjectApiKeys(ref, flags.reveal).pipe(
Effect.tapError(() => fetching?.fail() ?? Effect.void),
);
yield* fetching?.clear() ?? Effect.void;
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,6 +19,11 @@ const SAMPLE_KEYS: ApiKeys = [
{ name: "service_role", api_key: null },
];

const REVEALED_KEYS: ApiKeys = [
{ name: "anon", api_key: "anon-secret" },
{ name: "service_role", api_key: "sb_secret_revealed" },
];

const FLAG_REF = "qrstuvwxyzabcdefghij";

const tempRoot = useLegacyTempWorkdir("supabase-projects-apikeys-int-");
Expand DownExpand Up@@ -55,7 +60,7 @@ describe("legacy projects api-keys integration", () => {
it.live("lists api keys as a NAME / KEY VALUE table and masks null values", () => {
const { layer, out } = setup();
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(out.stdoutText).toContain("NAME");
expect(out.stdoutText).toContain("KEY VALUE");
expect(out.stdoutText).toContain("anon-secret");
Expand All@@ -66,23 +71,75 @@ describe("legacy projects api-keys integration", () => {
it.live("resolves the ref from --project-ref", () => {
const { layer, api } = setup();
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.some(FLAG_REF) });
yield* legacyProjectsApiKeys({ projectRef: Option.some(FLAG_REF), reveal: false });
expect(api.requests[0]?.url).toContain(`/v1/projects/${FLAG_REF}/api-keys`);
}).pipe(Effect.provide(layer));
});

it.live("resolves the ref from the linked project when --project-ref is omitted", () => {
const { layer, api } = setup();
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(api.requests[0]?.url).toContain(`/v1/projects/${LEGACY_VALID_REF}/api-keys`);
}).pipe(Effect.provide(layer));
});

it.live("omits the reveal query param by default (Go request parity)", () => {
const { layer, api } = setup();
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(api.requests[0]?.urlWithParams).not.toContain("reveal");
}).pipe(Effect.provide(layer));
});

it.live("sends reveal=true when --reveal is passed", () => {
const { layer, api } = setup({ response: REVEALED_KEYS });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: true });
expect(api.requests[0]?.urlWithParams).toContain("reveal=true");
}).pipe(Effect.provide(layer));
});

it.live("renders the revealed secret key in full in the text table", () => {
const { layer, out } = setup({ response: REVEALED_KEYS });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: true });
expect(out.stdoutText).toContain("sb_secret_revealed");
expect(out.stdoutText).not.toContain("******");
}).pipe(Effect.provide(layer));
});

it.live("includes the revealed secret in the env map for --output env --reveal", () => {
const { layer, out } = setup({ goOutput: "env", response: REVEALED_KEYS });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: true });
expect(out.stdoutText).toContain('SUPABASE_SERVICE_ROLE_KEY="sb_secret_revealed"');
}).pipe(Effect.provide(layer));
});

it.live("carries the revealed secret in the { keys } payload for --output-format json", () => {
const { layer, out } = setup({ format: "json", response: REVEALED_KEYS });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: true });
const success = out.messages.find((m) => m.type === "success");
expect(success?.data).toMatchObject({ keys: REVEALED_KEYS });
}).pipe(Effect.provide(layer));
});

it.live("emits the revealed secret in the Go json array for --output json --reveal", () => {
const { layer, out } = setup({ goOutput: "json", response: REVEALED_KEYS });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: true });
expect(out.stdoutText).toContain('"api_key": "sb_secret_revealed"');
}).pipe(Effect.provide(layer));
});

it.live("fails with LegacyProjectNotLinkedError when no ref can be resolved", () => {
const { layer } = setup({ projectId: Option.none() });
return Effect.gen(function* () {
const exit = yield* Effect.exit(legacyProjectsApiKeys({ projectRef: Option.none() }));
const exit = yield* Effect.exit(
legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false }),
);
expect(Exit.isFailure(exit)).toBe(true);
if (Exit.isFailure(exit)) {
expect(JSON.stringify(exit.cause)).toContain("LegacyProjectNotLinkedError");
Expand All@@ -93,7 +150,7 @@ describe("legacy projects api-keys integration", () => {
it.live("emits a success event with { keys } for --output-format json", () => {
const { layer, out } = setup({ format: "json" });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
const success = out.messages.find((m) => m.type === "success");
expect(success?.data).toMatchObject({ keys: SAMPLE_KEYS });
}).pipe(Effect.provide(layer));
Expand All@@ -102,15 +159,15 @@ describe("legacy projects api-keys integration", () => {
it.live("emits a success event for --output-format stream-json", () => {
const { layer, out } = setup({ format: "stream-json" });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(out.messages.find((m) => m.type === "success")).toBeDefined();
}).pipe(Effect.provide(layer));
});

it.live("encodes the SUPABASE_<NAME>_KEY map for --output env", () => {
const { layer, out } = setup({ goOutput: "env" });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(out.stdoutText).toContain('SUPABASE_ANON_KEY="anon-secret"');
expect(out.stdoutText).toContain('SUPABASE_SERVICE_ROLE_KEY="******"');
}).pipe(Effect.provide(layer));
Expand All@@ -119,15 +176,15 @@ describe("legacy projects api-keys integration", () => {
it.live("encodes the SUPABASE_<NAME>_KEY map for --output toml", () => {
const { layer, out } = setup({ goOutput: "toml" });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(out.stdoutText).toContain('SUPABASE_ANON_KEY = "anon-secret"');
}).pipe(Effect.provide(layer));
});

it.live("emits a JSON array of api keys for --output json", () => {
const { layer, out } = setup({ goOutput: "json" });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(out.stdoutText).toContain('"name": "anon"');
expect(out.stdoutText.startsWith("[\n")).toBe(true);
}).pipe(Effect.provide(layer));
Expand All@@ -136,15 +193,17 @@ describe("legacy projects api-keys integration", () => {
it.live("emits a YAML array for --output yaml", () => {
const { layer, out } = setup({ goOutput: "yaml" });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(out.stdoutText).toContain("name: anon");
}).pipe(Effect.provide(layer));
});

it.live("fails with LegacyProjectsApiKeysNetworkError on transport failure", () => {
const { layer } = setup({ network: "fail" });
return Effect.gen(function* () {
const exit = yield* Effect.exit(legacyProjectsApiKeys({ projectRef: Option.none() }));
const exit = yield* Effect.exit(
legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false }),
);
expect(Exit.isFailure(exit)).toBe(true);
if (Exit.isFailure(exit)) {
const json = JSON.stringify(exit.cause);
Expand All@@ -157,7 +216,9 @@ describe("legacy projects api-keys integration", () => {
it.live("maps HTTP 503 to `unexpected get api keys status 503`", () => {
const { layer } = setup({ status: 503, response: [] });
return Effect.gen(function* () {
const exit = yield* Effect.exit(legacyProjectsApiKeys({ projectRef: Option.none() }));
const exit = yield* Effect.exit(
legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false }),
);
expect(Exit.isFailure(exit)).toBe(true);
if (Exit.isFailure(exit)) {
const json = JSON.stringify(exit.cause);
Expand Down
17 changes: 11 additions & 6 deletions apps/cli/src/legacy/shared/legacy-get-api-keys.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,15 +19,20 @@ const mapApiKeysError = mapLegacyHttpError({

/**
* Ports Go's `apiKeys.RunGetApiKeys` (`apps/cli-go/internal/projects/apiKeys/api_keys.go:41-49`):
* `GET /v1/projects/{ref}/api-keys` with no `reveal` param, mapping transport /
* non-200 failures to the same `failed to get api keys` / `unexpected get api keys
* status` errors Go raises. Shared by `projects api-keys` (display) and `bootstrap`
* (which derives the `.env` keys).
* `GET /v1/projects/{ref}/api-keys`, mapping transport / non-200 failures to the same
* `failed to get api keys` / `unexpected get api keys status` errors Go raises. Shared by
* `projects api-keys` (display) and `bootstrap` (which derives the `.env` keys).
*
* When `reveal` is `true`, the `reveal=true` query param is sent so the Management API
* returns the full secret keys (prefix `sb_secret_`) in `api_key` instead of `null`
* (issue #4775). The param is omitted entirely when `reveal` is `false` to keep the
* default request byte-identical to Go's (`bootstrap` only consumes the never-redacted
* anon key, so it stays on the default path).
*/
export const legacyGetProjectApiKeys = Effect.fnUntraced(function* (ref: string) {
export const legacyGetProjectApiKeys = Effect.fnUntraced(function* (ref: string, reveal = false) {
const api = yield* LegacyPlatformApi;
const keys: ApiKeys = yield* api.v1
.getProjectApiKeys({ ref })
.getProjectApiKeys(reveal ? { ref, reveal: true } : { ref })
.pipe(Effect.catch(mapApiKeysError));
return keys;
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); feat(cli): add --reveal flag to projects api-keys by Coly010 · Pull Request #5633 · supabase/cli · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions apps/cli/docs/go-cli-porting-status.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -316,3 +316,10 @@ Legend:
| `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:

- `projects api-keys` has a TS-only `--reveal` flag (no Go equivalent). It sends
`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.
27 changes: 17 additions & 10 deletions apps/cli/src/legacy/commands/projects/api-keys/SIDE_EFFECTS.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,9 +15,13 @@

## API Routes

| Method | Path | Auth | Request body | Response (used fields) |
| ------ | ----------------------------- | ------------ | ------------ | ------------------------------------------- |
| `GET` | `/v1/projects/{ref}/api-keys` | Bearer token | none | `[{name: string, api_key: string \| null}]` |
| Method | Path | Auth | Request body | Response (used fields) |
| ------ | ------------------------------------------- | ------------ | ------------ | ------------------------------------------- |
| `GET` | `/v1/projects/{ref}/api-keys[?reveal=true]` | Bearer token | none | `[{name: string, api_key: string \| null}]` |

The `reveal=true` query param is sent only when `--reveal` is passed; it instructs the
Management API to return the full secret keys (`sb_secret_...`) in `api_key` instead of
`null`. Without `--reveal` the param is omitted entirely (default request).

## Environment Variables

Expand All@@ -28,9 +32,10 @@

## Flags

| Flag | Type | Required | Description |
| --------------- | ------ | -------- | --------------------------------------------------------------------------- |
| `--project-ref` | string | no | Project ref of the Supabase project (resolved from linked config if absent) |
| Flag | Type | Required | Description |
| --------------- | ------- | -------- | --------------------------------------------------------------------------- |
| `--project-ref` | string | no | Project ref of the Supabase project (resolved from linked config if absent) |
| `--reveal` | boolean | no | Reveal the secret API keys in full (sends `reveal=true`); default redacted |

## Exit Codes

Expand All@@ -44,9 +49,9 @@

## Telemetry Events Fired

| Event | When | Notable properties / groups |
| ---------------------- | ------------------------------------------ | ----------------------------------------------------------------------- |
| `cli_command_executed` | post-run, success or failure (via wrapper) | `exit_code`, `duration_ms`, `flags` (`--project-ref` is telemetry-safe) |
| Event | When | Notable properties / groups |
| ---------------------- | ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- |
| `cli_command_executed` | post-run, success or failure (via wrapper) | `exit_code`, `duration_ms`, `flags` (`--project-ref` is telemetry-safe; `--reveal`'s boolean value is logged but never the key values) |

## Output

Expand DownExpand Up@@ -95,7 +100,9 @@ On failure, an `error` event is emitted instead:
## Notes

- API keys with null values (redacted by the API) render as `******` in text mode and
in the toml/env env map; the json/yaml encodings preserve the raw `null`.
in the toml/env env map; the json/yaml encodings preserve the raw `null`. Passing
`--reveal` makes the API return the secret values, so they print in full across all
formats (issue #4775). This is a TS-only flag with no Go CLI equivalent.
- The `--project-ref` flag is optional when the CLI is linked to a project via `supabase link`.
When omitted, the ref is resolved flag → env → `.temp/project-ref` → prompt on a TTY,
failing with a not-linked error otherwise.
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,6 +10,9 @@ const config = {
Flag.withDescription("Project ref of the Supabase project."),
Flag.optional,
),
reveal: Flag.boolean("reveal").pipe(
Flag.withDescription("Reveal the secret API keys in full (e.g. sb_secret_...)."),
),
};
export type LegacyProjectsApiKeysFlags = CliCommand.Command.Config.Infer<typeof config>;

Expand All@@ -21,9 +24,16 @@ export const legacyProjectsApiKeysCommand = Command.make("api-keys", config).pip
command: "supabase projects api-keys --project-ref abcdefghijklmnopqrst",
description: "List all API keys for a project",
},
{
command: "supabase projects api-keys --reveal --output json",
description: "List API keys with the secret keys revealed in full",
},
]),
Command.withHandler((flags) =>
legacyProjectsApiKeys(flags).pipe(
// `reveal` is intentionally not in `safeFlags`: it is a boolean flag, and
// boolean values are always logged verbatim by the instrumentation. Only
// string flags Go marks with `markFlagTelemetrySafe` belong in `safeFlags`.
withLegacyCommandInstrumentation({ flags, safeFlags: ["project-ref"] }),
withJsonErrorHandling,
),
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,7 +35,7 @@ export const legacyProjectsApiKeys = Effect.fn("legacy.projects.api-keys")(funct
yield* Effect.gen(function* () {
const fetching =
output.format === "text" ? yield* output.task("Fetching API keys...") : undefined;
const keys: ApiKeys = yield* legacyGetProjectApiKeys(ref).pipe(
const keys: ApiKeys = yield* legacyGetProjectApiKeys(ref, flags.reveal).pipe(
Effect.tapError(() => fetching?.fail() ?? Effect.void),
);
yield* fetching?.clear() ?? Effect.void;
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,6 +19,11 @@ const SAMPLE_KEYS: ApiKeys = [
{ name: "service_role", api_key: null },
];

const REVEALED_KEYS: ApiKeys = [
{ name: "anon", api_key: "anon-secret" },
{ name: "service_role", api_key: "sb_secret_revealed" },
];

const FLAG_REF = "qrstuvwxyzabcdefghij";

const tempRoot = useLegacyTempWorkdir("supabase-projects-apikeys-int-");
Expand DownExpand Up@@ -55,7 +60,7 @@ describe("legacy projects api-keys integration", () => {
it.live("lists api keys as a NAME / KEY VALUE table and masks null values", () => {
const { layer, out } = setup();
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(out.stdoutText).toContain("NAME");
expect(out.stdoutText).toContain("KEY VALUE");
expect(out.stdoutText).toContain("anon-secret");
Expand All@@ -66,23 +71,75 @@ describe("legacy projects api-keys integration", () => {
it.live("resolves the ref from --project-ref", () => {
const { layer, api } = setup();
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.some(FLAG_REF) });
yield* legacyProjectsApiKeys({ projectRef: Option.some(FLAG_REF), reveal: false });
expect(api.requests[0]?.url).toContain(`/v1/projects/${FLAG_REF}/api-keys`);
}).pipe(Effect.provide(layer));
});

it.live("resolves the ref from the linked project when --project-ref is omitted", () => {
const { layer, api } = setup();
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(api.requests[0]?.url).toContain(`/v1/projects/${LEGACY_VALID_REF}/api-keys`);
}).pipe(Effect.provide(layer));
});

it.live("omits the reveal query param by default (Go request parity)", () => {
const { layer, api } = setup();
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(api.requests[0]?.urlWithParams).not.toContain("reveal");
}).pipe(Effect.provide(layer));
});

it.live("sends reveal=true when --reveal is passed", () => {
const { layer, api } = setup({ response: REVEALED_KEYS });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: true });
expect(api.requests[0]?.urlWithParams).toContain("reveal=true");
}).pipe(Effect.provide(layer));
});

it.live("renders the revealed secret key in full in the text table", () => {
const { layer, out } = setup({ response: REVEALED_KEYS });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: true });
expect(out.stdoutText).toContain("sb_secret_revealed");
expect(out.stdoutText).not.toContain("******");
}).pipe(Effect.provide(layer));
});

it.live("includes the revealed secret in the env map for --output env --reveal", () => {
const { layer, out } = setup({ goOutput: "env", response: REVEALED_KEYS });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: true });
expect(out.stdoutText).toContain('SUPABASE_SERVICE_ROLE_KEY="sb_secret_revealed"');
}).pipe(Effect.provide(layer));
});

it.live("carries the revealed secret in the { keys } payload for --output-format json", () => {
const { layer, out } = setup({ format: "json", response: REVEALED_KEYS });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: true });
const success = out.messages.find((m) => m.type === "success");
expect(success?.data).toMatchObject({ keys: REVEALED_KEYS });
}).pipe(Effect.provide(layer));
});

it.live("emits the revealed secret in the Go json array for --output json --reveal", () => {
const { layer, out } = setup({ goOutput: "json", response: REVEALED_KEYS });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: true });
expect(out.stdoutText).toContain('"api_key": "sb_secret_revealed"');
}).pipe(Effect.provide(layer));
});

it.live("fails with LegacyProjectNotLinkedError when no ref can be resolved", () => {
const { layer } = setup({ projectId: Option.none() });
return Effect.gen(function* () {
const exit = yield* Effect.exit(legacyProjectsApiKeys({ projectRef: Option.none() }));
const exit = yield* Effect.exit(
legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false }),
);
expect(Exit.isFailure(exit)).toBe(true);
if (Exit.isFailure(exit)) {
expect(JSON.stringify(exit.cause)).toContain("LegacyProjectNotLinkedError");
Expand All@@ -93,7 +150,7 @@ describe("legacy projects api-keys integration", () => {
it.live("emits a success event with { keys } for --output-format json", () => {
const { layer, out } = setup({ format: "json" });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
const success = out.messages.find((m) => m.type === "success");
expect(success?.data).toMatchObject({ keys: SAMPLE_KEYS });
}).pipe(Effect.provide(layer));
Expand All@@ -102,15 +159,15 @@ describe("legacy projects api-keys integration", () => {
it.live("emits a success event for --output-format stream-json", () => {
const { layer, out } = setup({ format: "stream-json" });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(out.messages.find((m) => m.type === "success")).toBeDefined();
}).pipe(Effect.provide(layer));
});

it.live("encodes the SUPABASE_<NAME>_KEY map for --output env", () => {
const { layer, out } = setup({ goOutput: "env" });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(out.stdoutText).toContain('SUPABASE_ANON_KEY="anon-secret"');
expect(out.stdoutText).toContain('SUPABASE_SERVICE_ROLE_KEY="******"');
}).pipe(Effect.provide(layer));
Expand All@@ -119,15 +176,15 @@ describe("legacy projects api-keys integration", () => {
it.live("encodes the SUPABASE_<NAME>_KEY map for --output toml", () => {
const { layer, out } = setup({ goOutput: "toml" });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(out.stdoutText).toContain('SUPABASE_ANON_KEY = "anon-secret"');
}).pipe(Effect.provide(layer));
});

it.live("emits a JSON array of api keys for --output json", () => {
const { layer, out } = setup({ goOutput: "json" });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(out.stdoutText).toContain('"name": "anon"');
expect(out.stdoutText.startsWith("[\n")).toBe(true);
}).pipe(Effect.provide(layer));
Expand All@@ -136,15 +193,17 @@ describe("legacy projects api-keys integration", () => {
it.live("emits a YAML array for --output yaml", () => {
const { layer, out } = setup({ goOutput: "yaml" });
return Effect.gen(function* () {
yield* legacyProjectsApiKeys({ projectRef: Option.none() });
yield* legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false });
expect(out.stdoutText).toContain("name: anon");
}).pipe(Effect.provide(layer));
});

it.live("fails with LegacyProjectsApiKeysNetworkError on transport failure", () => {
const { layer } = setup({ network: "fail" });
return Effect.gen(function* () {
const exit = yield* Effect.exit(legacyProjectsApiKeys({ projectRef: Option.none() }));
const exit = yield* Effect.exit(
legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false }),
);
expect(Exit.isFailure(exit)).toBe(true);
if (Exit.isFailure(exit)) {
const json = JSON.stringify(exit.cause);
Expand All@@ -157,7 +216,9 @@ describe("legacy projects api-keys integration", () => {
it.live("maps HTTP 503 to `unexpected get api keys status 503`", () => {
const { layer } = setup({ status: 503, response: [] });
return Effect.gen(function* () {
const exit = yield* Effect.exit(legacyProjectsApiKeys({ projectRef: Option.none() }));
const exit = yield* Effect.exit(
legacyProjectsApiKeys({ projectRef: Option.none(), reveal: false }),
);
expect(Exit.isFailure(exit)).toBe(true);
if (Exit.isFailure(exit)) {
const json = JSON.stringify(exit.cause);
Expand Down
17 changes: 11 additions & 6 deletions apps/cli/src/legacy/shared/legacy-get-api-keys.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,15 +19,20 @@ const mapApiKeysError = mapLegacyHttpError({

/**
* Ports Go's `apiKeys.RunGetApiKeys` (`apps/cli-go/internal/projects/apiKeys/api_keys.go:41-49`):
* `GET /v1/projects/{ref}/api-keys` with no `reveal` param, mapping transport /
* non-200 failures to the same `failed to get api keys` / `unexpected get api keys
* status` errors Go raises. Shared by `projects api-keys` (display) and `bootstrap`
* (which derives the `.env` keys).
* `GET /v1/projects/{ref}/api-keys`, mapping transport / non-200 failures to the same
* `failed to get api keys` / `unexpected get api keys status` errors Go raises. Shared by
* `projects api-keys` (display) and `bootstrap` (which derives the `.env` keys).
*
* When `reveal` is `true`, the `reveal=true` query param is sent so the Management API
* returns the full secret keys (prefix `sb_secret_`) in `api_key` instead of `null`
* (issue #4775). The param is omitted entirely when `reveal` is `false` to keep the
* default request byte-identical to Go's (`bootstrap` only consumes the never-redacted
* anon key, so it stays on the default path).
*/
export const legacyGetProjectApiKeys = Effect.fnUntraced(function* (ref: string) {
export const legacyGetProjectApiKeys = Effect.fnUntraced(function* (ref: string, reveal = false) {
const api = yield* LegacyPlatformApi;
const keys: ApiKeys = yield* api.v1
.getProjectApiKeys({ ref })
.getProjectApiKeys(reveal ? { ref, reveal: true } : { ref })
.pipe(Effect.catch(mapApiKeysError));
return keys;
});
Loading