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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/api-endpoint-discoverability.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
---
"clerk": patch
---

Make the `clerk api` endpoint catalog discoverable from help output and 404 responses.

- Root help now describes `api` as "Call any Clerk API endpoint (200+; `clerk api ls` to browse)" instead of "Make authenticated requests to the Clerk API", and `clerk api --help` explains that this command covers what the dedicated commands do not.
- `--platform` now explains that the Platform API has its own endpoint list, `clerk api ls` examples say which API they list (they cover the Backend API, not every endpoint), and `clerk api ls --platform` is shown as an example.
- A 404 with no parsed Clerk error code now suggests `clerk api ls <keyword>` on stderr, leaving stdout as the pipeable response body. The suggestion is scoped per surface: `--platform` appends that flag, and `--fapi` gets none since it has no endpoint catalog.
2 changes: 1 addition & 1 deletion README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,7 +47,7 @@ Commands:
telemetry Control CLI usage telemetry (status, disable, enable)
enable Enable Clerk features on the linked instance
disable Disable Clerk features on the linked instance
api [options] [endpoint] [filter] Make authenticated requests to the Clerk API
api [options] [endpoint] [filter] Call any Clerk API endpoint (200+; `clerk api ls` to browse)
doctor [options] Check your project's Clerk integration health
mcp Manage the Clerk remote MCP server connection for AI editors and CLIs
completion [shell] Generate shell autocompletion script
Expand Down
49 changes: 49 additions & 0 deletions packages/cli-core/src/commands/api/index.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -631,6 +631,55 @@ describe("api command", () => {
expect(captured.out).toContain(JSON.stringify(errorBody, null, 2));
expect(captured.err).toContain("Failed");
expect(captured.err).not.toContain("Done");
// A structured Clerk error means the endpoint resolved; don't suggest searching.
expect(captured.err).not.toContain("clerk api ls");
});

// --- 404 endpoint-search hint ---

test("suggests the endpoint search on an unstructured 404", async () => {
setMode("human");
stubFetch(async () => new Response("404 page not found", { status: 404 }));

await runApi("/organization_role");
expect(process.exitCode).toBe(1);
expect(captured.err).toContain("If the endpoint path was a guess");
expect(captured.err).toContain("clerk api ls <keyword>");
// Diagnostics on stderr only; stdout stays the raw response body for piping.
expect(captured.out).not.toContain("clerk api ls");
expect(captured.out).toContain("404 page not found");
});

test("scopes the endpoint search hint to the platform catalog with --platform", async () => {
setMode("human");
process.env.CLERK_PLATFORM_API_KEY = "plat_key_123";
stubFetch(async () => new Response("404 page not found", { status: 404 }));

await runApi("/v1/platform/bogus", { platform: true });
expect(captured.err).toContain("clerk api ls <keyword> --platform");
});

test("omits the endpoint search hint for FAPI, which has no catalog", async () => {
setMode("human");
process.env.CLERK_PLATFORM_API_KEY = "ak_test_platform";
const pk = `pk_test_${btoa("clerk.example.com$")}`;
stubFetch(async (input) => {
if (input.toString().includes("/v1/platform/applications/app_1")) {
return new Response(
JSON.stringify({
application_id: "app_1",
instances: [
{ instance_id: "ins_dev", environment_type: "development", publishable_key: pk },
],
}),
{ status: 200 },
);
}
return new Response("404 page not found", { status: 404 });
});

await runApi("/bogus", { fapi: true, app: "app_1", instance: "dev" });
expect(captured.err).not.toContain("clerk api ls");
});

test("shows Paused with instructions when a confirmation prompt is cancelled", async () => {
Expand Down
30 changes: 26 additions & 4 deletions packages/cli-core/src/commands/api/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -144,6 +144,15 @@ export async function api(
printHeaders(error.status, error.headers);
}
prettyPrint(error.body);
// A 404 can mean either "no such endpoint" or "endpoint exists, resource
// doesn't", so the wording stays conditional. A parsed Clerk error code is
// evidence the request reached the API and was rejected semantically, so we
// skip the hint there to keep it off the common resource-not-found path —
// a heuristic, not a guarantee. FAPI has no endpoint catalog to search.
if (error.status === 404 && error.code === null && !options.fapi) {
const scope = options.platform ? " --platform" : "";
log.info(`If the endpoint path was a guess, search with: clerk api ls <keyword>${scope}`);
}
process.exitCode = 1;
closeStatus = "failed";
return;
Expand DownExpand Up@@ -227,7 +236,13 @@ function prettyPrintToStderr(text: string): void {
export function registerApi(program: Program): void {
program
.command("api")
.description("Make authenticated requests to the Clerk API")
.summary("Call any Clerk API endpoint (200+; `clerk api ls` to browse)")
.description(
"Call any endpoint in the Clerk API directly.\n\n" +
"The other commands cover common operations. This one reaches everything " +
"else — invitations and waitlist entries, billing subscriptions and credits, " +
"organization roles and permissions, enterprise SSO connections.",
)
.argument(
"[endpoint]",
"API endpoint path, 'ls' to list endpoints, or omit for interactive mode",
Expand All@@ -240,16 +255,23 @@ export function registerApi(program: Program): void {
.option("--app <id>", "Application ID to target when resolving keys")
.option("--secret-key <key>", "Override the secret key")
.option("--instance <id>", "Instance to target (dev, prod, or instance ID)")
.option("--platform", "Use Platform API instead of Backend API")
.option(
"--platform",
"Use the Platform API (applications and instances) instead of the Backend API; has its own endpoint list",
)
.option(
"--fapi",
"Use the instance's public Frontend API (unauthenticated endpoints only; host derived from the publishable key)",
)
.option("--dry-run", "Show the request without executing it")
.option("--yes", "Skip confirmation for mutating requests")
.setExamples([
{ command: "clerk api ls", description: "List all available endpoints" },
{ command: "clerk api ls users", description: 'List endpoints matching "users"' },
{ command: "clerk api ls", description: "List Backend API endpoints" },
{ command: "clerk api ls users", description: 'List Backend API endpoints matching "users"' },
{
command: "clerk api ls --platform",
description: "List Platform API endpoints (applications, instances)",
},
{ command: "clerk api /users", description: "GET /v1/users" },
{
command: 'clerk api /users -d \'{"first_name":"Alice"}\'',
Expand Down
, '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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/api-endpoint-discoverability.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
---
"clerk": patch
---

Make the `clerk api` endpoint catalog discoverable from help output and 404 responses.

- Root help now describes `api` as "Call any Clerk API endpoint (200+; `clerk api ls` to browse)" instead of "Make authenticated requests to the Clerk API", and `clerk api --help` explains that this command covers what the dedicated commands do not.
- `--platform` now explains that the Platform API has its own endpoint list, `clerk api ls` examples say which API they list (they cover the Backend API, not every endpoint), and `clerk api ls --platform` is shown as an example.
- A 404 with no parsed Clerk error code now suggests `clerk api ls <keyword>` on stderr, leaving stdout as the pipeable response body. The suggestion is scoped per surface: `--platform` appends that flag, and `--fapi` gets none since it has no endpoint catalog.
2 changes: 1 addition & 1 deletion README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,7 +47,7 @@ Commands:
telemetry Control CLI usage telemetry (status, disable, enable)
enable Enable Clerk features on the linked instance
disable Disable Clerk features on the linked instance
api [options] [endpoint] [filter] Make authenticated requests to the Clerk API
api [options] [endpoint] [filter] Call any Clerk API endpoint (200+; `clerk api ls` to browse)
doctor [options] Check your project's Clerk integration health
mcp Manage the Clerk remote MCP server connection for AI editors and CLIs
completion [shell] Generate shell autocompletion script
Expand Down
49 changes: 49 additions & 0 deletions packages/cli-core/src/commands/api/index.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -631,6 +631,55 @@ describe("api command", () => {
expect(captured.out).toContain(JSON.stringify(errorBody, null, 2));
expect(captured.err).toContain("Failed");
expect(captured.err).not.toContain("Done");
// A structured Clerk error means the endpoint resolved; don't suggest searching.
expect(captured.err).not.toContain("clerk api ls");
});

// --- 404 endpoint-search hint ---

test("suggests the endpoint search on an unstructured 404", async () => {
setMode("human");
stubFetch(async () => new Response("404 page not found", { status: 404 }));

await runApi("/organization_role");
expect(process.exitCode).toBe(1);
expect(captured.err).toContain("If the endpoint path was a guess");
expect(captured.err).toContain("clerk api ls <keyword>");
// Diagnostics on stderr only; stdout stays the raw response body for piping.
expect(captured.out).not.toContain("clerk api ls");
expect(captured.out).toContain("404 page not found");
});

test("scopes the endpoint search hint to the platform catalog with --platform", async () => {
setMode("human");
process.env.CLERK_PLATFORM_API_KEY = "plat_key_123";
stubFetch(async () => new Response("404 page not found", { status: 404 }));

await runApi("/v1/platform/bogus", { platform: true });
expect(captured.err).toContain("clerk api ls <keyword> --platform");
});

test("omits the endpoint search hint for FAPI, which has no catalog", async () => {
setMode("human");
process.env.CLERK_PLATFORM_API_KEY = "ak_test_platform";
const pk = `pk_test_${btoa("clerk.example.com$")}`;
stubFetch(async (input) => {
if (input.toString().includes("/v1/platform/applications/app_1")) {
return new Response(
JSON.stringify({
application_id: "app_1",
instances: [
{ instance_id: "ins_dev", environment_type: "development", publishable_key: pk },
],
}),
{ status: 200 },
);
}
return new Response("404 page not found", { status: 404 });
});

await runApi("/bogus", { fapi: true, app: "app_1", instance: "dev" });
expect(captured.err).not.toContain("clerk api ls");
});

test("shows Paused with instructions when a confirmation prompt is cancelled", async () => {
Expand Down
30 changes: 26 additions & 4 deletions packages/cli-core/src/commands/api/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -144,6 +144,15 @@ export async function api(
printHeaders(error.status, error.headers);
}
prettyPrint(error.body);
// A 404 can mean either "no such endpoint" or "endpoint exists, resource
// doesn't", so the wording stays conditional. A parsed Clerk error code is
// evidence the request reached the API and was rejected semantically, so we
// skip the hint there to keep it off the common resource-not-found path —
// a heuristic, not a guarantee. FAPI has no endpoint catalog to search.
if (error.status === 404 && error.code === null && !options.fapi) {
const scope = options.platform ? " --platform" : "";
log.info(`If the endpoint path was a guess, search with: clerk api ls <keyword>${scope}`);
}
process.exitCode = 1;
closeStatus = "failed";
return;
Expand DownExpand Up@@ -227,7 +236,13 @@ function prettyPrintToStderr(text: string): void {
export function registerApi(program: Program): void {
program
.command("api")
.description("Make authenticated requests to the Clerk API")
.summary("Call any Clerk API endpoint (200+; `clerk api ls` to browse)")
.description(
"Call any endpoint in the Clerk API directly.\n\n" +
"The other commands cover common operations. This one reaches everything " +
"else — invitations and waitlist entries, billing subscriptions and credits, " +
"organization roles and permissions, enterprise SSO connections.",
)
.argument(
"[endpoint]",
"API endpoint path, 'ls' to list endpoints, or omit for interactive mode",
Expand All@@ -240,16 +255,23 @@ export function registerApi(program: Program): void {
.option("--app <id>", "Application ID to target when resolving keys")
.option("--secret-key <key>", "Override the secret key")
.option("--instance <id>", "Instance to target (dev, prod, or instance ID)")
.option("--platform", "Use Platform API instead of Backend API")
.option(
"--platform",
"Use the Platform API (applications and instances) instead of the Backend API; has its own endpoint list",
)
.option(
"--fapi",
"Use the instance's public Frontend API (unauthenticated endpoints only; host derived from the publishable key)",
)
.option("--dry-run", "Show the request without executing it")
.option("--yes", "Skip confirmation for mutating requests")
.setExamples([
{ command: "clerk api ls", description: "List all available endpoints" },
{ command: "clerk api ls users", description: 'List endpoints matching "users"' },
{ command: "clerk api ls", description: "List Backend API endpoints" },
{ command: "clerk api ls users", description: 'List Backend API endpoints matching "users"' },
{
command: "clerk api ls --platform",
description: "List Platform API endpoints (applications, instances)",
},
{ command: "clerk api /users", description: "GET /v1/users" },
{
command: 'clerk api /users -d \'{"first_name":"Alice"}\'',
Expand Down
, '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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/api-endpoint-discoverability.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
---
"clerk": patch
---

Make the `clerk api` endpoint catalog discoverable from help output and 404 responses.

- Root help now describes `api` as "Call any Clerk API endpoint (200+; `clerk api ls` to browse)" instead of "Make authenticated requests to the Clerk API", and `clerk api --help` explains that this command covers what the dedicated commands do not.
- `--platform` now explains that the Platform API has its own endpoint list, `clerk api ls` examples say which API they list (they cover the Backend API, not every endpoint), and `clerk api ls --platform` is shown as an example.
- A 404 with no parsed Clerk error code now suggests `clerk api ls <keyword>` on stderr, leaving stdout as the pipeable response body. The suggestion is scoped per surface: `--platform` appends that flag, and `--fapi` gets none since it has no endpoint catalog.
2 changes: 1 addition & 1 deletion README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,7 +47,7 @@ Commands:
telemetry Control CLI usage telemetry (status, disable, enable)
enable Enable Clerk features on the linked instance
disable Disable Clerk features on the linked instance
api [options] [endpoint] [filter] Make authenticated requests to the Clerk API
api [options] [endpoint] [filter] Call any Clerk API endpoint (200+; `clerk api ls` to browse)
doctor [options] Check your project's Clerk integration health
mcp Manage the Clerk remote MCP server connection for AI editors and CLIs
completion [shell] Generate shell autocompletion script
Expand Down
49 changes: 49 additions & 0 deletions packages/cli-core/src/commands/api/index.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -631,6 +631,55 @@ describe("api command", () => {
expect(captured.out).toContain(JSON.stringify(errorBody, null, 2));
expect(captured.err).toContain("Failed");
expect(captured.err).not.toContain("Done");
// A structured Clerk error means the endpoint resolved; don't suggest searching.
expect(captured.err).not.toContain("clerk api ls");
});

// --- 404 endpoint-search hint ---

test("suggests the endpoint search on an unstructured 404", async () => {
setMode("human");
stubFetch(async () => new Response("404 page not found", { status: 404 }));

await runApi("/organization_role");
expect(process.exitCode).toBe(1);
expect(captured.err).toContain("If the endpoint path was a guess");
expect(captured.err).toContain("clerk api ls <keyword>");
// Diagnostics on stderr only; stdout stays the raw response body for piping.
expect(captured.out).not.toContain("clerk api ls");
expect(captured.out).toContain("404 page not found");
});

test("scopes the endpoint search hint to the platform catalog with --platform", async () => {
setMode("human");
process.env.CLERK_PLATFORM_API_KEY = "plat_key_123";
stubFetch(async () => new Response("404 page not found", { status: 404 }));

await runApi("/v1/platform/bogus", { platform: true });
expect(captured.err).toContain("clerk api ls <keyword> --platform");
});

test("omits the endpoint search hint for FAPI, which has no catalog", async () => {
setMode("human");
process.env.CLERK_PLATFORM_API_KEY = "ak_test_platform";
const pk = `pk_test_${btoa("clerk.example.com$")}`;
stubFetch(async (input) => {
if (input.toString().includes("/v1/platform/applications/app_1")) {
return new Response(
JSON.stringify({
application_id: "app_1",
instances: [
{ instance_id: "ins_dev", environment_type: "development", publishable_key: pk },
],
}),
{ status: 200 },
);
}
return new Response("404 page not found", { status: 404 });
});

await runApi("/bogus", { fapi: true, app: "app_1", instance: "dev" });
expect(captured.err).not.toContain("clerk api ls");
});

test("shows Paused with instructions when a confirmation prompt is cancelled", async () => {
Expand Down
30 changes: 26 additions & 4 deletions packages/cli-core/src/commands/api/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -144,6 +144,15 @@ export async function api(
printHeaders(error.status, error.headers);
}
prettyPrint(error.body);
// A 404 can mean either "no such endpoint" or "endpoint exists, resource
// doesn't", so the wording stays conditional. A parsed Clerk error code is
// evidence the request reached the API and was rejected semantically, so we
// skip the hint there to keep it off the common resource-not-found path —
// a heuristic, not a guarantee. FAPI has no endpoint catalog to search.
if (error.status === 404 && error.code === null && !options.fapi) {
const scope = options.platform ? " --platform" : "";
log.info(`If the endpoint path was a guess, search with: clerk api ls <keyword>${scope}`);
}
process.exitCode = 1;
closeStatus = "failed";
return;
Expand DownExpand Up@@ -227,7 +236,13 @@ function prettyPrintToStderr(text: string): void {
export function registerApi(program: Program): void {
program
.command("api")
.description("Make authenticated requests to the Clerk API")
.summary("Call any Clerk API endpoint (200+; `clerk api ls` to browse)")
.description(
"Call any endpoint in the Clerk API directly.\n\n" +
"The other commands cover common operations. This one reaches everything " +
"else — invitations and waitlist entries, billing subscriptions and credits, " +
"organization roles and permissions, enterprise SSO connections.",
)
.argument(
"[endpoint]",
"API endpoint path, 'ls' to list endpoints, or omit for interactive mode",
Expand All@@ -240,16 +255,23 @@ export function registerApi(program: Program): void {
.option("--app <id>", "Application ID to target when resolving keys")
.option("--secret-key <key>", "Override the secret key")
.option("--instance <id>", "Instance to target (dev, prod, or instance ID)")
.option("--platform", "Use Platform API instead of Backend API")
.option(
"--platform",
"Use the Platform API (applications and instances) instead of the Backend API; has its own endpoint list",
)
.option(
"--fapi",
"Use the instance's public Frontend API (unauthenticated endpoints only; host derived from the publishable key)",
)
.option("--dry-run", "Show the request without executing it")
.option("--yes", "Skip confirmation for mutating requests")
.setExamples([
{ command: "clerk api ls", description: "List all available endpoints" },
{ command: "clerk api ls users", description: 'List endpoints matching "users"' },
{ command: "clerk api ls", description: "List Backend API endpoints" },
{ command: "clerk api ls users", description: 'List Backend API endpoints matching "users"' },
{
command: "clerk api ls --platform",
description: "List Platform API endpoints (applications, instances)",
},
{ command: "clerk api /users", description: "GET /v1/users" },
{
command: 'clerk api /users -d \'{"first_name":"Alice"}\'',
Expand Down
, '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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/api-endpoint-discoverability.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
---
"clerk": patch
---

Make the `clerk api` endpoint catalog discoverable from help output and 404 responses.

- Root help now describes `api` as "Call any Clerk API endpoint (200+; `clerk api ls` to browse)" instead of "Make authenticated requests to the Clerk API", and `clerk api --help` explains that this command covers what the dedicated commands do not.
- `--platform` now explains that the Platform API has its own endpoint list, `clerk api ls` examples say which API they list (they cover the Backend API, not every endpoint), and `clerk api ls --platform` is shown as an example.
- A 404 with no parsed Clerk error code now suggests `clerk api ls <keyword>` on stderr, leaving stdout as the pipeable response body. The suggestion is scoped per surface: `--platform` appends that flag, and `--fapi` gets none since it has no endpoint catalog.
2 changes: 1 addition & 1 deletion README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,7 +47,7 @@ Commands:
telemetry Control CLI usage telemetry (status, disable, enable)
enable Enable Clerk features on the linked instance
disable Disable Clerk features on the linked instance
api [options] [endpoint] [filter] Make authenticated requests to the Clerk API
api [options] [endpoint] [filter] Call any Clerk API endpoint (200+; `clerk api ls` to browse)
doctor [options] Check your project's Clerk integration health
mcp Manage the Clerk remote MCP server connection for AI editors and CLIs
completion [shell] Generate shell autocompletion script
Expand Down
49 changes: 49 additions & 0 deletions packages/cli-core/src/commands/api/index.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -631,6 +631,55 @@ describe("api command", () => {
expect(captured.out).toContain(JSON.stringify(errorBody, null, 2));
expect(captured.err).toContain("Failed");
expect(captured.err).not.toContain("Done");
// A structured Clerk error means the endpoint resolved; don't suggest searching.
expect(captured.err).not.toContain("clerk api ls");
});

// --- 404 endpoint-search hint ---

test("suggests the endpoint search on an unstructured 404", async () => {
setMode("human");
stubFetch(async () => new Response("404 page not found", { status: 404 }));

await runApi("/organization_role");
expect(process.exitCode).toBe(1);
expect(captured.err).toContain("If the endpoint path was a guess");
expect(captured.err).toContain("clerk api ls <keyword>");
// Diagnostics on stderr only; stdout stays the raw response body for piping.
expect(captured.out).not.toContain("clerk api ls");
expect(captured.out).toContain("404 page not found");
});

test("scopes the endpoint search hint to the platform catalog with --platform", async () => {
setMode("human");
process.env.CLERK_PLATFORM_API_KEY = "plat_key_123";
stubFetch(async () => new Response("404 page not found", { status: 404 }));

await runApi("/v1/platform/bogus", { platform: true });
expect(captured.err).toContain("clerk api ls <keyword> --platform");
});

test("omits the endpoint search hint for FAPI, which has no catalog", async () => {
setMode("human");
process.env.CLERK_PLATFORM_API_KEY = "ak_test_platform";
const pk = `pk_test_${btoa("clerk.example.com$")}`;
stubFetch(async (input) => {
if (input.toString().includes("/v1/platform/applications/app_1")) {
return new Response(
JSON.stringify({
application_id: "app_1",
instances: [
{ instance_id: "ins_dev", environment_type: "development", publishable_key: pk },
],
}),
{ status: 200 },
);
}
return new Response("404 page not found", { status: 404 });
});

await runApi("/bogus", { fapi: true, app: "app_1", instance: "dev" });
expect(captured.err).not.toContain("clerk api ls");
});

test("shows Paused with instructions when a confirmation prompt is cancelled", async () => {
Expand Down
30 changes: 26 additions & 4 deletions packages/cli-core/src/commands/api/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -144,6 +144,15 @@ export async function api(
printHeaders(error.status, error.headers);
}
prettyPrint(error.body);
// A 404 can mean either "no such endpoint" or "endpoint exists, resource
// doesn't", so the wording stays conditional. A parsed Clerk error code is
// evidence the request reached the API and was rejected semantically, so we
// skip the hint there to keep it off the common resource-not-found path —
// a heuristic, not a guarantee. FAPI has no endpoint catalog to search.
if (error.status === 404 && error.code === null && !options.fapi) {
const scope = options.platform ? " --platform" : "";
log.info(`If the endpoint path was a guess, search with: clerk api ls <keyword>${scope}`);
}
process.exitCode = 1;
closeStatus = "failed";
return;
Expand DownExpand Up@@ -227,7 +236,13 @@ function prettyPrintToStderr(text: string): void {
export function registerApi(program: Program): void {
program
.command("api")
.description("Make authenticated requests to the Clerk API")
.summary("Call any Clerk API endpoint (200+; `clerk api ls` to browse)")
.description(
"Call any endpoint in the Clerk API directly.\n\n" +
"The other commands cover common operations. This one reaches everything " +
"else — invitations and waitlist entries, billing subscriptions and credits, " +
"organization roles and permissions, enterprise SSO connections.",
)
.argument(
"[endpoint]",
"API endpoint path, 'ls' to list endpoints, or omit for interactive mode",
Expand All@@ -240,16 +255,23 @@ export function registerApi(program: Program): void {
.option("--app <id>", "Application ID to target when resolving keys")
.option("--secret-key <key>", "Override the secret key")
.option("--instance <id>", "Instance to target (dev, prod, or instance ID)")
.option("--platform", "Use Platform API instead of Backend API")
.option(
"--platform",
"Use the Platform API (applications and instances) instead of the Backend API; has its own endpoint list",
)
.option(
"--fapi",
"Use the instance's public Frontend API (unauthenticated endpoints only; host derived from the publishable key)",
)
.option("--dry-run", "Show the request without executing it")
.option("--yes", "Skip confirmation for mutating requests")
.setExamples([
{ command: "clerk api ls", description: "List all available endpoints" },
{ command: "clerk api ls users", description: 'List endpoints matching "users"' },
{ command: "clerk api ls", description: "List Backend API endpoints" },
{ command: "clerk api ls users", description: 'List Backend API endpoints matching "users"' },
{
command: "clerk api ls --platform",
description: "List Platform API endpoints (applications, instances)",
},
{ command: "clerk api /users", description: "GET /v1/users" },
{
command: 'clerk api /users -d \'{"first_name":"Alice"}\'',
Expand Down
, '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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/api-endpoint-discoverability.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
---
"clerk": patch
---

Make the `clerk api` endpoint catalog discoverable from help output and 404 responses.

- Root help now describes `api` as "Call any Clerk API endpoint (200+; `clerk api ls` to browse)" instead of "Make authenticated requests to the Clerk API", and `clerk api --help` explains that this command covers what the dedicated commands do not.
- `--platform` now explains that the Platform API has its own endpoint list, `clerk api ls` examples say which API they list (they cover the Backend API, not every endpoint), and `clerk api ls --platform` is shown as an example.
- A 404 with no parsed Clerk error code now suggests `clerk api ls <keyword>` on stderr, leaving stdout as the pipeable response body. The suggestion is scoped per surface: `--platform` appends that flag, and `--fapi` gets none since it has no endpoint catalog.
2 changes: 1 addition & 1 deletion README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,7 +47,7 @@ Commands:
telemetry Control CLI usage telemetry (status, disable, enable)
enable Enable Clerk features on the linked instance
disable Disable Clerk features on the linked instance
api [options] [endpoint] [filter] Make authenticated requests to the Clerk API
api [options] [endpoint] [filter] Call any Clerk API endpoint (200+; `clerk api ls` to browse)
doctor [options] Check your project's Clerk integration health
mcp Manage the Clerk remote MCP server connection for AI editors and CLIs
completion [shell] Generate shell autocompletion script
Expand Down
49 changes: 49 additions & 0 deletions packages/cli-core/src/commands/api/index.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -631,6 +631,55 @@ describe("api command", () => {
expect(captured.out).toContain(JSON.stringify(errorBody, null, 2));
expect(captured.err).toContain("Failed");
expect(captured.err).not.toContain("Done");
// A structured Clerk error means the endpoint resolved; don't suggest searching.
expect(captured.err).not.toContain("clerk api ls");
});

// --- 404 endpoint-search hint ---

test("suggests the endpoint search on an unstructured 404", async () => {
setMode("human");
stubFetch(async () => new Response("404 page not found", { status: 404 }));

await runApi("/organization_role");
expect(process.exitCode).toBe(1);
expect(captured.err).toContain("If the endpoint path was a guess");
expect(captured.err).toContain("clerk api ls <keyword>");
// Diagnostics on stderr only; stdout stays the raw response body for piping.
expect(captured.out).not.toContain("clerk api ls");
expect(captured.out).toContain("404 page not found");
});

test("scopes the endpoint search hint to the platform catalog with --platform", async () => {
setMode("human");
process.env.CLERK_PLATFORM_API_KEY = "plat_key_123";
stubFetch(async () => new Response("404 page not found", { status: 404 }));

await runApi("/v1/platform/bogus", { platform: true });
expect(captured.err).toContain("clerk api ls <keyword> --platform");
});

test("omits the endpoint search hint for FAPI, which has no catalog", async () => {
setMode("human");
process.env.CLERK_PLATFORM_API_KEY = "ak_test_platform";
const pk = `pk_test_${btoa("clerk.example.com$")}`;
stubFetch(async (input) => {
if (input.toString().includes("/v1/platform/applications/app_1")) {
return new Response(
JSON.stringify({
application_id: "app_1",
instances: [
{ instance_id: "ins_dev", environment_type: "development", publishable_key: pk },
],
}),
{ status: 200 },
);
}
return new Response("404 page not found", { status: 404 });
});

await runApi("/bogus", { fapi: true, app: "app_1", instance: "dev" });
expect(captured.err).not.toContain("clerk api ls");
});

test("shows Paused with instructions when a confirmation prompt is cancelled", async () => {
Expand Down
30 changes: 26 additions & 4 deletions packages/cli-core/src/commands/api/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -144,6 +144,15 @@ export async function api(
printHeaders(error.status, error.headers);
}
prettyPrint(error.body);
// A 404 can mean either "no such endpoint" or "endpoint exists, resource
// doesn't", so the wording stays conditional. A parsed Clerk error code is
// evidence the request reached the API and was rejected semantically, so we
// skip the hint there to keep it off the common resource-not-found path —
// a heuristic, not a guarantee. FAPI has no endpoint catalog to search.
if (error.status === 404 && error.code === null && !options.fapi) {
const scope = options.platform ? " --platform" : "";
log.info(`If the endpoint path was a guess, search with: clerk api ls <keyword>${scope}`);
}
process.exitCode = 1;
closeStatus = "failed";
return;
Expand DownExpand Up@@ -227,7 +236,13 @@ function prettyPrintToStderr(text: string): void {
export function registerApi(program: Program): void {
program
.command("api")
.description("Make authenticated requests to the Clerk API")
.summary("Call any Clerk API endpoint (200+; `clerk api ls` to browse)")
.description(
"Call any endpoint in the Clerk API directly.\n\n" +
"The other commands cover common operations. This one reaches everything " +
"else — invitations and waitlist entries, billing subscriptions and credits, " +
"organization roles and permissions, enterprise SSO connections.",
)
.argument(
"[endpoint]",
"API endpoint path, 'ls' to list endpoints, or omit for interactive mode",
Expand All@@ -240,16 +255,23 @@ export function registerApi(program: Program): void {
.option("--app <id>", "Application ID to target when resolving keys")
.option("--secret-key <key>", "Override the secret key")
.option("--instance <id>", "Instance to target (dev, prod, or instance ID)")
.option("--platform", "Use Platform API instead of Backend API")
.option(
"--platform",
"Use the Platform API (applications and instances) instead of the Backend API; has its own endpoint list",
)
.option(
"--fapi",
"Use the instance's public Frontend API (unauthenticated endpoints only; host derived from the publishable key)",
)
.option("--dry-run", "Show the request without executing it")
.option("--yes", "Skip confirmation for mutating requests")
.setExamples([
{ command: "clerk api ls", description: "List all available endpoints" },
{ command: "clerk api ls users", description: 'List endpoints matching "users"' },
{ command: "clerk api ls", description: "List Backend API endpoints" },
{ command: "clerk api ls users", description: 'List Backend API endpoints matching "users"' },
{
command: "clerk api ls --platform",
description: "List Platform API endpoints (applications, instances)",
},
{ command: "clerk api /users", description: "GET /v1/users" },
{
command: 'clerk api /users -d \'{"first_name":"Alice"}\'',
Expand Down
, '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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/api-endpoint-discoverability.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
---
"clerk": patch
---

Make the `clerk api` endpoint catalog discoverable from help output and 404 responses.

- Root help now describes `api` as "Call any Clerk API endpoint (200+; `clerk api ls` to browse)" instead of "Make authenticated requests to the Clerk API", and `clerk api --help` explains that this command covers what the dedicated commands do not.
- `--platform` now explains that the Platform API has its own endpoint list, `clerk api ls` examples say which API they list (they cover the Backend API, not every endpoint), and `clerk api ls --platform` is shown as an example.
- A 404 with no parsed Clerk error code now suggests `clerk api ls <keyword>` on stderr, leaving stdout as the pipeable response body. The suggestion is scoped per surface: `--platform` appends that flag, and `--fapi` gets none since it has no endpoint catalog.
2 changes: 1 addition & 1 deletion README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,7 +47,7 @@ Commands:
telemetry Control CLI usage telemetry (status, disable, enable)
enable Enable Clerk features on the linked instance
disable Disable Clerk features on the linked instance
api [options] [endpoint] [filter] Make authenticated requests to the Clerk API
api [options] [endpoint] [filter] Call any Clerk API endpoint (200+; `clerk api ls` to browse)
doctor [options] Check your project's Clerk integration health
mcp Manage the Clerk remote MCP server connection for AI editors and CLIs
completion [shell] Generate shell autocompletion script
Expand Down
49 changes: 49 additions & 0 deletions packages/cli-core/src/commands/api/index.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -631,6 +631,55 @@ describe("api command", () => {
expect(captured.out).toContain(JSON.stringify(errorBody, null, 2));
expect(captured.err).toContain("Failed");
expect(captured.err).not.toContain("Done");
// A structured Clerk error means the endpoint resolved; don't suggest searching.
expect(captured.err).not.toContain("clerk api ls");
});

// --- 404 endpoint-search hint ---

test("suggests the endpoint search on an unstructured 404", async () => {
setMode("human");
stubFetch(async () => new Response("404 page not found", { status: 404 }));

await runApi("/organization_role");
expect(process.exitCode).toBe(1);
expect(captured.err).toContain("If the endpoint path was a guess");
expect(captured.err).toContain("clerk api ls <keyword>");
// Diagnostics on stderr only; stdout stays the raw response body for piping.
expect(captured.out).not.toContain("clerk api ls");
expect(captured.out).toContain("404 page not found");
});

test("scopes the endpoint search hint to the platform catalog with --platform", async () => {
setMode("human");
process.env.CLERK_PLATFORM_API_KEY = "plat_key_123";
stubFetch(async () => new Response("404 page not found", { status: 404 }));

await runApi("/v1/platform/bogus", { platform: true });
expect(captured.err).toContain("clerk api ls <keyword> --platform");
});

test("omits the endpoint search hint for FAPI, which has no catalog", async () => {
setMode("human");
process.env.CLERK_PLATFORM_API_KEY = "ak_test_platform";
const pk = `pk_test_${btoa("clerk.example.com$")}`;
stubFetch(async (input) => {
if (input.toString().includes("/v1/platform/applications/app_1")) {
return new Response(
JSON.stringify({
application_id: "app_1",
instances: [
{ instance_id: "ins_dev", environment_type: "development", publishable_key: pk },
],
}),
{ status: 200 },
);
}
return new Response("404 page not found", { status: 404 });
});

await runApi("/bogus", { fapi: true, app: "app_1", instance: "dev" });
expect(captured.err).not.toContain("clerk api ls");
});

test("shows Paused with instructions when a confirmation prompt is cancelled", async () => {
Expand Down
30 changes: 26 additions & 4 deletions packages/cli-core/src/commands/api/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -144,6 +144,15 @@ export async function api(
printHeaders(error.status, error.headers);
}
prettyPrint(error.body);
// A 404 can mean either "no such endpoint" or "endpoint exists, resource
// doesn't", so the wording stays conditional. A parsed Clerk error code is
// evidence the request reached the API and was rejected semantically, so we
// skip the hint there to keep it off the common resource-not-found path —
// a heuristic, not a guarantee. FAPI has no endpoint catalog to search.
if (error.status === 404 && error.code === null && !options.fapi) {
const scope = options.platform ? " --platform" : "";
log.info(`If the endpoint path was a guess, search with: clerk api ls <keyword>${scope}`);
}
process.exitCode = 1;
closeStatus = "failed";
return;
Expand DownExpand Up@@ -227,7 +236,13 @@ function prettyPrintToStderr(text: string): void {
export function registerApi(program: Program): void {
program
.command("api")
.description("Make authenticated requests to the Clerk API")
.summary("Call any Clerk API endpoint (200+; `clerk api ls` to browse)")
.description(
"Call any endpoint in the Clerk API directly.\n\n" +
"The other commands cover common operations. This one reaches everything " +
"else — invitations and waitlist entries, billing subscriptions and credits, " +
"organization roles and permissions, enterprise SSO connections.",
)
.argument(
"[endpoint]",
"API endpoint path, 'ls' to list endpoints, or omit for interactive mode",
Expand All@@ -240,16 +255,23 @@ export function registerApi(program: Program): void {
.option("--app <id>", "Application ID to target when resolving keys")
.option("--secret-key <key>", "Override the secret key")
.option("--instance <id>", "Instance to target (dev, prod, or instance ID)")
.option("--platform", "Use Platform API instead of Backend API")
.option(
"--platform",
"Use the Platform API (applications and instances) instead of the Backend API; has its own endpoint list",
)
.option(
"--fapi",
"Use the instance's public Frontend API (unauthenticated endpoints only; host derived from the publishable key)",
)
.option("--dry-run", "Show the request without executing it")
.option("--yes", "Skip confirmation for mutating requests")
.setExamples([
{ command: "clerk api ls", description: "List all available endpoints" },
{ command: "clerk api ls users", description: 'List endpoints matching "users"' },
{ command: "clerk api ls", description: "List Backend API endpoints" },
{ command: "clerk api ls users", description: 'List Backend API endpoints matching "users"' },
{
command: "clerk api ls --platform",
description: "List Platform API endpoints (applications, instances)",
},
{ command: "clerk api /users", description: "GET /v1/users" },
{
command: 'clerk api /users -d \'{"first_name":"Alice"}\'',
Expand Down
, '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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/api-endpoint-discoverability.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
---
"clerk": patch
---

Make the `clerk api` endpoint catalog discoverable from help output and 404 responses.

- Root help now describes `api` as "Call any Clerk API endpoint (200+; `clerk api ls` to browse)" instead of "Make authenticated requests to the Clerk API", and `clerk api --help` explains that this command covers what the dedicated commands do not.
- `--platform` now explains that the Platform API has its own endpoint list, `clerk api ls` examples say which API they list (they cover the Backend API, not every endpoint), and `clerk api ls --platform` is shown as an example.
- A 404 with no parsed Clerk error code now suggests `clerk api ls <keyword>` on stderr, leaving stdout as the pipeable response body. The suggestion is scoped per surface: `--platform` appends that flag, and `--fapi` gets none since it has no endpoint catalog.
2 changes: 1 addition & 1 deletion README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,7 +47,7 @@ Commands:
telemetry Control CLI usage telemetry (status, disable, enable)
enable Enable Clerk features on the linked instance
disable Disable Clerk features on the linked instance
api [options] [endpoint] [filter] Make authenticated requests to the Clerk API
api [options] [endpoint] [filter] Call any Clerk API endpoint (200+; `clerk api ls` to browse)
doctor [options] Check your project's Clerk integration health
mcp Manage the Clerk remote MCP server connection for AI editors and CLIs
completion [shell] Generate shell autocompletion script
Expand Down
49 changes: 49 additions & 0 deletions packages/cli-core/src/commands/api/index.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -631,6 +631,55 @@ describe("api command", () => {
expect(captured.out).toContain(JSON.stringify(errorBody, null, 2));
expect(captured.err).toContain("Failed");
expect(captured.err).not.toContain("Done");
// A structured Clerk error means the endpoint resolved; don't suggest searching.
expect(captured.err).not.toContain("clerk api ls");
});

// --- 404 endpoint-search hint ---

test("suggests the endpoint search on an unstructured 404", async () => {
setMode("human");
stubFetch(async () => new Response("404 page not found", { status: 404 }));

await runApi("/organization_role");
expect(process.exitCode).toBe(1);
expect(captured.err).toContain("If the endpoint path was a guess");
expect(captured.err).toContain("clerk api ls <keyword>");
// Diagnostics on stderr only; stdout stays the raw response body for piping.
expect(captured.out).not.toContain("clerk api ls");
expect(captured.out).toContain("404 page not found");
});

test("scopes the endpoint search hint to the platform catalog with --platform", async () => {
setMode("human");
process.env.CLERK_PLATFORM_API_KEY = "plat_key_123";
stubFetch(async () => new Response("404 page not found", { status: 404 }));

await runApi("/v1/platform/bogus", { platform: true });
expect(captured.err).toContain("clerk api ls <keyword> --platform");
});

test("omits the endpoint search hint for FAPI, which has no catalog", async () => {
setMode("human");
process.env.CLERK_PLATFORM_API_KEY = "ak_test_platform";
const pk = `pk_test_${btoa("clerk.example.com$")}`;
stubFetch(async (input) => {
if (input.toString().includes("/v1/platform/applications/app_1")) {
return new Response(
JSON.stringify({
application_id: "app_1",
instances: [
{ instance_id: "ins_dev", environment_type: "development", publishable_key: pk },
],
}),
{ status: 200 },
);
}
return new Response("404 page not found", { status: 404 });
});

await runApi("/bogus", { fapi: true, app: "app_1", instance: "dev" });
expect(captured.err).not.toContain("clerk api ls");
});

test("shows Paused with instructions when a confirmation prompt is cancelled", async () => {
Expand Down
30 changes: 26 additions & 4 deletions packages/cli-core/src/commands/api/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -144,6 +144,15 @@ export async function api(
printHeaders(error.status, error.headers);
}
prettyPrint(error.body);
// A 404 can mean either "no such endpoint" or "endpoint exists, resource
// doesn't", so the wording stays conditional. A parsed Clerk error code is
// evidence the request reached the API and was rejected semantically, so we
// skip the hint there to keep it off the common resource-not-found path —
// a heuristic, not a guarantee. FAPI has no endpoint catalog to search.
if (error.status === 404 && error.code === null && !options.fapi) {
const scope = options.platform ? " --platform" : "";
log.info(`If the endpoint path was a guess, search with: clerk api ls <keyword>${scope}`);
}
process.exitCode = 1;
closeStatus = "failed";
return;
Expand DownExpand Up@@ -227,7 +236,13 @@ function prettyPrintToStderr(text: string): void {
export function registerApi(program: Program): void {
program
.command("api")
.description("Make authenticated requests to the Clerk API")
.summary("Call any Clerk API endpoint (200+; `clerk api ls` to browse)")
.description(
"Call any endpoint in the Clerk API directly.\n\n" +
"The other commands cover common operations. This one reaches everything " +
"else — invitations and waitlist entries, billing subscriptions and credits, " +
"organization roles and permissions, enterprise SSO connections.",
)
.argument(
"[endpoint]",
"API endpoint path, 'ls' to list endpoints, or omit for interactive mode",
Expand All@@ -240,16 +255,23 @@ export function registerApi(program: Program): void {
.option("--app <id>", "Application ID to target when resolving keys")
.option("--secret-key <key>", "Override the secret key")
.option("--instance <id>", "Instance to target (dev, prod, or instance ID)")
.option("--platform", "Use Platform API instead of Backend API")
.option(
"--platform",
"Use the Platform API (applications and instances) instead of the Backend API; has its own endpoint list",
)
.option(
"--fapi",
"Use the instance's public Frontend API (unauthenticated endpoints only; host derived from the publishable key)",
)
.option("--dry-run", "Show the request without executing it")
.option("--yes", "Skip confirmation for mutating requests")
.setExamples([
{ command: "clerk api ls", description: "List all available endpoints" },
{ command: "clerk api ls users", description: 'List endpoints matching "users"' },
{ command: "clerk api ls", description: "List Backend API endpoints" },
{ command: "clerk api ls users", description: 'List Backend API endpoints matching "users"' },
{
command: "clerk api ls --platform",
description: "List Platform API endpoints (applications, instances)",
},
{ command: "clerk api /users", description: "GET /v1/users" },
{
command: 'clerk api /users -d \'{"first_name":"Alice"}\'',
Expand Down
, '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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/api-endpoint-discoverability.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
---
"clerk": patch
---

Make the `clerk api` endpoint catalog discoverable from help output and 404 responses.

- Root help now describes `api` as "Call any Clerk API endpoint (200+; `clerk api ls` to browse)" instead of "Make authenticated requests to the Clerk API", and `clerk api --help` explains that this command covers what the dedicated commands do not.
- `--platform` now explains that the Platform API has its own endpoint list, `clerk api ls` examples say which API they list (they cover the Backend API, not every endpoint), and `clerk api ls --platform` is shown as an example.
- A 404 with no parsed Clerk error code now suggests `clerk api ls <keyword>` on stderr, leaving stdout as the pipeable response body. The suggestion is scoped per surface: `--platform` appends that flag, and `--fapi` gets none since it has no endpoint catalog.
2 changes: 1 addition & 1 deletion README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,7 +47,7 @@ Commands:
telemetry Control CLI usage telemetry (status, disable, enable)
enable Enable Clerk features on the linked instance
disable Disable Clerk features on the linked instance
api [options] [endpoint] [filter] Make authenticated requests to the Clerk API
api [options] [endpoint] [filter] Call any Clerk API endpoint (200+; `clerk api ls` to browse)
doctor [options] Check your project's Clerk integration health
mcp Manage the Clerk remote MCP server connection for AI editors and CLIs
completion [shell] Generate shell autocompletion script
Expand Down
49 changes: 49 additions & 0 deletions packages/cli-core/src/commands/api/index.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -631,6 +631,55 @@ describe("api command", () => {
expect(captured.out).toContain(JSON.stringify(errorBody, null, 2));
expect(captured.err).toContain("Failed");
expect(captured.err).not.toContain("Done");
// A structured Clerk error means the endpoint resolved; don't suggest searching.
expect(captured.err).not.toContain("clerk api ls");
});

// --- 404 endpoint-search hint ---

test("suggests the endpoint search on an unstructured 404", async () => {
setMode("human");
stubFetch(async () => new Response("404 page not found", { status: 404 }));

await runApi("/organization_role");
expect(process.exitCode).toBe(1);
expect(captured.err).toContain("If the endpoint path was a guess");
expect(captured.err).toContain("clerk api ls <keyword>");
// Diagnostics on stderr only; stdout stays the raw response body for piping.
expect(captured.out).not.toContain("clerk api ls");
expect(captured.out).toContain("404 page not found");
});

test("scopes the endpoint search hint to the platform catalog with --platform", async () => {
setMode("human");
process.env.CLERK_PLATFORM_API_KEY = "plat_key_123";
stubFetch(async () => new Response("404 page not found", { status: 404 }));

await runApi("/v1/platform/bogus", { platform: true });
expect(captured.err).toContain("clerk api ls <keyword> --platform");
});

test("omits the endpoint search hint for FAPI, which has no catalog", async () => {
setMode("human");
process.env.CLERK_PLATFORM_API_KEY = "ak_test_platform";
const pk = `pk_test_${btoa("clerk.example.com$")}`;
stubFetch(async (input) => {
if (input.toString().includes("/v1/platform/applications/app_1")) {
return new Response(
JSON.stringify({
application_id: "app_1",
instances: [
{ instance_id: "ins_dev", environment_type: "development", publishable_key: pk },
],
}),
{ status: 200 },
);
}
return new Response("404 page not found", { status: 404 });
});

await runApi("/bogus", { fapi: true, app: "app_1", instance: "dev" });
expect(captured.err).not.toContain("clerk api ls");
});

test("shows Paused with instructions when a confirmation prompt is cancelled", async () => {
Expand Down
30 changes: 26 additions & 4 deletions packages/cli-core/src/commands/api/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -144,6 +144,15 @@ export async function api(
printHeaders(error.status, error.headers);
}
prettyPrint(error.body);
// A 404 can mean either "no such endpoint" or "endpoint exists, resource
// doesn't", so the wording stays conditional. A parsed Clerk error code is
// evidence the request reached the API and was rejected semantically, so we
// skip the hint there to keep it off the common resource-not-found path —
// a heuristic, not a guarantee. FAPI has no endpoint catalog to search.
if (error.status === 404 && error.code === null && !options.fapi) {
const scope = options.platform ? " --platform" : "";
log.info(`If the endpoint path was a guess, search with: clerk api ls <keyword>${scope}`);
}
process.exitCode = 1;
closeStatus = "failed";
return;
Expand DownExpand Up@@ -227,7 +236,13 @@ function prettyPrintToStderr(text: string): void {
export function registerApi(program: Program): void {
program
.command("api")
.description("Make authenticated requests to the Clerk API")
.summary("Call any Clerk API endpoint (200+; `clerk api ls` to browse)")
.description(
"Call any endpoint in the Clerk API directly.\n\n" +
"The other commands cover common operations. This one reaches everything " +
"else — invitations and waitlist entries, billing subscriptions and credits, " +
"organization roles and permissions, enterprise SSO connections.",
)
.argument(
"[endpoint]",
"API endpoint path, 'ls' to list endpoints, or omit for interactive mode",
Expand All@@ -240,16 +255,23 @@ export function registerApi(program: Program): void {
.option("--app <id>", "Application ID to target when resolving keys")
.option("--secret-key <key>", "Override the secret key")
.option("--instance <id>", "Instance to target (dev, prod, or instance ID)")
.option("--platform", "Use Platform API instead of Backend API")
.option(
"--platform",
"Use the Platform API (applications and instances) instead of the Backend API; has its own endpoint list",
)
.option(
"--fapi",
"Use the instance's public Frontend API (unauthenticated endpoints only; host derived from the publishable key)",
)
.option("--dry-run", "Show the request without executing it")
.option("--yes", "Skip confirmation for mutating requests")
.setExamples([
{ command: "clerk api ls", description: "List all available endpoints" },
{ command: "clerk api ls users", description: 'List endpoints matching "users"' },
{ command: "clerk api ls", description: "List Backend API endpoints" },
{ command: "clerk api ls users", description: 'List Backend API endpoints matching "users"' },
{
command: "clerk api ls --platform",
description: "List Platform API endpoints (applications, instances)",
},
{ command: "clerk api /users", description: "GET /v1/users" },
{
command: 'clerk api /users -d \'{"first_name":"Alice"}\'',
Expand Down