Draft
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
5 changes: 5 additions & 0 deletions .changeset/accept-orgs-users-for-flags.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"clerk": minor
---

Add `clerk enable api-keys` and `clerk disable api-keys` for toggling API Keys on the linked instance. API Key targeting uses the canonical plural `orgs` and `users` values, while the singular `org` and `user` values continue to work as aliases.
69 changes: 69 additions & 0 deletions packages/cli-core/src/cli-program.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -48,6 +48,7 @@ import { update } from "./commands/update/index.ts";
import { isClerkSkillInstalled } from "./lib/skill-detection.ts";
import { orgsEnable, orgsDisable } from "./commands/orgs/index.ts";
import { billingEnable, billingDisable } from "./commands/billing/index.ts";
import { apiKeysEnable, apiKeysDisable } from "./commands/api-keys/index.ts";
import { registerExtras } from "@clerk/cli-extras";

const USER_LIST_ORDER_BY_FIELDS = [
Expand DownExpand Up@@ -636,6 +637,10 @@ Give AI agents better Clerk context: install the Clerk skills
command: "clerk enable billing",
description: "Enable billing for organizations and users",
},
{
command: "clerk enable api-keys",
description: "Enable API Keys for users",
},
]);

enable
Expand DownExpand Up@@ -703,6 +708,38 @@ Give AI agents better Clerk context: install the Clerk skills
])
.action(billingEnable);

enable
.command("api-keys")
.aliases(["apikeys", "api_keys"])
.description("Enable API Keys on the linked instance")
.option(
"--for <targets...>",
"API Keys targets (orgs and/or users), separated by spaces or commas (e.g. orgs users). Defaults to users when omitted.",
)
.option("--app <id>", "Application ID to target")
.option("--instance <id>", "Instance to target (dev, prod, or instance ID)")
.option("--yes", "Skip confirmation prompts")
.option("--dry-run", "Show the patch that would be sent without applying it")
.setExamples([
{
command: "clerk enable api-keys",
description: "Enable API Keys for users",
},
{
command: "clerk enable api-keys --for orgs",
description: "Enable API Keys for organizations",
},
{
command: "clerk enable api-keys --for users orgs",
description: "Enable API Keys for users and organizations",
},
{
command: "clerk enable api-keys --dry-run",
description: "Preview the patch without applying it",
},
])
.action(apiKeysEnable);

const disable = program
.command("disable")
.description("Disable Clerk features on the linked instance")
Expand All@@ -716,6 +753,10 @@ Give AI agents better Clerk context: install the Clerk skills
command: "clerk disable billing",
description: "Disable billing for organizations and users",
},
{
command: "clerk disable api-keys",
description: "Disable API Keys",
},
]);

disable
Expand DownExpand Up@@ -764,6 +805,34 @@ Give AI agents better Clerk context: install the Clerk skills
])
.action(billingDisable);

disable
.command("api-keys")
.aliases(["apikeys", "api_keys"])
.description("Disable API Keys on the linked instance")
.option(
"--for <targets...>",
"API Keys targets (orgs and/or users), separated by spaces or commas (e.g. orgs users). Omit to disable API Keys entirely.",
)
.option("--app <id>", "Application ID to target")
.option("--instance <id>", "Instance to target (dev, prod, or instance ID)")
.option("--yes", "Skip confirmation prompts")
.option("--dry-run", "Show the patch that would be sent without applying it")
.setExamples([
{
command: "clerk disable api-keys",
description: "Disable API Keys entirely",
},
{
command: "clerk disable api-keys --for orgs",
description: "Disable API Keys for organizations only",
},
{
command: "clerk disable api-keys --for users",
description: "Disable API Keys for users only",
},
])
.action(apiKeysDisable);

program
.command("api")
.description("Make authenticated requests to the Clerk API")
Expand Down
52 changes: 52 additions & 0 deletions packages/cli-core/src/commands/api-keys/README.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
# clerk api-keys (enable/disable)

Toggle Clerk API Keys on the linked instance. The handlers are wired to
top-level `clerk enable api-keys` and `clerk disable api-keys` commands.

For arbitrary API Keys settings edits, use
`clerk config patch --json '{"api_keys_settings":{...}}'`.

## Usage

```sh
clerk enable api-keys [--for <targets>] [options]
clerk disable api-keys [--for <targets>] [options]
```

`<targets>` is `orgs` and/or `users`, accepted as space-separated,
comma-separated, or repeated `--for` flags. The singular aliases `org` and
`user` are also accepted for backwards compatibility.

```sh
clerk enable api-keys # defaults to users
clerk enable api-keys --for orgs users
clerk enable api-keys --for orgs,users
clerk disable api-keys # disables API Keys entirely
clerk disable api-keys --for orgs # disables only organization API Keys
```

## Options

| Flag | Description |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `--for <targets>` | Targets (`orgs` and/or `users`), separated by spaces or commas. Enable defaults to users; disable without `--for` disables API Keys entirely. |
| `--app <id>` | Target a specific application |
| `--instance <id>` | Target a specific instance (dev, prod) |
| `--yes` | Skip the confirmation prompt |
| `--dry-run` | Preview the patch without applying it |

## Cascade behavior

- `enable api-keys --for orgs` also sets `organization_settings.enabled = true`.
Organization API Keys require organizations enabled, so this saves a separate
command. The cascade is idempotent.
- `disable api-keys --for orgs` disables only organization API Keys and leaves
organizations enabled.
- `disable api-keys` without `--for` disables API Keys entirely.

## Clerk API endpoints

| Method | Endpoint | Description |
| ------ | ----------------------------------------------------------------- | ----------------------------------------------------------------------- |
| GET | `/v1/platform/applications/{appId}/instances/{instanceId}/config` | Fetch current config for diff before mutation |
| PATCH | `/v1/platform/applications/{appId}/instances/{instanceId}/config` | Patch `api_keys_settings.*` (with `?dry_run=true` when `--dry-run` set) |
230 changes: 230 additions & 0 deletions packages/cli-core/src/commands/api-keys/index.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
import { test, expect, describe, beforeEach, afterEach, spyOn, mock } from "bun:test";
import { mkdtemp, rm } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { _setConfigDir, setProfile } from "../../lib/config.ts";
import {
captureLog,
credentialStoreStubs,
gitStubs,
promptsStubs,
stubFetch,
} from "../../test/lib/stubs.ts";

mock.module("../../lib/credential-store.ts", () => credentialStoreStubs);
mock.module("../../lib/git.ts", () => gitStubs);
mock.module("@inquirer/prompts", () => promptsStubs);
mock.module("../../lib/spinner.ts", () => ({
withSpinner: async (_msg: string, fn: () => Promise<unknown>) => fn(),
}));

describe("clerk enable/disable api-keys", () => {
const originalEnv = { ...process.env };
const originalFetch = globalThis.fetch;
let tempDir: string;
let logSpy: ReturnType<typeof spyOn>;
let errorSpy: ReturnType<typeof spyOn>;
let captured: ReturnType<typeof captureLog>;

beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), "clerk-api-keys-test-"));
_setConfigDir(tempDir);
process.env.CLERK_PLATFORM_API_KEY = "test_key";
process.env.CLERK_PLATFORM_API_URL = "https://test-api.clerk.com";

logSpy = spyOn(console, "log").mockImplementation(() => {});
errorSpy = spyOn(console, "error").mockImplementation(() => {});
captured = captureLog();

stubFetch(async () => {
return new Response(JSON.stringify({}), { status: 200 });
});
});

afterEach(async () => {
captured.teardown();
_setConfigDir(undefined);
process.env = { ...originalEnv };
globalThis.fetch = originalFetch;
logSpy.mockRestore();
errorSpy.mockRestore();
await rm(tempDir, { recursive: true, force: true });
});

async function setupProfile() {
await setProfile(process.cwd(), {
workspaceId: "org_1",
appId: "app_1",
instances: { development: "ins_dev" },
});
}

test("enable defaults to user API Keys", async () => {
let capturedBody = "";
stubFetch(async (_input, init) => {
if (init?.method === "PATCH") capturedBody = init.body as string;
return new Response(JSON.stringify({}), { status: 200 });
});

await setupProfile();
const { apiKeysEnable } = await import("./index.ts");
await captured.run(() => apiKeysEnable({}));

const parsed = JSON.parse(capturedBody);
expect(parsed.api_keys_settings.enabled).toBe(true);
expect(parsed.api_keys_settings.user_api_keys_enabled).toBe(true);
expect(parsed.api_keys_settings.orgs_api_keys_enabled).toBeUndefined();
expect(parsed.organization_settings).toBeUndefined();
});

test("enable --for orgs enables org API Keys and cascades organizations", async () => {
let capturedBody = "";
stubFetch(async (_input, init) => {
if (init?.method === "PATCH") capturedBody = init.body as string;
return new Response(JSON.stringify({}), { status: 200 });
});

await setupProfile();
const { apiKeysEnable } = await import("./index.ts");
await captured.run(() => apiKeysEnable({ for: ["orgs"] }));

const parsed = JSON.parse(capturedBody);
expect(parsed.api_keys_settings.enabled).toBe(true);
expect(parsed.api_keys_settings.orgs_api_keys_enabled).toBe(true);
expect(parsed.api_keys_settings.user_api_keys_enabled).toBeUndefined();
expect(parsed.organization_settings.enabled).toBe(true);
});

test("enable --for users,orgs sets both API Keys targets", async () => {
let capturedBody = "";
stubFetch(async (_input, init) => {
if (init?.method === "PATCH") capturedBody = init.body as string;
return new Response(JSON.stringify({}), { status: 200 });
});

await setupProfile();
const { apiKeysEnable } = await import("./index.ts");
await captured.run(() => apiKeysEnable({ for: ["users,orgs"] }));

const parsed = JSON.parse(capturedBody);
expect(parsed.api_keys_settings.user_api_keys_enabled).toBe(true);
expect(parsed.api_keys_settings.orgs_api_keys_enabled).toBe(true);
expect(parsed.organization_settings.enabled).toBe(true);
});

test("enable rejects invalid --for token", async () => {
await setupProfile();
const { apiKeysEnable } = await import("./index.ts");
await expect(captured.run(() => apiKeysEnable({ for: ["machine"] }))).rejects.toThrow(
'Invalid --for value: "machine". Expected "orgs" and/or "users".',
);
});

test("enable accepts singular --for aliases", async () => {
let capturedBody = "";
stubFetch(async (_input, init) => {
if (init?.method === "PATCH") capturedBody = init.body as string;
return new Response(JSON.stringify({}), { status: 200 });
});

await setupProfile();
const { apiKeysEnable } = await import("./index.ts");
await captured.run(() => apiKeysEnable({ for: ["user", "org"] }));

const parsed = JSON.parse(capturedBody);
expect(parsed.api_keys_settings.user_api_keys_enabled).toBe(true);
expect(parsed.api_keys_settings.orgs_api_keys_enabled).toBe(true);
expect(parsed.organization_settings.enabled).toBe(true);
});

test("enable --dry-run plumbs dry_run=true to the API", async () => {
let capturedUrl = "";
stubFetch(async (input, init) => {
if (init?.method === "PATCH") capturedUrl = input.toString();
return new Response(JSON.stringify({}), { status: 200 });
});

await setupProfile();
const { apiKeysEnable } = await import("./index.ts");
await captured.run(() => apiKeysEnable({ dryRun: true }));

expect(capturedUrl).toContain("dry_run=true");
expect(captured.err).toContain("[dry-run]");
});

test("disable with no --for disables API Keys entirely", async () => {
let capturedBody = "";
stubFetch(async (_input, init) => {
if (init?.method === "PATCH") capturedBody = init.body as string;
return new Response(
JSON.stringify({
api_keys_settings: {
enabled: true,
user_api_keys_enabled: true,
orgs_api_keys_enabled: true,
},
}),
{ status: 200 },
);
});

await setupProfile();
const { apiKeysDisable } = await import("./index.ts");
await captured.run(() => apiKeysDisable({}));

const parsed = JSON.parse(capturedBody);
expect(parsed.api_keys_settings.enabled).toBe(false);
expect(parsed.api_keys_settings.user_api_keys_enabled).toBe(false);
expect(parsed.api_keys_settings.orgs_api_keys_enabled).toBe(false);
});

test("disable --for orgs only disables org API Keys", async () => {
let capturedBody = "";
stubFetch(async (_input, init) => {
if (init?.method === "PATCH") capturedBody = init.body as string;
return new Response(
JSON.stringify({
api_keys_settings: {
enabled: true,
user_api_keys_enabled: true,
orgs_api_keys_enabled: true,
},
}),
{ status: 200 },
);
});

await setupProfile();
const { apiKeysDisable } = await import("./index.ts");
await captured.run(() => apiKeysDisable({ for: ["orgs"] }));

const parsed = JSON.parse(capturedBody);
expect(parsed.api_keys_settings.enabled).toBeUndefined();
expect(parsed.api_keys_settings.user_api_keys_enabled).toBeUndefined();
expect(parsed.api_keys_settings.orgs_api_keys_enabled).toBe(false);
});

test("disable shows no changes when already fully disabled", async () => {
let patchCalls = 0;
stubFetch(async (_input, init) => {
if (init?.method === "PATCH") patchCalls++;
return new Response(
JSON.stringify({
api_keys_settings: {
enabled: false,
user_api_keys_enabled: false,
orgs_api_keys_enabled: false,
},
}),
{ status: 200 },
);
});

await setupProfile();
const { apiKeysDisable } = await import("./index.ts");
await captured.run(() => apiKeysDisable({}));

expect(patchCalls).toBe(0);
expect(captured.err).toContain("No changes detected");
});
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n 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;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Draft
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
5 changes: 5 additions & 0 deletions .changeset/accept-orgs-users-for-flags.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"clerk": minor
---

Add `clerk enable api-keys` and `clerk disable api-keys` for toggling API Keys on the linked instance. API Key targeting uses the canonical plural `orgs` and `users` values, while the singular `org` and `user` values continue to work as aliases.
69 changes: 69 additions & 0 deletions packages/cli-core/src/cli-program.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -48,6 +48,7 @@ import { update } from "./commands/update/index.ts";
import { isClerkSkillInstalled } from "./lib/skill-detection.ts";
import { orgsEnable, orgsDisable } from "./commands/orgs/index.ts";
import { billingEnable, billingDisable } from "./commands/billing/index.ts";
import { apiKeysEnable, apiKeysDisable } from "./commands/api-keys/index.ts";
import { registerExtras } from "@clerk/cli-extras";

const USER_LIST_ORDER_BY_FIELDS = [
Expand DownExpand Up@@ -636,6 +637,10 @@ Give AI agents better Clerk context: install the Clerk skills
command: "clerk enable billing",
description: "Enable billing for organizations and users",
},
{
command: "clerk enable api-keys",
description: "Enable API Keys for users",
},
]);

enable
Expand DownExpand Up@@ -703,6 +708,38 @@ Give AI agents better Clerk context: install the Clerk skills
])
.action(billingEnable);

enable
.command("api-keys")
.aliases(["apikeys", "api_keys"])
.description("Enable API Keys on the linked instance")
.option(
"--for <targets...>",
"API Keys targets (orgs and/or users), separated by spaces or commas (e.g. orgs users). Defaults to users when omitted.",
)
.option("--app <id>", "Application ID to target")
.option("--instance <id>", "Instance to target (dev, prod, or instance ID)")
.option("--yes", "Skip confirmation prompts")
.option("--dry-run", "Show the patch that would be sent without applying it")
.setExamples([
{
command: "clerk enable api-keys",
description: "Enable API Keys for users",
},
{
command: "clerk enable api-keys --for orgs",
description: "Enable API Keys for organizations",
},
{
command: "clerk enable api-keys --for users orgs",
description: "Enable API Keys for users and organizations",
},
{
command: "clerk enable api-keys --dry-run",
description: "Preview the patch without applying it",
},
])
.action(apiKeysEnable);

const disable = program
.command("disable")
.description("Disable Clerk features on the linked instance")
Expand All@@ -716,6 +753,10 @@ Give AI agents better Clerk context: install the Clerk skills
command: "clerk disable billing",
description: "Disable billing for organizations and users",
},
{
command: "clerk disable api-keys",
description: "Disable API Keys",
},
]);

disable
Expand DownExpand Up@@ -764,6 +805,34 @@ Give AI agents better Clerk context: install the Clerk skills
])
.action(billingDisable);

disable
.command("api-keys")
.aliases(["apikeys", "api_keys"])
.description("Disable API Keys on the linked instance")
.option(
"--for <targets...>",
"API Keys targets (orgs and/or users), separated by spaces or commas (e.g. orgs users). Omit to disable API Keys entirely.",
)
.option("--app <id>", "Application ID to target")
.option("--instance <id>", "Instance to target (dev, prod, or instance ID)")
.option("--yes", "Skip confirmation prompts")
.option("--dry-run", "Show the patch that would be sent without applying it")
.setExamples([
{
command: "clerk disable api-keys",
description: "Disable API Keys entirely",
},
{
command: "clerk disable api-keys --for orgs",
description: "Disable API Keys for organizations only",
},
{
command: "clerk disable api-keys --for users",
description: "Disable API Keys for users only",
},
])
.action(apiKeysDisable);

program
.command("api")
.description("Make authenticated requests to the Clerk API")
Expand Down
52 changes: 52 additions & 0 deletions packages/cli-core/src/commands/api-keys/README.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
# clerk api-keys (enable/disable)

Toggle Clerk API Keys on the linked instance. The handlers are wired to
top-level `clerk enable api-keys` and `clerk disable api-keys` commands.

For arbitrary API Keys settings edits, use
`clerk config patch --json '{"api_keys_settings":{...}}'`.

## Usage

```sh
clerk enable api-keys [--for <targets>] [options]
clerk disable api-keys [--for <targets>] [options]
```

`<targets>` is `orgs` and/or `users`, accepted as space-separated,
comma-separated, or repeated `--for` flags. The singular aliases `org` and
`user` are also accepted for backwards compatibility.

```sh
clerk enable api-keys # defaults to users
clerk enable api-keys --for orgs users
clerk enable api-keys --for orgs,users
clerk disable api-keys # disables API Keys entirely
clerk disable api-keys --for orgs # disables only organization API Keys
```

## Options

| Flag | Description |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `--for <targets>` | Targets (`orgs` and/or `users`), separated by spaces or commas. Enable defaults to users; disable without `--for` disables API Keys entirely. |
| `--app <id>` | Target a specific application |
| `--instance <id>` | Target a specific instance (dev, prod) |
| `--yes` | Skip the confirmation prompt |
| `--dry-run` | Preview the patch without applying it |

## Cascade behavior

- `enable api-keys --for orgs` also sets `organization_settings.enabled = true`.
Organization API Keys require organizations enabled, so this saves a separate
command. The cascade is idempotent.
- `disable api-keys --for orgs` disables only organization API Keys and leaves
organizations enabled.
- `disable api-keys` without `--for` disables API Keys entirely.

## Clerk API endpoints

| Method | Endpoint | Description |
| ------ | ----------------------------------------------------------------- | ----------------------------------------------------------------------- |
| GET | `/v1/platform/applications/{appId}/instances/{instanceId}/config` | Fetch current config for diff before mutation |
| PATCH | `/v1/platform/applications/{appId}/instances/{instanceId}/config` | Patch `api_keys_settings.*` (with `?dry_run=true` when `--dry-run` set) |
230 changes: 230 additions & 0 deletions packages/cli-core/src/commands/api-keys/index.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
import { test, expect, describe, beforeEach, afterEach, spyOn, mock } from "bun:test";
import { mkdtemp, rm } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { _setConfigDir, setProfile } from "../../lib/config.ts";
import {
captureLog,
credentialStoreStubs,
gitStubs,
promptsStubs,
stubFetch,
} from "../../test/lib/stubs.ts";

mock.module("../../lib/credential-store.ts", () => credentialStoreStubs);
mock.module("../../lib/git.ts", () => gitStubs);
mock.module("@inquirer/prompts", () => promptsStubs);
mock.module("../../lib/spinner.ts", () => ({
withSpinner: async (_msg: string, fn: () => Promise<unknown>) => fn(),
}));

describe("clerk enable/disable api-keys", () => {
const originalEnv = { ...process.env };
const originalFetch = globalThis.fetch;
let tempDir: string;
let logSpy: ReturnType<typeof spyOn>;
let errorSpy: ReturnType<typeof spyOn>;
let captured: ReturnType<typeof captureLog>;

beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), "clerk-api-keys-test-"));
_setConfigDir(tempDir);
process.env.CLERK_PLATFORM_API_KEY = "test_key";
process.env.CLERK_PLATFORM_API_URL = "https://test-api.clerk.com";

logSpy = spyOn(console, "log").mockImplementation(() => {});
errorSpy = spyOn(console, "error").mockImplementation(() => {});
captured = captureLog();

stubFetch(async () => {
return new Response(JSON.stringify({}), { status: 200 });
});
});

afterEach(async () => {
captured.teardown();
_setConfigDir(undefined);
process.env = { ...originalEnv };
globalThis.fetch = originalFetch;
logSpy.mockRestore();
errorSpy.mockRestore();
await rm(tempDir, { recursive: true, force: true });
});

async function setupProfile() {
await setProfile(process.cwd(), {
workspaceId: "org_1",
appId: "app_1",
instances: { development: "ins_dev" },
});
}

test("enable defaults to user API Keys", async () => {
let capturedBody = "";
stubFetch(async (_input, init) => {
if (init?.method === "PATCH") capturedBody = init.body as string;
return new Response(JSON.stringify({}), { status: 200 });
});

await setupProfile();
const { apiKeysEnable } = await import("./index.ts");
await captured.run(() => apiKeysEnable({}));

const parsed = JSON.parse(capturedBody);
expect(parsed.api_keys_settings.enabled).toBe(true);
expect(parsed.api_keys_settings.user_api_keys_enabled).toBe(true);
expect(parsed.api_keys_settings.orgs_api_keys_enabled).toBeUndefined();
expect(parsed.organization_settings).toBeUndefined();
});

test("enable --for orgs enables org API Keys and cascades organizations", async () => {
let capturedBody = "";
stubFetch(async (_input, init) => {
if (init?.method === "PATCH") capturedBody = init.body as string;
return new Response(JSON.stringify({}), { status: 200 });
});

await setupProfile();
const { apiKeysEnable } = await import("./index.ts");
await captured.run(() => apiKeysEnable({ for: ["orgs"] }));

const parsed = JSON.parse(capturedBody);
expect(parsed.api_keys_settings.enabled).toBe(true);
expect(parsed.api_keys_settings.orgs_api_keys_enabled).toBe(true);
expect(parsed.api_keys_settings.user_api_keys_enabled).toBeUndefined();
expect(parsed.organization_settings.enabled).toBe(true);
});

test("enable --for users,orgs sets both API Keys targets", async () => {
let capturedBody = "";
stubFetch(async (_input, init) => {
if (init?.method === "PATCH") capturedBody = init.body as string;
return new Response(JSON.stringify({}), { status: 200 });
});

await setupProfile();
const { apiKeysEnable } = await import("./index.ts");
await captured.run(() => apiKeysEnable({ for: ["users,orgs"] }));

const parsed = JSON.parse(capturedBody);
expect(parsed.api_keys_settings.user_api_keys_enabled).toBe(true);
expect(parsed.api_keys_settings.orgs_api_keys_enabled).toBe(true);
expect(parsed.organization_settings.enabled).toBe(true);
});

test("enable rejects invalid --for token", async () => {
await setupProfile();
const { apiKeysEnable } = await import("./index.ts");
await expect(captured.run(() => apiKeysEnable({ for: ["machine"] }))).rejects.toThrow(
'Invalid --for value: "machine". Expected "orgs" and/or "users".',
);
});

test("enable accepts singular --for aliases", async () => {
let capturedBody = "";
stubFetch(async (_input, init) => {
if (init?.method === "PATCH") capturedBody = init.body as string;
return new Response(JSON.stringify({}), { status: 200 });
});

await setupProfile();
const { apiKeysEnable } = await import("./index.ts");
await captured.run(() => apiKeysEnable({ for: ["user", "org"] }));

const parsed = JSON.parse(capturedBody);
expect(parsed.api_keys_settings.user_api_keys_enabled).toBe(true);
expect(parsed.api_keys_settings.orgs_api_keys_enabled).toBe(true);
expect(parsed.organization_settings.enabled).toBe(true);
});

test("enable --dry-run plumbs dry_run=true to the API", async () => {
let capturedUrl = "";
stubFetch(async (input, init) => {
if (init?.method === "PATCH") capturedUrl = input.toString();
return new Response(JSON.stringify({}), { status: 200 });
});

await setupProfile();
const { apiKeysEnable } = await import("./index.ts");
await captured.run(() => apiKeysEnable({ dryRun: true }));

expect(capturedUrl).toContain("dry_run=true");
expect(captured.err).toContain("[dry-run]");
});

test("disable with no --for disables API Keys entirely", async () => {
let capturedBody = "";
stubFetch(async (_input, init) => {
if (init?.method === "PATCH") capturedBody = init.body as string;
return new Response(
JSON.stringify({
api_keys_settings: {
enabled: true,
user_api_keys_enabled: true,
orgs_api_keys_enabled: true,
},
}),
{ status: 200 },
);
});

await setupProfile();
const { apiKeysDisable } = await import("./index.ts");
await captured.run(() => apiKeysDisable({}));

const parsed = JSON.parse(capturedBody);
expect(parsed.api_keys_settings.enabled).toBe(false);
expect(parsed.api_keys_settings.user_api_keys_enabled).toBe(false);
expect(parsed.api_keys_settings.orgs_api_keys_enabled).toBe(false);
});

test("disable --for orgs only disables org API Keys", async () => {
let capturedBody = "";
stubFetch(async (_input, init) => {
if (init?.method === "PATCH") capturedBody = init.body as string;
return new Response(
JSON.stringify({
api_keys_settings: {
enabled: true,
user_api_keys_enabled: true,
orgs_api_keys_enabled: true,
},
}),
{ status: 200 },
);
});

await setupProfile();
const { apiKeysDisable } = await import("./index.ts");
await captured.run(() => apiKeysDisable({ for: ["orgs"] }));

const parsed = JSON.parse(capturedBody);
expect(parsed.api_keys_settings.enabled).toBeUndefined();
expect(parsed.api_keys_settings.user_api_keys_enabled).toBeUndefined();
expect(parsed.api_keys_settings.orgs_api_keys_enabled).toBe(false);
});

test("disable shows no changes when already fully disabled", async () => {
let patchCalls = 0;
stubFetch(async (_input, init) => {
if (init?.method === "PATCH") patchCalls++;
return new Response(
JSON.stringify({
api_keys_settings: {
enabled: false,
user_api_keys_enabled: false,
orgs_api_keys_enabled: false,
},
}),
{ status: 200 },
);
});

await setupProfile();
const { apiKeysDisable } = await import("./index.ts");
await captured.run(() => apiKeysDisable({}));

expect(patchCalls).toBe(0);
expect(captured.err).toContain("No changes detected");
});
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Draft
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
5 changes: 5 additions & 0 deletions .changeset/accept-orgs-users-for-flags.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"clerk": minor
---

Add `clerk enable api-keys` and `clerk disable api-keys` for toggling API Keys on the linked instance. API Key targeting uses the canonical plural `orgs` and `users` values, while the singular `org` and `user` values continue to work as aliases.
69 changes: 69 additions & 0 deletions packages/cli-core/src/cli-program.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -48,6 +48,7 @@ import { update } from "./commands/update/index.ts";
import { isClerkSkillInstalled } from "./lib/skill-detection.ts";
import { orgsEnable, orgsDisable } from "./commands/orgs/index.ts";
import { billingEnable, billingDisable } from "./commands/billing/index.ts";
import { apiKeysEnable, apiKeysDisable } from "./commands/api-keys/index.ts";
import { registerExtras } from "@clerk/cli-extras";

const USER_LIST_ORDER_BY_FIELDS = [
Expand DownExpand Up@@ -636,6 +637,10 @@ Give AI agents better Clerk context: install the Clerk skills
command: "clerk enable billing",
description: "Enable billing for organizations and users",
},
{
command: "clerk enable api-keys",
description: "Enable API Keys for users",
},
]);

enable
Expand DownExpand Up@@ -703,6 +708,38 @@ Give AI agents better Clerk context: install the Clerk skills
])
.action(billingEnable);

enable
.command("api-keys")
.aliases(["apikeys", "api_keys"])
.description("Enable API Keys on the linked instance")
.option(
"--for <targets...>",
"API Keys targets (orgs and/or users), separated by spaces or commas (e.g. orgs users). Defaults to users when omitted.",
)
.option("--app <id>", "Application ID to target")
.option("--instance <id>", "Instance to target (dev, prod, or instance ID)")
.option("--yes", "Skip confirmation prompts")
.option("--dry-run", "Show the patch that would be sent without applying it")
.setExamples([
{
command: "clerk enable api-keys",
description: "Enable API Keys for users",
},
{
command: "clerk enable api-keys --for orgs",
description: "Enable API Keys for organizations",
},
{
command: "clerk enable api-keys --for users orgs",
description: "Enable API Keys for users and organizations",
},
{
command: "clerk enable api-keys --dry-run",
description: "Preview the patch without applying it",
},
])
.action(apiKeysEnable);

const disable = program
.command("disable")
.description("Disable Clerk features on the linked instance")
Expand All@@ -716,6 +753,10 @@ Give AI agents better Clerk context: install the Clerk skills
command: "clerk disable billing",
description: "Disable billing for organizations and users",
},
{
command: "clerk disable api-keys",
description: "Disable API Keys",
},
]);

disable
Expand DownExpand Up@@ -764,6 +805,34 @@ Give AI agents better Clerk context: install the Clerk skills
])
.action(billingDisable);

disable
.command("api-keys")
.aliases(["apikeys", "api_keys"])
.description("Disable API Keys on the linked instance")
.option(
"--for <targets...>",
"API Keys targets (orgs and/or users), separated by spaces or commas (e.g. orgs users). Omit to disable API Keys entirely.",
)
.option("--app <id>", "Application ID to target")
.option("--instance <id>", "Instance to target (dev, prod, or instance ID)")
.option("--yes", "Skip confirmation prompts")
.option("--dry-run", "Show the patch that would be sent without applying it")
.setExamples([
{
command: "clerk disable api-keys",
description: "Disable API Keys entirely",
},
{
command: "clerk disable api-keys --for orgs",
description: "Disable API Keys for organizations only",
},
{
command: "clerk disable api-keys --for users",
description: "Disable API Keys for users only",
},
])
.action(apiKeysDisable);

program
.command("api")
.description("Make authenticated requests to the Clerk API")
Expand Down
52 changes: 52 additions & 0 deletions packages/cli-core/src/commands/api-keys/README.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
# clerk api-keys (enable/disable)

Toggle Clerk API Keys on the linked instance. The handlers are wired to
top-level `clerk enable api-keys` and `clerk disable api-keys` commands.

For arbitrary API Keys settings edits, use
`clerk config patch --json '{"api_keys_settings":{...}}'`.

## Usage

```sh
clerk enable api-keys [--for <targets>] [options]
clerk disable api-keys [--for <targets>] [options]
```

`<targets>` is `orgs` and/or `users`, accepted as space-separated,
comma-separated, or repeated `--for` flags. The singular aliases `org` and
`user` are also accepted for backwards compatibility.

```sh
clerk enable api-keys # defaults to users
clerk enable api-keys --for orgs users
clerk enable api-keys --for orgs,users
clerk disable api-keys # disables API Keys entirely
clerk disable api-keys --for orgs # disables only organization API Keys
```

## Options

| Flag | Description |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `--for <targets>` | Targets (`orgs` and/or `users`), separated by spaces or commas. Enable defaults to users; disable without `--for` disables API Keys entirely. |
| `--app <id>` | Target a specific application |
| `--instance <id>` | Target a specific instance (dev, prod) |
| `--yes` | Skip the confirmation prompt |
| `--dry-run` | Preview the patch without applying it |

## Cascade behavior

- `enable api-keys --for orgs` also sets `organization_settings.enabled = true`.
Organization API Keys require organizations enabled, so this saves a separate
command. The cascade is idempotent.
- `disable api-keys --for orgs` disables only organization API Keys and leaves
organizations enabled.
- `disable api-keys` without `--for` disables API Keys entirely.

## Clerk API endpoints

| Method | Endpoint | Description |
| ------ | ----------------------------------------------------------------- | ----------------------------------------------------------------------- |
| GET | `/v1/platform/applications/{appId}/instances/{instanceId}/config` | Fetch current config for diff before mutation |
| PATCH | `/v1/platform/applications/{appId}/instances/{instanceId}/config` | Patch `api_keys_settings.*` (with `?dry_run=true` when `--dry-run` set) |
230 changes: 230 additions & 0 deletions packages/cli-core/src/commands/api-keys/index.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
import { test, expect, describe, beforeEach, afterEach, spyOn, mock } from "bun:test";
import { mkdtemp, rm } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { _setConfigDir, setProfile } from "../../lib/config.ts";
import {
captureLog,
credentialStoreStubs,
gitStubs,
promptsStubs,
stubFetch,
} from "../../test/lib/stubs.ts";

mock.module("../../lib/credential-store.ts", () => credentialStoreStubs);
mock.module("../../lib/git.ts", () => gitStubs);
mock.module("@inquirer/prompts", () => promptsStubs);
mock.module("../../lib/spinner.ts", () => ({
withSpinner: async (_msg: string, fn: () => Promise<unknown>) => fn(),
}));

describe("clerk enable/disable api-keys", () => {
const originalEnv = { ...process.env };
const originalFetch = globalThis.fetch;
let tempDir: string;
let logSpy: ReturnType<typeof spyOn>;
let errorSpy: ReturnType<typeof spyOn>;
let captured: ReturnType<typeof captureLog>;

beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), "clerk-api-keys-test-"));
_setConfigDir(tempDir);
process.env.CLERK_PLATFORM_API_KEY = "test_key";
process.env.CLERK_PLATFORM_API_URL = "https://test-api.clerk.com";

logSpy = spyOn(console, "log").mockImplementation(() => {});
errorSpy = spyOn(console, "error").mockImplementation(() => {});
captured = captureLog();

stubFetch(async () => {
return new Response(JSON.stringify({}), { status: 200 });
});
});

afterEach(async () => {
captured.teardown();
_setConfigDir(undefined);
process.env = { ...originalEnv };
globalThis.fetch = originalFetch;
logSpy.mockRestore();
errorSpy.mockRestore();
await rm(tempDir, { recursive: true, force: true });
});

async function setupProfile() {
await setProfile(process.cwd(), {
workspaceId: "org_1",
appId: "app_1",
instances: { development: "ins_dev" },
});
}

test("enable defaults to user API Keys", async () => {
let capturedBody = "";
stubFetch(async (_input, init) => {
if (init?.method === "PATCH") capturedBody = init.body as string;
return new Response(JSON.stringify({}), { status: 200 });
});

await setupProfile();
const { apiKeysEnable } = await import("./index.ts");
await captured.run(() => apiKeysEnable({}));

const parsed = JSON.parse(capturedBody);
expect(parsed.api_keys_settings.enabled).toBe(true);
expect(parsed.api_keys_settings.user_api_keys_enabled).toBe(true);
expect(parsed.api_keys_settings.orgs_api_keys_enabled).toBeUndefined();
expect(parsed.organization_settings).toBeUndefined();
});

test("enable --for orgs enables org API Keys and cascades organizations", async () => {
let capturedBody = "";
stubFetch(async (_input, init) => {
if (init?.method === "PATCH") capturedBody = init.body as string;
return new Response(JSON.stringify({}), { status: 200 });
});

await setupProfile();
const { apiKeysEnable } = await import("./index.ts");
await captured.run(() => apiKeysEnable({ for: ["orgs"] }));

const parsed = JSON.parse(capturedBody);
expect(parsed.api_keys_settings.enabled).toBe(true);
expect(parsed.api_keys_settings.orgs_api_keys_enabled).toBe(true);
expect(parsed.api_keys_settings.user_api_keys_enabled).toBeUndefined();
expect(parsed.organization_settings.enabled).toBe(true);
});

test("enable --for users,orgs sets both API Keys targets", async () => {
let capturedBody = "";
stubFetch(async (_input, init) => {
if (init?.method === "PATCH") capturedBody = init.body as string;
return new Response(JSON.stringify({}), { status: 200 });
});

await setupProfile();
const { apiKeysEnable } = await import("./index.ts");
await captured.run(() => apiKeysEnable({ for: ["users,orgs"] }));

const parsed = JSON.parse(capturedBody);
expect(parsed.api_keys_settings.user_api_keys_enabled).toBe(true);
expect(parsed.api_keys_settings.orgs_api_keys_enabled).toBe(true);
expect(parsed.organization_settings.enabled).toBe(true);
});

test("enable rejects invalid --for token", async () => {
await setupProfile();
const { apiKeysEnable } = await import("./index.ts");
await expect(captured.run(() => apiKeysEnable({ for: ["machine"] }))).rejects.toThrow(
'Invalid --for value: "machine". Expected "orgs" and/or "users".',
);
});

test("enable accepts singular --for aliases", async () => {
let capturedBody = "";
stubFetch(async (_input, init) => {
if (init?.method === "PATCH") capturedBody = init.body as string;
return new Response(JSON.stringify({}), { status: 200 });
});

await setupProfile();
const { apiKeysEnable } = await import("./index.ts");
await captured.run(() => apiKeysEnable({ for: ["user", "org"] }));

const parsed = JSON.parse(capturedBody);
expect(parsed.api_keys_settings.user_api_keys_enabled).toBe(true);
expect(parsed.api_keys_settings.orgs_api_keys_enabled).toBe(true);
expect(parsed.organization_settings.enabled).toBe(true);
});

test("enable --dry-run plumbs dry_run=true to the API", async () => {
let capturedUrl = "";
stubFetch(async (input, init) => {
if (init?.method === "PATCH") capturedUrl = input.toString();
return new Response(JSON.stringify({}), { status: 200 });
});

await setupProfile();
const { apiKeysEnable } = await import("./index.ts");
await captured.run(() => apiKeysEnable({ dryRun: true }));

expect(capturedUrl).toContain("dry_run=true");
expect(captured.err).toContain("[dry-run]");
});

test("disable with no --for disables API Keys entirely", async () => {
let capturedBody = "";
stubFetch(async (_input, init) => {
if (init?.method === "PATCH") capturedBody = init.body as string;
return new Response(
JSON.stringify({
api_keys_settings: {
enabled: true,
user_api_keys_enabled: true,
orgs_api_keys_enabled: true,
},
}),
{ status: 200 },
);
});

await setupProfile();
const { apiKeysDisable } = await import("./index.ts");
await captured.run(() => apiKeysDisable({}));

const parsed = JSON.parse(capturedBody);
expect(parsed.api_keys_settings.enabled).toBe(false);
expect(parsed.api_keys_settings.user_api_keys_enabled).toBe(false);
expect(parsed.api_keys_settings.orgs_api_keys_enabled).toBe(false);
});

test("disable --for orgs only disables org API Keys", async () => {
let capturedBody = "";
stubFetch(async (_input, init) => {
if (init?.method === "PATCH") capturedBody = init.body as string;
return new Response(
JSON.stringify({
api_keys_settings: {
enabled: true,
user_api_keys_enabled: true,
orgs_api_keys_enabled: true,
},
}),
{ status: 200 },
);
});

await setupProfile();
const { apiKeysDisable } = await import("./index.ts");
await captured.run(() => apiKeysDisable({ for: ["orgs"] }));

const parsed = JSON.parse(capturedBody);
expect(parsed.api_keys_settings.enabled).toBeUndefined();
expect(parsed.api_keys_settings.user_api_keys_enabled).toBeUndefined();
expect(parsed.api_keys_settings.orgs_api_keys_enabled).toBe(false);
});

test("disable shows no changes when already fully disabled", async () => {
let patchCalls = 0;
stubFetch(async (_input, init) => {
if (init?.method === "PATCH") patchCalls++;
return new Response(
JSON.stringify({
api_keys_settings: {
enabled: false,
user_api_keys_enabled: false,
orgs_api_keys_enabled: false,
},
}),
{ status: 200 },
);
});

await setupProfile();
const { apiKeysDisable } = await import("./index.ts");
await captured.run(() => apiKeysDisable({}));

expect(patchCalls).toBe(0);
expect(captured.err).toContain("No changes detected");
});
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Draft
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
5 changes: 5 additions & 0 deletions .changeset/accept-orgs-users-for-flags.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"clerk": minor
---

Add `clerk enable api-keys` and `clerk disable api-keys` for toggling API Keys on the linked instance. API Key targeting uses the canonical plural `orgs` and `users` values, while the singular `org` and `user` values continue to work as aliases.
69 changes: 69 additions & 0 deletions packages/cli-core/src/cli-program.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -48,6 +48,7 @@ import { update } from "./commands/update/index.ts";
import { isClerkSkillInstalled } from "./lib/skill-detection.ts";
import { orgsEnable, orgsDisable } from "./commands/orgs/index.ts";
import { billingEnable, billingDisable } from "./commands/billing/index.ts";
import { apiKeysEnable, apiKeysDisable } from "./commands/api-keys/index.ts";
import { registerExtras } from "@clerk/cli-extras";

const USER_LIST_ORDER_BY_FIELDS = [
Expand DownExpand Up@@ -636,6 +637,10 @@ Give AI agents better Clerk context: install the Clerk skills
command: "clerk enable billing",
description: "Enable billing for organizations and users",
},
{
command: "clerk enable api-keys",
description: "Enable API Keys for users",
},
]);

enable
Expand DownExpand Up@@ -703,6 +708,38 @@ Give AI agents better Clerk context: install the Clerk skills
])
.action(billingEnable);

enable
.command("api-keys")
.aliases(["apikeys", "api_keys"])
.description("Enable API Keys on the linked instance")
.option(
"--for <targets...>",
"API Keys targets (orgs and/or users), separated by spaces or commas (e.g. orgs users). Defaults to users when omitted.",
)
.option("--app <id>", "Application ID to target")
.option("--instance <id>", "Instance to target (dev, prod, or instance ID)")
.option("--yes", "Skip confirmation prompts")
.option("--dry-run", "Show the patch that would be sent without applying it")
.setExamples([
{
command: "clerk enable api-keys",
description: "Enable API Keys for users",
},
{
command: "clerk enable api-keys --for orgs",
description: "Enable API Keys for organizations",
},
{
command: "clerk enable api-keys --for users orgs",
description: "Enable API Keys for users and organizations",
},
{
command: "clerk enable api-keys --dry-run",
description: "Preview the patch without applying it",
},
])
.action(apiKeysEnable);

const disable = program
.command("disable")
.description("Disable Clerk features on the linked instance")
Expand All@@ -716,6 +753,10 @@ Give AI agents better Clerk context: install the Clerk skills
command: "clerk disable billing",
description: "Disable billing for organizations and users",
},
{
command: "clerk disable api-keys",
description: "Disable API Keys",
},
]);

disable
Expand DownExpand Up@@ -764,6 +805,34 @@ Give AI agents better Clerk context: install the Clerk skills
])
.action(billingDisable);

disable
.command("api-keys")
.aliases(["apikeys", "api_keys"])
.description("Disable API Keys on the linked instance")
.option(
"--for <targets...>",
"API Keys targets (orgs and/or users), separated by spaces or commas (e.g. orgs users). Omit to disable API Keys entirely.",
)
.option("--app <id>", "Application ID to target")
.option("--instance <id>", "Instance to target (dev, prod, or instance ID)")
.option("--yes", "Skip confirmation prompts")
.option("--dry-run", "Show the patch that would be sent without applying it")
.setExamples([
{
command: "clerk disable api-keys",
description: "Disable API Keys entirely",
},
{
command: "clerk disable api-keys --for orgs",
description: "Disable API Keys for organizations only",
},
{
command: "clerk disable api-keys --for users",
description: "Disable API Keys for users only",
},
])
.action(apiKeysDisable);

program
.command("api")
.description("Make authenticated requests to the Clerk API")
Expand Down
52 changes: 52 additions & 0 deletions packages/cli-core/src/commands/api-keys/README.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
# clerk api-keys (enable/disable)

Toggle Clerk API Keys on the linked instance. The handlers are wired to
top-level `clerk enable api-keys` and `clerk disable api-keys` commands.

For arbitrary API Keys settings edits, use
`clerk config patch --json '{"api_keys_settings":{...}}'`.

## Usage

```sh
clerk enable api-keys [--for <targets>] [options]
clerk disable api-keys [--for <targets>] [options]
```

`<targets>` is `orgs` and/or `users`, accepted as space-separated,
comma-separated, or repeated `--for` flags. The singular aliases `org` and
`user` are also accepted for backwards compatibility.

```sh
clerk enable api-keys # defaults to users
clerk enable api-keys --for orgs users
clerk enable api-keys --for orgs,users
clerk disable api-keys # disables API Keys entirely
clerk disable api-keys --for orgs # disables only organization API Keys
```

## Options

| Flag | Description |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `--for <targets>` | Targets (`orgs` and/or `users`), separated by spaces or commas. Enable defaults to users; disable without `--for` disables API Keys entirely. |
| `--app <id>` | Target a specific application |
| `--instance <id>` | Target a specific instance (dev, prod) |
| `--yes` | Skip the confirmation prompt |
| `--dry-run` | Preview the patch without applying it |

## Cascade behavior

- `enable api-keys --for orgs` also sets `organization_settings.enabled = true`.
Organization API Keys require organizations enabled, so this saves a separate
command. The cascade is idempotent.
- `disable api-keys --for orgs` disables only organization API Keys and leaves
organizations enabled.
- `disable api-keys` without `--for` disables API Keys entirely.

## Clerk API endpoints

| Method | Endpoint | Description |
| ------ | ----------------------------------------------------------------- | ----------------------------------------------------------------------- |
| GET | `/v1/platform/applications/{appId}/instances/{instanceId}/config` | Fetch current config for diff before mutation |
| PATCH | `/v1/platform/applications/{appId}/instances/{instanceId}/config` | Patch `api_keys_settings.*` (with `?dry_run=true` when `--dry-run` set) |
230 changes: 230 additions & 0 deletions packages/cli-core/src/commands/api-keys/index.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
import { test, expect, describe, beforeEach, afterEach, spyOn, mock } from "bun:test";
import { mkdtemp, rm } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { _setConfigDir, setProfile } from "../../lib/config.ts";
import {
captureLog,
credentialStoreStubs,
gitStubs,
promptsStubs,
stubFetch,
} from "../../test/lib/stubs.ts";

mock.module("../../lib/credential-store.ts", () => credentialStoreStubs);
mock.module("../../lib/git.ts", () => gitStubs);
mock.module("@inquirer/prompts", () => promptsStubs);
mock.module("../../lib/spinner.ts", () => ({
withSpinner: async (_msg: string, fn: () => Promise<unknown>) => fn(),
}));

describe("clerk enable/disable api-keys", () => {
const originalEnv = { ...process.env };
const originalFetch = globalThis.fetch;
let tempDir: string;
let logSpy: ReturnType<typeof spyOn>;
let errorSpy: ReturnType<typeof spyOn>;
let captured: ReturnType<typeof captureLog>;

beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), "clerk-api-keys-test-"));
_setConfigDir(tempDir);
process.env.CLERK_PLATFORM_API_KEY = "test_key";
process.env.CLERK_PLATFORM_API_URL = "https://test-api.clerk.com";

logSpy = spyOn(console, "log").mockImplementation(() => {});
errorSpy = spyOn(console, "error").mockImplementation(() => {});
captured = captureLog();

stubFetch(async () => {
return new Response(JSON.stringify({}), { status: 200 });
});
});

afterEach(async () => {
captured.teardown();
_setConfigDir(undefined);
process.env = { ...originalEnv };
globalThis.fetch = originalFetch;
logSpy.mockRestore();
errorSpy.mockRestore();
await rm(tempDir, { recursive: true, force: true });
});

async function setupProfile() {
await setProfile(process.cwd(), {
workspaceId: "org_1",
appId: "app_1",
instances: { development: "ins_dev" },
});
}

test("enable defaults to user API Keys", async () => {
let capturedBody = "";
stubFetch(async (_input, init) => {
if (init?.method === "PATCH") capturedBody = init.body as string;
return new Response(JSON.stringify({}), { status: 200 });
});

await setupProfile();
const { apiKeysEnable } = await import("./index.ts");
await captured.run(() => apiKeysEnable({}));

const parsed = JSON.parse(capturedBody);
expect(parsed.api_keys_settings.enabled).toBe(true);
expect(parsed.api_keys_settings.user_api_keys_enabled).toBe(true);
expect(parsed.api_keys_settings.orgs_api_keys_enabled).toBeUndefined();
expect(parsed.organization_settings).toBeUndefined();
});

test("enable --for orgs enables org API Keys and cascades organizations", async () => {
let capturedBody = "";
stubFetch(async (_input, init) => {
if (init?.method === "PATCH") capturedBody = init.body as string;
return new Response(JSON.stringify({}), { status: 200 });
});

await setupProfile();
const { apiKeysEnable } = await import("./index.ts");
await captured.run(() => apiKeysEnable({ for: ["orgs"] }));

const parsed = JSON.parse(capturedBody);
expect(parsed.api_keys_settings.enabled).toBe(true);
expect(parsed.api_keys_settings.orgs_api_keys_enabled).toBe(true);
expect(parsed.api_keys_settings.user_api_keys_enabled).toBeUndefined();
expect(parsed.organization_settings.enabled).toBe(true);
});

test("enable --for users,orgs sets both API Keys targets", async () => {
let capturedBody = "";
stubFetch(async (_input, init) => {
if (init?.method === "PATCH") capturedBody = init.body as string;
return new Response(JSON.stringify({}), { status: 200 });
});

await setupProfile();
const { apiKeysEnable } = await import("./index.ts");
await captured.run(() => apiKeysEnable({ for: ["users,orgs"] }));

const parsed = JSON.parse(capturedBody);
expect(parsed.api_keys_settings.user_api_keys_enabled).toBe(true);
expect(parsed.api_keys_settings.orgs_api_keys_enabled).toBe(true);
expect(parsed.organization_settings.enabled).toBe(true);
});

test("enable rejects invalid --for token", async () => {
await setupProfile();
const { apiKeysEnable } = await import("./index.ts");
await expect(captured.run(() => apiKeysEnable({ for: ["machine"] }))).rejects.toThrow(
'Invalid --for value: "machine". Expected "orgs" and/or "users".',
);
});

test("enable accepts singular --for aliases", async () => {
let capturedBody = "";
stubFetch(async (_input, init) => {
if (init?.method === "PATCH") capturedBody = init.body as string;
return new Response(JSON.stringify({}), { status: 200 });
});

await setupProfile();
const { apiKeysEnable } = await import("./index.ts");
await captured.run(() => apiKeysEnable({ for: ["user", "org"] }));

const parsed = JSON.parse(capturedBody);
expect(parsed.api_keys_settings.user_api_keys_enabled).toBe(true);
expect(parsed.api_keys_settings.orgs_api_keys_enabled).toBe(true);
expect(parsed.organization_settings.enabled).toBe(true);
});

test("enable --dry-run plumbs dry_run=true to the API", async () => {
let capturedUrl = "";
stubFetch(async (input, init) => {
if (init?.method === "PATCH") capturedUrl = input.toString();
return new Response(JSON.stringify({}), { status: 200 });
});

await setupProfile();
const { apiKeysEnable } = await import("./index.ts");
await captured.run(() => apiKeysEnable({ dryRun: true }));

expect(capturedUrl).toContain("dry_run=true");
expect(captured.err).toContain("[dry-run]");
});

test("disable with no --for disables API Keys entirely", async () => {
let capturedBody = "";
stubFetch(async (_input, init) => {
if (init?.method === "PATCH") capturedBody = init.body as string;
return new Response(
JSON.stringify({
api_keys_settings: {
enabled: true,
user_api_keys_enabled: true,
orgs_api_keys_enabled: true,
},
}),
{ status: 200 },
);
});

await setupProfile();
const { apiKeysDisable } = await import("./index.ts");
await captured.run(() => apiKeysDisable({}));

const parsed = JSON.parse(capturedBody);
expect(parsed.api_keys_settings.enabled).toBe(false);
expect(parsed.api_keys_settings.user_api_keys_enabled).toBe(false);
expect(parsed.api_keys_settings.orgs_api_keys_enabled).toBe(false);
});

test("disable --for orgs only disables org API Keys", async () => {
let capturedBody = "";
stubFetch(async (_input, init) => {
if (init?.method === "PATCH") capturedBody = init.body as string;
return new Response(
JSON.stringify({
api_keys_settings: {
enabled: true,
user_api_keys_enabled: true,
orgs_api_keys_enabled: true,
},
}),
{ status: 200 },
);
});

await setupProfile();
const { apiKeysDisable } = await import("./index.ts");
await captured.run(() => apiKeysDisable({ for: ["orgs"] }));

const parsed = JSON.parse(capturedBody);
expect(parsed.api_keys_settings.enabled).toBeUndefined();
expect(parsed.api_keys_settings.user_api_keys_enabled).toBeUndefined();
expect(parsed.api_keys_settings.orgs_api_keys_enabled).toBe(false);
});

test("disable shows no changes when already fully disabled", async () => {
let patchCalls = 0;
stubFetch(async (_input, init) => {
if (init?.method === "PATCH") patchCalls++;
return new Response(
JSON.stringify({
api_keys_settings: {
enabled: false,
user_api_keys_enabled: false,
orgs_api_keys_enabled: false,
},
}),
{ status: 200 },
);
});

await setupProfile();
const { apiKeysDisable } = await import("./index.ts");
await captured.run(() => apiKeysDisable({}));

expect(patchCalls).toBe(0);
expect(captured.err).toContain("No changes detected");
});
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Draft
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
5 changes: 5 additions & 0 deletions .changeset/accept-orgs-users-for-flags.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"clerk": minor
---

Add `clerk enable api-keys` and `clerk disable api-keys` for toggling API Keys on the linked instance. API Key targeting uses the canonical plural `orgs` and `users` values, while the singular `org` and `user` values continue to work as aliases.
69 changes: 69 additions & 0 deletions packages/cli-core/src/cli-program.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -48,6 +48,7 @@ import { update } from "./commands/update/index.ts";
import { isClerkSkillInstalled } from "./lib/skill-detection.ts";
import { orgsEnable, orgsDisable } from "./commands/orgs/index.ts";
import { billingEnable, billingDisable } from "./commands/billing/index.ts";
import { apiKeysEnable, apiKeysDisable } from "./commands/api-keys/index.ts";
import { registerExtras } from "@clerk/cli-extras";

const USER_LIST_ORDER_BY_FIELDS = [
Expand DownExpand Up@@ -636,6 +637,10 @@ Give AI agents better Clerk context: install the Clerk skills
command: "clerk enable billing",
description: "Enable billing for organizations and users",
},
{
command: "clerk enable api-keys",
description: "Enable API Keys for users",
},
]);

enable
Expand DownExpand Up@@ -703,6 +708,38 @@ Give AI agents better Clerk context: install the Clerk skills
])
.action(billingEnable);

enable
.command("api-keys")
.aliases(["apikeys", "api_keys"])
.description("Enable API Keys on the linked instance")
.option(
"--for <targets...>",
"API Keys targets (orgs and/or users), separated by spaces or commas (e.g. orgs users). Defaults to users when omitted.",
)
.option("--app <id>", "Application ID to target")
.option("--instance <id>", "Instance to target (dev, prod, or instance ID)")
.option("--yes", "Skip confirmation prompts")
.option("--dry-run", "Show the patch that would be sent without applying it")
.setExamples([
{
command: "clerk enable api-keys",
description: "Enable API Keys for users",
},
{
command: "clerk enable api-keys --for orgs",
description: "Enable API Keys for organizations",
},
{
command: "clerk enable api-keys --for users orgs",
description: "Enable API Keys for users and organizations",
},
{
command: "clerk enable api-keys --dry-run",
description: "Preview the patch without applying it",
},
])
.action(apiKeysEnable);

const disable = program
.command("disable")
.description("Disable Clerk features on the linked instance")
Expand All@@ -716,6 +753,10 @@ Give AI agents better Clerk context: install the Clerk skills
command: "clerk disable billing",
description: "Disable billing for organizations and users",
},
{
command: "clerk disable api-keys",
description: "Disable API Keys",
},
]);

disable
Expand DownExpand Up@@ -764,6 +805,34 @@ Give AI agents better Clerk context: install the Clerk skills
])
.action(billingDisable);

disable
.command("api-keys")
.aliases(["apikeys", "api_keys"])
.description("Disable API Keys on the linked instance")
.option(
"--for <targets...>",
"API Keys targets (orgs and/or users), separated by spaces or commas (e.g. orgs users). Omit to disable API Keys entirely.",
)
.option("--app <id>", "Application ID to target")
.option("--instance <id>", "Instance to target (dev, prod, or instance ID)")
.option("--yes", "Skip confirmation prompts")
.option("--dry-run", "Show the patch that would be sent without applying it")
.setExamples([
{
command: "clerk disable api-keys",
description: "Disable API Keys entirely",
},
{
command: "clerk disable api-keys --for orgs",
description: "Disable API Keys for organizations only",
},
{
command: "clerk disable api-keys --for users",
description: "Disable API Keys for users only",
},
])
.action(apiKeysDisable);

program
.command("api")
.description("Make authenticated requests to the Clerk API")
Expand Down
52 changes: 52 additions & 0 deletions packages/cli-core/src/commands/api-keys/README.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
# clerk api-keys (enable/disable)

Toggle Clerk API Keys on the linked instance. The handlers are wired to
top-level `clerk enable api-keys` and `clerk disable api-keys` commands.

For arbitrary API Keys settings edits, use
`clerk config patch --json '{"api_keys_settings":{...}}'`.

## Usage

```sh
clerk enable api-keys [--for <targets>] [options]
clerk disable api-keys [--for <targets>] [options]
```

`<targets>` is `orgs` and/or `users`, accepted as space-separated,
comma-separated, or repeated `--for` flags. The singular aliases `org` and
`user` are also accepted for backwards compatibility.

```sh
clerk enable api-keys # defaults to users
clerk enable api-keys --for orgs users
clerk enable api-keys --for orgs,users
clerk disable api-keys # disables API Keys entirely
clerk disable api-keys --for orgs # disables only organization API Keys
```

## Options

| Flag | Description |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `--for <targets>` | Targets (`orgs` and/or `users`), separated by spaces or commas. Enable defaults to users; disable without `--for` disables API Keys entirely. |
| `--app <id>` | Target a specific application |
| `--instance <id>` | Target a specific instance (dev, prod) |
| `--yes` | Skip the confirmation prompt |
| `--dry-run` | Preview the patch without applying it |

## Cascade behavior

- `enable api-keys --for orgs` also sets `organization_settings.enabled = true`.
Organization API Keys require organizations enabled, so this saves a separate
command. The cascade is idempotent.
- `disable api-keys --for orgs` disables only organization API Keys and leaves
organizations enabled.
- `disable api-keys` without `--for` disables API Keys entirely.

## Clerk API endpoints

| Method | Endpoint | Description |
| ------ | ----------------------------------------------------------------- | ----------------------------------------------------------------------- |
| GET | `/v1/platform/applications/{appId}/instances/{instanceId}/config` | Fetch current config for diff before mutation |
| PATCH | `/v1/platform/applications/{appId}/instances/{instanceId}/config` | Patch `api_keys_settings.*` (with `?dry_run=true` when `--dry-run` set) |
230 changes: 230 additions & 0 deletions packages/cli-core/src/commands/api-keys/index.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
import { test, expect, describe, beforeEach, afterEach, spyOn, mock } from "bun:test";
import { mkdtemp, rm } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { _setConfigDir, setProfile } from "../../lib/config.ts";
import {
captureLog,
credentialStoreStubs,
gitStubs,
promptsStubs,
stubFetch,
} from "../../test/lib/stubs.ts";

mock.module("../../lib/credential-store.ts", () => credentialStoreStubs);
mock.module("../../lib/git.ts", () => gitStubs);
mock.module("@inquirer/prompts", () => promptsStubs);
mock.module("../../lib/spinner.ts", () => ({
withSpinner: async (_msg: string, fn: () => Promise<unknown>) => fn(),
}));

describe("clerk enable/disable api-keys", () => {
const originalEnv = { ...process.env };
const originalFetch = globalThis.fetch;
let tempDir: string;
let logSpy: ReturnType<typeof spyOn>;
let errorSpy: ReturnType<typeof spyOn>;
let captured: ReturnType<typeof captureLog>;

beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), "clerk-api-keys-test-"));
_setConfigDir(tempDir);
process.env.CLERK_PLATFORM_API_KEY = "test_key";
process.env.CLERK_PLATFORM_API_URL = "https://test-api.clerk.com";

logSpy = spyOn(console, "log").mockImplementation(() => {});
errorSpy = spyOn(console, "error").mockImplementation(() => {});
captured = captureLog();

stubFetch(async () => {
return new Response(JSON.stringify({}), { status: 200 });
});
});

afterEach(async () => {
captured.teardown();
_setConfigDir(undefined);
process.env = { ...originalEnv };
globalThis.fetch = originalFetch;
logSpy.mockRestore();
errorSpy.mockRestore();
await rm(tempDir, { recursive: true, force: true });
});

async function setupProfile() {
await setProfile(process.cwd(), {
workspaceId: "org_1",
appId: "app_1",
instances: { development: "ins_dev" },
});
}

test("enable defaults to user API Keys", async () => {
let capturedBody = "";
stubFetch(async (_input, init) => {
if (init?.method === "PATCH") capturedBody = init.body as string;
return new Response(JSON.stringify({}), { status: 200 });
});

await setupProfile();
const { apiKeysEnable } = await import("./index.ts");
await captured.run(() => apiKeysEnable({}));

const parsed = JSON.parse(capturedBody);
expect(parsed.api_keys_settings.enabled).toBe(true);
expect(parsed.api_keys_settings.user_api_keys_enabled).toBe(true);
expect(parsed.api_keys_settings.orgs_api_keys_enabled).toBeUndefined();
expect(parsed.organization_settings).toBeUndefined();
});

test("enable --for orgs enables org API Keys and cascades organizations", async () => {
let capturedBody = "";
stubFetch(async (_input, init) => {
if (init?.method === "PATCH") capturedBody = init.body as string;
return new Response(JSON.stringify({}), { status: 200 });
});

await setupProfile();
const { apiKeysEnable } = await import("./index.ts");
await captured.run(() => apiKeysEnable({ for: ["orgs"] }));

const parsed = JSON.parse(capturedBody);
expect(parsed.api_keys_settings.enabled).toBe(true);
expect(parsed.api_keys_settings.orgs_api_keys_enabled).toBe(true);
expect(parsed.api_keys_settings.user_api_keys_enabled).toBeUndefined();
expect(parsed.organization_settings.enabled).toBe(true);
});

test("enable --for users,orgs sets both API Keys targets", async () => {
let capturedBody = "";
stubFetch(async (_input, init) => {
if (init?.method === "PATCH") capturedBody = init.body as string;
return new Response(JSON.stringify({}), { status: 200 });
});

await setupProfile();
const { apiKeysEnable } = await import("./index.ts");
await captured.run(() => apiKeysEnable({ for: ["users,orgs"] }));

const parsed = JSON.parse(capturedBody);
expect(parsed.api_keys_settings.user_api_keys_enabled).toBe(true);
expect(parsed.api_keys_settings.orgs_api_keys_enabled).toBe(true);
expect(parsed.organization_settings.enabled).toBe(true);
});

test("enable rejects invalid --for token", async () => {
await setupProfile();
const { apiKeysEnable } = await import("./index.ts");
await expect(captured.run(() => apiKeysEnable({ for: ["machine"] }))).rejects.toThrow(
'Invalid --for value: "machine". Expected "orgs" and/or "users".',
);
});

test("enable accepts singular --for aliases", async () => {
let capturedBody = "";
stubFetch(async (_input, init) => {
if (init?.method === "PATCH") capturedBody = init.body as string;
return new Response(JSON.stringify({}), { status: 200 });
});

await setupProfile();
const { apiKeysEnable } = await import("./index.ts");
await captured.run(() => apiKeysEnable({ for: ["user", "org"] }));

const parsed = JSON.parse(capturedBody);
expect(parsed.api_keys_settings.user_api_keys_enabled).toBe(true);
expect(parsed.api_keys_settings.orgs_api_keys_enabled).toBe(true);
expect(parsed.organization_settings.enabled).toBe(true);
});

test("enable --dry-run plumbs dry_run=true to the API", async () => {
let capturedUrl = "";
stubFetch(async (input, init) => {
if (init?.method === "PATCH") capturedUrl = input.toString();
return new Response(JSON.stringify({}), { status: 200 });
});

await setupProfile();
const { apiKeysEnable } = await import("./index.ts");
await captured.run(() => apiKeysEnable({ dryRun: true }));

expect(capturedUrl).toContain("dry_run=true");
expect(captured.err).toContain("[dry-run]");
});

test("disable with no --for disables API Keys entirely", async () => {
let capturedBody = "";
stubFetch(async (_input, init) => {
if (init?.method === "PATCH") capturedBody = init.body as string;
return new Response(
JSON.stringify({
api_keys_settings: {
enabled: true,
user_api_keys_enabled: true,
orgs_api_keys_enabled: true,
},
}),
{ status: 200 },
);
});

await setupProfile();
const { apiKeysDisable } = await import("./index.ts");
await captured.run(() => apiKeysDisable({}));

const parsed = JSON.parse(capturedBody);
expect(parsed.api_keys_settings.enabled).toBe(false);
expect(parsed.api_keys_settings.user_api_keys_enabled).toBe(false);
expect(parsed.api_keys_settings.orgs_api_keys_enabled).toBe(false);
});

test("disable --for orgs only disables org API Keys", async () => {
let capturedBody = "";
stubFetch(async (_input, init) => {
if (init?.method === "PATCH") capturedBody = init.body as string;
return new Response(
JSON.stringify({
api_keys_settings: {
enabled: true,
user_api_keys_enabled: true,
orgs_api_keys_enabled: true,
},
}),
{ status: 200 },
);
});

await setupProfile();
const { apiKeysDisable } = await import("./index.ts");
await captured.run(() => apiKeysDisable({ for: ["orgs"] }));

const parsed = JSON.parse(capturedBody);
expect(parsed.api_keys_settings.enabled).toBeUndefined();
expect(parsed.api_keys_settings.user_api_keys_enabled).toBeUndefined();
expect(parsed.api_keys_settings.orgs_api_keys_enabled).toBe(false);
});

test("disable shows no changes when already fully disabled", async () => {
let patchCalls = 0;
stubFetch(async (_input, init) => {
if (init?.method === "PATCH") patchCalls++;
return new Response(
JSON.stringify({
api_keys_settings: {
enabled: false,
user_api_keys_enabled: false,
orgs_api_keys_enabled: false,
},
}),
{ status: 200 },
);
});

await setupProfile();
const { apiKeysDisable } = await import("./index.ts");
await captured.run(() => apiKeysDisable({}));

expect(patchCalls).toBe(0);
expect(captured.err).toContain("No changes detected");
});
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Draft
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
5 changes: 5 additions & 0 deletions .changeset/accept-orgs-users-for-flags.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"clerk": minor
---

Add `clerk enable api-keys` and `clerk disable api-keys` for toggling API Keys on the linked instance. API Key targeting uses the canonical plural `orgs` and `users` values, while the singular `org` and `user` values continue to work as aliases.
69 changes: 69 additions & 0 deletions packages/cli-core/src/cli-program.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -48,6 +48,7 @@ import { update } from "./commands/update/index.ts";
import { isClerkSkillInstalled } from "./lib/skill-detection.ts";
import { orgsEnable, orgsDisable } from "./commands/orgs/index.ts";
import { billingEnable, billingDisable } from "./commands/billing/index.ts";
import { apiKeysEnable, apiKeysDisable } from "./commands/api-keys/index.ts";
import { registerExtras } from "@clerk/cli-extras";

const USER_LIST_ORDER_BY_FIELDS = [
Expand DownExpand Up@@ -636,6 +637,10 @@ Give AI agents better Clerk context: install the Clerk skills
command: "clerk enable billing",
description: "Enable billing for organizations and users",
},
{
command: "clerk enable api-keys",
description: "Enable API Keys for users",
},
]);

enable
Expand DownExpand Up@@ -703,6 +708,38 @@ Give AI agents better Clerk context: install the Clerk skills
])
.action(billingEnable);

enable
.command("api-keys")
.aliases(["apikeys", "api_keys"])
.description("Enable API Keys on the linked instance")
.option(
"--for <targets...>",
"API Keys targets (orgs and/or users), separated by spaces or commas (e.g. orgs users). Defaults to users when omitted.",
)
.option("--app <id>", "Application ID to target")
.option("--instance <id>", "Instance to target (dev, prod, or instance ID)")
.option("--yes", "Skip confirmation prompts")
.option("--dry-run", "Show the patch that would be sent without applying it")
.setExamples([
{
command: "clerk enable api-keys",
description: "Enable API Keys for users",
},
{
command: "clerk enable api-keys --for orgs",
description: "Enable API Keys for organizations",
},
{
command: "clerk enable api-keys --for users orgs",
description: "Enable API Keys for users and organizations",
},
{
command: "clerk enable api-keys --dry-run",
description: "Preview the patch without applying it",
},
])
.action(apiKeysEnable);

const disable = program
.command("disable")
.description("Disable Clerk features on the linked instance")
Expand All@@ -716,6 +753,10 @@ Give AI agents better Clerk context: install the Clerk skills
command: "clerk disable billing",
description: "Disable billing for organizations and users",
},
{
command: "clerk disable api-keys",
description: "Disable API Keys",
},
]);

disable
Expand DownExpand Up@@ -764,6 +805,34 @@ Give AI agents better Clerk context: install the Clerk skills
])
.action(billingDisable);

disable
.command("api-keys")
.aliases(["apikeys", "api_keys"])
.description("Disable API Keys on the linked instance")
.option(
"--for <targets...>",
"API Keys targets (orgs and/or users), separated by spaces or commas (e.g. orgs users). Omit to disable API Keys entirely.",
)
.option("--app <id>", "Application ID to target")
.option("--instance <id>", "Instance to target (dev, prod, or instance ID)")
.option("--yes", "Skip confirmation prompts")
.option("--dry-run", "Show the patch that would be sent without applying it")
.setExamples([
{
command: "clerk disable api-keys",
description: "Disable API Keys entirely",
},
{
command: "clerk disable api-keys --for orgs",
description: "Disable API Keys for organizations only",
},
{
command: "clerk disable api-keys --for users",
description: "Disable API Keys for users only",
},
])
.action(apiKeysDisable);

program
.command("api")
.description("Make authenticated requests to the Clerk API")
Expand Down
52 changes: 52 additions & 0 deletions packages/cli-core/src/commands/api-keys/README.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
# clerk api-keys (enable/disable)

Toggle Clerk API Keys on the linked instance. The handlers are wired to
top-level `clerk enable api-keys` and `clerk disable api-keys` commands.

For arbitrary API Keys settings edits, use
`clerk config patch --json '{"api_keys_settings":{...}}'`.

## Usage

```sh
clerk enable api-keys [--for <targets>] [options]
clerk disable api-keys [--for <targets>] [options]
```

`<targets>` is `orgs` and/or `users`, accepted as space-separated,
comma-separated, or repeated `--for` flags. The singular aliases `org` and
`user` are also accepted for backwards compatibility.

```sh
clerk enable api-keys # defaults to users
clerk enable api-keys --for orgs users
clerk enable api-keys --for orgs,users
clerk disable api-keys # disables API Keys entirely
clerk disable api-keys --for orgs # disables only organization API Keys
```

## Options

| Flag | Description |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `--for <targets>` | Targets (`orgs` and/or `users`), separated by spaces or commas. Enable defaults to users; disable without `--for` disables API Keys entirely. |
| `--app <id>` | Target a specific application |
| `--instance <id>` | Target a specific instance (dev, prod) |
| `--yes` | Skip the confirmation prompt |
| `--dry-run` | Preview the patch without applying it |

## Cascade behavior

- `enable api-keys --for orgs` also sets `organization_settings.enabled = true`.
Organization API Keys require organizations enabled, so this saves a separate
command. The cascade is idempotent.
- `disable api-keys --for orgs` disables only organization API Keys and leaves
organizations enabled.
- `disable api-keys` without `--for` disables API Keys entirely.

## Clerk API endpoints

| Method | Endpoint | Description |
| ------ | ----------------------------------------------------------------- | ----------------------------------------------------------------------- |
| GET | `/v1/platform/applications/{appId}/instances/{instanceId}/config` | Fetch current config for diff before mutation |
| PATCH | `/v1/platform/applications/{appId}/instances/{instanceId}/config` | Patch `api_keys_settings.*` (with `?dry_run=true` when `--dry-run` set) |
230 changes: 230 additions & 0 deletions packages/cli-core/src/commands/api-keys/index.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
import { test, expect, describe, beforeEach, afterEach, spyOn, mock } from "bun:test";
import { mkdtemp, rm } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { _setConfigDir, setProfile } from "../../lib/config.ts";
import {
captureLog,
credentialStoreStubs,
gitStubs,
promptsStubs,
stubFetch,
} from "../../test/lib/stubs.ts";

mock.module("../../lib/credential-store.ts", () => credentialStoreStubs);
mock.module("../../lib/git.ts", () => gitStubs);
mock.module("@inquirer/prompts", () => promptsStubs);
mock.module("../../lib/spinner.ts", () => ({
withSpinner: async (_msg: string, fn: () => Promise<unknown>) => fn(),
}));

describe("clerk enable/disable api-keys", () => {
const originalEnv = { ...process.env };
const originalFetch = globalThis.fetch;
let tempDir: string;
let logSpy: ReturnType<typeof spyOn>;
let errorSpy: ReturnType<typeof spyOn>;
let captured: ReturnType<typeof captureLog>;

beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), "clerk-api-keys-test-"));
_setConfigDir(tempDir);
process.env.CLERK_PLATFORM_API_KEY = "test_key";
process.env.CLERK_PLATFORM_API_URL = "https://test-api.clerk.com";

logSpy = spyOn(console, "log").mockImplementation(() => {});
errorSpy = spyOn(console, "error").mockImplementation(() => {});
captured = captureLog();

stubFetch(async () => {
return new Response(JSON.stringify({}), { status: 200 });
});
});

afterEach(async () => {
captured.teardown();
_setConfigDir(undefined);
process.env = { ...originalEnv };
globalThis.fetch = originalFetch;
logSpy.mockRestore();
errorSpy.mockRestore();
await rm(tempDir, { recursive: true, force: true });
});

async function setupProfile() {
await setProfile(process.cwd(), {
workspaceId: "org_1",
appId: "app_1",
instances: { development: "ins_dev" },
});
}

test("enable defaults to user API Keys", async () => {
let capturedBody = "";
stubFetch(async (_input, init) => {
if (init?.method === "PATCH") capturedBody = init.body as string;
return new Response(JSON.stringify({}), { status: 200 });
});

await setupProfile();
const { apiKeysEnable } = await import("./index.ts");
await captured.run(() => apiKeysEnable({}));

const parsed = JSON.parse(capturedBody);
expect(parsed.api_keys_settings.enabled).toBe(true);
expect(parsed.api_keys_settings.user_api_keys_enabled).toBe(true);
expect(parsed.api_keys_settings.orgs_api_keys_enabled).toBeUndefined();
expect(parsed.organization_settings).toBeUndefined();
});

test("enable --for orgs enables org API Keys and cascades organizations", async () => {
let capturedBody = "";
stubFetch(async (_input, init) => {
if (init?.method === "PATCH") capturedBody = init.body as string;
return new Response(JSON.stringify({}), { status: 200 });
});

await setupProfile();
const { apiKeysEnable } = await import("./index.ts");
await captured.run(() => apiKeysEnable({ for: ["orgs"] }));

const parsed = JSON.parse(capturedBody);
expect(parsed.api_keys_settings.enabled).toBe(true);
expect(parsed.api_keys_settings.orgs_api_keys_enabled).toBe(true);
expect(parsed.api_keys_settings.user_api_keys_enabled).toBeUndefined();
expect(parsed.organization_settings.enabled).toBe(true);
});

test("enable --for users,orgs sets both API Keys targets", async () => {
let capturedBody = "";
stubFetch(async (_input, init) => {
if (init?.method === "PATCH") capturedBody = init.body as string;
return new Response(JSON.stringify({}), { status: 200 });
});

await setupProfile();
const { apiKeysEnable } = await import("./index.ts");
await captured.run(() => apiKeysEnable({ for: ["users,orgs"] }));

const parsed = JSON.parse(capturedBody);
expect(parsed.api_keys_settings.user_api_keys_enabled).toBe(true);
expect(parsed.api_keys_settings.orgs_api_keys_enabled).toBe(true);
expect(parsed.organization_settings.enabled).toBe(true);
});

test("enable rejects invalid --for token", async () => {
await setupProfile();
const { apiKeysEnable } = await import("./index.ts");
await expect(captured.run(() => apiKeysEnable({ for: ["machine"] }))).rejects.toThrow(
'Invalid --for value: "machine". Expected "orgs" and/or "users".',
);
});

test("enable accepts singular --for aliases", async () => {
let capturedBody = "";
stubFetch(async (_input, init) => {
if (init?.method === "PATCH") capturedBody = init.body as string;
return new Response(JSON.stringify({}), { status: 200 });
});

await setupProfile();
const { apiKeysEnable } = await import("./index.ts");
await captured.run(() => apiKeysEnable({ for: ["user", "org"] }));

const parsed = JSON.parse(capturedBody);
expect(parsed.api_keys_settings.user_api_keys_enabled).toBe(true);
expect(parsed.api_keys_settings.orgs_api_keys_enabled).toBe(true);
expect(parsed.organization_settings.enabled).toBe(true);
});

test("enable --dry-run plumbs dry_run=true to the API", async () => {
let capturedUrl = "";
stubFetch(async (input, init) => {
if (init?.method === "PATCH") capturedUrl = input.toString();
return new Response(JSON.stringify({}), { status: 200 });
});

await setupProfile();
const { apiKeysEnable } = await import("./index.ts");
await captured.run(() => apiKeysEnable({ dryRun: true }));

expect(capturedUrl).toContain("dry_run=true");
expect(captured.err).toContain("[dry-run]");
});

test("disable with no --for disables API Keys entirely", async () => {
let capturedBody = "";
stubFetch(async (_input, init) => {
if (init?.method === "PATCH") capturedBody = init.body as string;
return new Response(
JSON.stringify({
api_keys_settings: {
enabled: true,
user_api_keys_enabled: true,
orgs_api_keys_enabled: true,
},
}),
{ status: 200 },
);
});

await setupProfile();
const { apiKeysDisable } = await import("./index.ts");
await captured.run(() => apiKeysDisable({}));

const parsed = JSON.parse(capturedBody);
expect(parsed.api_keys_settings.enabled).toBe(false);
expect(parsed.api_keys_settings.user_api_keys_enabled).toBe(false);
expect(parsed.api_keys_settings.orgs_api_keys_enabled).toBe(false);
});

test("disable --for orgs only disables org API Keys", async () => {
let capturedBody = "";
stubFetch(async (_input, init) => {
if (init?.method === "PATCH") capturedBody = init.body as string;
return new Response(
JSON.stringify({
api_keys_settings: {
enabled: true,
user_api_keys_enabled: true,
orgs_api_keys_enabled: true,
},
}),
{ status: 200 },
);
});

await setupProfile();
const { apiKeysDisable } = await import("./index.ts");
await captured.run(() => apiKeysDisable({ for: ["orgs"] }));

const parsed = JSON.parse(capturedBody);
expect(parsed.api_keys_settings.enabled).toBeUndefined();
expect(parsed.api_keys_settings.user_api_keys_enabled).toBeUndefined();
expect(parsed.api_keys_settings.orgs_api_keys_enabled).toBe(false);
});

test("disable shows no changes when already fully disabled", async () => {
let patchCalls = 0;
stubFetch(async (_input, init) => {
if (init?.method === "PATCH") patchCalls++;
return new Response(
JSON.stringify({
api_keys_settings: {
enabled: false,
user_api_keys_enabled: false,
orgs_api_keys_enabled: false,
},
}),
{ status: 200 },
);
});

await setupProfile();
const { apiKeysDisable } = await import("./index.ts");
await captured.run(() => apiKeysDisable({}));

expect(patchCalls).toBe(0);
expect(captured.err).toContain("No changes detected");
});
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Draft
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
5 changes: 5 additions & 0 deletions .changeset/accept-orgs-users-for-flags.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"clerk": minor
---

Add `clerk enable api-keys` and `clerk disable api-keys` for toggling API Keys on the linked instance. API Key targeting uses the canonical plural `orgs` and `users` values, while the singular `org` and `user` values continue to work as aliases.
69 changes: 69 additions & 0 deletions packages/cli-core/src/cli-program.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -48,6 +48,7 @@ import { update } from "./commands/update/index.ts";
import { isClerkSkillInstalled } from "./lib/skill-detection.ts";
import { orgsEnable, orgsDisable } from "./commands/orgs/index.ts";
import { billingEnable, billingDisable } from "./commands/billing/index.ts";
import { apiKeysEnable, apiKeysDisable } from "./commands/api-keys/index.ts";
import { registerExtras } from "@clerk/cli-extras";

const USER_LIST_ORDER_BY_FIELDS = [
Expand DownExpand Up@@ -636,6 +637,10 @@ Give AI agents better Clerk context: install the Clerk skills
command: "clerk enable billing",
description: "Enable billing for organizations and users",
},
{
command: "clerk enable api-keys",
description: "Enable API Keys for users",
},
]);

enable
Expand DownExpand Up@@ -703,6 +708,38 @@ Give AI agents better Clerk context: install the Clerk skills
])
.action(billingEnable);

enable
.command("api-keys")
.aliases(["apikeys", "api_keys"])
.description("Enable API Keys on the linked instance")
.option(
"--for <targets...>",
"API Keys targets (orgs and/or users), separated by spaces or commas (e.g. orgs users). Defaults to users when omitted.",
)
.option("--app <id>", "Application ID to target")
.option("--instance <id>", "Instance to target (dev, prod, or instance ID)")
.option("--yes", "Skip confirmation prompts")
.option("--dry-run", "Show the patch that would be sent without applying it")
.setExamples([
{
command: "clerk enable api-keys",
description: "Enable API Keys for users",
},
{
command: "clerk enable api-keys --for orgs",
description: "Enable API Keys for organizations",
},
{
command: "clerk enable api-keys --for users orgs",
description: "Enable API Keys for users and organizations",
},
{
command: "clerk enable api-keys --dry-run",
description: "Preview the patch without applying it",
},
])
.action(apiKeysEnable);

const disable = program
.command("disable")
.description("Disable Clerk features on the linked instance")
Expand All@@ -716,6 +753,10 @@ Give AI agents better Clerk context: install the Clerk skills
command: "clerk disable billing",
description: "Disable billing for organizations and users",
},
{
command: "clerk disable api-keys",
description: "Disable API Keys",
},
]);

disable
Expand DownExpand Up@@ -764,6 +805,34 @@ Give AI agents better Clerk context: install the Clerk skills
])
.action(billingDisable);

disable
.command("api-keys")
.aliases(["apikeys", "api_keys"])
.description("Disable API Keys on the linked instance")
.option(
"--for <targets...>",
"API Keys targets (orgs and/or users), separated by spaces or commas (e.g. orgs users). Omit to disable API Keys entirely.",
)
.option("--app <id>", "Application ID to target")
.option("--instance <id>", "Instance to target (dev, prod, or instance ID)")
.option("--yes", "Skip confirmation prompts")
.option("--dry-run", "Show the patch that would be sent without applying it")
.setExamples([
{
command: "clerk disable api-keys",
description: "Disable API Keys entirely",
},
{
command: "clerk disable api-keys --for orgs",
description: "Disable API Keys for organizations only",
},
{
command: "clerk disable api-keys --for users",
description: "Disable API Keys for users only",
},
])
.action(apiKeysDisable);

program
.command("api")
.description("Make authenticated requests to the Clerk API")
Expand Down
52 changes: 52 additions & 0 deletions packages/cli-core/src/commands/api-keys/README.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
# clerk api-keys (enable/disable)

Toggle Clerk API Keys on the linked instance. The handlers are wired to
top-level `clerk enable api-keys` and `clerk disable api-keys` commands.

For arbitrary API Keys settings edits, use
`clerk config patch --json '{"api_keys_settings":{...}}'`.

## Usage

```sh
clerk enable api-keys [--for <targets>] [options]
clerk disable api-keys [--for <targets>] [options]
```

`<targets>` is `orgs` and/or `users`, accepted as space-separated,
comma-separated, or repeated `--for` flags. The singular aliases `org` and
`user` are also accepted for backwards compatibility.

```sh
clerk enable api-keys # defaults to users
clerk enable api-keys --for orgs users
clerk enable api-keys --for orgs,users
clerk disable api-keys # disables API Keys entirely
clerk disable api-keys --for orgs # disables only organization API Keys
```

## Options

| Flag | Description |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `--for <targets>` | Targets (`orgs` and/or `users`), separated by spaces or commas. Enable defaults to users; disable without `--for` disables API Keys entirely. |
| `--app <id>` | Target a specific application |
| `--instance <id>` | Target a specific instance (dev, prod) |
| `--yes` | Skip the confirmation prompt |
| `--dry-run` | Preview the patch without applying it |

## Cascade behavior

- `enable api-keys --for orgs` also sets `organization_settings.enabled = true`.
Organization API Keys require organizations enabled, so this saves a separate
command. The cascade is idempotent.
- `disable api-keys --for orgs` disables only organization API Keys and leaves
organizations enabled.
- `disable api-keys` without `--for` disables API Keys entirely.

## Clerk API endpoints

| Method | Endpoint | Description |
| ------ | ----------------------------------------------------------------- | ----------------------------------------------------------------------- |
| GET | `/v1/platform/applications/{appId}/instances/{instanceId}/config` | Fetch current config for diff before mutation |
| PATCH | `/v1/platform/applications/{appId}/instances/{instanceId}/config` | Patch `api_keys_settings.*` (with `?dry_run=true` when `--dry-run` set) |
230 changes: 230 additions & 0 deletions packages/cli-core/src/commands/api-keys/index.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
import { test, expect, describe, beforeEach, afterEach, spyOn, mock } from "bun:test";
import { mkdtemp, rm } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { _setConfigDir, setProfile } from "../../lib/config.ts";
import {
captureLog,
credentialStoreStubs,
gitStubs,
promptsStubs,
stubFetch,
} from "../../test/lib/stubs.ts";

mock.module("../../lib/credential-store.ts", () => credentialStoreStubs);
mock.module("../../lib/git.ts", () => gitStubs);
mock.module("@inquirer/prompts", () => promptsStubs);
mock.module("../../lib/spinner.ts", () => ({
withSpinner: async (_msg: string, fn: () => Promise<unknown>) => fn(),
}));

describe("clerk enable/disable api-keys", () => {
const originalEnv = { ...process.env };
const originalFetch = globalThis.fetch;
let tempDir: string;
let logSpy: ReturnType<typeof spyOn>;
let errorSpy: ReturnType<typeof spyOn>;
let captured: ReturnType<typeof captureLog>;

beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), "clerk-api-keys-test-"));
_setConfigDir(tempDir);
process.env.CLERK_PLATFORM_API_KEY = "test_key";
process.env.CLERK_PLATFORM_API_URL = "https://test-api.clerk.com";

logSpy = spyOn(console, "log").mockImplementation(() => {});
errorSpy = spyOn(console, "error").mockImplementation(() => {});
captured = captureLog();

stubFetch(async () => {
return new Response(JSON.stringify({}), { status: 200 });
});
});

afterEach(async () => {
captured.teardown();
_setConfigDir(undefined);
process.env = { ...originalEnv };
globalThis.fetch = originalFetch;
logSpy.mockRestore();
errorSpy.mockRestore();
await rm(tempDir, { recursive: true, force: true });
});

async function setupProfile() {
await setProfile(process.cwd(), {
workspaceId: "org_1",
appId: "app_1",
instances: { development: "ins_dev" },
});
}

test("enable defaults to user API Keys", async () => {
let capturedBody = "";
stubFetch(async (_input, init) => {
if (init?.method === "PATCH") capturedBody = init.body as string;
return new Response(JSON.stringify({}), { status: 200 });
});

await setupProfile();
const { apiKeysEnable } = await import("./index.ts");
await captured.run(() => apiKeysEnable({}));

const parsed = JSON.parse(capturedBody);
expect(parsed.api_keys_settings.enabled).toBe(true);
expect(parsed.api_keys_settings.user_api_keys_enabled).toBe(true);
expect(parsed.api_keys_settings.orgs_api_keys_enabled).toBeUndefined();
expect(parsed.organization_settings).toBeUndefined();
});

test("enable --for orgs enables org API Keys and cascades organizations", async () => {
let capturedBody = "";
stubFetch(async (_input, init) => {
if (init?.method === "PATCH") capturedBody = init.body as string;
return new Response(JSON.stringify({}), { status: 200 });
});

await setupProfile();
const { apiKeysEnable } = await import("./index.ts");
await captured.run(() => apiKeysEnable({ for: ["orgs"] }));

const parsed = JSON.parse(capturedBody);
expect(parsed.api_keys_settings.enabled).toBe(true);
expect(parsed.api_keys_settings.orgs_api_keys_enabled).toBe(true);
expect(parsed.api_keys_settings.user_api_keys_enabled).toBeUndefined();
expect(parsed.organization_settings.enabled).toBe(true);
});

test("enable --for users,orgs sets both API Keys targets", async () => {
let capturedBody = "";
stubFetch(async (_input, init) => {
if (init?.method === "PATCH") capturedBody = init.body as string;
return new Response(JSON.stringify({}), { status: 200 });
});

await setupProfile();
const { apiKeysEnable } = await import("./index.ts");
await captured.run(() => apiKeysEnable({ for: ["users,orgs"] }));

const parsed = JSON.parse(capturedBody);
expect(parsed.api_keys_settings.user_api_keys_enabled).toBe(true);
expect(parsed.api_keys_settings.orgs_api_keys_enabled).toBe(true);
expect(parsed.organization_settings.enabled).toBe(true);
});

test("enable rejects invalid --for token", async () => {
await setupProfile();
const { apiKeysEnable } = await import("./index.ts");
await expect(captured.run(() => apiKeysEnable({ for: ["machine"] }))).rejects.toThrow(
'Invalid --for value: "machine". Expected "orgs" and/or "users".',
);
});

test("enable accepts singular --for aliases", async () => {
let capturedBody = "";
stubFetch(async (_input, init) => {
if (init?.method === "PATCH") capturedBody = init.body as string;
return new Response(JSON.stringify({}), { status: 200 });
});

await setupProfile();
const { apiKeysEnable } = await import("./index.ts");
await captured.run(() => apiKeysEnable({ for: ["user", "org"] }));

const parsed = JSON.parse(capturedBody);
expect(parsed.api_keys_settings.user_api_keys_enabled).toBe(true);
expect(parsed.api_keys_settings.orgs_api_keys_enabled).toBe(true);
expect(parsed.organization_settings.enabled).toBe(true);
});

test("enable --dry-run plumbs dry_run=true to the API", async () => {
let capturedUrl = "";
stubFetch(async (input, init) => {
if (init?.method === "PATCH") capturedUrl = input.toString();
return new Response(JSON.stringify({}), { status: 200 });
});

await setupProfile();
const { apiKeysEnable } = await import("./index.ts");
await captured.run(() => apiKeysEnable({ dryRun: true }));

expect(capturedUrl).toContain("dry_run=true");
expect(captured.err).toContain("[dry-run]");
});

test("disable with no --for disables API Keys entirely", async () => {
let capturedBody = "";
stubFetch(async (_input, init) => {
if (init?.method === "PATCH") capturedBody = init.body as string;
return new Response(
JSON.stringify({
api_keys_settings: {
enabled: true,
user_api_keys_enabled: true,
orgs_api_keys_enabled: true,
},
}),
{ status: 200 },
);
});

await setupProfile();
const { apiKeysDisable } = await import("./index.ts");
await captured.run(() => apiKeysDisable({}));

const parsed = JSON.parse(capturedBody);
expect(parsed.api_keys_settings.enabled).toBe(false);
expect(parsed.api_keys_settings.user_api_keys_enabled).toBe(false);
expect(parsed.api_keys_settings.orgs_api_keys_enabled).toBe(false);
});

test("disable --for orgs only disables org API Keys", async () => {
let capturedBody = "";
stubFetch(async (_input, init) => {
if (init?.method === "PATCH") capturedBody = init.body as string;
return new Response(
JSON.stringify({
api_keys_settings: {
enabled: true,
user_api_keys_enabled: true,
orgs_api_keys_enabled: true,
},
}),
{ status: 200 },
);
});

await setupProfile();
const { apiKeysDisable } = await import("./index.ts");
await captured.run(() => apiKeysDisable({ for: ["orgs"] }));

const parsed = JSON.parse(capturedBody);
expect(parsed.api_keys_settings.enabled).toBeUndefined();
expect(parsed.api_keys_settings.user_api_keys_enabled).toBeUndefined();
expect(parsed.api_keys_settings.orgs_api_keys_enabled).toBe(false);
});

test("disable shows no changes when already fully disabled", async () => {
let patchCalls = 0;
stubFetch(async (_input, init) => {
if (init?.method === "PATCH") patchCalls++;
return new Response(
JSON.stringify({
api_keys_settings: {
enabled: false,
user_api_keys_enabled: false,
orgs_api_keys_enabled: false,
},
}),
{ status: 200 },
);
});

await setupProfile();
const { apiKeysDisable } = await import("./index.ts");
await captured.run(() => apiKeysDisable({}));

expect(patchCalls).toBe(0);
expect(captured.err).toContain("No changes detected");
});
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Draft
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
5 changes: 5 additions & 0 deletions .changeset/accept-orgs-users-for-flags.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"clerk": minor
---

Add `clerk enable api-keys` and `clerk disable api-keys` for toggling API Keys on the linked instance. API Key targeting uses the canonical plural `orgs` and `users` values, while the singular `org` and `user` values continue to work as aliases.
69 changes: 69 additions & 0 deletions packages/cli-core/src/cli-program.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -48,6 +48,7 @@ import { update } from "./commands/update/index.ts";
import { isClerkSkillInstalled } from "./lib/skill-detection.ts";
import { orgsEnable, orgsDisable } from "./commands/orgs/index.ts";
import { billingEnable, billingDisable } from "./commands/billing/index.ts";
import { apiKeysEnable, apiKeysDisable } from "./commands/api-keys/index.ts";
import { registerExtras } from "@clerk/cli-extras";

const USER_LIST_ORDER_BY_FIELDS = [
Expand DownExpand Up@@ -636,6 +637,10 @@ Give AI agents better Clerk context: install the Clerk skills
command: "clerk enable billing",
description: "Enable billing for organizations and users",
},
{
command: "clerk enable api-keys",
description: "Enable API Keys for users",
},
]);

enable
Expand DownExpand Up@@ -703,6 +708,38 @@ Give AI agents better Clerk context: install the Clerk skills
])
.action(billingEnable);

enable
.command("api-keys")
.aliases(["apikeys", "api_keys"])
.description("Enable API Keys on the linked instance")
.option(
"--for <targets...>",
"API Keys targets (orgs and/or users), separated by spaces or commas (e.g. orgs users). Defaults to users when omitted.",
)
.option("--app <id>", "Application ID to target")
.option("--instance <id>", "Instance to target (dev, prod, or instance ID)")
.option("--yes", "Skip confirmation prompts")
.option("--dry-run", "Show the patch that would be sent without applying it")
.setExamples([
{
command: "clerk enable api-keys",
description: "Enable API Keys for users",
},
{
command: "clerk enable api-keys --for orgs",
description: "Enable API Keys for organizations",
},
{
command: "clerk enable api-keys --for users orgs",
description: "Enable API Keys for users and organizations",
},
{
command: "clerk enable api-keys --dry-run",
description: "Preview the patch without applying it",
},
])
.action(apiKeysEnable);

const disable = program
.command("disable")
.description("Disable Clerk features on the linked instance")
Expand All@@ -716,6 +753,10 @@ Give AI agents better Clerk context: install the Clerk skills
command: "clerk disable billing",
description: "Disable billing for organizations and users",
},
{
command: "clerk disable api-keys",
description: "Disable API Keys",
},
]);

disable
Expand DownExpand Up@@ -764,6 +805,34 @@ Give AI agents better Clerk context: install the Clerk skills
])
.action(billingDisable);

disable
.command("api-keys")
.aliases(["apikeys", "api_keys"])
.description("Disable API Keys on the linked instance")
.option(
"--for <targets...>",
"API Keys targets (orgs and/or users), separated by spaces or commas (e.g. orgs users). Omit to disable API Keys entirely.",
)
.option("--app <id>", "Application ID to target")
.option("--instance <id>", "Instance to target (dev, prod, or instance ID)")
.option("--yes", "Skip confirmation prompts")
.option("--dry-run", "Show the patch that would be sent without applying it")
.setExamples([
{
command: "clerk disable api-keys",
description: "Disable API Keys entirely",
},
{
command: "clerk disable api-keys --for orgs",
description: "Disable API Keys for organizations only",
},
{
command: "clerk disable api-keys --for users",
description: "Disable API Keys for users only",
},
])
.action(apiKeysDisable);

program
.command("api")
.description("Make authenticated requests to the Clerk API")
Expand Down
52 changes: 52 additions & 0 deletions packages/cli-core/src/commands/api-keys/README.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
# clerk api-keys (enable/disable)

Toggle Clerk API Keys on the linked instance. The handlers are wired to
top-level `clerk enable api-keys` and `clerk disable api-keys` commands.

For arbitrary API Keys settings edits, use
`clerk config patch --json '{"api_keys_settings":{...}}'`.

## Usage

```sh
clerk enable api-keys [--for <targets>] [options]
clerk disable api-keys [--for <targets>] [options]
```

`<targets>` is `orgs` and/or `users`, accepted as space-separated,
comma-separated, or repeated `--for` flags. The singular aliases `org` and
`user` are also accepted for backwards compatibility.

```sh
clerk enable api-keys # defaults to users
clerk enable api-keys --for orgs users
clerk enable api-keys --for orgs,users
clerk disable api-keys # disables API Keys entirely
clerk disable api-keys --for orgs # disables only organization API Keys
```

## Options

| Flag | Description |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `--for <targets>` | Targets (`orgs` and/or `users`), separated by spaces or commas. Enable defaults to users; disable without `--for` disables API Keys entirely. |
| `--app <id>` | Target a specific application |
| `--instance <id>` | Target a specific instance (dev, prod) |
| `--yes` | Skip the confirmation prompt |
| `--dry-run` | Preview the patch without applying it |

## Cascade behavior

- `enable api-keys --for orgs` also sets `organization_settings.enabled = true`.
Organization API Keys require organizations enabled, so this saves a separate
command. The cascade is idempotent.
- `disable api-keys --for orgs` disables only organization API Keys and leaves
organizations enabled.
- `disable api-keys` without `--for` disables API Keys entirely.

## Clerk API endpoints

| Method | Endpoint | Description |
| ------ | ----------------------------------------------------------------- | ----------------------------------------------------------------------- |
| GET | `/v1/platform/applications/{appId}/instances/{instanceId}/config` | Fetch current config for diff before mutation |
| PATCH | `/v1/platform/applications/{appId}/instances/{instanceId}/config` | Patch `api_keys_settings.*` (with `?dry_run=true` when `--dry-run` set) |
230 changes: 230 additions & 0 deletions packages/cli-core/src/commands/api-keys/index.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
import { test, expect, describe, beforeEach, afterEach, spyOn, mock } from "bun:test";
import { mkdtemp, rm } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { _setConfigDir, setProfile } from "../../lib/config.ts";
import {
captureLog,
credentialStoreStubs,
gitStubs,
promptsStubs,
stubFetch,
} from "../../test/lib/stubs.ts";

mock.module("../../lib/credential-store.ts", () => credentialStoreStubs);
mock.module("../../lib/git.ts", () => gitStubs);
mock.module("@inquirer/prompts", () => promptsStubs);
mock.module("../../lib/spinner.ts", () => ({
withSpinner: async (_msg: string, fn: () => Promise<unknown>) => fn(),
}));

describe("clerk enable/disable api-keys", () => {
const originalEnv = { ...process.env };
const originalFetch = globalThis.fetch;
let tempDir: string;
let logSpy: ReturnType<typeof spyOn>;
let errorSpy: ReturnType<typeof spyOn>;
let captured: ReturnType<typeof captureLog>;

beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), "clerk-api-keys-test-"));
_setConfigDir(tempDir);
process.env.CLERK_PLATFORM_API_KEY = "test_key";
process.env.CLERK_PLATFORM_API_URL = "https://test-api.clerk.com";

logSpy = spyOn(console, "log").mockImplementation(() => {});
errorSpy = spyOn(console, "error").mockImplementation(() => {});
captured = captureLog();

stubFetch(async () => {
return new Response(JSON.stringify({}), { status: 200 });
});
});

afterEach(async () => {
captured.teardown();
_setConfigDir(undefined);
process.env = { ...originalEnv };
globalThis.fetch = originalFetch;
logSpy.mockRestore();
errorSpy.mockRestore();
await rm(tempDir, { recursive: true, force: true });
});

async function setupProfile() {
await setProfile(process.cwd(), {
workspaceId: "org_1",
appId: "app_1",
instances: { development: "ins_dev" },
});
}

test("enable defaults to user API Keys", async () => {
let capturedBody = "";
stubFetch(async (_input, init) => {
if (init?.method === "PATCH") capturedBody = init.body as string;
return new Response(JSON.stringify({}), { status: 200 });
});

await setupProfile();
const { apiKeysEnable } = await import("./index.ts");
await captured.run(() => apiKeysEnable({}));

const parsed = JSON.parse(capturedBody);
expect(parsed.api_keys_settings.enabled).toBe(true);
expect(parsed.api_keys_settings.user_api_keys_enabled).toBe(true);
expect(parsed.api_keys_settings.orgs_api_keys_enabled).toBeUndefined();
expect(parsed.organization_settings).toBeUndefined();
});

test("enable --for orgs enables org API Keys and cascades organizations", async () => {
let capturedBody = "";
stubFetch(async (_input, init) => {
if (init?.method === "PATCH") capturedBody = init.body as string;
return new Response(JSON.stringify({}), { status: 200 });
});

await setupProfile();
const { apiKeysEnable } = await import("./index.ts");
await captured.run(() => apiKeysEnable({ for: ["orgs"] }));

const parsed = JSON.parse(capturedBody);
expect(parsed.api_keys_settings.enabled).toBe(true);
expect(parsed.api_keys_settings.orgs_api_keys_enabled).toBe(true);
expect(parsed.api_keys_settings.user_api_keys_enabled).toBeUndefined();
expect(parsed.organization_settings.enabled).toBe(true);
});

test("enable --for users,orgs sets both API Keys targets", async () => {
let capturedBody = "";
stubFetch(async (_input, init) => {
if (init?.method === "PATCH") capturedBody = init.body as string;
return new Response(JSON.stringify({}), { status: 200 });
});

await setupProfile();
const { apiKeysEnable } = await import("./index.ts");
await captured.run(() => apiKeysEnable({ for: ["users,orgs"] }));

const parsed = JSON.parse(capturedBody);
expect(parsed.api_keys_settings.user_api_keys_enabled).toBe(true);
expect(parsed.api_keys_settings.orgs_api_keys_enabled).toBe(true);
expect(parsed.organization_settings.enabled).toBe(true);
});

test("enable rejects invalid --for token", async () => {
await setupProfile();
const { apiKeysEnable } = await import("./index.ts");
await expect(captured.run(() => apiKeysEnable({ for: ["machine"] }))).rejects.toThrow(
'Invalid --for value: "machine". Expected "orgs" and/or "users".',
);
});

test("enable accepts singular --for aliases", async () => {
let capturedBody = "";
stubFetch(async (_input, init) => {
if (init?.method === "PATCH") capturedBody = init.body as string;
return new Response(JSON.stringify({}), { status: 200 });
});

await setupProfile();
const { apiKeysEnable } = await import("./index.ts");
await captured.run(() => apiKeysEnable({ for: ["user", "org"] }));

const parsed = JSON.parse(capturedBody);
expect(parsed.api_keys_settings.user_api_keys_enabled).toBe(true);
expect(parsed.api_keys_settings.orgs_api_keys_enabled).toBe(true);
expect(parsed.organization_settings.enabled).toBe(true);
});

test("enable --dry-run plumbs dry_run=true to the API", async () => {
let capturedUrl = "";
stubFetch(async (input, init) => {
if (init?.method === "PATCH") capturedUrl = input.toString();
return new Response(JSON.stringify({}), { status: 200 });
});

await setupProfile();
const { apiKeysEnable } = await import("./index.ts");
await captured.run(() => apiKeysEnable({ dryRun: true }));

expect(capturedUrl).toContain("dry_run=true");
expect(captured.err).toContain("[dry-run]");
});

test("disable with no --for disables API Keys entirely", async () => {
let capturedBody = "";
stubFetch(async (_input, init) => {
if (init?.method === "PATCH") capturedBody = init.body as string;
return new Response(
JSON.stringify({
api_keys_settings: {
enabled: true,
user_api_keys_enabled: true,
orgs_api_keys_enabled: true,
},
}),
{ status: 200 },
);
});

await setupProfile();
const { apiKeysDisable } = await import("./index.ts");
await captured.run(() => apiKeysDisable({}));

const parsed = JSON.parse(capturedBody);
expect(parsed.api_keys_settings.enabled).toBe(false);
expect(parsed.api_keys_settings.user_api_keys_enabled).toBe(false);
expect(parsed.api_keys_settings.orgs_api_keys_enabled).toBe(false);
});

test("disable --for orgs only disables org API Keys", async () => {
let capturedBody = "";
stubFetch(async (_input, init) => {
if (init?.method === "PATCH") capturedBody = init.body as string;
return new Response(
JSON.stringify({
api_keys_settings: {
enabled: true,
user_api_keys_enabled: true,
orgs_api_keys_enabled: true,
},
}),
{ status: 200 },
);
});

await setupProfile();
const { apiKeysDisable } = await import("./index.ts");
await captured.run(() => apiKeysDisable({ for: ["orgs"] }));

const parsed = JSON.parse(capturedBody);
expect(parsed.api_keys_settings.enabled).toBeUndefined();
expect(parsed.api_keys_settings.user_api_keys_enabled).toBeUndefined();
expect(parsed.api_keys_settings.orgs_api_keys_enabled).toBe(false);
});

test("disable shows no changes when already fully disabled", async () => {
let patchCalls = 0;
stubFetch(async (_input, init) => {
if (init?.method === "PATCH") patchCalls++;
return new Response(
JSON.stringify({
api_keys_settings: {
enabled: false,
user_api_keys_enabled: false,
orgs_api_keys_enabled: false,
},
}),
{ status: 200 },
);
});

await setupProfile();
const { apiKeysDisable } = await import("./index.ts");
await captured.run(() => apiKeysDisable({}));

expect(patchCalls).toBe(0);
expect(captured.err).toContain("No changes detected");
});
});
Loading